OLD | NEW |
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. |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
48 directory and then take away permissions from sub-parts, or the reverse. | 48 directory and then take away permissions from sub-parts, or the reverse. |
49 | 49 |
50 Note that all directory separators must be slashes (Unix-style) and not | 50 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 | 51 backslashes. All directories should be relative to the source root and use |
52 only lowercase. | 52 only lowercase. |
53 """ | 53 """ |
54 | 54 |
55 import os | 55 import os |
56 import optparse | 56 import optparse |
57 import pipes | 57 import pipes |
58 import re | |
59 import sys | 58 import sys |
60 import copy | 59 import copy |
61 | 60 |
| 61 import cpp_checker |
| 62 import java_checker |
| 63 |
| 64 |
62 # Variable name used in the DEPS file to add or subtract include files from | 65 # Variable name used in the DEPS file to add or subtract include files from |
63 # the module-level deps. | 66 # the module-level deps. |
64 INCLUDE_RULES_VAR_NAME = "include_rules" | 67 INCLUDE_RULES_VAR_NAME = "include_rules" |
65 | 68 |
66 # Optionally present in the DEPS file to list subdirectories which should not | 69 # Optionally present in the DEPS file to list subdirectories which should not |
67 # be checked. This allows us to skip third party code, for example. | 70 # be checked. This allows us to skip third party code, for example. |
68 SKIP_SUBDIRS_VAR_NAME = "skip_child_includes" | 71 SKIP_SUBDIRS_VAR_NAME = "skip_child_includes" |
69 | 72 |
70 # The maximum number of non-include lines we can see before giving up. | |
71 MAX_UNINTERESTING_LINES = 50 | |
72 | |
73 # The maximum line length, this is to be efficient in the case of very long | |
74 # lines (which can't be #includes). | |
75 MAX_LINE_LENGTH = 128 | |
76 | |
77 # Set to true for more output. This is set by the command line options. | 73 # Set to true for more output. This is set by the command line options. |
78 VERBOSE = False | 74 VERBOSE = False |
79 | 75 |
80 # This regular expression will be used to extract filenames from include | |
81 # statements. | |
82 EXTRACT_INCLUDE_PATH = re.compile('[ \t]*#[ \t]*(?:include|import)[ \t]+"(.*)"') | |
83 | |
84 # In lowercase, using forward slashes as directory separators, ending in a | 76 # In lowercase, using forward slashes as directory separators, ending in a |
85 # forward slash. Set by the command line options. | 77 # forward slash. Set by the command line options. |
86 BASE_DIRECTORY = "" | 78 BASE_DIRECTORY = "" |
87 | 79 |
88 # The directories which contain the sources managed by git. | 80 # The directories which contain the sources managed by git. |
89 GIT_SOURCE_DIRECTORY = set() | 81 GIT_SOURCE_DIRECTORY = set() |
90 | 82 |
91 | 83 |
92 # Specifies a single rule for an include, which can be either allow or disallow. | 84 # Specifies a single rule for an include, which can be either allow or disallow. |
93 class Rule(object): | 85 class Rule(object): |
94 def __init__(self, allow, dir, source): | 86 def __init__(self, allow, directory, source): |
95 self._allow = allow | 87 self.allow = allow |
96 self._dir = dir | 88 self._dir = directory |
97 self._source = source | 89 self._source = source |
98 | 90 |
99 def __str__(self): | 91 def __str__(self): |
100 if (self._allow): | 92 if (self.allow): |
101 return '"+%s" from %s.' % (self._dir, self._source) | 93 return '"+%s" from %s.' % (self._dir, self._source) |
102 return '"-%s" from %s.' % (self._dir, self._source) | 94 return '"-%s" from %s.' % (self._dir, self._source) |
103 | 95 |
104 def ParentOrMatch(self, other): | 96 def ParentOrMatch(self, other): |
105 """Returns true if the input string is an exact match or is a parent | 97 """Returns true if the input string is an exact match or is a parent |
106 of the current rule. For example, the input "foo" would match "foo/bar".""" | 98 of the current rule. For example, the input "foo" would match "foo/bar".""" |
107 return self._dir == other or self._dir.startswith(other + "/") | 99 return self._dir == other or self._dir.startswith(other + "/") |
108 | 100 |
109 def ChildOrMatch(self, other): | 101 def ChildOrMatch(self, other): |
110 """Returns true if the input string would be covered by this rule. For | 102 """Returns true if the input string would be covered by this rule. For |
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
153 self._rules = [x for x in self._rules if not x.ParentOrMatch(rule_dir)] | 145 self._rules = [x for x in self._rules if not x.ParentOrMatch(rule_dir)] |
154 self._rules.insert(0, Rule(add_rule, rule_dir, source)) | 146 self._rules.insert(0, Rule(add_rule, rule_dir, source)) |
155 | 147 |
156 def DirAllowed(self, allowed_dir): | 148 def DirAllowed(self, allowed_dir): |
157 """Returns a tuple (success, message), where success indicates if the given | 149 """Returns a tuple (success, message), where success indicates if the given |
158 directory is allowed given the current set of rules, and the message tells | 150 directory is allowed given the current set of rules, and the message tells |
159 why if the comparison failed.""" | 151 why if the comparison failed.""" |
160 for rule in self._rules: | 152 for rule in self._rules: |
161 if rule.ChildOrMatch(allowed_dir): | 153 if rule.ChildOrMatch(allowed_dir): |
162 # This rule applies. | 154 # This rule applies. |
163 if rule._allow: | 155 if rule.allow: |
164 return (True, "") | 156 return (True, "") |
165 return (False, rule.__str__()) | 157 return (False, rule.__str__()) |
166 # No rules apply, fail. | 158 # No rules apply, fail. |
167 return (False, "no rule applying") | 159 return (False, "no rule applying") |
168 | 160 |
169 | 161 |
170 def ApplyRules(existing_rules, includes, cur_dir): | 162 def ApplyRules(existing_rules, includes, cur_dir): |
171 """Applies the given include rules, returning the new rules. | 163 """Applies the given include rules, returning the new rules. |
172 | 164 |
173 Args: | 165 Args: |
(...skipping 15 matching lines...) Expand all Loading... |
189 source = relative_dir | 181 source = relative_dir |
190 if len(source) == 0: | 182 if len(source) == 0: |
191 source = "top level" # Make the help string a little more meaningful. | 183 source = "top level" # Make the help string a little more meaningful. |
192 rules.AddRule("+" + relative_dir, "Default rule for " + source) | 184 rules.AddRule("+" + relative_dir, "Default rule for " + source) |
193 else: | 185 else: |
194 raise Exception("Internal error: base directory is not at the beginning" + | 186 raise Exception("Internal error: base directory is not at the beginning" + |
195 " for\n %s and base dir\n %s" % | 187 " for\n %s and base dir\n %s" % |
196 (cur_dir, BASE_DIRECTORY)) | 188 (cur_dir, BASE_DIRECTORY)) |
197 | 189 |
198 # Last, apply the additional explicit rules. | 190 # Last, apply the additional explicit rules. |
199 for (index, rule_str) in enumerate(includes): | 191 for (_, rule_str) in enumerate(includes): |
200 if not len(relative_dir): | 192 if not len(relative_dir): |
201 rule_description = "the top level include_rules" | 193 rule_description = "the top level include_rules" |
202 else: | 194 else: |
203 rule_description = relative_dir + "'s include_rules" | 195 rule_description = relative_dir + "'s include_rules" |
204 rules.AddRule(rule_str, rule_description) | 196 rules.AddRule(rule_str, rule_description) |
205 | 197 |
206 return rules | 198 return rules |
207 | 199 |
208 | 200 |
209 def ApplyDirectoryRules(existing_rules, dir_name): | 201 def ApplyDirectoryRules(existing_rules, dir_name): |
(...skipping 16 matching lines...) Expand all Loading... |
226 # Check for a .svn directory in this directory or check this directory is | 218 # Check for a .svn directory in this directory or check this directory is |
227 # contained in git source direcotries. This will tell us if it's a source | 219 # contained in git source direcotries. This will tell us if it's a source |
228 # directory and should be checked. | 220 # directory and should be checked. |
229 if not (os.path.exists(os.path.join(dir_name, ".svn")) or | 221 if not (os.path.exists(os.path.join(dir_name, ".svn")) or |
230 (dir_name.lower() in GIT_SOURCE_DIRECTORY)): | 222 (dir_name.lower() in GIT_SOURCE_DIRECTORY)): |
231 return (None, []) | 223 return (None, []) |
232 | 224 |
233 # Check the DEPS file in this directory. | 225 # Check the DEPS file in this directory. |
234 if VERBOSE: | 226 if VERBOSE: |
235 print "Applying rules from", dir_name | 227 print "Applying rules from", dir_name |
236 def FromImpl(unused, unused2): | 228 def FromImpl(_unused, _unused2): |
237 pass # NOP function so "From" doesn't fail. | 229 pass # NOP function so "From" doesn't fail. |
238 | 230 |
239 def FileImpl(unused): | 231 def FileImpl(_unused): |
240 pass # NOP function so "File" doesn't fail. | 232 pass # NOP function so "File" doesn't fail. |
241 | 233 |
242 class _VarImpl: | 234 class _VarImpl: |
243 def __init__(self, local_scope): | 235 def __init__(self, local_scope): |
244 self._local_scope = local_scope | 236 self._local_scope = local_scope |
245 | 237 |
246 def Lookup(self, var_name): | 238 def Lookup(self, var_name): |
247 """Implements the Var syntax.""" | 239 """Implements the Var syntax.""" |
248 if var_name in self._local_scope.get("vars", {}): | 240 if var_name in self._local_scope.get("vars", {}): |
249 return self._local_scope["vars"][var_name] | 241 return self._local_scope["vars"][var_name] |
250 raise Error("Var is not defined: %s" % var_name) | 242 raise Exception("Var is not defined: %s" % var_name) |
251 | 243 |
252 local_scope = {} | 244 local_scope = {} |
253 global_scope = { | 245 global_scope = { |
254 "File": FileImpl, | 246 "File": FileImpl, |
255 "From": FromImpl, | 247 "From": FromImpl, |
256 "Var": _VarImpl(local_scope).Lookup, | 248 "Var": _VarImpl(local_scope).Lookup, |
257 } | 249 } |
258 deps_file = os.path.join(dir_name, "DEPS") | 250 deps_file = os.path.join(dir_name, "DEPS") |
259 | 251 |
260 if os.path.isfile(deps_file): | 252 if os.path.isfile(deps_file): |
261 execfile(deps_file, global_scope, local_scope) | 253 execfile(deps_file, global_scope, local_scope) |
262 elif VERBOSE: | 254 elif VERBOSE: |
263 print " No deps file found in", dir_name | 255 print " No deps file found in", dir_name |
264 | 256 |
265 # Even if a DEPS file does not exist we still invoke ApplyRules | 257 # Even if a DEPS file does not exist we still invoke ApplyRules |
266 # to apply the implicit "allow" rule for the current directory | 258 # to apply the implicit "allow" rule for the current directory |
267 include_rules = local_scope.get(INCLUDE_RULES_VAR_NAME, []) | 259 include_rules = local_scope.get(INCLUDE_RULES_VAR_NAME, []) |
268 skip_subdirs = local_scope.get(SKIP_SUBDIRS_VAR_NAME, []) | 260 skip_subdirs = local_scope.get(SKIP_SUBDIRS_VAR_NAME, []) |
269 | 261 |
270 return (ApplyRules(existing_rules, include_rules, dir_name), skip_subdirs) | 262 return (ApplyRules(existing_rules, include_rules, dir_name), skip_subdirs) |
271 | 263 |
272 | 264 |
273 def ShouldCheckFile(file_name): | 265 def CheckDirectory(parent_rules, checkers, dir_name): |
274 """Returns True if the given file is a type we want to check.""" | |
275 checked_extensions = [ | |
276 '.h', | |
277 '.cc', | |
278 '.m', | |
279 '.mm', | |
280 ] | |
281 basename, extension = os.path.splitext(file_name) | |
282 return extension in checked_extensions | |
283 | |
284 | |
285 def CheckLine(rules, line): | |
286 """Checks the given file with the given rule set. | |
287 Returns a tuple (is_include, illegal_description). | |
288 If the line is an #include directive the first value will be True. | |
289 If it is also an illegal include, the second value will be a string describing | |
290 the error. Otherwise, it will be None.""" | |
291 found_item = EXTRACT_INCLUDE_PATH.match(line) | |
292 if not found_item: | |
293 return False, None # Not a match | |
294 | |
295 include_path = found_item.group(1) | |
296 | |
297 # Fix up backslashes in case somebody accidentally used them. | |
298 include_path.replace("\\", "/") | |
299 | |
300 if include_path.find("/") < 0: | |
301 # Don't fail when no directory is specified. We may want to be more | |
302 # strict about this in the future. | |
303 if VERBOSE: | |
304 print " WARNING: directory specified with no path: " + include_path | |
305 return True, None | |
306 | |
307 (allowed, why_failed) = rules.DirAllowed(include_path) | |
308 if not allowed: | |
309 if VERBOSE: | |
310 retval = "\nFor " + rules.__str__() | |
311 else: | |
312 retval = "" | |
313 return True, retval + ('Illegal include: "%s"\n Because of %s' % | |
314 (include_path, why_failed)) | |
315 | |
316 return True, None | |
317 | |
318 | |
319 def CheckFile(rules, file_name): | |
320 """Checks the given file with the given rule set. | |
321 | |
322 Args: | |
323 rules: The set of rules that apply to files in this directory. | |
324 file_name: The source file to check. | |
325 | |
326 Returns: Either a string describing the error if there was one, or None if | |
327 the file checked out OK. | |
328 """ | |
329 if VERBOSE: | |
330 print "Checking: " + file_name | |
331 | |
332 ret_val = "" # We'll collect the error messages in here | |
333 last_include = 0 | |
334 try: | |
335 cur_file = open(file_name, "r") | |
336 in_if0 = 0 | |
337 for line_num in xrange(sys.maxint): | |
338 if line_num - last_include > MAX_UNINTERESTING_LINES: | |
339 break | |
340 | |
341 cur_line = cur_file.readline(MAX_LINE_LENGTH) | |
342 if cur_line == "": | |
343 break | |
344 cur_line = cur_line.strip() | |
345 | |
346 # Check to see if we're at / inside a #if 0 block | |
347 if cur_line == '#if 0': | |
348 in_if0 += 1 | |
349 continue | |
350 if in_if0 > 0: | |
351 if cur_line.startswith('#if'): | |
352 in_if0 += 1 | |
353 elif cur_line == '#endif': | |
354 in_if0 -= 1 | |
355 continue | |
356 | |
357 is_include, line_status = CheckLine(rules, cur_line) | |
358 if is_include: | |
359 last_include = line_num | |
360 if line_status is not None: | |
361 if len(line_status) > 0: # Add newline to separate messages. | |
362 line_status += "\n" | |
363 ret_val += line_status | |
364 cur_file.close() | |
365 | |
366 except IOError: | |
367 if VERBOSE: | |
368 print "Unable to open file: " + file_name | |
369 cur_file.close() | |
370 | |
371 # Map empty string to None for easier checking. | |
372 if len(ret_val) == 0: | |
373 return None | |
374 return ret_val | |
375 | |
376 | |
377 def CheckDirectory(parent_rules, dir_name): | |
378 (rules, skip_subdirs) = ApplyDirectoryRules(parent_rules, dir_name) | 266 (rules, skip_subdirs) = ApplyDirectoryRules(parent_rules, dir_name) |
379 if rules == None: | 267 if rules == None: |
380 return True | 268 return True |
381 | 269 |
382 # Collect a list of all files and directories to check. | 270 # Collect a list of all files and directories to check. |
383 files_to_check = [] | 271 files_to_check = [] |
384 dirs_to_check = [] | 272 dirs_to_check = [] |
385 success = True | 273 success = True |
386 contents = os.listdir(dir_name) | 274 contents = os.listdir(dir_name) |
387 for cur in contents: | 275 for cur in contents: |
388 if cur in skip_subdirs: | 276 if cur in skip_subdirs: |
389 continue # Don't check children that DEPS has asked us to skip. | 277 continue # Don't check children that DEPS has asked us to skip. |
390 full_name = os.path.join(dir_name, cur) | 278 full_name = os.path.join(dir_name, cur) |
391 if os.path.isdir(full_name): | 279 if os.path.isdir(full_name): |
392 dirs_to_check.append(full_name) | 280 dirs_to_check.append(full_name) |
393 elif ShouldCheckFile(full_name): | 281 elif os.path.splitext(full_name)[1] in checkers: |
394 files_to_check.append(full_name) | 282 files_to_check.append(full_name) |
395 | 283 |
396 # First check all files in this directory. | 284 # First check all files in this directory. |
397 for cur in files_to_check: | 285 for cur in files_to_check: |
398 file_status = CheckFile(rules, cur) | 286 checker = checkers[os.path.splitext(cur)[1]] |
399 if file_status != None: | 287 file_status = checker.CheckFile(rules, cur) |
| 288 if file_status: |
400 print "ERROR in " + cur + "\n" + file_status | 289 print "ERROR in " + cur + "\n" + file_status |
401 success = False | 290 success = False |
402 | 291 |
403 # Next recurse into the subdirectories. | 292 # Next recurse into the subdirectories. |
404 for cur in dirs_to_check: | 293 for cur in dirs_to_check: |
405 if not CheckDirectory(rules, cur): | 294 if not CheckDirectory(rules, checkers, cur): |
406 success = False | 295 success = False |
407 | 296 |
408 return success | 297 return success |
409 | 298 |
410 | 299 |
411 def GetGitSourceDirectory(root): | 300 def GetGitSourceDirectory(root): |
412 """Returns a set of the directories to be checked. | 301 """Returns a set of the directories to be checked. |
413 | 302 |
414 Args: | 303 Args: |
415 root: The repository root where .git directory exists. | 304 root: The repository root where .git directory exists. |
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
481 # systems. Plus, we always use slashes here since the include parsing code | 370 # systems. Plus, we always use slashes here since the include parsing code |
482 # will also normalize to slashes. | 371 # will also normalize to slashes. |
483 BASE_DIRECTORY = BASE_DIRECTORY.lower() | 372 BASE_DIRECTORY = BASE_DIRECTORY.lower() |
484 BASE_DIRECTORY = BASE_DIRECTORY.replace("\\", "/") | 373 BASE_DIRECTORY = BASE_DIRECTORY.replace("\\", "/") |
485 start_dir = start_dir.replace("\\", "/") | 374 start_dir = start_dir.replace("\\", "/") |
486 | 375 |
487 if os.path.exists(os.path.join(BASE_DIRECTORY, ".git")): | 376 if os.path.exists(os.path.join(BASE_DIRECTORY, ".git")): |
488 global GIT_SOURCE_DIRECTORY | 377 global GIT_SOURCE_DIRECTORY |
489 GIT_SOURCE_DIRECTORY = GetGitSourceDirectory(BASE_DIRECTORY) | 378 GIT_SOURCE_DIRECTORY = GetGitSourceDirectory(BASE_DIRECTORY) |
490 | 379 |
491 success = CheckDirectory(base_rules, start_dir) | 380 java = java_checker.JavaChecker(BASE_DIRECTORY, VERBOSE) |
| 381 cpp = cpp_checker.CppChecker(VERBOSE) |
| 382 checkers = dict( |
| 383 (extension, checker) |
| 384 for checker in [java, cpp] for extension in checker.EXTENSIONS) |
| 385 success = CheckDirectory(base_rules, checkers, start_dir) |
492 if not success: | 386 if not success: |
493 print "\nFAILED\n" | 387 print "\nFAILED\n" |
494 return 1 | 388 return 1 |
495 print "\nSUCCESS\n" | 389 print "\nSUCCESS\n" |
496 return 0 | 390 return 0 |
497 | 391 |
498 | 392 |
499 def main(): | 393 def main(): |
500 option_parser = optparse.OptionParser() | 394 option_parser = optparse.OptionParser() |
501 option_parser.add_option("", "--root", default="", dest="base_directory", | 395 option_parser.add_option("", "--root", default="", dest="base_directory", |
502 help='Specifies the repository root. This defaults ' | 396 help='Specifies the repository root. This defaults ' |
503 'to "../../.." relative to the script file, which ' | 397 'to "../../.." relative to the script file, which ' |
504 'will normally be the repository root.') | 398 'will normally be the repository root.') |
505 option_parser.add_option("-v", "--verbose", action="store_true", | 399 option_parser.add_option("-v", "--verbose", action="store_true", |
506 default=False, help="Print debug logging") | 400 default=False, help="Print debug logging") |
507 options, args = option_parser.parse_args() | 401 options, args = option_parser.parse_args() |
508 return checkdeps(options, args) | 402 return checkdeps(options, args) |
509 | 403 |
510 | 404 |
511 if '__main__' == __name__: | 405 if '__main__' == __name__: |
512 sys.exit(main()) | 406 sys.exit(main()) |
OLD | NEW |