#!/usr/bin/env python3
# Copyright 2012 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Makes sure that files include headers from allowed directories.
Checks DEPS files in the source tree for rules, and applies those rules to
"#include" and "import" directives in the .cpp, .java, and .proto source files.
Any source file including something not permitted by the DEPS files will fail.
See README.md for a detailed description of the DEPS format.
"""
import os
import optparse
import re
import sys
import proto_checker
import cpp_checker
import java_checker
import results
from builddeps import DepsBuilder
from rules import Rule, Rules
def _IsTestFile(filename):
"""Does a rudimentary check to try to skip test files; this could be
improved but is good enough for now.
"""
return re.match(r'(test|mock|dummy)_.*|.*_[a-z]*test\.(cc|mm|java)', filename)
class DepsChecker(DepsBuilder):
"""Parses include_rules from DEPS files and verifies files in the
source tree against them.
"""
def __init__(self,
base_directory=None,
extra_repos=[],
verbose=False,
being_tested=False,
ignore_temp_rules=False,
skip_tests=False,
resolve_dotdot=True):
"""Creates a new DepsChecker.
Args:
base_directory: OS-compatible path to root of checkout, e.g. C:\chr\src.
verbose: Set to true for debug output.
being_tested: Set to true to ignore the DEPS file at
buildtools/checkdeps/DEPS.
ignore_temp_rules: Ignore rules that start with Rule.TEMP_ALLOW ("!").
"""
DepsBuilder.__init__(
self, base_directory, extra_repos, verbose, being_tested,
ignore_temp_rules)
self._skip_tests = skip_tests
self._resolve_dotdot = resolve_dotdot
self.results_formatter = results.NormalResultsFormatter(verbose)
def Report(self):
"""Prints a report of results, and returns an exit code for the process."""
if self.results_formatter.GetResults():
self.results_formatter.PrintResults()
return 1
print('\nSUCCESS\n')
return 0
def CheckDirectory(self, start_dir):
"""Checks all relevant source files in the specified directory and
its subdirectories for compliance with DEPS rules throughout the
tree (starting at |self.base_directory|). |start_dir| must be a
subdirectory of |self.base_directory|.
On completion, self.results_formatter has the results of
processing, and calling Report() will print a report of results.
"""
java = java_checker.JavaChecker(self.base_directory, self.verbose)
cpp = cpp_checker.CppChecker(
self.verbose, self._resolve_dotdot, self.base_directory)
proto = proto_checker.ProtoChecker(
self.verbose, self._resolve_dotdot, self.base_directory)
checkers = dict(
(extension, checker)
for checker in [java, cpp, proto] for extension in checker.EXTENSIONS)
for rules, file_paths in self.GetAllRulesAndFiles(start_dir):
for full_name in file_paths:
if self._skip_tests and _IsTestFile(os.path.basename(full_name)):
continue
file_extension = os.path.splitext(full_name)[1]
if not file_extension in checkers:
continue
checker = checkers[file_extension]
file_status = checker.CheckFile(rules, full_name)
if file_status.HasViolations():
self.results_formatter.AddError(file_status)
def CheckIncludesAndImports(self, added_lines, checker):
"""Check new import/#include statements added in the change
being presubmit checked.
Args:
added_lines: ((file_path, (changed_line, changed_line, ...), ...)
checker: CppChecker/JavaChecker/ProtoChecker checker instance
Return:
A list of tuples, (bad_file_path, rule_type, rule_description)
where rule_type is one of Rule.DISALLOW or Rule.TEMP_ALLOW and
rule_description is human-readable. Empty if no problems.
"""
problems = []
for file_path, changed_lines in added_lines:
if not checker.ShouldCheck(file_path):
continue
rules_for_file = self.GetDirectoryRules(os.path.dirname(file_path))
if not rules_for_file:
continue
for line in changed_lines:
is_include, violation = checker.CheckLine(
rules_for_file, line, file_path, True)
if not violation:
continue
rule_type = violation.violated_rule.allow
if rule_type == Rule.ALLOW:
continue
violation_text = results.NormalResultsFormatter.FormatViolation(
violation, self.verbose)
problems.append((file_path, rule_type, violation_text))
return problems
def CheckAddedCppIncludes(self, added_includes):
"""This is used from PRESUBMIT.py to check new #include statements added in
the change being presubmit checked.
Args:
added_includes: ((file_path, (include_line, include_line, ...), ...)
Return:
A list of tuples, (bad_file_path, rule_type, rule_description)
where rule_type is one of Rule.DISALLOW or Rule.TEMP_ALLOW and
rule_description is human-readable. Empty if no problems.
"""
return self.CheckIncludesAndImports(
added_includes, cpp_checker.CppChecker(self.verbose))
def CheckAddedJavaImports(self, added_imports,
allow_multiple_definitions=None):
"""This is used from PRESUBMIT.py to check new import statements added in
the change being presubmit checked.
Args:
added_imports: ((file_path, (import_line, import_line, ...), ...)
allow_multiple_definitions: [file_name, file_name, ...]. List of java
file names allowing multiple definitions in
presubmit check.
Return:
A list of tuples, (bad_file_path, rule_type, rule_description)
where rule_type is one of Rule.DISALLOW or Rule.TEMP_ALLOW and
rule_description is human-readable. Empty if no problems.
"""
return self.CheckIncludesAndImports(
added_imports,
java_checker.JavaChecker(self.base_directory, self.verbose,
added_imports, allow_multiple_definitions))
def CheckAddedProtoImports(self, added_imports):
"""This is used from PRESUBMIT.py to check new #import statements added in
the change being presubmit checked.
Args:
added_imports : ((file_path, (import_line, import_line, ...), ...)
Return:
A list of tuples, (bad_file_path, rule_type, rule_description)
where rule_type is one of Rule.DISALLOW or Rule.TEMP_ALLOW and
rule_description is human-readable. Empty if no problems.
"""
return self.CheckIncludesAndImports(
added_imports, proto_checker.ProtoChecker(
verbose=self.verbose, root_dir=self.base_directory))
def PrintUsage():
print("""Usage: python checkdeps.py [--root <root>] [tocheck]
--root ROOT Specifies the repository root. This defaults to "../../.."
relative to the script file. This will be correct given the
normal location of the script in "<root>/buildtools/checkdeps".
--(others) There are a few lesser-used options; run with --help to show them.
tocheck Specifies the directory, relative to root, to check. This defaults
to "." so it checks everything.
Examples:
python checkdeps.py
python checkdeps.py --root c:\\source chrome""")
def main():
option_parser = optparse.OptionParser()
option_parser.add_option(
'', '--root',
default='', dest='base_directory',
help='Specifies the repository root. This defaults '
'to "../../.." relative to the script file, which '
'will normally be the repository root.')
option_parser.add_option(
'', '--extra-repos',
action='append', dest='extra_repos', default=[],
help='Specifies extra repositories relative to root repository.')
option_parser.add_option(
'', '--ignore-temp-rules',
action='store_true', dest='ignore_temp_rules', default=False,
help='Ignore !-prefixed (temporary) rules.')
option_parser.add_option(
'', '--generate-temp-rules',
action='store_true', dest='generate_temp_rules', default=False,
help='Print rules to temporarily allow files that fail '
'dependency checking.')
option_parser.add_option(
'', '--count-violations',
action='store_true', dest='count_violations', default=False,
help='Count #includes in violation of intended rules.')
option_parser.add_option(
'', '--skip-tests',
action='store_true', dest='skip_tests', default=False,
help='Skip checking test files (best effort).')
option_parser.add_option(
'-v', '--verbose',
action='store_true', default=False,
help='Print debug logging')
option_parser.add_option(
'', '--json',
help='Path to JSON output file')
option_parser.add_option(
'', '--no-resolve-dotdot',
action='store_false', dest='resolve_dotdot', default=True,
help='resolve leading ../ in include directive paths relative '
'to the file perfoming the inclusion.')
options, args = option_parser.parse_args()
deps_checker = DepsChecker(options.base_directory,
extra_repos=options.extra_repos,
verbose=options.verbose,
ignore_temp_rules=options.ignore_temp_rules,
skip_tests=options.skip_tests,
resolve_dotdot=options.resolve_dotdot)
base_directory = deps_checker.base_directory # Default if needed, normalized
# Figure out which directory we have to check.
start_dir = base_directory
if len(args) == 1:
# Directory specified. Start here. It's supposed to be relative to the
# base directory.
start_dir = os.path.abspath(os.path.join(base_directory, args[0]))
elif len(args) >= 2 or (options.generate_temp_rules and
options.count_violations):
# More than one argument, or incompatible flags, we don't handle this.
PrintUsage()
return 1
if not start_dir.startswith(deps_checker.base_directory):
print('Directory to check must be a subdirectory of the base directory,')
print('but %s is not a subdirectory of %s' % (start_dir, base_directory))
return 1
print('Using base directory:', base_directory)
print('Checking:', start_dir)
if options.generate_temp_rules:
deps_checker.results_formatter = results.TemporaryRulesFormatter()
elif options.count_violations:
deps_checker.results_formatter = results.CountViolationsFormatter()
if options.json:
deps_checker.results_formatter = results.JSONResultsFormatter(
options.json, deps_checker.results_formatter)
deps_checker.CheckDirectory(start_dir)
return deps_checker.Report()
if '__main__' == __name__:
sys.exit(main())