Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(288)

Side by Side Diff: tools/checkdeps/checkdeps.py

Issue 10805042: Implement ability to specify temporarily-allowed dependencies in DEPS (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Respond to review comments. Created 8 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 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 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Makes sure that files include headers from allowed directories. 6 """Makes sure that files include headers from allowed directories.
7 7
8 Checks DEPS files in the source tree for rules, and applies those rules to 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 9 "#include" commands in source files. Any source file including something not
10 permitted by the DEPS files will fail. 10 permitted by the DEPS files will fail.
11 11
12 The format of the deps file: 12 The format of the deps file:
13 13
14 First you have the normal module-level deps. These are the ones used by 14 First you have the normal module-level deps. These are the ones used by
15 gclient. An example would be: 15 gclient. An example would be:
16 16
17 deps = { 17 deps = {
18 "base":"http://foo.bar/trunk/base" 18 "base":"http://foo.bar/trunk/base"
19 } 19 }
20 20
21 DEPS files not in the top-level of a module won't need this. Then you have 21 DEPS files not in the top-level of a module won't need this. Then you
22 any additional include rules. You can add (using "+") or subtract (using "-") 22 have any additional include rules. You can add (using "+") or subtract
23 from the previously specified rules (including module-level deps). 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 "!".
29
30 Note that for .java files, there is currently no difference between
31 "+" and "!", even in the presubmit step.
24 32
25 include_rules = { 33 include_rules = {
26 # Code should be able to use base (it's specified in the module-level 34 # Code should be able to use base (it's specified in the module-level
27 # deps above), but nothing in "base/evil" because it's evil. 35 # deps above), but nothing in "base/evil" because it's evil.
28 "-base/evil", 36 "-base/evil",
29 37
30 # But this one subdirectory of evil is OK. 38 # But this one subdirectory of evil is OK.
31 "+base/evil/not", 39 "+base/evil/not",
32 40
33 # And it can include files from this other directory even though there is 41 # And it can include files from this other directory even though there is
34 # no deps rule for it. 42 # no deps rule for it.
35 "+tools/crime_fighter" 43 "+tools/crime_fighter",
44
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",
36 } 48 }
37 49
38 DEPS files may be placed anywhere in the tree. Each one applies to all 50 DEPS files may be placed anywhere in the tree. Each one applies to all
39 subdirectories, where there may be more DEPS files that provide additions or 51 subdirectories, where there may be more DEPS files that provide additions or
40 subtractions for their own sub-trees. 52 subtractions for their own sub-trees.
41 53
42 There is an implicit rule for the current directory (where the DEPS file lives) 54 There is an implicit rule for the current directory (where the DEPS file lives)
43 and all of its subdirectories. This prevents you from having to explicitly 55 and all of its subdirectories. This prevents you from having to explicitly
44 allow the current directory everywhere. This implicit rule is applied first, 56 allow the current directory everywhere. This implicit rule is applied first,
45 so you can modify or remove it using the normal include rules. 57 so you can modify or remove it using the normal include rules.
46 58
47 The rules are processed in order. This means you can explicitly allow a higher 59 The rules are processed in order. This means you can explicitly allow a higher
48 directory and then take away permissions from sub-parts, or the reverse. 60 directory and then take away permissions from sub-parts, or the reverse.
49 61
50 Note that all directory separators must be slashes (Unix-style) and not 62 Note that all directory separators must be slashes (Unix-style) and not
51 backslashes. All directories should be relative to the source root and use 63 backslashes. All directories should be relative to the source root and use
52 only lowercase. 64 only lowercase.
53 """ 65 """
54 66
55 import os 67 import os
56 import optparse 68 import optparse
57 import pipes 69 import subprocess
58 import sys 70 import sys
59 import copy 71 import copy
60 72
61 import cpp_checker 73 import cpp_checker
62 import java_checker 74 import java_checker
75 from rules import Rule, Rules
63 76
64 77
65 # Variable name used in the DEPS file to add or subtract include files from 78 # Variable name used in the DEPS file to add or subtract include files from
66 # the module-level deps. 79 # the module-level deps.
67 INCLUDE_RULES_VAR_NAME = "include_rules" 80 INCLUDE_RULES_VAR_NAME = "include_rules"
68 81
69 # Optionally present in the DEPS file to list subdirectories which should not 82 # Optionally present in the DEPS file to list subdirectories which should not
70 # be checked. This allows us to skip third party code, for example. 83 # be checked. This allows us to skip third party code, for example.
71 SKIP_SUBDIRS_VAR_NAME = "skip_child_includes" 84 SKIP_SUBDIRS_VAR_NAME = "skip_child_includes"
72 85
73 # Set to true for more output. This is set by the command line options. 86 # Set to true for more output. This is set by the command line options.
74 VERBOSE = False 87 VERBOSE = False
75 88
76 # In lowercase, using forward slashes as directory separators, ending in a 89 # OS-compatible path for base directory (e.g. C:\chrome\src). Set
77 # forward slash. Set by the command line options. 90 # implicitly or by command-line option.
78 BASE_DIRECTORY = "" 91 BASE_DIRECTORY = ""
79 92
80 # The directories which contain the sources managed by git. 93 # The directories which contain the sources managed by git.
81 GIT_SOURCE_DIRECTORY = set() 94 GIT_SOURCE_DIRECTORY = set()
82 95
83 96
84 # Specifies a single rule for an include, which can be either allow or disallow.
85 class Rule(object):
86 def __init__(self, allow, directory, source):
87 self.allow = allow
88 self._dir = directory
89 self._source = source
90
91 def __str__(self):
92 if (self.allow):
93 return '"+%s" from %s.' % (self._dir, self._source)
94 return '"-%s" from %s.' % (self._dir, self._source)
95
96 def ParentOrMatch(self, other):
97 """Returns true if the input string is an exact match or is a parent
98 of the current rule. For example, the input "foo" would match "foo/bar"."""
99 return self._dir == other or self._dir.startswith(other + "/")
100
101 def ChildOrMatch(self, other):
102 """Returns true if the input string would be covered by this rule. For
103 example, the input "foo/bar" would match the rule "foo"."""
104 return self._dir == other or other.startswith(self._dir + "/")
105
106
107 def ParseRuleString(rule_string, source):
108 """Returns a tuple of a boolean indicating whether the directory is an allow
109 rule, and a string holding the directory name.
110 """
111 if len(rule_string) < 1:
112 raise Exception('The rule string "%s" is too short\nin %s' %
113 (rule_string, source))
114
115 if rule_string[0] == "+":
116 return (True, rule_string[1:])
117 if rule_string[0] == "-":
118 return (False, rule_string[1:])
119 raise Exception('The rule string "%s" does not begin with a "+" or a "-"' %
120 rule_string)
121
122
123 class Rules:
124 def __init__(self):
125 """Initializes the current rules with an empty rule list."""
126 self._rules = []
127
128 def __str__(self):
129 ret = "Rules = [\n"
130 ret += "\n".join([" %s" % x for x in self._rules])
131 ret += "]\n"
132 return ret
133
134 def AddRule(self, rule_string, source):
135 """Adds a rule for the given rule string.
136
137 Args:
138 rule_string: The include_rule string read from the DEPS file to apply.
139 source: A string representing the location of that string (filename, etc.)
140 so that we can give meaningful errors.
141 """
142 (add_rule, rule_dir) = ParseRuleString(rule_string, source)
143 # Remove any existing rules or sub-rules that apply. For example, if we're
144 # passed "foo", we should remove "foo", "foo/bar", but not "foobar".
145 self._rules = [x for x in self._rules if not x.ParentOrMatch(rule_dir)]
146 self._rules.insert(0, Rule(add_rule, rule_dir, source))
147
148 def DirAllowed(self, allowed_dir):
149 """Returns a tuple (success, message), where success indicates if the given
150 directory is allowed given the current set of rules, and the message tells
151 why if the comparison failed."""
152 for rule in self._rules:
153 if rule.ChildOrMatch(allowed_dir):
154 # This rule applies.
155 if rule.allow:
156 return (True, "")
157 return (False, rule.__str__())
158 # No rules apply, fail.
159 return (False, "no rule applying")
160
161
162 def ApplyRules(existing_rules, includes, cur_dir): 97 def ApplyRules(existing_rules, includes, cur_dir):
163 """Applies the given include rules, returning the new rules. 98 """Applies the given include rules, returning the new rules.
164 99
165 Args: 100 Args:
166 existing_rules: A set of existing rules that will be combined. 101 existing_rules: A set of existing rules that will be combined.
167 include: The list of rules from the "include_rules" section of DEPS. 102 include: The list of rules from the "include_rules" section of DEPS.
168 cur_dir: The current directory. We will create an implicit rule that 103 cur_dir: The current directory. We will create an implicit rule that
169 allows inclusion from this directory. 104 allows inclusion from this directory.
170 105
171 Returns: A new set of rules combining the existing_rules with the other 106 Returns: A new set of rules combining the existing_rules with the other
172 arguments. 107 arguments.
173 """ 108 """
174 rules = copy.copy(existing_rules) 109 rules = copy.copy(existing_rules)
175 110
176 # First apply the implicit "allow" rule for the current directory. 111 # First apply the implicit "allow" rule for the current directory.
177 if cur_dir.lower().startswith(BASE_DIRECTORY): 112 if os.path.normpath(cur_dir).lower().startswith(
M-A Ruel 2012/07/26 13:50:51 BTW, is cur_dir an absolute path? FYI only: I lik
Jói 2012/07/26 17:05:52 It's an absolute path.
113 os.path.normpath(BASE_DIRECTORY).lower()):
178 relative_dir = cur_dir[len(BASE_DIRECTORY) + 1:] 114 relative_dir = cur_dir[len(BASE_DIRECTORY) + 1:]
179 # Normalize path separators to slashes.
180 relative_dir = relative_dir.replace("\\", "/")
181 source = relative_dir 115 source = relative_dir
182 if len(source) == 0: 116 if len(source) == 0:
183 source = "top level" # Make the help string a little more meaningful. 117 source = "top level" # Make the help string a little more meaningful.
184 rules.AddRule("+" + relative_dir, "Default rule for " + source) 118 rules.AddRule("+" + relative_dir, "Default rule for " + source)
185 else: 119 else:
186 raise Exception("Internal error: base directory is not at the beginning" + 120 raise Exception("Internal error: base directory is not at the beginning" +
187 " for\n %s and base dir\n %s" % 121 " for\n %s and base dir\n %s" %
188 (cur_dir, BASE_DIRECTORY)) 122 (cur_dir, BASE_DIRECTORY))
189 123
190 # Last, apply the additional explicit rules. 124 # Last, apply the additional explicit rules.
(...skipping 99 matching lines...) Expand 10 before | Expand all | Expand 10 after
290 success = False 224 success = False
291 225
292 # Next recurse into the subdirectories. 226 # Next recurse into the subdirectories.
293 for cur in dirs_to_check: 227 for cur in dirs_to_check:
294 if not CheckDirectory(rules, checkers, cur): 228 if not CheckDirectory(rules, checkers, cur):
295 success = False 229 success = False
296 230
297 return success 231 return success
298 232
299 233
234 def CheckAddedIncludes(added_includes):
235 """This is used from PRESUBMIT.py to check new #include statements added in
236 the change being presubmit checked.
237
238 Args:
239 added_includes: ((file_path, (include_line, include_line, ...), ...)
240
241 Return:
242 A list of tuples, (bad_file_path, rule_type, rule_description)
243 where rule_type is one of Rule.DISALLOW or Rule.TEMP_ALLOW and
244 rule_description is human-readable. Empty if no problems.
245 """
246 CommonSetup()
247
248 # Map of directory paths to rules to use for those directories, or
249 # None for directories that should be skipped.
250 directory_rules = {}
251
252 def ApplyDirectoryRulesAndSkipSubdirs(parent_rules, dir_path):
253 rules_tuple = ApplyDirectoryRules(parent_rules, dir_path)
254 directory_rules[dir_path] = rules_tuple[0]
255 for subdir in rules_tuple[1]:
256 directory_rules[os.path.join(dir_path, subdir)] = None
257
258 ApplyDirectoryRulesAndSkipSubdirs(Rules(), BASE_DIRECTORY)
259
260 def GetDirectoryRules(dir_path):
261 """Returns a Rules object to use for the given directory, or None
262 if the given directory should be skipped.
263 """
264 if not dir_path.startswith(BASE_DIRECTORY):
265 dir_path = os.path.join(BASE_DIRECTORY, dir_path)
266
267 parent_dir = os.path.dirname(dir_path)
268 parent_rules = None
269 if not dir_path in directory_rules:
270 parent_rules = GetDirectoryRules(parent_dir)
271
272 # We need to check for an entry for our dir_path again, in case we
273 # are at a path e.g. A/B/C where A/B/DEPS specifies the C
274 # subdirectory to be skipped; in this case, the invocation to
275 # GetDirectoryRules(parent_dir) has already filled in an entry for
276 # A/B/C.
277 if not dir_path in directory_rules:
278 if not parent_rules:
279 # If the parent directory should be skipped, then the current
280 # directory should also be skipped.
281 directory_rules[dir_path] = None
282 else:
283 ApplyDirectoryRulesAndSkipSubdirs(parent_rules, dir_path)
284 return directory_rules[dir_path]
285
286 cpp = cpp_checker.CppChecker(VERBOSE)
287
288 problems = []
289 for file_path, include_lines in added_includes:
290 # TODO(joi): Make this cover Java as well.
291 if not os.path.splitext(file_path)[1] in cpp.EXTENSIONS:
292 pass
293 rules_for_file = GetDirectoryRules(os.path.dirname(file_path))
294 if rules_for_file:
295 for line in include_lines:
296 is_include, line_status, rule_type = cpp.CheckLine(
297 rules_for_file, line, True)
298 if rule_type != Rule.ALLOW:
299 problems.append((file_path, rule_type, line_status))
300 return problems
301
302
300 def GetGitSourceDirectory(root): 303 def GetGitSourceDirectory(root):
301 """Returns a set of the directories to be checked. 304 """Returns a set of the directories to be checked.
302 305
303 Args: 306 Args:
304 root: The repository root where .git directory exists. 307 root: The repository root where .git directory exists.
305 308
306 Returns: 309 Returns:
307 A set of directories which contain sources managed by git. 310 A set of directories which contain sources managed by git.
308 """ 311 """
309 git_source_directory = set() 312 git_source_directory = set()
310 popen_out = os.popen("cd %s && git ls-files --full-name ." % 313 popen_out = os.popen("cd %s && git ls-files --full-name ." %
311 pipes.quote(root)) 314 subprocess.list2cmdline([root]))
312 for line in popen_out.readlines(): 315 for line in popen_out.readlines():
313 dir_name = os.path.join(root, os.path.dirname(line)) 316 dir_name = os.path.join(root, os.path.dirname(line))
314 # Add the directory as well as all the parent directories. 317 # Add the directory as well as all the parent directories.
315 while dir_name != root: 318 while dir_name != root:
316 git_source_directory.add(dir_name) 319 git_source_directory.add(dir_name.lower())
317 dir_name = os.path.dirname(dir_name) 320 dir_name = os.path.dirname(dir_name)
318 git_source_directory.add(root) 321 git_source_directory.add(root.lower())
319 return git_source_directory 322 return git_source_directory
320 323
321 324
325 def CommonSetup(base_dir=None):
326 """Setup that is in common between the main() entrypoint to this
327 script and the CheckAddedIncludes (presubmit check) entrypoint.
328 """
329 global BASE_DIRECTORY
330 global GIT_SOURCE_DIRECTORY
331
332 if base_dir:
333 BASE_DIRECTORY = base_dir
334 else:
335 BASE_DIRECTORY = os.path.abspath(
M-A Ruel 2012/07/26 13:50:51 Did you mean normpath?
Jói 2012/07/26 17:05:52 No, needs to be an absolute path.
336 os.path.join(os.path.abspath(os.path.dirname(__file__)), "../.."))
M-A Ruel 2012/07/26 13:50:51 '..', '..' instead of '../..'
Jói 2012/07/26 17:05:52 Done.
337
338 if os.path.exists(os.path.join(BASE_DIRECTORY, ".git")):
339 GIT_SOURCE_DIRECTORY = GetGitSourceDirectory(BASE_DIRECTORY)
340
341
322 def PrintUsage(): 342 def PrintUsage():
323 print """Usage: python checkdeps.py [--root <root>] [tocheck] 343 print """Usage: python checkdeps.py [--root <root>] [tocheck]
324 --root Specifies the repository root. This defaults to "../../.." relative 344 --root Specifies the repository root. This defaults to "../../.." relative
325 to the script file. This will be correct given the normal location 345 to the script file. This will be correct given the normal location
326 of the script in "<root>/tools/checkdeps". 346 of the script in "<root>/tools/checkdeps".
327 347
328 tocheck Specifies the directory, relative to root, to check. This defaults 348 tocheck Specifies the directory, relative to root, to check. This defaults
329 to "." so it checks everything. Only one level deep is currently 349 to "." so it checks everything. Only one level deep is currently
330 supported, so you can say "chrome" but not "chrome/browser". 350 supported, so you can say "chrome" but not "chrome/browser".
331 351
332 Examples: 352 Examples:
333 python checkdeps.py 353 python checkdeps.py
334 python checkdeps.py --root c:\\source chrome""" 354 python checkdeps.py --root c:\\source chrome"""
335 355
336 356
337 def checkdeps(options, args): 357 def checkdeps(options, args):
358 global BASE_DIRECTORY
338 global VERBOSE 359 global VERBOSE
339 if options.verbose: 360 if options.verbose:
340 VERBOSE = True 361 VERBOSE = True
341 362
342 # Optional base directory of the repository. 363 # Optional base directory of the repository.
343 global BASE_DIRECTORY
344 if not options.base_directory: 364 if not options.base_directory:
345 BASE_DIRECTORY = os.path.abspath( 365 CommonSetup()
346 os.path.join(os.path.abspath(os.path.dirname(__file__)), "../.."))
347 else: 366 else:
348 BASE_DIRECTORY = os.path.abspath(options.base_directory) 367 CommonSetup(os.path.abspath(options.base_directory))
349 368
350 # Figure out which directory we have to check. 369 # Figure out which directory we have to check.
351 if len(args) == 0: 370 if len(args) == 0:
352 # No directory to check specified, use the repository root. 371 # No directory to check specified, use the repository root.
353 start_dir = BASE_DIRECTORY 372 start_dir = BASE_DIRECTORY
354 elif len(args) == 1: 373 elif len(args) == 1:
355 # Directory specified. Start here. It's supposed to be relative to the 374 # Directory specified. Start here. It's supposed to be relative to the
356 # base directory. 375 # base directory.
357 start_dir = os.path.abspath(os.path.join(BASE_DIRECTORY, args[0])) 376 start_dir = os.path.abspath(os.path.join(BASE_DIRECTORY, args[0]))
358 else: 377 else:
359 # More than one argument, we don't handle this. 378 # More than one argument, we don't handle this.
360 PrintUsage() 379 PrintUsage()
361 return 1 380 return 1
362 381
363 print "Using base directory:", BASE_DIRECTORY 382 print "Using base directory:", BASE_DIRECTORY
364 print "Checking:", start_dir 383 print "Checking:", start_dir
365 384
366 base_rules = Rules() 385 base_rules = Rules()
367
368 # The base directory should be lower case from here on since it will be used
369 # for substring matching on the includes, and we compile on case-insensitive
370 # systems. Plus, we always use slashes here since the include parsing code
371 # will also normalize to slashes.
372 BASE_DIRECTORY = BASE_DIRECTORY.lower()
373 BASE_DIRECTORY = BASE_DIRECTORY.replace("\\", "/")
374 start_dir = start_dir.replace("\\", "/")
375
376 if os.path.exists(os.path.join(BASE_DIRECTORY, ".git")):
377 global GIT_SOURCE_DIRECTORY
378 GIT_SOURCE_DIRECTORY = GetGitSourceDirectory(BASE_DIRECTORY)
379
380 java = java_checker.JavaChecker(BASE_DIRECTORY, VERBOSE) 386 java = java_checker.JavaChecker(BASE_DIRECTORY, VERBOSE)
381 cpp = cpp_checker.CppChecker(VERBOSE) 387 cpp = cpp_checker.CppChecker(VERBOSE)
382 checkers = dict( 388 checkers = dict(
383 (extension, checker) 389 (extension, checker)
384 for checker in [java, cpp] for extension in checker.EXTENSIONS) 390 for checker in [java, cpp] for extension in checker.EXTENSIONS)
385 success = CheckDirectory(base_rules, checkers, start_dir) 391 success = CheckDirectory(base_rules, checkers, start_dir)
386 if not success: 392 if not success:
387 print "\nFAILED\n" 393 print "\nFAILED\n"
388 return 1 394 return 1
389 print "\nSUCCESS\n" 395 print "\nSUCCESS\n"
390 return 0 396 return 0
391 397
392 398
393 def main(): 399 def main():
394 option_parser = optparse.OptionParser() 400 option_parser = optparse.OptionParser()
395 option_parser.add_option("", "--root", default="", dest="base_directory", 401 option_parser.add_option("", "--root", default="", dest="base_directory",
396 help='Specifies the repository root. This defaults ' 402 help='Specifies the repository root. This defaults '
397 'to "../../.." relative to the script file, which ' 403 'to "../../.." relative to the script file, which '
398 'will normally be the repository root.') 404 'will normally be the repository root.')
399 option_parser.add_option("-v", "--verbose", action="store_true", 405 option_parser.add_option("-v", "--verbose", action="store_true",
400 default=False, help="Print debug logging") 406 default=False, help="Print debug logging")
401 options, args = option_parser.parse_args() 407 options, args = option_parser.parse_args()
402 return checkdeps(options, args) 408 return checkdeps(options, args)
403 409
404 410
405 if '__main__' == __name__: 411 if '__main__' == __name__:
406 sys.exit(main()) 412 sys.exit(main())
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698