Add tests to verify accelerators properly work on constrained window.
[chromium-blink-merge.git] / tools / checkdeps / checkdeps.py
blob8333ef6901ddb7a5520d2ec927e4742b2e65a85c
1 #!/usr/bin/env python
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Makes sure that files include headers from allowed directories.
8 Checks DEPS files in the source tree for rules, and applies those rules to
9 "#include" commands in source files. Any source file including something not
10 permitted by the DEPS files will fail.
12 The format of the deps file:
14 First you have the normal module-level deps. These are the ones used by
15 gclient. An example would be:
17 deps = {
18 "base":"http://foo.bar/trunk/base"
21 DEPS files not in the top-level of a module won't need this. Then you
22 have any additional include rules. You can add (using "+") or subtract
23 (using "-") from the previously specified rules (including
24 module-level deps). You can also specify a path that is allowed for
25 now but that we intend to remove, using "!"; this is treated the same
26 as "+" when check_deps is run by our bots, but a presubmit step will
27 show a warning if you add a new include of a file that is only allowed
28 by "!".
30 Note that for .java files, there is currently no difference between
31 "+" and "!", even in the presubmit step.
33 include_rules = {
34 # Code should be able to use base (it's specified in the module-level
35 # deps above), but nothing in "base/evil" because it's evil.
36 "-base/evil",
38 # But this one subdirectory of evil is OK.
39 "+base/evil/not",
41 # And it can include files from this other directory even though there is
42 # no deps rule for it.
43 "+tools/crime_fighter",
45 # This dependency is allowed for now but work is ongoing to remove it,
46 # so you shouldn't add further dependencies on it.
47 "!base/evil/ok_for_now.h",
50 If you have certain include rules that should only be applied for some
51 files within this directory and subdirectories, you can write a
52 section named specific_include_rules that is a hash map of regular
53 expressions to the list of rules that should apply to files matching
54 them. Note that such rules will always be applied before the rules
55 from 'include_rules' have been applied, but the order in which rules
56 associated with different regular expressions is applied is arbitrary.
58 specific_include_rules = {
59 ".*_(unit|browser|api)test\.cc": [
60 "+libraries/testsupport",
64 DEPS files may be placed anywhere in the tree. Each one applies to all
65 subdirectories, where there may be more DEPS files that provide additions or
66 subtractions for their own sub-trees.
68 There is an implicit rule for the current directory (where the DEPS file lives)
69 and all of its subdirectories. This prevents you from having to explicitly
70 allow the current directory everywhere. This implicit rule is applied first,
71 so you can modify or remove it using the normal include rules.
73 The rules are processed in order. This means you can explicitly allow a higher
74 directory and then take away permissions from sub-parts, or the reverse.
76 Note that all directory separators must be slashes (Unix-style) and not
77 backslashes. All directories should be relative to the source root and use
78 only lowercase.
79 """
81 import os
82 import optparse
83 import re
84 import subprocess
85 import sys
86 import copy
88 import cpp_checker
89 import java_checker
90 import results
91 from rules import Rule, Rules
94 # Variable name used in the DEPS file to add or subtract include files from
95 # the module-level deps.
96 INCLUDE_RULES_VAR_NAME = 'include_rules'
98 # Variable name used in the DEPS file to add or subtract include files
99 # from module-level deps specific to files whose basename (last
100 # component of path) matches a given regular expression.
101 SPECIFIC_INCLUDE_RULES_VAR_NAME = 'specific_include_rules'
103 # Optionally present in the DEPS file to list subdirectories which should not
104 # be checked. This allows us to skip third party code, for example.
105 SKIP_SUBDIRS_VAR_NAME = 'skip_child_includes'
108 def NormalizePath(path):
109 """Returns a path normalized to how we write DEPS rules and compare paths.
111 return path.lower().replace('\\', '/')
114 def _IsTestFile(filename):
115 """Does a rudimentary check to try to skip test files; this could be
116 improved but is good enough for now.
118 return re.match('(test|mock|dummy)_.*|.*_[a-z]*test\.(cc|mm|java)', filename)
121 class DepsChecker(object):
122 """Parses include_rules from DEPS files and can verify files in the
123 source tree against them.
126 def __init__(self,
127 base_directory=None,
128 verbose=False,
129 being_tested=False,
130 ignore_temp_rules=False,
131 skip_tests=False):
132 """Creates a new DepsChecker.
134 Args:
135 base_directory: OS-compatible path to root of checkout, e.g. C:\chr\src.
136 verbose: Set to true for debug output.
137 being_tested: Set to true to ignore the DEPS file at tools/checkdeps/DEPS.
139 self.base_directory = base_directory
140 self.verbose = verbose
141 self._under_test = being_tested
142 self._ignore_temp_rules = ignore_temp_rules
143 self._skip_tests = skip_tests
145 if not base_directory:
146 self.base_directory = os.path.abspath(
147 os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', '..'))
149 self.results_formatter = results.NormalResultsFormatter(verbose)
151 self.git_source_directories = set()
152 self._AddGitSourceDirectories()
154 # Map of normalized directory paths to rules to use for those
155 # directories, or None for directories that should be skipped.
156 self.directory_rules = {}
157 self._ApplyDirectoryRulesAndSkipSubdirs(Rules(), self.base_directory)
159 def Report(self):
160 """Prints a report of results, and returns an exit code for the process."""
161 if self.results_formatter.GetResults():
162 self.results_formatter.PrintResults()
163 return 1
164 print '\nSUCCESS\n'
165 return 0
167 def _ApplyRules(self, existing_rules, includes, specific_includes, cur_dir):
168 """Applies the given include rules, returning the new rules.
170 Args:
171 existing_rules: A set of existing rules that will be combined.
172 include: The list of rules from the "include_rules" section of DEPS.
173 specific_includes: E.g. {'.*_unittest\.cc': ['+foo', '-blat']} rules
174 from the "specific_include_rules" section of DEPS.
175 cur_dir: The current directory, normalized path. We will create an
176 implicit rule that allows inclusion from this directory.
178 Returns: A new set of rules combining the existing_rules with the other
179 arguments.
181 rules = copy.deepcopy(existing_rules)
183 # First apply the implicit "allow" rule for the current directory.
184 if cur_dir.startswith(
185 NormalizePath(os.path.normpath(self.base_directory))):
186 relative_dir = cur_dir[len(self.base_directory) + 1:]
188 source = relative_dir
189 if len(source) == 0:
190 source = 'top level' # Make the help string a little more meaningful.
191 rules.AddRule('+' + relative_dir, 'Default rule for ' + source)
192 else:
193 raise Exception('Internal error: base directory is not at the beginning' +
194 ' for\n %s and base dir\n %s' %
195 (cur_dir, self.base_directory))
197 def ApplyOneRule(rule_str, dependee_regexp=None):
198 """Deduces a sensible description for the rule being added, and
199 adds the rule with its description to |rules|.
201 If we are ignoring temporary rules, this function does nothing
202 for rules beginning with the Rule.TEMP_ALLOW character.
204 if self._ignore_temp_rules and rule_str.startswith(Rule.TEMP_ALLOW):
205 return
207 rule_block_name = 'include_rules'
208 if dependee_regexp:
209 rule_block_name = 'specific_include_rules'
210 if not relative_dir:
211 rule_description = 'the top level %s' % rule_block_name
212 else:
213 rule_description = relative_dir + "'s %s" % rule_block_name
214 rules.AddRule(rule_str, rule_description, dependee_regexp)
216 # Apply the additional explicit rules.
217 for (_, rule_str) in enumerate(includes):
218 ApplyOneRule(rule_str)
220 # Finally, apply the specific rules.
221 for regexp, specific_rules in specific_includes.iteritems():
222 for rule_str in specific_rules:
223 ApplyOneRule(rule_str, regexp)
225 return rules
227 def _ApplyDirectoryRules(self, existing_rules, dir_name):
228 """Combines rules from the existing rules and the new directory.
230 Any directory can contain a DEPS file. Toplevel DEPS files can contain
231 module dependencies which are used by gclient. We use these, along with
232 additional include rules and implicit rules for the given directory, to
233 come up with a combined set of rules to apply for the directory.
235 Args:
236 existing_rules: The rules for the parent directory. We'll add-on to these.
237 dir_name: The directory name that the deps file may live in (if
238 it exists). This will also be used to generate the
239 implicit rules. This is a non-normalized path.
241 Returns: A tuple containing: (1) the combined set of rules to apply to the
242 sub-tree, and (2) a list of all subdirectories that should NOT be
243 checked, as specified in the DEPS file (if any).
245 norm_dir_name = NormalizePath(dir_name)
247 # Check for a .svn directory in this directory or check this directory is
248 # contained in git source direcotries. This will tell us if it's a source
249 # directory and should be checked.
250 if not (os.path.exists(os.path.join(dir_name, ".svn")) or
251 (norm_dir_name in self.git_source_directories)):
252 return (None, [])
254 # Check the DEPS file in this directory.
255 if self.verbose:
256 print 'Applying rules from', dir_name
257 def FromImpl(_unused, _unused2):
258 pass # NOP function so "From" doesn't fail.
260 def FileImpl(_unused):
261 pass # NOP function so "File" doesn't fail.
263 class _VarImpl:
264 def __init__(self, local_scope):
265 self._local_scope = local_scope
267 def Lookup(self, var_name):
268 """Implements the Var syntax."""
269 if var_name in self._local_scope.get('vars', {}):
270 return self._local_scope['vars'][var_name]
271 raise Exception('Var is not defined: %s' % var_name)
273 local_scope = {}
274 global_scope = {
275 'File': FileImpl,
276 'From': FromImpl,
277 'Var': _VarImpl(local_scope).Lookup,
279 deps_file = os.path.join(dir_name, 'DEPS')
281 # The second conditional here is to disregard the
282 # tools/checkdeps/DEPS file while running tests. This DEPS file
283 # has a skip_child_includes for 'testdata' which is necessary for
284 # running production tests, since there are intentional DEPS
285 # violations under the testdata directory. On the other hand when
286 # running tests, we absolutely need to verify the contents of that
287 # directory to trigger those intended violations and see that they
288 # are handled correctly.
289 if os.path.isfile(deps_file) and (
290 not self._under_test or not os.path.split(dir_name)[1] == 'checkdeps'):
291 execfile(deps_file, global_scope, local_scope)
292 elif self.verbose:
293 print ' No deps file found in', dir_name
295 # Even if a DEPS file does not exist we still invoke ApplyRules
296 # to apply the implicit "allow" rule for the current directory
297 include_rules = local_scope.get(INCLUDE_RULES_VAR_NAME, [])
298 specific_include_rules = local_scope.get(SPECIFIC_INCLUDE_RULES_VAR_NAME,
300 skip_subdirs = local_scope.get(SKIP_SUBDIRS_VAR_NAME, [])
302 return (self._ApplyRules(existing_rules, include_rules,
303 specific_include_rules, norm_dir_name),
304 skip_subdirs)
306 def _ApplyDirectoryRulesAndSkipSubdirs(self, parent_rules, dir_path):
307 """Given |parent_rules| and a subdirectory |dir_path| from the
308 directory that owns the |parent_rules|, add |dir_path|'s rules to
309 |self.directory_rules|, and add None entries for any of its
310 subdirectories that should be skipped.
312 directory_rules, excluded_subdirs = self._ApplyDirectoryRules(parent_rules,
313 dir_path)
314 self.directory_rules[NormalizePath(dir_path)] = directory_rules
315 for subdir in excluded_subdirs:
316 self.directory_rules[NormalizePath(
317 os.path.normpath(os.path.join(dir_path, subdir)))] = None
319 def GetDirectoryRules(self, dir_path):
320 """Returns a Rules object to use for the given directory, or None
321 if the given directory should be skipped. This takes care of
322 first building rules for parent directories (up to
323 self.base_directory) if needed.
325 Args:
326 dir_path: A real (non-normalized) path to the directory you want
327 rules for.
329 norm_dir_path = NormalizePath(dir_path)
331 if not norm_dir_path.startswith(
332 NormalizePath(os.path.normpath(self.base_directory))):
333 dir_path = os.path.join(self.base_directory, dir_path)
334 norm_dir_path = NormalizePath(dir_path)
336 parent_dir = os.path.dirname(dir_path)
337 parent_rules = None
338 if not norm_dir_path in self.directory_rules:
339 parent_rules = self.GetDirectoryRules(parent_dir)
341 # We need to check for an entry for our dir_path again, in case we
342 # are at a path e.g. A/B/C where A/B/DEPS specifies the C
343 # subdirectory to be skipped; in this case, the invocation to
344 # GetDirectoryRules(parent_dir) has already filled in an entry for
345 # A/B/C.
346 if not norm_dir_path in self.directory_rules:
347 if not parent_rules:
348 # If the parent directory should be skipped, then the current
349 # directory should also be skipped.
350 self.directory_rules[norm_dir_path] = None
351 else:
352 self._ApplyDirectoryRulesAndSkipSubdirs(parent_rules, dir_path)
353 return self.directory_rules[norm_dir_path]
355 def CheckDirectory(self, start_dir):
356 """Checks all relevant source files in the specified directory and
357 its subdirectories for compliance with DEPS rules throughout the
358 tree (starting at |self.base_directory|). |start_dir| must be a
359 subdirectory of |self.base_directory|.
361 On completion, self.results_formatter has the results of
362 processing, and calling Report() will print a report of results.
364 java = java_checker.JavaChecker(self.base_directory, self.verbose)
365 cpp = cpp_checker.CppChecker(self.verbose)
366 checkers = dict(
367 (extension, checker)
368 for checker in [java, cpp] for extension in checker.EXTENSIONS)
369 self._CheckDirectoryImpl(checkers, start_dir)
371 def _CheckDirectoryImpl(self, checkers, dir_name):
372 rules = self.GetDirectoryRules(dir_name)
373 if rules == None:
374 return
376 # Collect a list of all files and directories to check.
377 files_to_check = []
378 dirs_to_check = []
379 contents = os.listdir(dir_name)
380 for cur in contents:
381 full_name = os.path.join(dir_name, cur)
382 if os.path.isdir(full_name):
383 dirs_to_check.append(full_name)
384 elif os.path.splitext(full_name)[1] in checkers:
385 if not self._skip_tests or not _IsTestFile(cur):
386 files_to_check.append(full_name)
388 # First check all files in this directory.
389 for cur in files_to_check:
390 checker = checkers[os.path.splitext(cur)[1]]
391 file_status = checker.CheckFile(rules, cur)
392 if file_status.HasViolations():
393 self.results_formatter.AddError(file_status)
395 # Next recurse into the subdirectories.
396 for cur in dirs_to_check:
397 self._CheckDirectoryImpl(checkers, cur)
399 def CheckAddedCppIncludes(self, added_includes):
400 """This is used from PRESUBMIT.py to check new #include statements added in
401 the change being presubmit checked.
403 Args:
404 added_includes: ((file_path, (include_line, include_line, ...), ...)
406 Return:
407 A list of tuples, (bad_file_path, rule_type, rule_description)
408 where rule_type is one of Rule.DISALLOW or Rule.TEMP_ALLOW and
409 rule_description is human-readable. Empty if no problems.
411 cpp = cpp_checker.CppChecker(self.verbose)
412 problems = []
413 for file_path, include_lines in added_includes:
414 # TODO(joi): Make this cover Java as well.
415 if not cpp.IsCppFile(file_path):
416 pass
417 rules_for_file = self.GetDirectoryRules(os.path.dirname(file_path))
418 if rules_for_file:
419 for line in include_lines:
420 is_include, violation = cpp.CheckLine(
421 rules_for_file, line, file_path, True)
422 if violation:
423 rule_type = violation.violated_rule.allow
424 if rule_type != Rule.ALLOW:
425 violation_text = results.NormalResultsFormatter.FormatViolation(
426 violation, self.verbose)
427 problems.append((file_path, rule_type, violation_text))
428 return problems
430 def _AddGitSourceDirectories(self):
431 """Adds any directories containing sources managed by git to
432 self.git_source_directories.
434 if not os.path.exists(os.path.join(self.base_directory, '.git')):
435 return
437 popen_out = os.popen('cd %s && git ls-files --full-name .' %
438 subprocess.list2cmdline([self.base_directory]))
439 for line in popen_out.readlines():
440 dir_name = os.path.join(self.base_directory, os.path.dirname(line))
441 # Add the directory as well as all the parent directories. Use
442 # forward slashes and lower case to normalize paths.
443 while dir_name != self.base_directory:
444 self.git_source_directories.add(NormalizePath(dir_name))
445 dir_name = os.path.dirname(dir_name)
446 self.git_source_directories.add(NormalizePath(self.base_directory))
449 def PrintUsage():
450 print """Usage: python checkdeps.py [--root <root>] [tocheck]
452 --root ROOT Specifies the repository root. This defaults to "../../.."
453 relative to the script file. This will be correct given the
454 normal location of the script in "<root>/tools/checkdeps".
456 --(others) There are a few lesser-used options; run with --help to show them.
458 tocheck Specifies the directory, relative to root, to check. This defaults
459 to "." so it checks everything.
461 Examples:
462 python checkdeps.py
463 python checkdeps.py --root c:\\source chrome"""
466 def main():
467 option_parser = optparse.OptionParser()
468 option_parser.add_option(
469 '', '--root',
470 default='', dest='base_directory',
471 help='Specifies the repository root. This defaults '
472 'to "../../.." relative to the script file, which '
473 'will normally be the repository root.')
474 option_parser.add_option(
475 '', '--ignore-temp-rules',
476 action='store_true', dest='ignore_temp_rules', default=False,
477 help='Ignore !-prefixed (temporary) rules.')
478 option_parser.add_option(
479 '', '--generate-temp-rules',
480 action='store_true', dest='generate_temp_rules', default=False,
481 help='Print rules to temporarily allow files that fail '
482 'dependency checking.')
483 option_parser.add_option(
484 '', '--count-violations',
485 action='store_true', dest='count_violations', default=False,
486 help='Count #includes in violation of intended rules.')
487 option_parser.add_option(
488 '', '--skip-tests',
489 action='store_true', dest='skip_tests', default=False,
490 help='Skip checking test files (best effort).')
491 option_parser.add_option(
492 '-v', '--verbose',
493 action='store_true', default=False,
494 help='Print debug logging')
495 options, args = option_parser.parse_args()
497 deps_checker = DepsChecker(options.base_directory,
498 verbose=options.verbose,
499 ignore_temp_rules=options.ignore_temp_rules,
500 skip_tests=options.skip_tests)
502 # Figure out which directory we have to check.
503 start_dir = deps_checker.base_directory
504 if len(args) == 1:
505 # Directory specified. Start here. It's supposed to be relative to the
506 # base directory.
507 start_dir = os.path.abspath(
508 os.path.join(deps_checker.base_directory, args[0]))
509 elif len(args) >= 2 or (options.generate_temp_rules and
510 options.count_violations):
511 # More than one argument, or incompatible flags, we don't handle this.
512 PrintUsage()
513 return 1
515 print 'Using base directory:', deps_checker.base_directory
516 print 'Checking:', start_dir
518 if options.generate_temp_rules:
519 deps_checker.results_formatter = results.TemporaryRulesFormatter()
520 elif options.count_violations:
521 deps_checker.results_formatter = results.CountViolationsFormatter()
522 deps_checker.CheckDirectory(start_dir)
523 return deps_checker.Report()
526 if '__main__' == __name__:
527 sys.exit(main())