config/check_spidermonkey_style.py

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/config/check_spidermonkey_style.py	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,588 @@
     1.4 +# vim: set ts=8 sts=4 et sw=4 tw=99:
     1.5 +# This Source Code Form is subject to the terms of the Mozilla Public
     1.6 +# License, v. 2.0. If a copy of the MPL was not distributed with this
     1.7 +# file, You can obtain one at http://mozilla.org/MPL/2.0/.
     1.8 +
     1.9 +#----------------------------------------------------------------------------
    1.10 +# This script checks various aspects of SpiderMonkey code style.  The current checks are as
    1.11 +# follows.
    1.12 +#
    1.13 +# We check the following things in headers.
    1.14 +#
    1.15 +# - No cyclic dependencies.
    1.16 +#
    1.17 +# - No normal header should #include a inlines.h/-inl.h file.
    1.18 +#
    1.19 +# - #ifndef wrappers should have the right form. (XXX: not yet implemented)
    1.20 +#   - Every header file should have one.
    1.21 +#   - The guard name used should be appropriate for the filename.
    1.22 +#
    1.23 +# We check the following things in all files.
    1.24 +#
    1.25 +# - #includes should have full paths, e.g. "jit/Ion.h", not "Ion.h".
    1.26 +#
    1.27 +# - #includes should use the appropriate form for system headers (<...>) and
    1.28 +#   local headers ("...").
    1.29 +#
    1.30 +# - #includes should be ordered correctly.
    1.31 +#   - Each one should be in the correct section.
    1.32 +#   - Alphabetical order should be used within sections.
    1.33 +#   - Sections should be in the right order.
    1.34 +#   Note that the presence of #if/#endif blocks complicates things, to the
    1.35 +#   point that it's not always clear where a conditionally-compiled #include
    1.36 +#   statement should go, even to a human.  Therefore, we check the #include
    1.37 +#   statements within each #if/#endif block (including nested ones) in
    1.38 +#   isolation, but don't try to do any order checking between such blocks.
    1.39 +#----------------------------------------------------------------------------
    1.40 +
    1.41 +from __future__ import print_function
    1.42 +
    1.43 +import difflib
    1.44 +import os
    1.45 +import re
    1.46 +import subprocess
    1.47 +import sys
    1.48 +import traceback
    1.49 +
    1.50 +# We don't bother checking files in these directories, because they're (a) auxiliary or (b)
    1.51 +# imported code that doesn't follow our coding style.
    1.52 +ignored_js_src_dirs = [
    1.53 +   'js/src/config/',            # auxiliary stuff
    1.54 +   'js/src/ctypes/libffi/',     # imported code
    1.55 +   'js/src/devtools/',          # auxiliary stuff
    1.56 +   'js/src/editline/',          # imported code
    1.57 +   'js/src/gdb/',               # auxiliary stuff
    1.58 +   'js/src/vtune/'              # imported code
    1.59 +]
    1.60 +
    1.61 +# We ignore #includes of these files, because they don't follow the usual rules.
    1.62 +included_inclnames_to_ignore = set([
    1.63 +    'ffi.h',                    # generated in ctypes/libffi/
    1.64 +    'devtools/sharkctl.h',      # we ignore devtools/ in general
    1.65 +    'devtools/Instruments.h',   # we ignore devtools/ in general
    1.66 +    'double-conversion.h',      # strange MFBT case
    1.67 +    'javascript-trace.h',       # generated in $OBJDIR if HAVE_DTRACE is defined
    1.68 +    'jsautokw.h',               # generated in $OBJDIR
    1.69 +    'jscustomallocator.h',      # provided by embedders;  allowed to be missing
    1.70 +    'js-config.h',              # generated in $OBJDIR
    1.71 +    'pratom.h',                 # NSPR
    1.72 +    'prcvar.h',                 # NSPR
    1.73 +    'prinit.h',                 # NSPR
    1.74 +    'prlink.h',                 # NSPR
    1.75 +    'prlock.h',                 # NSPR
    1.76 +    'prprf.h',                  # NSPR
    1.77 +    'prthread.h',               # NSPR
    1.78 +    'prtypes.h',                # NSPR
    1.79 +    'selfhosted.out.h',         # generated in $OBJDIR
    1.80 +    'unicode/locid.h',          # ICU
    1.81 +    'unicode/numsys.h',         # ICU
    1.82 +    'unicode/ucal.h',           # ICU
    1.83 +    'unicode/uclean.h',         # ICU
    1.84 +    'unicode/ucol.h',           # ICU
    1.85 +    'unicode/udat.h',           # ICU
    1.86 +    'unicode/udatpg.h',         # ICU
    1.87 +    'unicode/uenum.h',          # ICU
    1.88 +    'unicode/unorm.h',          # ICU
    1.89 +    'unicode/unum.h',           # ICU
    1.90 +    'unicode/ustring.h',        # ICU
    1.91 +    'unicode/utypes.h',         # ICU
    1.92 +    'vtune/VTuneWrapper.h'      # VTune
    1.93 +])
    1.94 +
    1.95 +# These files have additional constraints on where they are #included, so we
    1.96 +# ignore #includes of them when checking #include ordering.
    1.97 +oddly_ordered_inclnames = set([
    1.98 +    'ctypes/typedefs.h',        # Included multiple times in the body of ctypes/CTypes.h
    1.99 +    'jsautokw.h',               # Included in the body of frontend/TokenStream.h
   1.100 +    'jswin.h',                  # Must be #included before <psapi.h>
   1.101 +    'machine/endian.h',         # Must be included after <sys/types.h> on BSD
   1.102 +    'winbase.h',                # Must precede other system headers(?)
   1.103 +    'windef.h'                  # Must precede other system headers(?)
   1.104 +])
   1.105 +
   1.106 +# The files in tests/style/ contain code that fails this checking in various
   1.107 +# ways.  Here is the output we expect.  If the actual output differs from
   1.108 +# this, one of the following must have happened.
   1.109 +# - New SpiderMonkey code violates one of the checked rules.
   1.110 +# - The tests/style/ files have changed without expected_output being changed
   1.111 +#   accordingly.
   1.112 +# - This script has been broken somehow.
   1.113 +#
   1.114 +expected_output = '''\
   1.115 +js/src/tests/style/BadIncludes2.h:1: error:
   1.116 +    vanilla header includes an inline-header file "tests/style/BadIncludes2-inl.h"
   1.117 +
   1.118 +js/src/tests/style/BadIncludes.h:3: error:
   1.119 +    the file includes itself
   1.120 +
   1.121 +js/src/tests/style/BadIncludes.h:6: error:
   1.122 +    "BadIncludes2.h" is included using the wrong path;
   1.123 +    did you forget a prefix, or is the file not yet committed?
   1.124 +
   1.125 +js/src/tests/style/BadIncludes.h:8: error:
   1.126 +    <tests/style/BadIncludes2.h> should be included using
   1.127 +    the #include "..." form
   1.128 +
   1.129 +js/src/tests/style/BadIncludes.h:10: error:
   1.130 +    "stdio.h" is included using the wrong path;
   1.131 +    did you forget a prefix, or is the file not yet committed?
   1.132 +
   1.133 +js/src/tests/style/BadIncludesOrder-inl.h:5:6: error:
   1.134 +    "vm/Interpreter-inl.h" should be included after "jsscriptinlines.h"
   1.135 +
   1.136 +js/src/tests/style/BadIncludesOrder-inl.h:6:7: error:
   1.137 +    "jsscriptinlines.h" should be included after "js/Value.h"
   1.138 +
   1.139 +js/src/tests/style/BadIncludesOrder-inl.h:7:8: error:
   1.140 +    "js/Value.h" should be included after "ds/LifoAlloc.h"
   1.141 +
   1.142 +js/src/tests/style/BadIncludesOrder-inl.h:8:9: error:
   1.143 +    "ds/LifoAlloc.h" should be included after "jsapi.h"
   1.144 +
   1.145 +js/src/tests/style/BadIncludesOrder-inl.h:9:10: error:
   1.146 +    "jsapi.h" should be included after <stdio.h>
   1.147 +
   1.148 +js/src/tests/style/BadIncludesOrder-inl.h:10:11: error:
   1.149 +    <stdio.h> should be included after "mozilla/HashFunctions.h"
   1.150 +
   1.151 +js/src/tests/style/BadIncludesOrder-inl.h:27:28: error:
   1.152 +    "jsobj.h" should be included after "jsfun.h"
   1.153 +
   1.154 +(multiple files): error:
   1.155 +    header files form one or more cycles
   1.156 +
   1.157 +   tests/style/HeaderCycleA1.h
   1.158 +   -> tests/style/HeaderCycleA2.h
   1.159 +      -> tests/style/HeaderCycleA3.h
   1.160 +         -> tests/style/HeaderCycleA1.h
   1.161 +
   1.162 +   tests/style/HeaderCycleB1-inl.h
   1.163 +   -> tests/style/HeaderCycleB2-inl.h
   1.164 +      -> tests/style/HeaderCycleB3-inl.h
   1.165 +         -> tests/style/HeaderCycleB4-inl.h
   1.166 +            -> tests/style/HeaderCycleB1-inl.h
   1.167 +            -> tests/style/jsheadercycleB5inlines.h
   1.168 +               -> tests/style/HeaderCycleB1-inl.h
   1.169 +      -> tests/style/HeaderCycleB4-inl.h
   1.170 +
   1.171 +'''.splitlines(True)
   1.172 +
   1.173 +actual_output = []
   1.174 +
   1.175 +
   1.176 +def out(*lines):
   1.177 +    for line in lines:
   1.178 +        actual_output.append(line + '\n')
   1.179 +
   1.180 +
   1.181 +def error(filename, linenum, *lines):
   1.182 +    location = filename
   1.183 +    if linenum is not None:
   1.184 +        location += ':' + str(linenum)
   1.185 +    out(location + ': error:')
   1.186 +    for line in (lines):
   1.187 +        out('    ' + line)
   1.188 +    out('')
   1.189 +
   1.190 +
   1.191 +class FileKind(object):
   1.192 +    C = 1
   1.193 +    CPP = 2
   1.194 +    INL_H = 3
   1.195 +    H = 4
   1.196 +    TBL = 5
   1.197 +    MSG = 6
   1.198 +
   1.199 +    @staticmethod
   1.200 +    def get(filename):
   1.201 +        if filename.endswith('.c'):
   1.202 +            return FileKind.C
   1.203 +
   1.204 +        if filename.endswith('.cpp'):
   1.205 +            return FileKind.CPP
   1.206 +
   1.207 +        if filename.endswith(('inlines.h', '-inl.h')):
   1.208 +            return FileKind.INL_H
   1.209 +
   1.210 +        if filename.endswith('.h'):
   1.211 +            return FileKind.H
   1.212 +
   1.213 +        if filename.endswith('.tbl'):
   1.214 +            return FileKind.TBL
   1.215 +
   1.216 +        if filename.endswith('.msg'):
   1.217 +            return FileKind.MSG
   1.218 +
   1.219 +        error(filename, None, 'unknown file kind')
   1.220 +
   1.221 +
   1.222 +def get_all_filenames():
   1.223 +    '''Get a list of all the files in the (Mercurial or Git) repository.'''
   1.224 +    cmds = [['hg', 'manifest', '-q'], ['git', 'ls-files', '--full-name', '../..']]
   1.225 +    for cmd in cmds:
   1.226 +        try:
   1.227 +            all_filenames = subprocess.check_output(cmd, universal_newlines=True,
   1.228 +                                                    stderr=subprocess.PIPE).split('\n')
   1.229 +            return all_filenames
   1.230 +        except:
   1.231 +            continue
   1.232 +    else:
   1.233 +        raise Exception('failed to run any of the repo manifest commands', cmds)
   1.234 +
   1.235 +
   1.236 +def check_style():
   1.237 +    # We deal with two kinds of name.
   1.238 +    # - A "filename" is a full path to a file from the repository root.
   1.239 +    # - An "inclname" is how a file is referred to in a #include statement.
   1.240 +    #
   1.241 +    # Examples (filename -> inclname)
   1.242 +    # - "mfbt/Attributes.h"  -> "mozilla/Attributes.h"
   1.243 +    # - "js/public/Vector.h" -> "js/Vector.h"
   1.244 +    # - "js/src/vm/String.h" -> "vm/String.h"
   1.245 +
   1.246 +    mfbt_inclnames = set()      # type: set(inclname)
   1.247 +    js_names = dict()           # type: dict(filename, inclname)
   1.248 +
   1.249 +    # Select the appropriate files.
   1.250 +    for filename in get_all_filenames():
   1.251 +        if filename.startswith('mfbt/') and filename.endswith('.h'):
   1.252 +            inclname = 'mozilla/' + filename[len('mfbt/'):]
   1.253 +            mfbt_inclnames.add(inclname)
   1.254 +
   1.255 +        if filename.startswith('js/public/') and filename.endswith('.h'):
   1.256 +            inclname = 'js/' + filename[len('js/public/'):]
   1.257 +            js_names[filename] = inclname
   1.258 +
   1.259 +        if filename.startswith('js/src/') and \
   1.260 +           not filename.startswith(tuple(ignored_js_src_dirs)) and \
   1.261 +           filename.endswith(('.c', '.cpp', '.h', '.tbl', '.msg')):
   1.262 +            inclname = filename[len('js/src/'):]
   1.263 +            js_names[filename] = inclname
   1.264 +
   1.265 +    all_inclnames = mfbt_inclnames | set(js_names.values())
   1.266 +
   1.267 +    edges = dict()      # type: dict(inclname, set(inclname))
   1.268 +
   1.269 +    # We don't care what's inside the MFBT files, but because they are
   1.270 +    # #included from JS files we have to add them to the inclusion graph.
   1.271 +    for inclname in mfbt_inclnames:
   1.272 +        edges[inclname] = set()
   1.273 +
   1.274 +    # Process all the JS files.
   1.275 +    for filename in js_names.keys():
   1.276 +        inclname = js_names[filename]
   1.277 +        file_kind = FileKind.get(filename)
   1.278 +        if file_kind == FileKind.C or file_kind == FileKind.CPP or \
   1.279 +           file_kind == FileKind.H or file_kind == FileKind.INL_H:
   1.280 +            included_h_inclnames = set()    # type: set(inclname)
   1.281 +
   1.282 +            # This script is run in js/src/, so prepend '../../' to get to the root of the Mozilla
   1.283 +            # source tree.
   1.284 +            with open(os.path.join('../..', filename)) as f:
   1.285 +                do_file(filename, inclname, file_kind, f, all_inclnames, included_h_inclnames)
   1.286 +
   1.287 +        edges[inclname] = included_h_inclnames
   1.288 +
   1.289 +    find_cycles(all_inclnames, edges)
   1.290 +
   1.291 +    # Compare expected and actual output.
   1.292 +    difflines = difflib.unified_diff(expected_output, actual_output,
   1.293 +                                     fromfile='check_spider_monkey_style.py expected output',
   1.294 +                                       tofile='check_spider_monkey_style.py actual output')
   1.295 +    ok = True
   1.296 +    for diffline in difflines:
   1.297 +        ok = False
   1.298 +        print(diffline, end='')
   1.299 +
   1.300 +    return ok
   1.301 +
   1.302 +
   1.303 +def module_name(name):
   1.304 +    '''Strip the trailing .cpp, .h, inlines.h or -inl.h from a filename.'''
   1.305 +
   1.306 +    return name.replace('inlines.h', '').replace('-inl.h', '').replace('.h', '').replace('.cpp', '')
   1.307 +
   1.308 +
   1.309 +def is_module_header(enclosing_inclname, header_inclname):
   1.310 +    '''Determine if an included name is the "module header", i.e. should be
   1.311 +    first in the file.'''
   1.312 +
   1.313 +    module = module_name(enclosing_inclname)
   1.314 +
   1.315 +    # Normal case, e.g. module == "foo/Bar", header_inclname == "foo/Bar.h".
   1.316 +    if module == module_name(header_inclname):
   1.317 +        return True
   1.318 +
   1.319 +    # A public header, e.g. module == "foo/Bar", header_inclname == "js/Bar.h".
   1.320 +    m = re.match(r'js\/(.*)\.h', header_inclname)
   1.321 +    if m is not None and module.endswith('/' + m.group(1)):
   1.322 +        return True
   1.323 +
   1.324 +    return False
   1.325 +
   1.326 +
   1.327 +class Include(object):
   1.328 +    '''Important information for a single #include statement.'''
   1.329 +
   1.330 +    def __init__(self, inclname, linenum, is_system):
   1.331 +        self.inclname = inclname
   1.332 +        self.linenum = linenum
   1.333 +        self.is_system = is_system
   1.334 +
   1.335 +    def isLeaf(self):
   1.336 +        return True
   1.337 +
   1.338 +    def section(self, enclosing_inclname):
   1.339 +        '''Identify which section inclname belongs to.
   1.340 +
   1.341 +        The section numbers are as follows.
   1.342 +          0. Module header (e.g. jsfoo.h or jsfooinlines.h within jsfoo.cpp)
   1.343 +          1. mozilla/Foo.h
   1.344 +          2. <foo.h> or <foo>
   1.345 +          3. jsfoo.h, prmjtime.h, etc
   1.346 +          4. foo/Bar.h
   1.347 +          5. jsfooinlines.h
   1.348 +          6. foo/Bar-inl.h
   1.349 +          7. non-.h, e.g. *.tbl, *.msg
   1.350 +        '''
   1.351 +
   1.352 +        if self.is_system:
   1.353 +            return 2
   1.354 +
   1.355 +        if not self.inclname.endswith('.h'):
   1.356 +            return 7
   1.357 +
   1.358 +        # A couple of modules have the .h file in js/ and the .cpp file elsewhere and so need
   1.359 +        # special handling.
   1.360 +        if is_module_header(enclosing_inclname, self.inclname):
   1.361 +            return 0
   1.362 +
   1.363 +        if '/' in self.inclname:
   1.364 +            if self.inclname.startswith('mozilla/'):
   1.365 +                return 1
   1.366 +
   1.367 +            if self.inclname.endswith('-inl.h'):
   1.368 +                return 6
   1.369 +
   1.370 +            return 4
   1.371 +
   1.372 +        if self.inclname.endswith('inlines.h'):
   1.373 +            return 5
   1.374 +
   1.375 +        return 3
   1.376 +
   1.377 +    def quote(self):
   1.378 +        if self.is_system:
   1.379 +            return '<' + self.inclname + '>'
   1.380 +        else:
   1.381 +            return '"' + self.inclname + '"'
   1.382 +
   1.383 +
   1.384 +class HashIfBlock(object):
   1.385 +    '''Important information about a #if/#endif block.
   1.386 +
   1.387 +    A #if/#endif block is the contents of a #if/#endif (or similar) section.
   1.388 +    The top-level block, which is not within a #if/#endif pair, is also
   1.389 +    considered a block.
   1.390 +
   1.391 +    Each leaf is either an Include (representing a #include), or another
   1.392 +    nested HashIfBlock.'''
   1.393 +    def __init__(self):
   1.394 +        self.kids = []
   1.395 +
   1.396 +    def isLeaf(self):
   1.397 +        return False
   1.398 +
   1.399 +
   1.400 +def do_file(filename, inclname, file_kind, f, all_inclnames, included_h_inclnames):
   1.401 +    block_stack = [HashIfBlock()]
   1.402 +
   1.403 +    # Extract the #include statements as a tree of IBlocks and IIncludes.
   1.404 +    for linenum, line in enumerate(f, start=1):
   1.405 +        # Look for a |#include "..."| line.
   1.406 +        m = re.match(r'\s*#\s*include\s+"([^"]*)"', line)
   1.407 +        if m is not None:
   1.408 +            block_stack[-1].kids.append(Include(m.group(1), linenum, False))
   1.409 +
   1.410 +        # Look for a |#include <...>| line.
   1.411 +        m = re.match(r'\s*#\s*include\s+<([^>]*)>', line)
   1.412 +        if m is not None:
   1.413 +            block_stack[-1].kids.append(Include(m.group(1), linenum, True))
   1.414 +
   1.415 +        # Look for a |#{if,ifdef,ifndef}| line.
   1.416 +        m = re.match(r'\s*#\s*(if|ifdef|ifndef)\b', line)
   1.417 +        if m is not None:
   1.418 +            # Open a new block.
   1.419 +            new_block = HashIfBlock()
   1.420 +            block_stack[-1].kids.append(new_block)
   1.421 +            block_stack.append(new_block)
   1.422 +
   1.423 +        # Look for a |#{elif,else}| line.
   1.424 +        m = re.match(r'\s*#\s*(elif|else)\b', line)
   1.425 +        if m is not None:
   1.426 +            # Close the current block, and open an adjacent one.
   1.427 +            block_stack.pop()
   1.428 +            new_block = HashIfBlock()
   1.429 +            block_stack[-1].kids.append(new_block)
   1.430 +            block_stack.append(new_block)
   1.431 +
   1.432 +        # Look for a |#endif| line.
   1.433 +        m = re.match(r'\s*#\s*endif\b', line)
   1.434 +        if m is not None:
   1.435 +            # Close the current block.
   1.436 +            block_stack.pop()
   1.437 +
   1.438 +    def check_include_statement(include):
   1.439 +        '''Check the style of a single #include statement.'''
   1.440 +
   1.441 +        if include.is_system:
   1.442 +            # Check it is not a known local file (in which case it's probably a system header).
   1.443 +            if include.inclname in included_inclnames_to_ignore or \
   1.444 +               include.inclname in all_inclnames:
   1.445 +                error(filename, include.linenum,
   1.446 +                      include.quote() + ' should be included using',
   1.447 +                      'the #include "..." form')
   1.448 +
   1.449 +        else:
   1.450 +            if include.inclname not in included_inclnames_to_ignore:
   1.451 +                included_kind = FileKind.get(include.inclname)
   1.452 +
   1.453 +                # Check the #include path has the correct form.
   1.454 +                if include.inclname not in all_inclnames:
   1.455 +                    error(filename, include.linenum,
   1.456 +                          include.quote() + ' is included ' + 'using the wrong path;',
   1.457 +                          'did you forget a prefix, or is the file not yet committed?')
   1.458 +
   1.459 +                # Record inclusions of .h files for cycle detection later.
   1.460 +                # (Exclude .tbl and .msg files.)
   1.461 +                elif included_kind == FileKind.H or included_kind == FileKind.INL_H:
   1.462 +                    included_h_inclnames.add(include.inclname)
   1.463 +
   1.464 +                # Check a H file doesn't #include an INL_H file.
   1.465 +                if file_kind == FileKind.H and included_kind == FileKind.INL_H:
   1.466 +                    error(filename, include.linenum,
   1.467 +                          'vanilla header includes an inline-header file ' + include.quote())
   1.468 +
   1.469 +                # Check a file doesn't #include itself.  (We do this here because the cycle
   1.470 +                # detection below doesn't detect this case.)
   1.471 +                if inclname == include.inclname:
   1.472 +                    error(filename, include.linenum, 'the file includes itself')
   1.473 +
   1.474 +    def check_includes_order(include1, include2):
   1.475 +        '''Check the ordering of two #include statements.'''
   1.476 +
   1.477 +        if include1.inclname in oddly_ordered_inclnames or \
   1.478 +           include2.inclname in oddly_ordered_inclnames:
   1.479 +            return
   1.480 +
   1.481 +        section1 = include1.section(inclname)
   1.482 +        section2 = include2.section(inclname)
   1.483 +        if (section1 > section2) or \
   1.484 +           ((section1 == section2) and (include1.inclname.lower() > include2.inclname.lower())):
   1.485 +            error(filename, str(include1.linenum) + ':' + str(include2.linenum),
   1.486 +                  include1.quote() + ' should be included after ' + include2.quote())
   1.487 +
   1.488 +    # The #include statements in the files in assembler/ and yarr/ have all manner of implicit
   1.489 +    # ordering requirements.  Boo.  Ignore them.
   1.490 +    skip_order_checking = inclname.startswith(('assembler/', 'yarr/'))
   1.491 +
   1.492 +    # Check the extracted #include statements, both individually, and the ordering of
   1.493 +    # adjacent pairs that live in the same block.
   1.494 +    def pair_traverse(prev, this):
   1.495 +        if this.isLeaf():
   1.496 +            check_include_statement(this)
   1.497 +            if prev is not None and prev.isLeaf() and not skip_order_checking:
   1.498 +                check_includes_order(prev, this)
   1.499 +        else:
   1.500 +            for prev2, this2 in zip([None] + this.kids[0:-1], this.kids):
   1.501 +                pair_traverse(prev2, this2)
   1.502 +
   1.503 +    pair_traverse(None, block_stack[-1])
   1.504 +
   1.505 +
   1.506 +def find_cycles(all_inclnames, edges):
   1.507 +    '''Find and draw any cycles.'''
   1.508 +
   1.509 +    SCCs = tarjan(all_inclnames, edges)
   1.510 +
   1.511 +    # The various sorted() calls below ensure the output is deterministic.
   1.512 +
   1.513 +    def draw_SCC(c):
   1.514 +        cset = set(c)
   1.515 +        drawn = set()
   1.516 +        def draw(v, indent):
   1.517 +            out('   ' * indent + ('-> ' if indent else '   ') + v)
   1.518 +            if v in drawn:
   1.519 +                return
   1.520 +            drawn.add(v)
   1.521 +            for succ in sorted(edges[v]):
   1.522 +                if succ in cset:
   1.523 +                    draw(succ, indent + 1)
   1.524 +        draw(sorted(c)[0], 0)
   1.525 +        out('')
   1.526 +
   1.527 +    have_drawn_an_SCC = False
   1.528 +    for scc in sorted(SCCs):
   1.529 +        if len(scc) != 1:
   1.530 +            if not have_drawn_an_SCC:
   1.531 +                error('(multiple files)', None, 'header files form one or more cycles')
   1.532 +                have_drawn_an_SCC = True
   1.533 +
   1.534 +            draw_SCC(scc)
   1.535 +
   1.536 +
   1.537 +# Tarjan's algorithm for finding the strongly connected components (SCCs) of a graph.
   1.538 +# https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
   1.539 +def tarjan(V, E):
   1.540 +    vertex_index = {}
   1.541 +    vertex_lowlink = {}
   1.542 +    index = 0
   1.543 +    S = []
   1.544 +    all_SCCs = []
   1.545 +
   1.546 +    def strongconnect(v, index):
   1.547 +        # Set the depth index for v to the smallest unused index
   1.548 +        vertex_index[v] = index
   1.549 +        vertex_lowlink[v] = index
   1.550 +        index += 1
   1.551 +        S.append(v)
   1.552 +
   1.553 +        # Consider successors of v
   1.554 +        for w in E[v]:
   1.555 +            if w not in vertex_index:
   1.556 +                # Successor w has not yet been visited; recurse on it
   1.557 +                index = strongconnect(w, index)
   1.558 +                vertex_lowlink[v] = min(vertex_lowlink[v], vertex_lowlink[w])
   1.559 +            elif w in S:
   1.560 +                # Successor w is in stack S and hence in the current SCC
   1.561 +                vertex_lowlink[v] = min(vertex_lowlink[v], vertex_index[w])
   1.562 +
   1.563 +        # If v is a root node, pop the stack and generate an SCC
   1.564 +        if vertex_lowlink[v] == vertex_index[v]:
   1.565 +            i = S.index(v)
   1.566 +            scc = S[i:]
   1.567 +            del S[i:]
   1.568 +            all_SCCs.append(scc)
   1.569 +
   1.570 +        return index
   1.571 +
   1.572 +    for v in V:
   1.573 +        if v not in vertex_index:
   1.574 +            index = strongconnect(v, index)
   1.575 +
   1.576 +    return all_SCCs
   1.577 +
   1.578 +
   1.579 +def main():
   1.580 +    ok = check_style()
   1.581 +
   1.582 +    if ok:
   1.583 +        print('TEST-PASS | check_spidermonkey_style.py | ok')
   1.584 +    else:
   1.585 +        print('TEST-UNEXPECTED-FAIL | check_spidermonkey_style.py | actual output does not match expected output;  diff is above')
   1.586 +
   1.587 +    sys.exit(0 if ok else 1)
   1.588 +
   1.589 +
   1.590 +if __name__ == '__main__':
   1.591 +    main()

mercurial