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()