1#!/usr/bin/env python3 2# Copyright 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. 5 6"""Makes sure that files include headers from allowed directories. 7 8Checks DEPS files in the source tree for rules, and applies those rules to 9"#include" and "import" directives in the .cpp, .java, and .proto source files. 10Any source file including something not permitted by the DEPS files will fail. 11 12See README.md for a detailed description of the DEPS format. 13""" 14 15 16 17import os 18import optparse 19import re 20import sys 21 22import proto_checker 23import cpp_checker 24import java_checker 25import results 26 27from builddeps import DepsBuilder 28from rules import Rule, Rules 29 30 31def _IsTestFile(filename): 32 """Does a rudimentary check to try to skip test files; this could be 33 improved but is good enough for now. 34 """ 35 return re.match(r'(test|mock|dummy)_.*|.*_[a-z]*test\.(cc|mm|java)', filename) 36 37 38class DepsChecker(DepsBuilder): 39 """Parses include_rules from DEPS files and verifies files in the 40 source tree against them. 41 """ 42 43 def __init__(self, 44 base_directory=None, 45 extra_repos=[], 46 verbose=False, 47 being_tested=False, 48 ignore_temp_rules=False, 49 skip_tests=False, 50 resolve_dotdot=True): 51 """Creates a new DepsChecker. 52 53 Args: 54 base_directory: OS-compatible path to root of checkout, e.g. C:\chr\src. 55 verbose: Set to true for debug output. 56 being_tested: Set to true to ignore the DEPS file at 57 buildtools/checkdeps/DEPS. 58 ignore_temp_rules: Ignore rules that start with Rule.TEMP_ALLOW ("!"). 59 """ 60 DepsBuilder.__init__( 61 self, base_directory, extra_repos, verbose, being_tested, 62 ignore_temp_rules) 63 64 self._skip_tests = skip_tests 65 self._resolve_dotdot = resolve_dotdot 66 self.results_formatter = results.NormalResultsFormatter(verbose) 67 68 def Report(self): 69 """Prints a report of results, and returns an exit code for the process.""" 70 if self.results_formatter.GetResults(): 71 self.results_formatter.PrintResults() 72 return 1 73 print('\nSUCCESS\n') 74 return 0 75 76 def CheckDirectory(self, start_dir): 77 """Checks all relevant source files in the specified directory and 78 its subdirectories for compliance with DEPS rules throughout the 79 tree (starting at |self.base_directory|). |start_dir| must be a 80 subdirectory of |self.base_directory|. 81 82 On completion, self.results_formatter has the results of 83 processing, and calling Report() will print a report of results. 84 """ 85 java = java_checker.JavaChecker(self.base_directory, self.verbose) 86 cpp = cpp_checker.CppChecker( 87 self.verbose, self._resolve_dotdot, self.base_directory) 88 proto = proto_checker.ProtoChecker( 89 self.verbose, self._resolve_dotdot, self.base_directory) 90 checkers = dict( 91 (extension, checker) 92 for checker in [java, cpp, proto] for extension in checker.EXTENSIONS) 93 94 for rules, file_paths in self.GetAllRulesAndFiles(start_dir): 95 for full_name in file_paths: 96 if self._skip_tests and _IsTestFile(os.path.basename(full_name)): 97 continue 98 file_extension = os.path.splitext(full_name)[1] 99 if not file_extension in checkers: 100 continue 101 checker = checkers[file_extension] 102 file_status = checker.CheckFile(rules, full_name) 103 if file_status.HasViolations(): 104 self.results_formatter.AddError(file_status) 105 106 def CheckIncludesAndImports(self, added_lines, checker): 107 """Check new import/#include statements added in the change 108 being presubmit checked. 109 110 Args: 111 added_lines: ((file_path, (changed_line, changed_line, ...), ...) 112 checker: CppChecker/JavaChecker/ProtoChecker checker instance 113 114 Return: 115 A list of tuples, (bad_file_path, rule_type, rule_description) 116 where rule_type is one of Rule.DISALLOW or Rule.TEMP_ALLOW and 117 rule_description is human-readable. Empty if no problems. 118 """ 119 problems = [] 120 for file_path, changed_lines in added_lines: 121 if not checker.ShouldCheck(file_path): 122 continue 123 rules_for_file = self.GetDirectoryRules(os.path.dirname(file_path)) 124 if not rules_for_file: 125 continue 126 for line in changed_lines: 127 is_include, violation = checker.CheckLine( 128 rules_for_file, line, file_path, True) 129 if not violation: 130 continue 131 rule_type = violation.violated_rule.allow 132 if rule_type == Rule.ALLOW: 133 continue 134 violation_text = results.NormalResultsFormatter.FormatViolation( 135 violation, self.verbose) 136 problems.append((file_path, rule_type, violation_text)) 137 return problems 138 139 def CheckAddedCppIncludes(self, added_includes): 140 """This is used from PRESUBMIT.py to check new #include statements added in 141 the change being presubmit checked. 142 143 Args: 144 added_includes: ((file_path, (include_line, include_line, ...), ...) 145 146 Return: 147 A list of tuples, (bad_file_path, rule_type, rule_description) 148 where rule_type is one of Rule.DISALLOW or Rule.TEMP_ALLOW and 149 rule_description is human-readable. Empty if no problems. 150 """ 151 return self.CheckIncludesAndImports( 152 added_includes, cpp_checker.CppChecker(self.verbose)) 153 154 def CheckAddedJavaImports(self, added_imports, 155 allow_multiple_definitions=None): 156 """This is used from PRESUBMIT.py to check new import statements added in 157 the change being presubmit checked. 158 159 Args: 160 added_imports: ((file_path, (import_line, import_line, ...), ...) 161 allow_multiple_definitions: [file_name, file_name, ...]. List of java 162 file names allowing multiple definitions in 163 presubmit check. 164 165 Return: 166 A list of tuples, (bad_file_path, rule_type, rule_description) 167 where rule_type is one of Rule.DISALLOW or Rule.TEMP_ALLOW and 168 rule_description is human-readable. Empty if no problems. 169 """ 170 return self.CheckIncludesAndImports( 171 added_imports, 172 java_checker.JavaChecker(self.base_directory, self.verbose, 173 added_imports, allow_multiple_definitions)) 174 175 def CheckAddedProtoImports(self, added_imports): 176 """This is used from PRESUBMIT.py to check new #import statements added in 177 the change being presubmit checked. 178 179 Args: 180 added_imports : ((file_path, (import_line, import_line, ...), ...) 181 182 Return: 183 A list of tuples, (bad_file_path, rule_type, rule_description) 184 where rule_type is one of Rule.DISALLOW or Rule.TEMP_ALLOW and 185 rule_description is human-readable. Empty if no problems. 186 """ 187 return self.CheckIncludesAndImports( 188 added_imports, proto_checker.ProtoChecker( 189 verbose=self.verbose, root_dir=self.base_directory)) 190 191def PrintUsage(): 192 print("""Usage: python checkdeps.py [--root <root>] [tocheck] 193 194 --root ROOT Specifies the repository root. This defaults to "../../.." 195 relative to the script file. This will be correct given the 196 normal location of the script in "<root>/buildtools/checkdeps". 197 198 --(others) There are a few lesser-used options; run with --help to show them. 199 200 tocheck Specifies the directory, relative to root, to check. This defaults 201 to "." so it checks everything. 202 203Examples: 204 python checkdeps.py 205 python checkdeps.py --root c:\\source chrome""") 206 207 208def main(): 209 option_parser = optparse.OptionParser() 210 option_parser.add_option( 211 '', '--root', 212 default='', dest='base_directory', 213 help='Specifies the repository root. This defaults ' 214 'to "../../.." relative to the script file, which ' 215 'will normally be the repository root.') 216 option_parser.add_option( 217 '', '--extra-repos', 218 action='append', dest='extra_repos', default=[], 219 help='Specifies extra repositories relative to root repository.') 220 option_parser.add_option( 221 '', '--ignore-temp-rules', 222 action='store_true', dest='ignore_temp_rules', default=False, 223 help='Ignore !-prefixed (temporary) rules.') 224 option_parser.add_option( 225 '', '--generate-temp-rules', 226 action='store_true', dest='generate_temp_rules', default=False, 227 help='Print rules to temporarily allow files that fail ' 228 'dependency checking.') 229 option_parser.add_option( 230 '', '--count-violations', 231 action='store_true', dest='count_violations', default=False, 232 help='Count #includes in violation of intended rules.') 233 option_parser.add_option( 234 '', '--skip-tests', 235 action='store_true', dest='skip_tests', default=False, 236 help='Skip checking test files (best effort).') 237 option_parser.add_option( 238 '-v', '--verbose', 239 action='store_true', default=False, 240 help='Print debug logging') 241 option_parser.add_option( 242 '', '--json', 243 help='Path to JSON output file') 244 option_parser.add_option( 245 '', '--no-resolve-dotdot', 246 action='store_false', dest='resolve_dotdot', default=True, 247 help='resolve leading ../ in include directive paths relative ' 248 'to the file perfoming the inclusion.') 249 250 options, args = option_parser.parse_args() 251 252 deps_checker = DepsChecker(options.base_directory, 253 extra_repos=options.extra_repos, 254 verbose=options.verbose, 255 ignore_temp_rules=options.ignore_temp_rules, 256 skip_tests=options.skip_tests, 257 resolve_dotdot=options.resolve_dotdot) 258 base_directory = deps_checker.base_directory # Default if needed, normalized 259 260 # Figure out which directory we have to check. 261 start_dir = base_directory 262 if len(args) == 1: 263 # Directory specified. Start here. It's supposed to be relative to the 264 # base directory. 265 start_dir = os.path.abspath(os.path.join(base_directory, args[0])) 266 elif len(args) >= 2 or (options.generate_temp_rules and 267 options.count_violations): 268 # More than one argument, or incompatible flags, we don't handle this. 269 PrintUsage() 270 return 1 271 272 if not start_dir.startswith(deps_checker.base_directory): 273 print('Directory to check must be a subdirectory of the base directory,') 274 print('but %s is not a subdirectory of %s' % (start_dir, base_directory)) 275 return 1 276 277 print('Using base directory:', base_directory) 278 print('Checking:', start_dir) 279 280 if options.generate_temp_rules: 281 deps_checker.results_formatter = results.TemporaryRulesFormatter() 282 elif options.count_violations: 283 deps_checker.results_formatter = results.CountViolationsFormatter() 284 285 if options.json: 286 deps_checker.results_formatter = results.JSONResultsFormatter( 287 options.json, deps_checker.results_formatter) 288 289 deps_checker.CheckDirectory(start_dir) 290 return deps_checker.Report() 291 292 293if '__main__' == __name__: 294 sys.exit(main()) 295