• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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