Backed out changeset 8c25245410c5 (bug 909888) for being far from NPOTB and breaking...
[gecko.git] / config / check_spidermonkey_style.py
blob737ad1a2f6fdc81fbdf6d9c0cc55d62542af1e0a
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/.
6 #----------------------------------------------------------------------------
7 # This script checks various aspects of SpiderMonkey code style. The current checks are as
8 # follows.
10 # We check the following things in headers.
12 # - No cyclic dependencies.
14 # - No normal header should #include a inlines.h/-inl.h file.
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.
20 # We check the following things in all files.
22 # - #includes should have full paths, e.g. "jit/Ion.h", not "Ion.h".
24 # - #includes should use the appropriate form for system headers (<...>) and
25 # local headers ("...").
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 #----------------------------------------------------------------------------
38 from __future__ import print_function
40 import difflib
41 import os
42 import re
43 import subprocess
44 import sys
45 import traceback
47 # We don't bother checking files in these directories, because they're (a) auxiliary or (b)
48 # imported code that doesn't follow our coding style.
49 ignored_js_src_dirs = [
50 'js/src/config/', # auxiliary stuff
51 'js/src/ctypes/libffi/', # imported code
52 'js/src/devtools/', # auxiliary stuff
53 'js/src/editline/', # imported code
54 'js/src/gdb/', # auxiliary stuff
55 'js/src/vtune/' # imported code
58 # We ignore #includes of these files, because they don't follow the usual rules.
59 included_inclnames_to_ignore = set([
60 'ffi.h', # generated in ctypes/libffi/
61 'devtools/sharkctl.h', # we ignore devtools/ in general
62 'devtools/Instruments.h', # we ignore devtools/ in general
63 'double-conversion.h', # strange MFBT case
64 'javascript-trace.h', # generated in $OBJDIR if HAVE_DTRACE is defined
65 'jsautokw.h', # generated in $OBJDIR
66 'jsautooplen.h', # generated in $OBJDIR
67 'jscustomallocator.h', # provided by embedders; allowed to be missing
68 'js-config.h', # generated in $OBJDIR
69 'pratom.h', # NSPR
70 'prcvar.h', # NSPR
71 'prinit.h', # NSPR
72 'prlink.h', # NSPR
73 'prlock.h', # NSPR
74 'prprf.h', # NSPR
75 'prthread.h', # NSPR
76 'prtypes.h', # NSPR
77 'selfhosted.out.h', # generated in $OBJDIR
78 'unicode/locid.h', # ICU
79 'unicode/numsys.h', # ICU
80 'unicode/ucal.h', # ICU
81 'unicode/uclean.h', # ICU
82 'unicode/ucol.h', # ICU
83 'unicode/udat.h', # ICU
84 'unicode/udatpg.h', # ICU
85 'unicode/uenum.h', # ICU
86 'unicode/unum.h', # ICU
87 'unicode/ustring.h', # ICU
88 'unicode/utypes.h', # ICU
89 'vtune/VTuneWrapper.h' # VTune
92 # These files have additional constraints on where they are #included, so we
93 # ignore #includes of them when checking #include ordering.
94 oddly_ordered_inclnames = set([
95 'ctypes/typedefs.h', # Included multiple times in the body of ctypes/CTypes.h
96 'jsautokw.h', # Included in the body of frontend/TokenStream.h
97 'jswin.h', # Must be #included before <psapi.h>
98 'winbase.h', # Must precede other system headers(?)
99 'windef.h' # Must precede other system headers(?)
102 # The files in tests/style/ contain code that fails this checking in various
103 # ways. Here is the output we expect. If the actual output differs from
104 # this, one of the following must have happened.
105 # - New SpiderMonkey code violates one of the checked rules.
106 # - The tests/style/ files have changed without expected_output being changed
107 # accordingly.
108 # - This script has been broken somehow.
110 expected_output = '''\
111 js/src/tests/style/BadIncludes2.h:1: error:
112 vanilla header includes an inline-header file "tests/style/BadIncludes2-inl.h"
114 js/src/tests/style/BadIncludes.h:3: error:
115 the file includes itself
117 js/src/tests/style/BadIncludes.h:6: error:
118 "BadIncludes2.h" is included using the wrong path;
119 did you forget a prefix, or is the file not yet committed?
121 js/src/tests/style/BadIncludes.h:8: error:
122 <tests/style/BadIncludes2.h> should be included using
123 the #include "..." form
125 js/src/tests/style/BadIncludes.h:10: error:
126 "stdio.h" is included using the wrong path;
127 did you forget a prefix, or is the file not yet committed?
129 js/src/tests/style/BadIncludesOrder-inl.h:5:6: error:
130 "vm/Interpreter-inl.h" should be included after "jsscriptinlines.h"
132 js/src/tests/style/BadIncludesOrder-inl.h:6:7: error:
133 "jsscriptinlines.h" should be included after "js/Value.h"
135 js/src/tests/style/BadIncludesOrder-inl.h:7:8: error:
136 "js/Value.h" should be included after "ds/LifoAlloc.h"
138 js/src/tests/style/BadIncludesOrder-inl.h:8:9: error:
139 "ds/LifoAlloc.h" should be included after "jsapi.h"
141 js/src/tests/style/BadIncludesOrder-inl.h:9:10: error:
142 "jsapi.h" should be included after <stdio.h>
144 js/src/tests/style/BadIncludesOrder-inl.h:10:11: error:
145 <stdio.h> should be included after "mozilla/HashFunctions.h"
147 js/src/tests/style/BadIncludesOrder-inl.h:27:28: error:
148 "jsobj.h" should be included after "jsfun.h"
150 (multiple files): error:
151 header files form one or more cycles
153 tests/style/HeaderCycleA1.h
154 -> tests/style/HeaderCycleA2.h
155 -> tests/style/HeaderCycleA3.h
156 -> tests/style/HeaderCycleA1.h
158 tests/style/HeaderCycleB1-inl.h
159 -> tests/style/HeaderCycleB2-inl.h
160 -> tests/style/HeaderCycleB3-inl.h
161 -> tests/style/HeaderCycleB4-inl.h
162 -> tests/style/HeaderCycleB1-inl.h
163 -> tests/style/jsheadercycleB5inlines.h
164 -> tests/style/HeaderCycleB1-inl.h
165 -> tests/style/HeaderCycleB4-inl.h
167 '''.splitlines(True)
169 actual_output = []
172 def out(*lines):
173 for line in lines:
174 actual_output.append(line + '\n')
177 def error(filename, linenum, *lines):
178 location = filename
179 if linenum is not None:
180 location += ':' + str(linenum)
181 out(location + ': error:')
182 for line in (lines):
183 out(' ' + line)
184 out('')
187 class FileKind(object):
188 C = 1
189 CPP = 2
190 INL_H = 3
191 H = 4
192 TBL = 5
193 MSG = 6
195 @staticmethod
196 def get(filename):
197 if filename.endswith('.c'):
198 return FileKind.C
200 if filename.endswith('.cpp'):
201 return FileKind.CPP
203 if filename.endswith(('inlines.h', '-inl.h', 'Inlines.h')):
204 return FileKind.INL_H
206 if filename.endswith('.h'):
207 return FileKind.H
209 if filename.endswith('.tbl'):
210 return FileKind.TBL
212 if filename.endswith('.msg'):
213 return FileKind.MSG
215 error(filename, None, 'unknown file kind')
218 def get_all_filenames():
219 '''Get a list of all the files in the (Mercurial or Git) repository.'''
220 cmds = [['hg', 'manifest', '-q'], ['git', 'ls-files']]
221 for cmd in cmds:
222 try:
223 all_filenames = subprocess.check_output(cmd, universal_newlines=True,
224 stderr=subprocess.PIPE).split('\n')
225 return all_filenames
226 except:
227 continue
228 else:
229 raise Exception('failed to run any of the repo manifest commands', cmds)
232 def check_style():
233 # We deal with two kinds of name.
234 # - A "filename" is a full path to a file from the repository root.
235 # - An "inclname" is how a file is referred to in a #include statement.
237 # Examples (filename -> inclname)
238 # - "mfbt/Attributes.h" -> "mozilla/Attributes.h"
239 # - "js/public/Vector.h" -> "js/Vector.h"
240 # - "js/src/vm/String.h" -> "vm/String.h"
242 mfbt_inclnames = set() # type: set(inclname)
243 js_names = dict() # type: dict(filename, inclname)
245 # Select the appropriate files.
246 for filename in get_all_filenames():
247 if filename.startswith('mfbt/') and filename.endswith('.h'):
248 inclname = 'mozilla/' + filename[len('mfbt/'):]
249 mfbt_inclnames.add(inclname)
251 if filename.startswith('js/public/') and filename.endswith('.h'):
252 inclname = 'js/' + filename[len('js/public/'):]
253 js_names[filename] = inclname
255 if filename.startswith('js/src/') and \
256 not filename.startswith(tuple(ignored_js_src_dirs)) and \
257 filename.endswith(('.c', '.cpp', '.h', '.tbl', '.msg')):
258 inclname = filename[len('js/src/'):]
259 js_names[filename] = inclname
261 all_inclnames = mfbt_inclnames | set(js_names.values())
263 edges = dict() # type: dict(inclname, set(inclname))
265 # We don't care what's inside the MFBT files, but because they are
266 # #included from JS files we have to add them to the inclusion graph.
267 for inclname in mfbt_inclnames:
268 edges[inclname] = set()
270 # Process all the JS files.
271 for filename in js_names.keys():
272 inclname = js_names[filename]
273 file_kind = FileKind.get(filename)
274 if file_kind == FileKind.C or file_kind == FileKind.CPP or \
275 file_kind == FileKind.H or file_kind == FileKind.INL_H:
276 included_h_inclnames = set() # type: set(inclname)
278 # This script is run in js/src/, so prepend '../../' to get to the root of the Mozilla
279 # source tree.
280 with open(os.path.join('../..', filename)) as f:
281 do_file(filename, inclname, file_kind, f, all_inclnames, included_h_inclnames)
283 edges[inclname] = included_h_inclnames
285 find_cycles(all_inclnames, edges)
287 # Compare expected and actual output.
288 difflines = difflib.unified_diff(expected_output, actual_output,
289 fromfile='check_spider_monkey_style.py expected output',
290 tofile='check_spider_monkey_style.py actual output')
291 ok = True
292 for diffline in difflines:
293 ok = False
294 print(diffline, end='')
296 return ok
299 def module_name(name):
300 '''Strip the trailing .cpp, .h, inlines.h or -inl.h from a filename.'''
302 return name.replace('inlines.h', '').replace('-inl.h', '').replace('.h', '').replace('.cpp', '')
305 def is_module_header(enclosing_inclname, header_inclname):
306 '''Determine if an included name is the "module header", i.e. should be
307 first in the file.'''
309 module = module_name(enclosing_inclname)
311 # Normal case, e.g. module == "foo/Bar", header_inclname == "foo/Bar.h".
312 if module == module_name(header_inclname):
313 return True
315 # A public header, e.g. module == "foo/Bar", header_inclname == "js/Bar.h".
316 m = re.match(r'js\/(.*)\.h', header_inclname)
317 if m is not None and module.endswith('/' + m.group(1)):
318 return True
320 # A weird public header case.
321 if module == 'jsmemorymetrics' and header_inclname == 'js/MemoryMetrics.h':
322 return True
324 return False
327 class Include(object):
328 '''Important information for a single #include statement.'''
330 def __init__(self, inclname, linenum, is_system):
331 self.inclname = inclname
332 self.linenum = linenum
333 self.is_system = is_system
335 def isLeaf(self):
336 return True
338 def section(self, enclosing_inclname):
339 '''Identify which section inclname belongs to.
341 The section numbers are as follows.
342 0. Module header (e.g. jsfoo.h or jsfooinlines.h within jsfoo.cpp)
343 1. mozilla/Foo.h
344 2. <foo.h> or <foo>
345 3. jsfoo.h, prmjtime.h, etc
346 4. foo/Bar.h
347 5. jsfooinlines.h
348 6. foo/Bar-inl.h
349 7. non-.h, e.g. *.tbl, *.msg
352 if self.is_system:
353 return 2
355 if not self.inclname.endswith('.h'):
356 return 7
358 # A couple of modules have the .h file in js/ and the .cpp file elsewhere and so need
359 # special handling.
360 if is_module_header(enclosing_inclname, self.inclname):
361 return 0
363 if '/' in self.inclname:
364 if self.inclname.startswith('mozilla/'):
365 return 1
367 if self.inclname.endswith('-inl.h'):
368 return 6
370 return 4
372 if self.inclname.endswith('inlines.h'):
373 return 5
375 return 3
377 def quote(self):
378 if self.is_system:
379 return '<' + self.inclname + '>'
380 else:
381 return '"' + self.inclname + '"'
384 class HashIfBlock(object):
385 '''Important information about a #if/#endif block.
387 A #if/#endif block is the contents of a #if/#endif (or similar) section.
388 The top-level block, which is not within a #if/#endif pair, is also
389 considered a block.
391 Each leaf is either an Include (representing a #include), or another
392 nested HashIfBlock.'''
393 def __init__(self):
394 self.kids = []
396 def isLeaf(self):
397 return False
400 def do_file(filename, inclname, file_kind, f, all_inclnames, included_h_inclnames):
401 block_stack = [HashIfBlock()]
403 # Extract the #include statements as a tree of IBlocks and IIncludes.
404 for linenum, line in enumerate(f, start=1):
405 # Look for a |#include "..."| line.
406 m = re.match(r'\s*#\s*include\s+"([^"]*)"', line)
407 if m is not None:
408 block_stack[-1].kids.append(Include(m.group(1), linenum, False))
410 # Look for a |#include <...>| line.
411 m = re.match(r'\s*#\s*include\s+<([^>]*)>', line)
412 if m is not None:
413 block_stack[-1].kids.append(Include(m.group(1), linenum, True))
415 # Look for a |#{if,ifdef,ifndef}| line.
416 m = re.match(r'\s*#\s*(if|ifdef|ifndef)\b', line)
417 if m is not None:
418 # Open a new block.
419 new_block = HashIfBlock()
420 block_stack[-1].kids.append(new_block)
421 block_stack.append(new_block)
423 # Look for a |#{elif,else}| line.
424 m = re.match(r'\s*#\s*(elif|else)\b', line)
425 if m is not None:
426 # Close the current block, and open an adjacent one.
427 block_stack.pop()
428 new_block = HashIfBlock()
429 block_stack[-1].kids.append(new_block)
430 block_stack.append(new_block)
432 # Look for a |#endif| line.
433 m = re.match(r'\s*#\s*endif\b', line)
434 if m is not None:
435 # Close the current block.
436 block_stack.pop()
438 def check_include_statement(include):
439 '''Check the style of a single #include statement.'''
441 if include.is_system:
442 # Check it is not a known local file (in which case it's probably a system header).
443 if include.inclname in included_inclnames_to_ignore or \
444 include.inclname in all_inclnames:
445 error(filename, include.linenum,
446 include.quote() + ' should be included using',
447 'the #include "..." form')
449 else:
450 if include.inclname not in included_inclnames_to_ignore:
451 included_kind = FileKind.get(include.inclname)
453 # Check the #include path has the correct form.
454 if include.inclname not in all_inclnames:
455 error(filename, include.linenum,
456 include.quote() + ' is included ' + 'using the wrong path;',
457 'did you forget a prefix, or is the file not yet committed?')
459 # Record inclusions of .h files for cycle detection later.
460 # (Exclude .tbl and .msg files.)
461 elif included_kind == FileKind.H or included_kind == FileKind.INL_H:
462 included_h_inclnames.add(include.inclname)
464 # Check a H file doesn't #include an INL_H file.
465 if file_kind == FileKind.H and included_kind == FileKind.INL_H:
466 error(filename, include.linenum,
467 'vanilla header includes an inline-header file ' + include.quote())
469 # Check a file doesn't #include itself. (We do this here because the cycle
470 # detection below doesn't detect this case.)
471 if inclname == include.inclname:
472 error(filename, include.linenum, 'the file includes itself')
474 def check_includes_order(include1, include2):
475 '''Check the ordering of two #include statements.'''
477 if include1.inclname in oddly_ordered_inclnames or \
478 include2.inclname in oddly_ordered_inclnames:
479 return
481 section1 = include1.section(inclname)
482 section2 = include2.section(inclname)
483 if (section1 > section2) or \
484 ((section1 == section2) and (include1.inclname.lower() > include2.inclname.lower())):
485 error(filename, str(include1.linenum) + ':' + str(include2.linenum),
486 include1.quote() + ' should be included after ' + include2.quote())
488 # The #include statements in the files in assembler/ and yarr/ have all manner of implicit
489 # ordering requirements. Boo. Ignore them.
490 skip_order_checking = inclname.startswith(('assembler/', 'yarr/'))
492 # Check the extracted #include statements, both individually, and the ordering of
493 # adjacent pairs that live in the same block.
494 def pair_traverse(prev, this):
495 if this.isLeaf():
496 check_include_statement(this)
497 if prev is not None and prev.isLeaf() and not skip_order_checking:
498 check_includes_order(prev, this)
499 else:
500 for prev2, this2 in zip([None] + this.kids[0:-1], this.kids):
501 pair_traverse(prev2, this2)
503 pair_traverse(None, block_stack[-1])
506 def find_cycles(all_inclnames, edges):
507 '''Find and draw any cycles.'''
509 SCCs = tarjan(all_inclnames, edges)
511 # The various sorted() calls below ensure the output is deterministic.
513 def draw_SCC(c):
514 cset = set(c)
515 drawn = set()
516 def draw(v, indent):
517 out(' ' * indent + ('-> ' if indent else ' ') + v)
518 if v in drawn:
519 return
520 drawn.add(v)
521 for succ in sorted(edges[v]):
522 if succ in cset:
523 draw(succ, indent + 1)
524 draw(sorted(c)[0], 0)
525 out('')
527 have_drawn_an_SCC = False
528 for scc in sorted(SCCs):
529 if len(scc) != 1:
530 if not have_drawn_an_SCC:
531 error('(multiple files)', None, 'header files form one or more cycles')
532 have_drawn_an_SCC = True
534 draw_SCC(scc)
537 # Tarjan's algorithm for finding the strongly connected components (SCCs) of a graph.
538 # https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
539 def tarjan(V, E):
540 vertex_index = {}
541 vertex_lowlink = {}
542 index = 0
543 S = []
544 all_SCCs = []
546 def strongconnect(v, index):
547 # Set the depth index for v to the smallest unused index
548 vertex_index[v] = index
549 vertex_lowlink[v] = index
550 index += 1
551 S.append(v)
553 # Consider successors of v
554 for w in E[v]:
555 if w not in vertex_index:
556 # Successor w has not yet been visited; recurse on it
557 index = strongconnect(w, index)
558 vertex_lowlink[v] = min(vertex_lowlink[v], vertex_lowlink[w])
559 elif w in S:
560 # Successor w is in stack S and hence in the current SCC
561 vertex_lowlink[v] = min(vertex_lowlink[v], vertex_index[w])
563 # If v is a root node, pop the stack and generate an SCC
564 if vertex_lowlink[v] == vertex_index[v]:
565 i = S.index(v)
566 scc = S[i:]
567 del S[i:]
568 all_SCCs.append(scc)
570 return index
572 for v in V:
573 if v not in vertex_index:
574 index = strongconnect(v, index)
576 return all_SCCs
579 def main():
580 ok = check_style()
582 if ok:
583 print('TEST-PASS | check_spidermonkey_style.py | ok')
584 else:
585 print('TEST-UNEXPECTED-FAIL | check_spidermonkey_style.py | actual output does not match expected output; diff is above')
587 sys.exit(0 if ok else 1)
590 if __name__ == '__main__':
591 main()