Home | History | Annotate | Download | only in checkdeps
      1 #!/usr/bin/env python
      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
      4 # found in the LICENSE file.
      5 
      6 """Makes sure that files include headers from allowed directories.
      7 
      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
     10 permitted by the DEPS files will fail.
     11 
     12 The format of the deps file:
     13 
     14 First you have the normal module-level deps. These are the ones used by
     15 gclient. An example would be:
     16 
     17   deps = {
     18     "base":"http://foo.bar/trunk/base"
     19   }
     20 
     21 DEPS files not in the top-level of a module won't need this. Then you
     22 have any additional include rules. You can add (using "+") or subtract
     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.
     32 
     33   include_rules = {
     34     # Code should be able to use base (it's specified in the module-level
     35     # deps above), but nothing in "base/evil" because it's evil.
     36     "-base/evil",
     37 
     38     # But this one subdirectory of evil is OK.
     39     "+base/evil/not",
     40 
     41     # And it can include files from this other directory even though there is
     42     # no deps rule for it.
     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",
     48   }
     49 
     50 If you have certain include rules that should only be applied for some
     51 files within this directory and subdirectories, you can write a
     52 section named specific_include_rules that is a hash map of regular
     53 expressions to the list of rules that should apply to files matching
     54 them.  Note that such rules will always be applied before the rules
     55 from 'include_rules' have been applied, but the order in which rules
     56 associated with different regular expressions is applied is arbitrary.
     57 
     58   specific_include_rules = {
     59     ".*_(unit|browser|api)test\.cc": [
     60       "+libraries/testsupport",
     61     ],
     62   }
     63 
     64 DEPS files may be placed anywhere in the tree. Each one applies to all
     65 subdirectories, where there may be more DEPS files that provide additions or
     66 subtractions for their own sub-trees.
     67 
     68 There is an implicit rule for the current directory (where the DEPS file lives)
     69 and all of its subdirectories. This prevents you from having to explicitly
     70 allow the current directory everywhere.  This implicit rule is applied first,
     71 so you can modify or remove it using the normal include rules.
     72 
     73 The rules are processed in order. This means you can explicitly allow a higher
     74 directory and then take away permissions from sub-parts, or the reverse.
     75 
     76 Note that all directory separators must be slashes (Unix-style) and not
     77 backslashes. All directories should be relative to the source root and use
     78 only lowercase.
     79 """
     80 
     81 import os
     82 import optparse
     83 import re
     84 import subprocess
     85 import sys
     86 import copy
     87 
     88 import cpp_checker
     89 import java_checker
     90 import results
     91 from rules import Rule, Rules
     92 
     93 
     94 # Variable name used in the DEPS file to add or subtract include files from
     95 # the module-level deps.
     96 INCLUDE_RULES_VAR_NAME = 'include_rules'
     97 
     98 # Variable name used in the DEPS file to add or subtract include files
     99 # from module-level deps specific to files whose basename (last
    100 # component of path) matches a given regular expression.
    101 SPECIFIC_INCLUDE_RULES_VAR_NAME = 'specific_include_rules'
    102 
    103 # Optionally present in the DEPS file to list subdirectories which should not
    104 # be checked. This allows us to skip third party code, for example.
    105 SKIP_SUBDIRS_VAR_NAME = 'skip_child_includes'
    106 
    107 
    108 def NormalizePath(path):
    109   """Returns a path normalized to how we write DEPS rules and compare paths.
    110   """
    111   return path.lower().replace('\\', '/')
    112 
    113 
    114 def _IsTestFile(filename):
    115   """Does a rudimentary check to try to skip test files; this could be
    116   improved but is good enough for now.
    117   """
    118   return re.match('(test|mock|dummy)_.*|.*_[a-z]*test\.(cc|mm|java)', filename)
    119 
    120 
    121 class DepsChecker(object):
    122   """Parses include_rules from DEPS files and can verify files in the
    123   source tree against them.
    124   """
    125 
    126   def __init__(self,
    127                base_directory=None,
    128                verbose=False,
    129                being_tested=False,
    130                ignore_temp_rules=False,
    131                skip_tests=False):
    132     """Creates a new DepsChecker.
    133 
    134     Args:
    135       base_directory: OS-compatible path to root of checkout, e.g. C:\chr\src.
    136       verbose: Set to true for debug output.
    137       being_tested: Set to true to ignore the DEPS file at tools/checkdeps/DEPS.
    138     """
    139     self.base_directory = base_directory
    140     self.verbose = verbose
    141     self._under_test = being_tested
    142     self._ignore_temp_rules = ignore_temp_rules
    143     self._skip_tests = skip_tests
    144 
    145     if not base_directory:
    146       self.base_directory = os.path.abspath(
    147         os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', '..'))
    148 
    149     self.results_formatter = results.NormalResultsFormatter(verbose)
    150 
    151     self.git_source_directories = set()
    152     self._AddGitSourceDirectories()
    153 
    154     # Map of normalized directory paths to rules to use for those
    155     # directories, or None for directories that should be skipped.
    156     self.directory_rules = {}
    157     self._ApplyDirectoryRulesAndSkipSubdirs(Rules(), self.base_directory)
    158 
    159   def Report(self):
    160     """Prints a report of results, and returns an exit code for the process."""
    161     if self.results_formatter.GetResults():
    162       self.results_formatter.PrintResults()
    163       return 1
    164     print '\nSUCCESS\n'
    165     return 0
    166 
    167   def _ApplyRules(self, existing_rules, includes, specific_includes, cur_dir):
    168     """Applies the given include rules, returning the new rules.
    169 
    170     Args:
    171       existing_rules: A set of existing rules that will be combined.
    172       include: The list of rules from the "include_rules" section of DEPS.
    173       specific_includes: E.g. {'.*_unittest\.cc': ['+foo', '-blat']} rules
    174                          from the "specific_include_rules" section of DEPS.
    175       cur_dir: The current directory, normalized path. We will create an
    176                implicit rule that allows inclusion from this directory.
    177 
    178     Returns: A new set of rules combining the existing_rules with the other
    179              arguments.
    180     """
    181     rules = copy.deepcopy(existing_rules)
    182 
    183     # First apply the implicit "allow" rule for the current directory.
    184     if cur_dir.startswith(
    185           NormalizePath(os.path.normpath(self.base_directory))):
    186       relative_dir = cur_dir[len(self.base_directory) + 1:]
    187 
    188       source = relative_dir
    189       if len(source) == 0:
    190         source = 'top level'  # Make the help string a little more meaningful.
    191       rules.AddRule('+' + relative_dir, 'Default rule for ' + source)
    192     else:
    193       raise Exception('Internal error: base directory is not at the beginning' +
    194                       ' for\n  %s and base dir\n  %s' %
    195                       (cur_dir, self.base_directory))
    196 
    197     def ApplyOneRule(rule_str, dependee_regexp=None):
    198       """Deduces a sensible description for the rule being added, and
    199       adds the rule with its description to |rules|.
    200 
    201       If we are ignoring temporary rules, this function does nothing
    202       for rules beginning with the Rule.TEMP_ALLOW character.
    203       """
    204       if self._ignore_temp_rules and rule_str.startswith(Rule.TEMP_ALLOW):
    205         return
    206 
    207       rule_block_name = 'include_rules'
    208       if dependee_regexp:
    209         rule_block_name = 'specific_include_rules'
    210       if not relative_dir:
    211         rule_description = 'the top level %s' % rule_block_name
    212       else:
    213         rule_description = relative_dir + "'s %s" % rule_block_name
    214       rules.AddRule(rule_str, rule_description, dependee_regexp)
    215 
    216     # Apply the additional explicit rules.
    217     for (_, rule_str) in enumerate(includes):
    218       ApplyOneRule(rule_str)
    219 
    220     # Finally, apply the specific rules.
    221     for regexp, specific_rules in specific_includes.iteritems():
    222       for rule_str in specific_rules:
    223         ApplyOneRule(rule_str, regexp)
    224 
    225     return rules
    226 
    227   def _ApplyDirectoryRules(self, existing_rules, dir_name):
    228     """Combines rules from the existing rules and the new directory.
    229 
    230     Any directory can contain a DEPS file. Toplevel DEPS files can contain
    231     module dependencies which are used by gclient. We use these, along with
    232     additional include rules and implicit rules for the given directory, to
    233     come up with a combined set of rules to apply for the directory.
    234 
    235     Args:
    236       existing_rules: The rules for the parent directory. We'll add-on to these.
    237       dir_name: The directory name that the deps file may live in (if
    238                 it exists).  This will also be used to generate the
    239                 implicit rules.  This is a non-normalized path.
    240 
    241     Returns: A tuple containing: (1) the combined set of rules to apply to the
    242              sub-tree, and (2) a list of all subdirectories that should NOT be
    243              checked, as specified in the DEPS file (if any).
    244     """
    245     norm_dir_name = NormalizePath(dir_name)
    246 
    247     # Check for a .svn directory in this directory or check this directory is
    248     # contained in git source direcotries. This will tell us if it's a source
    249     # directory and should be checked.
    250     if not (os.path.exists(os.path.join(dir_name, ".svn")) or
    251             (norm_dir_name in self.git_source_directories)):
    252       return (None, [])
    253 
    254     # Check the DEPS file in this directory.
    255     if self.verbose:
    256       print 'Applying rules from', dir_name
    257     def FromImpl(_unused, _unused2):
    258       pass  # NOP function so "From" doesn't fail.
    259 
    260     def FileImpl(_unused):
    261       pass  # NOP function so "File" doesn't fail.
    262 
    263     class _VarImpl:
    264       def __init__(self, local_scope):
    265         self._local_scope = local_scope
    266 
    267       def Lookup(self, var_name):
    268         """Implements the Var syntax."""
    269         if var_name in self._local_scope.get('vars', {}):
    270           return self._local_scope['vars'][var_name]
    271         raise Exception('Var is not defined: %s' % var_name)
    272 
    273     local_scope = {}
    274     global_scope = {
    275         'File': FileImpl,
    276         'From': FromImpl,
    277         'Var': _VarImpl(local_scope).Lookup,
    278         }
    279     deps_file = os.path.join(dir_name, 'DEPS')
    280 
    281     # The second conditional here is to disregard the
    282     # tools/checkdeps/DEPS file while running tests.  This DEPS file
    283     # has a skip_child_includes for 'testdata' which is necessary for
    284     # running production tests, since there are intentional DEPS
    285     # violations under the testdata directory.  On the other hand when
    286     # running tests, we absolutely need to verify the contents of that
    287     # directory to trigger those intended violations and see that they
    288     # are handled correctly.
    289     if os.path.isfile(deps_file) and (
    290         not self._under_test or not os.path.split(dir_name)[1] == 'checkdeps'):
    291       execfile(deps_file, global_scope, local_scope)
    292     elif self.verbose:
    293       print '  No deps file found in', dir_name
    294 
    295     # Even if a DEPS file does not exist we still invoke ApplyRules
    296     # to apply the implicit "allow" rule for the current directory
    297     include_rules = local_scope.get(INCLUDE_RULES_VAR_NAME, [])
    298     specific_include_rules = local_scope.get(SPECIFIC_INCLUDE_RULES_VAR_NAME,
    299                                              {})
    300     skip_subdirs = local_scope.get(SKIP_SUBDIRS_VAR_NAME, [])
    301 
    302     return (self._ApplyRules(existing_rules, include_rules,
    303                              specific_include_rules, norm_dir_name),
    304             skip_subdirs)
    305 
    306   def _ApplyDirectoryRulesAndSkipSubdirs(self, parent_rules, dir_path):
    307     """Given |parent_rules| and a subdirectory |dir_path| from the
    308     directory that owns the |parent_rules|, add |dir_path|'s rules to
    309     |self.directory_rules|, and add None entries for any of its
    310     subdirectories that should be skipped.
    311     """
    312     directory_rules, excluded_subdirs = self._ApplyDirectoryRules(parent_rules,
    313                                                                   dir_path)
    314     self.directory_rules[NormalizePath(dir_path)] = directory_rules
    315     for subdir in excluded_subdirs:
    316       self.directory_rules[NormalizePath(
    317           os.path.normpath(os.path.join(dir_path, subdir)))] = None
    318 
    319   def GetDirectoryRules(self, dir_path):
    320     """Returns a Rules object to use for the given directory, or None
    321     if the given directory should be skipped.  This takes care of
    322     first building rules for parent directories (up to
    323     self.base_directory) if needed.
    324 
    325     Args:
    326       dir_path: A real (non-normalized) path to the directory you want
    327       rules for.
    328     """
    329     norm_dir_path = NormalizePath(dir_path)
    330 
    331     if not norm_dir_path.startswith(
    332         NormalizePath(os.path.normpath(self.base_directory))):
    333       dir_path = os.path.join(self.base_directory, dir_path)
    334       norm_dir_path = NormalizePath(dir_path)
    335 
    336     parent_dir = os.path.dirname(dir_path)
    337     parent_rules = None
    338     if not norm_dir_path in self.directory_rules:
    339       parent_rules = self.GetDirectoryRules(parent_dir)
    340 
    341     # We need to check for an entry for our dir_path again, in case we
    342     # are at a path e.g. A/B/C where A/B/DEPS specifies the C
    343     # subdirectory to be skipped; in this case, the invocation to
    344     # GetDirectoryRules(parent_dir) has already filled in an entry for
    345     # A/B/C.
    346     if not norm_dir_path in self.directory_rules:
    347       if not parent_rules:
    348         # If the parent directory should be skipped, then the current
    349         # directory should also be skipped.
    350         self.directory_rules[norm_dir_path] = None
    351       else:
    352         self._ApplyDirectoryRulesAndSkipSubdirs(parent_rules, dir_path)
    353     return self.directory_rules[norm_dir_path]
    354 
    355   def CheckDirectory(self, start_dir):
    356     """Checks all relevant source files in the specified directory and
    357     its subdirectories for compliance with DEPS rules throughout the
    358     tree (starting at |self.base_directory|).  |start_dir| must be a
    359     subdirectory of |self.base_directory|.
    360 
    361     On completion, self.results_formatter has the results of
    362     processing, and calling Report() will print a report of results.
    363     """
    364     java = java_checker.JavaChecker(self.base_directory, self.verbose)
    365     cpp = cpp_checker.CppChecker(self.verbose)
    366     checkers = dict(
    367         (extension, checker)
    368         for checker in [java, cpp] for extension in checker.EXTENSIONS)
    369     self._CheckDirectoryImpl(checkers, start_dir)
    370 
    371   def _CheckDirectoryImpl(self, checkers, dir_name):
    372     rules = self.GetDirectoryRules(dir_name)
    373     if rules == None:
    374       return
    375 
    376     # Collect a list of all files and directories to check.
    377     files_to_check = []
    378     dirs_to_check = []
    379     contents = os.listdir(dir_name)
    380     for cur in contents:
    381       full_name = os.path.join(dir_name, cur)
    382       if os.path.isdir(full_name):
    383         dirs_to_check.append(full_name)
    384       elif os.path.splitext(full_name)[1] in checkers:
    385         if not self._skip_tests or not _IsTestFile(cur):
    386           files_to_check.append(full_name)
    387 
    388     # First check all files in this directory.
    389     for cur in files_to_check:
    390       checker = checkers[os.path.splitext(cur)[1]]
    391       file_status = checker.CheckFile(rules, cur)
    392       if file_status.HasViolations():
    393         self.results_formatter.AddError(file_status)
    394 
    395     # Next recurse into the subdirectories.
    396     for cur in dirs_to_check:
    397       self._CheckDirectoryImpl(checkers, cur)
    398 
    399   def CheckAddedCppIncludes(self, added_includes):
    400     """This is used from PRESUBMIT.py to check new #include statements added in
    401     the change being presubmit checked.
    402 
    403     Args:
    404       added_includes: ((file_path, (include_line, include_line, ...), ...)
    405 
    406     Return:
    407       A list of tuples, (bad_file_path, rule_type, rule_description)
    408       where rule_type is one of Rule.DISALLOW or Rule.TEMP_ALLOW and
    409       rule_description is human-readable. Empty if no problems.
    410     """
    411     cpp = cpp_checker.CppChecker(self.verbose)
    412     problems = []
    413     for file_path, include_lines in added_includes:
    414       if not cpp.IsCppFile(file_path):
    415         pass
    416       rules_for_file = self.GetDirectoryRules(os.path.dirname(file_path))
    417       if rules_for_file:
    418         for line in include_lines:
    419           is_include, violation = cpp.CheckLine(
    420               rules_for_file, line, file_path, True)
    421           if violation:
    422             rule_type = violation.violated_rule.allow
    423             if rule_type != Rule.ALLOW:
    424               violation_text = results.NormalResultsFormatter.FormatViolation(
    425                   violation, self.verbose)
    426               problems.append((file_path, rule_type, violation_text))
    427     return problems
    428 
    429   def _AddGitSourceDirectories(self):
    430     """Adds any directories containing sources managed by git to
    431     self.git_source_directories.
    432     """
    433     if not os.path.exists(os.path.join(self.base_directory, '.git')):
    434       return
    435 
    436     popen_out = os.popen('cd %s && git ls-files --full-name .' %
    437                          subprocess.list2cmdline([self.base_directory]))
    438     for line in popen_out.readlines():
    439       dir_name = os.path.join(self.base_directory, os.path.dirname(line))
    440       # Add the directory as well as all the parent directories. Use
    441       # forward slashes and lower case to normalize paths.
    442       while dir_name != self.base_directory:
    443         self.git_source_directories.add(NormalizePath(dir_name))
    444         dir_name = os.path.dirname(dir_name)
    445     self.git_source_directories.add(NormalizePath(self.base_directory))
    446 
    447 
    448 def PrintUsage():
    449   print """Usage: python checkdeps.py [--root <root>] [tocheck]
    450 
    451   --root ROOT Specifies the repository root. This defaults to "../../.."
    452               relative to the script file. This will be correct given the
    453               normal location of the script in "<root>/tools/checkdeps".
    454 
    455   --(others)  There are a few lesser-used options; run with --help to show them.
    456 
    457   tocheck  Specifies the directory, relative to root, to check. This defaults
    458            to "." so it checks everything.
    459 
    460 Examples:
    461   python checkdeps.py
    462   python checkdeps.py --root c:\\source chrome"""
    463 
    464 
    465 def main():
    466   option_parser = optparse.OptionParser()
    467   option_parser.add_option(
    468       '', '--root',
    469       default='', dest='base_directory',
    470       help='Specifies the repository root. This defaults '
    471            'to "../../.." relative to the script file, which '
    472            'will normally be the repository root.')
    473   option_parser.add_option(
    474       '', '--ignore-temp-rules',
    475       action='store_true', dest='ignore_temp_rules', default=False,
    476       help='Ignore !-prefixed (temporary) rules.')
    477   option_parser.add_option(
    478       '', '--generate-temp-rules',
    479       action='store_true', dest='generate_temp_rules', default=False,
    480       help='Print rules to temporarily allow files that fail '
    481            'dependency checking.')
    482   option_parser.add_option(
    483       '', '--count-violations',
    484       action='store_true', dest='count_violations', default=False,
    485       help='Count #includes in violation of intended rules.')
    486   option_parser.add_option(
    487       '', '--skip-tests',
    488       action='store_true', dest='skip_tests', default=False,
    489       help='Skip checking test files (best effort).')
    490   option_parser.add_option(
    491       '-v', '--verbose',
    492       action='store_true', default=False,
    493       help='Print debug logging')
    494   options, args = option_parser.parse_args()
    495 
    496   deps_checker = DepsChecker(options.base_directory,
    497                              verbose=options.verbose,
    498                              ignore_temp_rules=options.ignore_temp_rules,
    499                              skip_tests=options.skip_tests)
    500 
    501   # Figure out which directory we have to check.
    502   start_dir = deps_checker.base_directory
    503   if len(args) == 1:
    504     # Directory specified. Start here. It's supposed to be relative to the
    505     # base directory.
    506     start_dir = os.path.abspath(
    507         os.path.join(deps_checker.base_directory, args[0]))
    508   elif len(args) >= 2 or (options.generate_temp_rules and
    509                           options.count_violations):
    510     # More than one argument, or incompatible flags, we don't handle this.
    511     PrintUsage()
    512     return 1
    513 
    514   print 'Using base directory:', deps_checker.base_directory
    515   print 'Checking:', start_dir
    516 
    517   if options.generate_temp_rules:
    518     deps_checker.results_formatter = results.TemporaryRulesFormatter()
    519   elif options.count_violations:
    520     deps_checker.results_formatter = results.CountViolationsFormatter()
    521   deps_checker.CheckDirectory(start_dir)
    522   return deps_checker.Report()
    523 
    524 
    525 if '__main__' == __name__:
    526   sys.exit(main())
    527