tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

check_spidermonkey_style.py (31748B)


      1 # vim: set ts=8 sts=4 et sw=4 tw=99:
      2 # This Source Code Form is subject to the terms of the Mozilla Public
      3 # License, v. 2.0. If a copy of the MPL was not distributed with this
      4 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      5 
      6 # ----------------------------------------------------------------------------
      7 # This script checks various aspects of SpiderMonkey code style.  The current checks are as
      8 # follows.
      9 #
     10 # We check the following things in headers.
     11 #
     12 # - No cyclic dependencies.
     13 #
     14 # - No normal header should #include a -inl.h file.
     15 #
     16 # - #ifndef wrappers should have the right form. (XXX: not yet implemented)
     17 #   - Every header file should have one.
     18 #   - The guard name used should be appropriate for the filename.
     19 #
     20 # We check the following things in all files.
     21 #
     22 # - #includes should have full paths, e.g. "jit/Ion.h", not "Ion.h".
     23 #
     24 # - #includes should use the appropriate form for system headers (<...>) and
     25 #   local headers ("...").
     26 #
     27 # - #includes should be ordered correctly.
     28 #   - Each one should be in the correct section.
     29 #   - Alphabetical order should be used within sections.
     30 #   - Sections should be in the right order.
     31 #   Note that the presence of #if/#endif blocks complicates things, to the
     32 #   point that it's not always clear where a conditionally-compiled #include
     33 #   statement should go, even to a human.  Therefore, we check the #include
     34 #   statements within each #if/#endif block (including nested ones) in
     35 #   isolation, but don't try to do any order checking between such blocks.
     36 # ----------------------------------------------------------------------------
     37 
     38 import difflib
     39 import os
     40 import re
     41 import sys
     42 
     43 # We don't bother checking files in these directories, because they're (a) auxiliary or (b)
     44 # imported code that doesn't follow our coding style.
     45 ignored_js_src_dirs = [
     46    "js/src/config/",  # auxiliary stuff
     47    "js/src/ctypes/libffi/",  # imported code
     48    "js/src/devtools/",  # auxiliary stuff
     49    "js/src/editline/",  # imported code
     50    "js/src/gdb/",  # auxiliary stuff
     51    "js/src/vtune/",  # imported code
     52    "js/src/zydis/",  # imported code
     53    "js/src/xsum/",  # imported code
     54 ]
     55 
     56 # We ignore #includes of these files, because they don't follow the usual rules.
     57 included_inclnames_to_ignore = set([
     58    "ffi.h",  # generated in ctypes/libffi/
     59    "devtools/Instruments.h",  # we ignore devtools/ in general
     60    "diplomat_runtime.hpp",  # ICU4X
     61    "double-conversion/double-conversion.h",  # strange MFBT case
     62    "frontend/ReservedWordsGenerated.h",  # generated in $OBJDIR
     63    "gc/StatsPhasesGenerated.h",  # generated in $OBJDIR
     64    "gc/StatsPhasesGenerated.inc",  # generated in $OBJDIR
     65    "icu4x/Calendar.hpp",  # ICU4X
     66    "icu4x/Date.hpp",  # ICU4X
     67    "icu4x/GraphemeClusterSegmenter.hpp",  # ICU4X
     68    "icu4x/IsoDate.hpp",  # ICU4X
     69    "icu4x/Locale.hpp",  # ICU4X
     70    "icu4x/SentenceSegmenter.hpp",  # ICU4X
     71    "icu4x/WordSegmenter.hpp",  # ICU4X
     72    "jit/ABIFunctionTypeGenerated.h",  # generated in $OBJDIR"
     73    "jit/AtomicOperationsGenerated.h",  # generated in $OBJDIR
     74    "jit/CacheIROpsGenerated.h",  # generated in $OBJDIR
     75    "jit/CacheIRAOTGenerated.h",  # generated in $OBJDIR
     76    "jit/LIROpsGenerated.h",  # generated in $OBJDIR
     77    "jit/MIROpsGenerated.h",  # generated in $OBJDIR
     78    "js/PrefsGenerated.h",  # generated in $OBJDIR
     79    "mozilla/ProfilingCategoryList.h",  # comes from mozglue/baseprofiler
     80    "mozilla/glue/Debug.h",  # comes from mozglue/misc, shadowed by <mozilla/Debug.h>
     81    "jscustomallocator.h",  # provided by embedders;  allowed to be missing
     82    "js-config.h",  # generated in $OBJDIR
     83    "fdlibm.h",  # fdlibm
     84    "FuzzerDefs.h",  # included without a path
     85    "FuzzingInterface.h",  # included without a path
     86    "mozmemory.h",  # included without a path
     87    "mozmemory_stall.h",  # included without a path
     88    "pratom.h",  # NSPR
     89    "prcvar.h",  # NSPR
     90    "prerror.h",  # NSPR
     91    "prinit.h",  # NSPR
     92    "prio.h",  # NSPR
     93    "private/pprio.h",  # NSPR
     94    "prlink.h",  # NSPR
     95    "prlock.h",  # NSPR
     96    "prprf.h",  # NSPR
     97    "prthread.h",  # NSPR
     98    "prtypes.h",  # NSPR
     99    "selfhosted.out.h",  # generated in $OBJDIR
    100    "shellmoduleloader.out.h",  # generated in $OBJDIR
    101    "unicode/locid.h",  # ICU
    102    "unicode/uchar.h",  # ICU
    103    "unicode/uniset.h",  # ICU
    104    "unicode/unistr.h",  # ICU
    105    "unicode/utypes.h",  # ICU
    106    "vtune/VTuneWrapper.h",  # VTune
    107    "wasm/WasmBuiltinModuleGenerated.h",  # generated in $OBJDIR"
    108    "zydis/ZydisAPI.h",  # Zydis
    109    "xsum/xsum.h",  # xsum
    110    "fmt/format.h",  # {fmt} main header
    111 ])
    112 
    113 # JSAPI functions should be included through headers from js/public instead of
    114 # using the old, catch-all jsapi.h file.
    115 deprecated_inclnames_in_header = {
    116    "jsapi.h": "Prefer including headers from js/public.",
    117 }
    118 
    119 # Temporary exclusions for files which still need to include jsapi.h.
    120 deprecated_inclnames_in_header_excludes = {
    121    "js/src/vm/Compartment-inl.h",  # for JS::InformalValueTypeName
    122    "js/src/jsapi-tests/tests.h",  # for JS_ValueToSource
    123 }
    124 
    125 # These files have additional constraints on where they are #included, so we
    126 # ignore #includes of them when checking #include ordering.
    127 oddly_ordered_inclnames = set([
    128    "ctypes/typedefs.h",  # Included multiple times in the body of ctypes/CTypes.h
    129    # Included in the body of frontend/TokenStream.h
    130    "frontend/ReservedWordsGenerated.h",
    131    "gc/StatsPhasesGenerated.h",  # Included in the body of gc/Statistics.h
    132    "gc/StatsPhasesGenerated.inc",  # Included in the body of gc/Statistics.cpp
    133    "psapi.h",  # Must be included after "util/WindowsWrapper.h" on Windows
    134    "machine/endian.h",  # Must be included after <sys/types.h> on BSD
    135    "process.h",  # Windows-specific
    136    "util/WindowsWrapper.h",  # Must precede other system headers(?)
    137 ])
    138 
    139 # System headers which shouldn't be included directly, but instead use the
    140 # designated wrapper.
    141 wrapper_system_inclnames = {
    142    "windows.h": "util/WindowsWrapper.h",
    143    "windef.h": "util/WindowsWrapper.h",
    144    "winbase.h": "util/WindowsWrapper.h",
    145 }
    146 
    147 # The files in tests/style/ contain code that fails this checking in various
    148 # ways.  Here is the output we expect.  If the actual output differs from
    149 # this, one of the following must have happened.
    150 # - New SpiderMonkey code violates one of the checked rules.
    151 # - The tests/style/ files have changed without expected_output being changed
    152 #   accordingly.
    153 # - This script has been broken somehow.
    154 #
    155 expected_output = """\
    156 js/src/tests/style/BadIncludes.h:3: error:
    157    the file includes itself
    158 
    159 js/src/tests/style/BadIncludes.h:6: error:
    160    "BadIncludes2.h" is included using the wrong path;
    161    did you forget a prefix, or is the file not yet committed?
    162 
    163 js/src/tests/style/BadIncludes.h:8: error:
    164    <tests/style/BadIncludes2.h> should be included using
    165    the #include "..." form
    166 
    167 js/src/tests/style/BadIncludes.h:10: error:
    168    "stdio.h" is included using the wrong path;
    169    did you forget a prefix, or is the file not yet committed?
    170 
    171 js/src/tests/style/BadIncludes2.h:1: error:
    172    vanilla header includes an inline-header file "tests/style/BadIncludes2-inl.h"
    173 
    174 js/src/tests/style/BadIncludesOrder-inl.h:5:6: error:
    175    "vm/JSScript-inl.h" should be included after "vm/Interpreter-inl.h"
    176 
    177 js/src/tests/style/BadIncludesOrder-inl.h:6:7: error:
    178    "vm/Interpreter-inl.h" should be included after "js/Value.h"
    179 
    180 js/src/tests/style/BadIncludesOrder-inl.h:7:8: error:
    181    "js/Value.h" should be included after "ds/LifoAlloc.h"
    182 
    183 js/src/tests/style/BadIncludesOrder-inl.h:9: error:
    184    "jsapi.h" is deprecated: Prefer including headers from js/public.
    185 
    186 js/src/tests/style/BadIncludesOrder-inl.h:8:9: error:
    187    "ds/LifoAlloc.h" should be included after "jsapi.h"
    188 
    189 js/src/tests/style/BadIncludesOrder-inl.h:9:10: error:
    190    "jsapi.h" should be included after <stdio.h>
    191 
    192 js/src/tests/style/BadIncludesOrder-inl.h:10:11: error:
    193    <stdio.h> should be included after "mozilla/HashFunctions.h"
    194 
    195 js/src/tests/style/BadIncludesOrder-inl.h:20: error:
    196    "jsapi.h" is deprecated: Prefer including headers from js/public.
    197 
    198 js/src/tests/style/BadIncludesOrder-inl.h:28:29: error:
    199    "vm/JSScript.h" should be included after "vm/JSFunction.h"
    200 
    201 (multiple files): error:
    202    header files form one or more cycles
    203 
    204   tests/style/HeaderCycleA1.h
    205   -> tests/style/HeaderCycleA2.h
    206      -> tests/style/HeaderCycleA3.h
    207         -> tests/style/HeaderCycleA1.h
    208 
    209   tests/style/HeaderCycleB1-inl.h
    210   -> tests/style/HeaderCycleB2-inl.h
    211      -> tests/style/HeaderCycleB3-inl.h
    212         -> tests/style/HeaderCycleB4-inl.h
    213            -> tests/style/HeaderCycleB1-inl.h
    214      -> tests/style/HeaderCycleB4-inl.h
    215 
    216 """.splitlines(True)
    217 
    218 actual_output = []
    219 
    220 
    221 def out(*lines):
    222    for line in lines:
    223        actual_output.append(line + "\n")
    224 
    225 
    226 def error(filename, linenum, *lines):
    227    location = filename
    228    if linenum is not None:
    229        location += ":" + str(linenum)
    230    out(location + ": error:")
    231    for line in lines:
    232        out("    " + line)
    233    out("")
    234 
    235 
    236 class FileKind:
    237    C = 1
    238    CPP = 2
    239    INL_H = 3
    240    H = 4
    241    TBL = 5
    242    MSG = 6
    243 
    244    @staticmethod
    245    def get(filename):
    246        if filename.endswith(".c"):
    247            return FileKind.C
    248 
    249        if filename.endswith(".cpp"):
    250            return FileKind.CPP
    251 
    252        if filename.endswith("-inl.h"):
    253            return FileKind.INL_H
    254 
    255        if filename.endswith((".h", ".hpp")):
    256            return FileKind.H
    257 
    258        if filename.endswith(".tbl"):
    259            return FileKind.TBL
    260 
    261        if filename.endswith(".msg"):
    262            return FileKind.MSG
    263 
    264        error(filename, None, "unknown file kind")
    265 
    266 
    267 def check_style(enable_fixup):
    268    # We deal with two kinds of name.
    269    # - A "filename" is a full path to a file from the repository root.
    270    # - An "inclname" is how a file is referred to in a #include statement.
    271    #
    272    # Examples (filename -> inclname)
    273    # - "mfbt/Attributes.h"         -> "mozilla/Attributes.h"
    274    # - "mozglue/misc/TimeStamp.h   -> "mozilla/TimeStamp.h"
    275    # - "memory/mozalloc/mozalloc.h -> "mozilla/mozalloc.h"
    276    # - "js/public/Vector.h"        -> "js/Vector.h"
    277    # - "js/src/vm/String.h"        -> "vm/String.h"
    278 
    279    non_js_dirnames = (
    280        "mfbt/",
    281        "memory/mozalloc/",
    282        "mozglue/",
    283        "intl/components/",
    284    )  # type: tuple(str)
    285    non_js_inclnames = set()  # type: set(inclname)
    286    js_names = dict()  # type: dict(filename, inclname)
    287 
    288    # Process files in js/src.
    289    js_src_root = os.path.join("js", "src")
    290    for dirpath, dirnames, filenames in os.walk(js_src_root):
    291        if dirpath == js_src_root:
    292            # Skip any subdirectories that contain a config.status file
    293            # (likely objdirs).
    294            builddirs = []
    295            for dirname in dirnames:
    296                path = os.path.join(dirpath, dirname, "config.status")
    297                if os.path.isfile(path):
    298                    builddirs.append(dirname)
    299            for dirname in builddirs:
    300                dirnames.remove(dirname)
    301        for filename in filenames:
    302            filepath = os.path.join(dirpath, filename).replace("\\", "/")
    303            if not filepath.startswith(
    304                tuple(ignored_js_src_dirs)
    305            ) and filepath.endswith((".c", ".cpp", ".h", ".tbl", ".msg")):
    306                inclname = filepath[len("js/src/") :]
    307                js_names[filepath] = inclname
    308 
    309    # Look for header files in directories in non_js_dirnames.
    310    for non_js_dir in non_js_dirnames:
    311        for dirpath, dirnames, filenames in os.walk(non_js_dir):
    312            for filename in filenames:
    313                if filename.endswith(".h"):
    314                    inclname = "mozilla/" + filename
    315                    if non_js_dir == "intl/components/":
    316                        inclname = "mozilla/intl/" + filename
    317                    non_js_inclnames.add(inclname)
    318 
    319    # Look for header files in js/public.
    320    js_public_root = os.path.join("js", "public")
    321    for dirpath, dirnames, filenames in os.walk(js_public_root):
    322        for filename in filenames:
    323            if filename.endswith((".h", ".msg")):
    324                filepath = os.path.join(dirpath, filename).replace("\\", "/")
    325                inclname = "js/" + filepath[len("js/public/") :]
    326                js_names[filepath] = inclname
    327 
    328    all_inclnames = non_js_inclnames | set(js_names.values())
    329 
    330    edges = dict()  # type: dict(inclname, set(inclname))
    331 
    332    # We don't care what's inside the MFBT and MOZALLOC files, but because they
    333    # are #included from JS files we have to add them to the inclusion graph.
    334    for inclname in non_js_inclnames:
    335        edges[inclname] = set()
    336 
    337    # Process all the JS files.
    338    for filename in sorted(js_names.keys()):
    339        inclname = js_names[filename]
    340        file_kind = FileKind.get(filename)
    341        if file_kind in {FileKind.C, FileKind.CPP, FileKind.H, FileKind.INL_H}:
    342            included_h_inclnames = set()  # type: set(inclname)
    343 
    344            with open(filename, encoding="utf-8") as f:
    345                code = read_file(f)
    346 
    347            if enable_fixup:
    348                code = code.sorted(inclname)
    349                with open(filename, "w") as f:
    350                    f.write(code.to_source())
    351 
    352            check_file(
    353                filename, inclname, file_kind, code, all_inclnames, included_h_inclnames
    354            )
    355 
    356        edges[inclname] = included_h_inclnames
    357 
    358    find_cycles(all_inclnames, edges)
    359 
    360    # Compare expected and actual output.
    361    difflines = difflib.unified_diff(
    362        expected_output,
    363        actual_output,
    364        fromfile="check_spidermonkey_style.py expected output",
    365        tofile="check_spidermonkey_style.py actual output",
    366    )
    367    ok = True
    368    for diffline in difflines:
    369        ok = False
    370        print(diffline, end="")
    371 
    372    return ok
    373 
    374 
    375 def module_name(name):
    376    """Strip the trailing .cpp, .h, or -inl.h from a filename."""
    377 
    378    return name.replace("-inl.h", "").replace(".h", "").replace(".cpp", "")  # NOQA: E501
    379 
    380 
    381 def is_module_header(enclosing_inclname, header_inclname):
    382    """Determine if an included name is the "module header", i.e. should be
    383    first in the file."""
    384 
    385    module = module_name(enclosing_inclname)
    386 
    387    # Normal case, for example:
    388    #   module == "vm/Runtime", header_inclname == "vm/Runtime.h".
    389    if module == module_name(header_inclname):
    390        return True
    391 
    392    # A public header, for example:
    393    #
    394    #   module == "vm/CharacterEncoding",
    395    #   header_inclname == "js/CharacterEncoding.h"
    396    #
    397    # or (for implementation files for js/public/*/*.h headers)
    398    #
    399    #   module == "vm/SourceHook",
    400    #   header_inclname == "js/experimental/SourceHook.h"
    401    m = re.match(r"js\/.*?([^\/]+)\.h", header_inclname)
    402    if m is not None and module.endswith("/" + m.group(1)):
    403        return True
    404 
    405    return False
    406 
    407 
    408 class Include:
    409    """Important information for a single #include statement."""
    410 
    411    def __init__(self, include_prefix, inclname, line_suffix, linenum, is_system):
    412        self.include_prefix = include_prefix
    413        self.line_suffix = line_suffix
    414        self.inclname = inclname
    415        self.linenum = linenum
    416        self.is_system = is_system
    417 
    418    def is_style_relevant(self):
    419        # Includes are style-relevant; that is, they're checked by the pairwise
    420        # style-checking algorithm in check_file.
    421        return True
    422 
    423    def section(self, enclosing_inclname):
    424        """Identify which section inclname belongs to.
    425 
    426        The section numbers are as follows.
    427          0. Module header (e.g. jsfoo.h or jsfoo-inl.h within jsfoo.cpp)
    428          1. mozilla/Foo.h
    429          2. <foo.h> or <foo>
    430          3. jsfoo.h, prmjtime.h, etc
    431          4. foo/Bar.h
    432          5. foo/Bar-inl.h
    433          6. non-.h, e.g. *.tbl, *.msg (these can be scattered throughout files)
    434        """
    435 
    436        if self.is_system:
    437            return 2
    438 
    439        if not self.inclname.endswith((".h", ".hpp")):
    440            return 6
    441 
    442        # A couple of modules have the .h file in js/ and the .cpp file elsewhere and so need
    443        # special handling.
    444        if is_module_header(enclosing_inclname, self.inclname):
    445            return 0
    446 
    447        if "/" in self.inclname:
    448            if self.inclname.startswith("mozilla/"):
    449                return 1
    450 
    451            if self.inclname.endswith("-inl.h"):
    452                return 5
    453 
    454            return 4
    455 
    456        return 3
    457 
    458    def quote(self):
    459        if self.is_system:
    460            return "<" + self.inclname + ">"
    461        else:
    462            return '"' + self.inclname + '"'
    463 
    464    def sort_key(self, enclosing_inclname):
    465        return (self.section(enclosing_inclname), self.inclname.lower())
    466 
    467    def to_source(self):
    468        return self.include_prefix + self.quote() + self.line_suffix + "\n"
    469 
    470 
    471 class CppBlock:
    472    """C preprocessor block: a whole file or a single #if/#elif/#else block.
    473 
    474    A #if/#endif block is the contents of a #if/#endif (or similar) section.
    475    The top-level block, which is not within a #if/#endif pair, is also
    476    considered a block.
    477 
    478    Each kid is either an Include (representing a #include), OrdinaryCode, or
    479    a nested CppBlock."""
    480 
    481    def __init__(self, start_line=""):
    482        self.start = start_line
    483        self.end = ""
    484        self.kids = []
    485 
    486    def is_style_relevant(self):
    487        return True
    488 
    489    def append_ordinary_line(self, line):
    490        if len(self.kids) == 0 or not isinstance(self.kids[-1], OrdinaryCode):
    491            self.kids.append(OrdinaryCode())
    492        self.kids[-1].lines.append(line)
    493 
    494    def style_relevant_kids(self):
    495        """Return a list of kids in this block that are style-relevant."""
    496        return [kid for kid in self.kids if kid.is_style_relevant()]
    497 
    498    def sorted(self, enclosing_inclname):
    499        """Return a hopefully-sorted copy of this block. Implements --fixup.
    500 
    501        When in doubt, this leaves the code unchanged.
    502        """
    503 
    504        def pretty_sorted_includes(includes):
    505            """Return a new list containing the given includes, in order,
    506            with blank lines separating sections."""
    507            keys = [inc.sort_key(enclosing_inclname) for inc in includes]
    508            if sorted(keys) == keys:
    509                return includes  # if nothing is out of order, don't touch anything
    510 
    511            output = []
    512            current_section = None
    513            for (section, _), inc in sorted(zip(keys, includes)):
    514                if current_section is not None and section != current_section:
    515                    output.append(OrdinaryCode(["\n"]))  # blank line
    516                output.append(inc)
    517                current_section = section
    518            return output
    519 
    520        def should_try_to_sort(includes):
    521            if "tests/style/BadIncludes" in enclosing_inclname:
    522                return False  # don't straighten the counterexample
    523            if any(inc.inclname in oddly_ordered_inclnames for inc in includes):
    524                return False  # don't sort batches containing odd includes
    525            if includes == sorted(
    526                includes, key=lambda inc: inc.sort_key(enclosing_inclname)
    527            ):
    528                return False  # it's already sorted, avoid whitespace-only fixups
    529            return True
    530 
    531        # The content of the eventual output of this method.
    532        output = []
    533 
    534        # The current batch of includes to sort. This list only ever contains Include objects
    535        # and whitespace OrdinaryCode objects.
    536        batch = []
    537 
    538        def flush_batch():
    539            """Sort the contents of `batch` and move it to `output`."""
    540 
    541            assert all(
    542                isinstance(item, Include)
    543                or (isinstance(item, OrdinaryCode) and "".join(item.lines).isspace())
    544                for item in batch
    545            )
    546 
    547            # Here we throw away the blank lines.
    548            # `pretty_sorted_includes` puts them back.
    549            includes = []
    550            last_include_index = -1
    551            for i, item in enumerate(batch):
    552                if isinstance(item, Include):
    553                    includes.append(item)
    554                    last_include_index = i
    555            cutoff = last_include_index + 1
    556 
    557            if should_try_to_sort(includes):
    558                output.extend(pretty_sorted_includes(includes) + batch[cutoff:])
    559            else:
    560                output.extend(batch)
    561            del batch[:]
    562 
    563        for kid in self.kids:
    564            if isinstance(kid, CppBlock):
    565                flush_batch()
    566                output.append(kid.sorted(enclosing_inclname))
    567            elif isinstance(kid, Include):
    568                batch.append(kid)
    569            else:
    570                assert isinstance(kid, OrdinaryCode)
    571                if kid.to_source().isspace():
    572                    batch.append(kid)
    573                else:
    574                    flush_batch()
    575                    output.append(kid)
    576        flush_batch()
    577 
    578        result = CppBlock()
    579        result.start = self.start
    580        result.end = self.end
    581        result.kids = output
    582        return result
    583 
    584    def to_source(self):
    585        return self.start + "".join(kid.to_source() for kid in self.kids) + self.end
    586 
    587 
    588 class OrdinaryCode:
    589    """A list of lines of code that aren't #include/#if/#else/#endif lines."""
    590 
    591    def __init__(self, lines=None):
    592        self.lines = lines if lines is not None else []
    593 
    594    def is_style_relevant(self):
    595        return False
    596 
    597    def to_source(self):
    598        return "".join(self.lines)
    599 
    600 
    601 # A "snippet" is one of:
    602 #
    603 # *   Include - representing an #include line
    604 # *   CppBlock - a whole file or #if/#elif/#else block; contains a list of snippets
    605 # *   OrdinaryCode - representing lines of non-#include-relevant code
    606 
    607 
    608 def read_file(f):
    609    block_stack = [CppBlock()]
    610 
    611    # Extract the #include statements as a tree of snippets.
    612    for linenum, line in enumerate(f, start=1):
    613        if line.lstrip().startswith("#"):
    614            # Look for a |#include "..."| line.
    615            m = re.match(r'(\s*#\s*include\s+)"([^"]*)"(.*)', line)
    616            if m is not None:
    617                prefix, inclname, suffix = m.groups()
    618                block_stack[-1].kids.append(
    619                    Include(prefix, inclname, suffix, linenum, is_system=False)
    620                )
    621                continue
    622 
    623            # Look for a |#include <...>| line.
    624            m = re.match(r"(\s*#\s*include\s+)<([^>]*)>(.*)", line)
    625            if m is not None:
    626                prefix, inclname, suffix = m.groups()
    627                block_stack[-1].kids.append(
    628                    Include(prefix, inclname, suffix, linenum, is_system=True)
    629                )
    630                continue
    631 
    632            # Look for a |#{if,ifdef,ifndef}| line.
    633            m = re.match(r"\s*#\s*(if|ifdef|ifndef)\b", line)
    634            if m is not None:
    635                # Open a new block.
    636                new_block = CppBlock(line)
    637                block_stack[-1].kids.append(new_block)
    638                block_stack.append(new_block)
    639                continue
    640 
    641            # Look for a |#{elif,else}| line.
    642            m = re.match(r"\s*#\s*(elif|else)\b", line)
    643            if m is not None:
    644                # Close the current block, and open an adjacent one.
    645                block_stack.pop()
    646                new_block = CppBlock(line)
    647                block_stack[-1].kids.append(new_block)
    648                block_stack.append(new_block)
    649                continue
    650 
    651            # Look for a |#endif| line.
    652            m = re.match(r"\s*#\s*endif\b", line)
    653            if m is not None:
    654                # Close the current block.
    655                block_stack.pop().end = line
    656                if len(block_stack) == 0:
    657                    raise ValueError("#endif without #if at line " + str(linenum))
    658                continue
    659 
    660        # Otherwise, we have an ordinary line.
    661        block_stack[-1].append_ordinary_line(line)
    662 
    663    if len(block_stack) > 1:
    664        raise ValueError("unmatched #if")
    665    return block_stack[-1]
    666 
    667 
    668 def check_file(
    669    filename, inclname, file_kind, code, all_inclnames, included_h_inclnames
    670 ):
    671    def check_include_statement(include):
    672        """Check the style of a single #include statement."""
    673 
    674        if include.is_system:
    675            # Check it is not a known local file (in which case it's probably a system header).
    676            if (
    677                include.inclname in included_inclnames_to_ignore
    678                or include.inclname in all_inclnames
    679            ):
    680                error(
    681                    filename,
    682                    include.linenum,
    683                    include.quote() + " should be included using",
    684                    'the #include "..." form',
    685                )
    686 
    687            # Check for system header which shouldn't be included directly.
    688            if (
    689                include.inclname in wrapper_system_inclnames
    690                and wrapper_system_inclnames[include.inclname] != inclname
    691            ):
    692                wrapper_inclname = wrapper_system_inclnames[include.inclname]
    693                error(
    694                    filename,
    695                    include.linenum,
    696                    f"{include.quote()} should not be included directly, "
    697                    f'instead use "{wrapper_inclname}"',
    698                )
    699        else:
    700            if file_kind in {FileKind.H, FileKind.INL_H}:
    701                msg = deprecated_inclnames_in_header.get(include.inclname)
    702                if msg and filename not in deprecated_inclnames_in_header_excludes:
    703                    error(
    704                        filename,
    705                        include.linenum,
    706                        include.quote() + " is deprecated: " + msg,
    707                    )
    708 
    709            if include.inclname not in included_inclnames_to_ignore:
    710                included_kind = FileKind.get(include.inclname)
    711 
    712                # Check the #include path has the correct form.
    713                if include.inclname not in all_inclnames:
    714                    error(
    715                        filename,
    716                        include.linenum,
    717                        include.quote() + " is included using the wrong path;",
    718                        "did you forget a prefix, or is the file not yet committed?",
    719                    )
    720 
    721                # Record inclusions of .h files for cycle detection later.
    722                # (Exclude .tbl and .msg files.)
    723                elif included_kind in {FileKind.H, FileKind.INL_H}:
    724                    included_h_inclnames.add(include.inclname)
    725 
    726                # Check a H file doesn't #include an INL_H file.
    727                if file_kind == FileKind.H and included_kind == FileKind.INL_H:
    728                    error(
    729                        filename,
    730                        include.linenum,
    731                        "vanilla header includes an inline-header file "
    732                        + include.quote(),
    733                    )
    734 
    735                # Check a file doesn't #include itself.  (We do this here because the cycle
    736                # detection below doesn't detect this case.)
    737                if inclname == include.inclname:
    738                    error(filename, include.linenum, "the file includes itself")
    739 
    740    def check_includes_order(include1, include2):
    741        """Check the ordering of two #include statements."""
    742 
    743        if (
    744            include1.inclname in oddly_ordered_inclnames
    745            or include2.inclname in oddly_ordered_inclnames
    746        ):
    747            return
    748 
    749        section1 = include1.section(inclname)
    750        section2 = include2.section(inclname)
    751        if (section1 > section2) or (
    752            (section1 == section2)
    753            and (include1.inclname.lower() > include2.inclname.lower())
    754        ):
    755            error(
    756                filename,
    757                str(include1.linenum) + ":" + str(include2.linenum),
    758                include1.quote() + " should be included after " + include2.quote(),
    759            )
    760 
    761    # Check the extracted #include statements, both individually, and the ordering of
    762    # adjacent pairs that live in the same block.
    763    def pair_traverse(prev, this):
    764        if isinstance(this, Include):
    765            check_include_statement(this)
    766            if isinstance(prev, Include):
    767                check_includes_order(prev, this)
    768        else:
    769            kids = this.style_relevant_kids()
    770            for prev2, this2 in zip([None] + kids[0:-1], kids):
    771                pair_traverse(prev2, this2)
    772 
    773    pair_traverse(None, code)
    774 
    775 
    776 def find_cycles(all_inclnames, edges):
    777    """Find and draw any cycles."""
    778 
    779    SCCs = tarjan(all_inclnames, edges)
    780 
    781    # The various sorted() calls below ensure the output is deterministic.
    782 
    783    def draw_SCC(c):
    784        cset = set(c)
    785        drawn = set()
    786 
    787        def draw(v, indent):
    788            out("   " * indent + ("-> " if indent else "   ") + v)
    789            if v in drawn:
    790                return
    791            drawn.add(v)
    792            for succ in sorted(edges[v]):
    793                if succ in cset:
    794                    draw(succ, indent + 1)
    795 
    796        draw(sorted(c)[0], 0)
    797        out("")
    798 
    799    have_drawn_an_SCC = False
    800    for scc in sorted(SCCs):
    801        if len(scc) != 1:
    802            if not have_drawn_an_SCC:
    803                error("(multiple files)", None, "header files form one or more cycles")
    804                have_drawn_an_SCC = True
    805 
    806            draw_SCC(scc)
    807 
    808 
    809 # Tarjan's algorithm for finding the strongly connected components (SCCs) of a graph.
    810 # https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
    811 def tarjan(V, E):
    812    vertex_index = {}
    813    vertex_lowlink = {}
    814    index = 0
    815    S = []
    816    all_SCCs = []
    817 
    818    def strongconnect(v, index):
    819        # Set the depth index for v to the smallest unused index
    820        vertex_index[v] = index
    821        vertex_lowlink[v] = index
    822        index += 1
    823        S.append(v)
    824 
    825        # Consider successors of v
    826        for w in E[v]:
    827            if w not in vertex_index:
    828                # Successor w has not yet been visited; recurse on it
    829                index = strongconnect(w, index)
    830                vertex_lowlink[v] = min(vertex_lowlink[v], vertex_lowlink[w])
    831            elif w in S:
    832                # Successor w is in stack S and hence in the current SCC
    833                vertex_lowlink[v] = min(vertex_lowlink[v], vertex_index[w])
    834 
    835        # If v is a root node, pop the stack and generate an SCC
    836        if vertex_lowlink[v] == vertex_index[v]:
    837            i = S.index(v)
    838            scc = S[i:]
    839            del S[i:]
    840            all_SCCs.append(scc)
    841 
    842        return index
    843 
    844    for v in V:
    845        if v not in vertex_index:
    846            index = strongconnect(v, index)
    847 
    848    return all_SCCs
    849 
    850 
    851 def main():
    852    if sys.argv[1:] == ["--fixup"]:
    853        # Sort #include directives in-place.  Fixup mode doesn't solve
    854        # all possible silliness that the script checks for; it's just a
    855        # hack for the common case where renaming a header causes style
    856        # errors.
    857        fixup = True
    858    elif sys.argv[1:] == []:
    859        fixup = False
    860    else:
    861        print(
    862            "TEST-UNEXPECTED-FAIL | check_spidermonkey_style.py | unexpected command "
    863            "line options: " + repr(sys.argv[1:])
    864        )
    865        sys.exit(1)
    866 
    867    ok = check_style(fixup)
    868 
    869    if ok:
    870        print("TEST-PASS | check_spidermonkey_style.py | ok")
    871    else:
    872        print(
    873            "TEST-UNEXPECTED-FAIL | check_spidermonkey_style.py | "
    874            + "actual output does not match expected output;  diff is above."
    875        )
    876        print(
    877            "TEST-UNEXPECTED-FAIL | check_spidermonkey_style.py | "
    878            + "Hint: If the problem is that you renamed a header, and many #includes "
    879            + "are no longer in alphabetical order, commit your work and then try "
    880            + "`check_spidermonkey_style.py --fixup`. "
    881            + "You need to commit first because --fixup modifies your files in place."
    882        )
    883 
    884    sys.exit(0 if ok else 1)
    885 
    886 
    887 if __name__ == "__main__":
    888    main()