diff options
author | brettw@google.com <brettw@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2008-08-07 23:55:10 +0000 |
---|---|---|
committer | brettw@google.com <brettw@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2008-08-07 23:55:10 +0000 |
commit | 0814e98845b27d80fbd9788d408a58c8a29c89a7 (patch) | |
tree | c762a5b2e758ec701361b5621354529f5e954a78 /tools | |
parent | bda7500e2a9e8460715558c74c4c01c83193a183 (diff) | |
download | chromium_src-0814e98845b27d80fbd9788d408a58c8a29c89a7.zip chromium_src-0814e98845b27d80fbd9788d408a58c8a29c89a7.tar.gz chromium_src-0814e98845b27d80fbd9788d408a58c8a29c89a7.tar.bz2 |
Recursively search directories in the source tree and verify includes.
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@545 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'tools')
-rw-r--r-- | tools/checkdeps/checkdeps.py | 461 |
1 files changed, 461 insertions, 0 deletions
diff --git a/tools/checkdeps/checkdeps.py b/tools/checkdeps/checkdeps.py new file mode 100644 index 0000000..9dfa033 --- /dev/null +++ b/tools/checkdeps/checkdeps.py @@ -0,0 +1,461 @@ +# Copyright 2008, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Makes sure that files include headers from allowed directories. + +Checks DEPS files in the source tree for rules, and applies those rules to +"#include" commands in source files. Any source file including something not +permitted by the DEPS files will fail. + +The format of the deps file: + +First you have the normal module-level deps. These are the ones used by +gclient. An example would be: + + deps = { + "base":"http://foo.bar/trunk/base" + } + +DEPS files not in the top-level of a module won't need this. Then you have +any additional include rules. You can add (using "+") or subtract (using "-") +from the previously specified rules (including module-level deps). + + include_rules = { + # Code should be able to use base (it's specified in the module-level + # deps above), but nothing in "base/evil" because it's evil. + "-base/evil", + + # But this one subdirectory of evil is OK. + "+base/evil/not", + + # And it can include files from this other directory even though there is + # no deps rule for it. + "+tools/crime_fighter" + } + +DEPS files may be placed anywhere in the tree. Each one applies to all +subdirectories, where there may be more DEPS files that provide additions or +subtractions for their own sub-trees. + +There is an implicit rule for the current directory (where the DEPS file lives) +and all of its subdirectories. This prevents you from having to explicitly +allow the current directory everywhere. This implicit rule is applied first, +so you can modify or remove it using the normal include rules. + +The rules are processed in order. This means you can explicitly allow a higher +directory and then take away permissions from sub-parts, or the reverse. + +Note that all directory separators must be slashes (Unix-style) and not +backslashes. All directories should be relative to the source root and use +only lowercase. +""" + +import os +import optparse +import re +import sys + +# Variable name used in the DEPS file. Its presence tells us not to check +# the sub-tree. For example, third_party directories would use this, where we +# have no control over their includes. +SKIP_VAR_NAME = "skip_subtree_includes" + +# Variable name used in the DEPS file to specify module-level deps. +DEPS_VAR_NAME = "deps" + +# Variable name used in the DEPS file to add or subtract include files from +# the module-level deps. +INCLUDE_RULES_VAR_NAME = "include_rules" + +# We'll search for lines beginning with this string for checking. +INCLUDE_PREFIX = "#include" + +# The maximum number of lines to check in each source file before giving up. +MAX_LINES = 150 + +# The maximum line length, this is to be efficient in the case of very long +# lines (which can't be #includes). +MAX_LINE_LENGTH = 128 + +# Set to true for more output. This is set by the command line options. +VERBOSE = False + +# This regular expression will be used to extract filenames from include +# statements. +EXTRACT_INCLUDE_FILENAME = re.compile(INCLUDE_PREFIX + ' *"(.*)"') + +# In lowercase, using forward slashes as directory separators, ending in a +# forward slash. Set by the command line options. +BASE_DIRECTORY = "" + +# Specifies a single rule for an include, which can be either allow or disallow. +class Rule(object): + def __init__(self, allow, dir, source): + self._allow = allow + self._dir = dir + self._source = source + + def __str__(self): + if (self._allow): + return '"+%s" from %s.' % (self._dir, self._source) + return '"-%s" from %s.' % (self._dir, self._source) + + def ParentOrMatch(self, other): + """Returns true if the input string is an exact match or is a parent + of the current rule. For example, the input "foo" would match "foo/bar".""" + return self._dir == other or self._dir.startswith(other + "/") + + def ChildOrMatch(self, other): + """Returns true if the input string would be covered by this rule. For + example, the input "foo/bar" would match the rule "foo".""" + return self._dir == other or other.startswith(self._dir + "/") + + +def ParseRuleString(rule_string, source): + """Returns a tuple of a boolean indicating whether the directory is an allow + rule, and a string holding the directory name. + """ + if len(rule_string) < 1: + raise Exception('The rule string "%s" is too short\nin %s' % + (rule_string, source)) + + if rule_string[0] == "+": + return (True, rule_string[1:]) + if rule_string[0] == "-": + return (False, rule_string[1:]) + raise Exception('The rule string "%s" does not begin with a "+" or a "-"' % + rule_string) + + +class Rules: + def __init__(self): + """Initializes the current rules with an empty rule list.""" + self._rules = [] + + def __str__(self): + ret = "Rules = [\n" + ret += "\n".join([" %s" % x for x in self._rules]) + ret += "]\n" + return ret + + def AddRule(self, rule_string, source): + """Adds a rule for the given rule string. + + Args: + rule_string: The include_rule string read from the DEPS file to apply. + source: A string representing the location of that string (filename, etc.) + so that we can give meaningful errors. + """ + (add_rule, rule_dir) = ParseRuleString(rule_string, source) + # Remove any existing rules or sub-rules that apply. For example, if we're + # passed "foo", we should remove "foo", "foo/bar", but not "foobar". + self._rules = [x for x in self._rules if not x.ParentOrMatch(rule_dir)] + self._rules.insert(0, Rule(add_rule, rule_dir, source)) + + def DirAllowed(self, allowed_dir): + """Returns a tuple (success, message), where success indicates if the given + directory is allowed given the current set of rules, and the message tells + why if the comparison failed.""" + for rule in self._rules: + if rule.ChildOrMatch(allowed_dir): + # This rule applies. + if rule._allow: + return (True, "") + return (False, rule.__str__()) + # No rules apply, fail. + return (False, "no rule applying") + + +def ApplyRules(existing_rules, deps, includes, cur_dir): + """Applies the given deps and include rules, returning the new rules. + + Args: + existing_rules: A set of existing rules that will be combined. + deps: The list of imports from the "deps" section of the DEPS file. + include: The list of rules from the "include_rules" section of DEPS. + cur_dir: The current directory. We will create an implicit rule that + allows inclusion from this directory. + + Returns: A new set of rules combining the existing_rules with the other + arguments. + """ + rules = existing_rules + + # First apply the implicit "allow" rule for the current directory. + if cur_dir.lower().startswith(BASE_DIRECTORY): + relative_dir = cur_dir[len(BASE_DIRECTORY):] + # Normalize path separators to slashes. + relative_dir = relative_dir.replace("\\", "/") + source = relative_dir + if len(source) == 0: + source = "." # Make the help string a little more meaningful. + rules.AddRule("+" + relative_dir, "Default rule for " + source) + else: + raise Exception("Internal error: base directory is not at the beginning" + + " for\n %s and base dir\n %s" % + (cur_dir, BASE_DIRECTORY)) + + # Next apply the DEPS additions, these are all allowed. + for (index, key) in enumerate(deps): + rules.AddRule("+" + key, relative_dir + "'s deps for " + key) + + # Last, apply the additional explicit rules. + for (index, rule_str) in enumerate(includes): + rules.AddRule(rule_str, relative_dir + "'s include_rules") + + return rules + + +def ApplyDirectoryRules(existing_rules, dir_name): + """Combines rules from the existing rules and the new directory. + + Any directory can contain a DEPS file. Toplevel DEPS files can contain + module dependencies which are used by gclient. We use these, along with + additional include rules and implicit rules for the given directory, to + come up with a combined set of rules to apply for the directory. + + Args: + existing_rules: The rules for the parent directory. We'll add-on to these. + dir_name: The directory name that the deps file may live in (if it exists). + This will also be used to generate the implicit rules. + + Returns: The combined set of rules to apply to the sub-tree. + """ + # Check for a .svn directory in this directory. This will tell us if it's + # a source directory and should be checked. + if not os.path.exists(os.path.join(dir_name, ".svn")): + return None + + # Check the DEPS file in this directory. + if VERBOSE: + print "Applying rules from", dir_name + def FromImpl(unused): + pass # NOP function so "From" doesn't fail. + scope = {"From": FromImpl} + deps_file = os.path.join(dir_name, "DEPS") + if not os.path.exists(deps_file): + if VERBOSE: + print " No deps file found in", dir_name + return existing_rules # Nothing to change from the input rules. + + execfile(deps_file, scope) + + # Check the "skip" flag to see if we should check this directory at all. + if scope.get(SKIP_VAR_NAME): + if VERBOSE: + print " Deps file specifies skipping this directory." + return None + + deps = scope.get(DEPS_VAR_NAME, {}) + include_rules = scope.get(INCLUDE_RULES_VAR_NAME, []) + + return ApplyRules(existing_rules, deps, include_rules, dir_name) + + +def ShouldCheckFile(file_name): + """Returns True if the given file is a type we want to check.""" + if len(file_name) < 2: + return False + return file_name.endswith(".cc") or file_name.endswith(".h") + + +def CheckLine(rules, line): + """Checks the given file with the given rule set. If the line is an #include + directive and is illegal, a string describing the error will be returned. + Otherwise, None will be returned.""" + if line[0:8] != "#include": + return None # Not an include line + + found_item = EXTRACT_INCLUDE_FILENAME.match(line) + if not found_item: + return None # Not a match + + include_path = found_item.group(1) + + # Fix up backslashes in case somebody accidentally used them. + include_path.replace("\\", "/") + + if include_path.find("/") < 0: + # Don't fail when no directory is specified. We may want to be more + # strict about this in the future. + if VERBOSE: + print " WARNING: directory specified with no path: " + include_path + return None + + (allowed, why_failed) = rules.DirAllowed(include_path) + if not allowed: + if VERBOSE: + retval = "\nFor " + rules.__str__() + else: + retval = "" + return retval + ('Illegal include: "%s include_path"\n Because of %s' % + (include_path, why_failed)) + + return None + + +def CheckFile(rules, file_name): + """Checks the given file with the given rule set. + + Args: + rules: The set of rules that apply to files in this directory. + file_name: The source file to check. + + Returns: Either a string describing the error if there was one, or None if + the file checked out OK. + """ + if VERBOSE: + print "Checking: " + file_name + + ret_val = "" # We'll collect the error messages in here + try: + cur_file = open(file_name, "r") + for cur_line in range(MAX_LINES): + cur_line = cur_file.readline(MAX_LINE_LENGTH) + line_status = CheckLine(rules, cur_line) + if line_status is not None: + if len(line_status) > 0: # Add newline to separate messages. + line_status += "\n" + ret_val += line_status + cur_file.close() + + except IOError: + if VERBOSE: + print "Unable to open file: " + file_name + cur_file.close() + + # Map empty string to None for easier checking. + if len(ret_val) == 0: + return None + return ret_val + + +def CheckDirectory(rules, dir_name): + rules = ApplyDirectoryRules(rules, dir_name) + if rules == None: + return True + + # Collect a list of all files and directories to check. + files_to_check = [] + dirs_to_check = [] + success = True + contents = os.listdir(dir_name) + for cur in contents: + full_name = os.path.join(dir_name, cur) + if os.path.isdir(full_name): + dirs_to_check.append(full_name) + elif ShouldCheckFile(full_name): + files_to_check.append(full_name) + + # First check all files in this directory. + for cur in files_to_check: + file_status = CheckFile(rules, cur) + if file_status != None: + print "ERROR in " + cur + "\n" + file_status + success = False + + # Next recurse into the subdirectories. + for cur in dirs_to_check: + if not CheckDirectory(rules, cur): + success = False + + return success + +def PrintUsage(): + print """Usage: python checkdeps.py [--root <root>] [tocheck] + --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>/tools/checkdeps". + + tocheck Specifies the directory, relative to root, to check. This defaults + to "." so it checks everything. Only one level deep is currently + supported, so you can say "chrome" but not "chrome/browser". + +Examples: + python checkdeps.py + python checkdeps.py --root c:\\source chrome""" + +def main(options, args): + global VERBOSE + if options.verbose: + VERBOSE = True + + # Optional base directory of the repository. + global BASE_DIRECTORY + if not options.base_directory: + BASE_DIRECTORY = os.path.abspath( + os.path.join(os.path.abspath(sys.argv[0]), "..\..")) + else: + BASE_DIRECTORY = os.path.abspath(sys.argv[2]) + + # Figure out which directory we have to check. + if len(args) == 0: + # No directory to check specified, use the repository root. + start_dir = BASE_DIRECTORY + elif 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])) + else: + # More than one argument, we don't handle this. + PrintUsage() + sys.exit(1) + + print "Using base directory:", BASE_DIRECTORY + print "Checking:", start_dir + + base_rules = Rules() + + # The base directory should be lower case from here on since it will be used + # for substring matching on the includes, and we compile on case-insensitive + # systems. Plus, we always use slashes here since the include parsing code + # will also normalize to slashes. + BASE_DIRECTORY = BASE_DIRECTORY.lower() + BASE_DIRECTORY = BASE_DIRECTORY.replace("\\", "/") + start_dir = start_dir.replace("\\", "/") + + success = CheckDirectory(base_rules, start_dir) + success = False + if not success: + print "\nFAILED\n" + sys.exit(1) + print "\nSUCCESS\n" + sys.exit(0) + +if '__main__' == __name__: + 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("-v", "--verbose", action="store_true", + default=False, help="Print debug logging") + options, args = option_parser.parse_args() + main(options, args) + |