| # Copyright 2012 The Chromium Authors. All rights reserved. | 
 | # Use of this source code is governed by a BSD-style license that can be | 
 | # found in the LICENSE file. | 
 |  | 
 | """Base classes to represent dependency rules, used by checkdeps.py""" | 
 |  | 
 |  | 
 | import os | 
 | import re | 
 |  | 
 |  | 
 | class Rule(object): | 
 |   """Specifies a single rule for an include, which can be one of | 
 |   ALLOW, DISALLOW and TEMP_ALLOW. | 
 |   """ | 
 |  | 
 |   # These are the prefixes used to indicate each type of rule. These | 
 |   # are also used as values for self.allow to indicate which type of | 
 |   # rule this is. | 
 |   ALLOW = '+' | 
 |   DISALLOW = '-' | 
 |   TEMP_ALLOW = '!' | 
 |  | 
 |   def __init__(self, allow, directory, dependent_directory, source): | 
 |     self.allow = allow | 
 |     self._dir = directory | 
 |     self._dependent_dir = dependent_directory | 
 |     self._source = source | 
 |  | 
 |   def __str__(self): | 
 |     return '"%s%s" from %s.' % (self.allow, self._dir, self._source) | 
 |  | 
 |   def AsDependencyTuple(self): | 
 |     """Returns a tuple (allow, dependent dir, dependee dir) for this rule, | 
 |     which is fully self-sufficient to answer the question whether the dependent | 
 |     is allowed to depend on the dependee, without knowing the external | 
 |     context.""" | 
 |     return self.allow, self._dependent_dir or '.', self._dir or '.' | 
 |  | 
 |   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 + '/') | 
 |  | 
 |  | 
 | class MessageRule(Rule): | 
 |   """A rule that has a simple message as the reason for failing, | 
 |   unrelated to directory or source. | 
 |   """ | 
 |  | 
 |   def __init__(self, reason): | 
 |     super(MessageRule, self).__init__(Rule.DISALLOW, '', '', '') | 
 |     self._reason = reason | 
 |  | 
 |   def __str__(self): | 
 |     return self._reason | 
 |  | 
 |  | 
 | def ParseRuleString(rule_string, source): | 
 |   """Returns a tuple of a character indicating what type of rule this | 
 |   is, and a string holding the path the rule applies to. | 
 |   """ | 
 |   if not rule_string: | 
 |     raise Exception('The rule string "%s" is empty\nin %s' % | 
 |                     (rule_string, source)) | 
 |  | 
 |   if not rule_string[0] in [Rule.ALLOW, Rule.DISALLOW, Rule.TEMP_ALLOW]: | 
 |     raise Exception( | 
 |       'The rule string "%s" does not begin with a "+", "-" or "!".' % | 
 |       rule_string) | 
 |  | 
 |   return rule_string[0], rule_string[1:] | 
 |  | 
 |  | 
 | class Rules(object): | 
 |   """Sets of rules for files in a directory. | 
 |  | 
 |   By default, rules are added to the set of rules applicable to all | 
 |   dependee files in the directory.  Rules may also be added that apply | 
 |   only to dependee files whose filename (last component of their path) | 
 |   matches a given regular expression; hence there is one additional | 
 |   set of rules per unique regular expression. | 
 |   """ | 
 |  | 
 |   def __init__(self): | 
 |     """Initializes the current rules with an empty rule list for all | 
 |     files. | 
 |     """ | 
 |     # We keep the general rules out of the specific rules dictionary, | 
 |     # as we need to always process them last. | 
 |     self._general_rules = [] | 
 |  | 
 |     # Keys are regular expression strings, values are arrays of rules | 
 |     # that apply to dependee files whose basename matches the regular | 
 |     # expression.  These are applied before the general rules, but | 
 |     # their internal order is arbitrary. | 
 |     self._specific_rules = {} | 
 |  | 
 |   def __str__(self): | 
 |     result = ['Rules = {\n    (apply to all files): [\n%s\n    ],' % '\n'.join( | 
 |         '      %s' % x for x in self._general_rules)] | 
 |     for regexp, rules in self._specific_rules.iteritems(): | 
 |       result.append('    (limited to files matching %s): [\n%s\n    ]' % ( | 
 |           regexp, '\n'.join('      %s' % x for x in rules))) | 
 |     result.append('  }') | 
 |     return '\n'.join(result) | 
 |  | 
 |   def AsDependencyTuples(self, include_general_rules, include_specific_rules): | 
 |     """Returns a list of tuples (allow, dependent dir, dependee dir) for the | 
 |     specified rules (general/specific). Currently only general rules are | 
 |     supported.""" | 
 |     def AddDependencyTuplesImpl(deps, rules, extra_dependent_suffix=""): | 
 |       for rule in rules: | 
 |         (allow, dependent, dependee) = rule.AsDependencyTuple() | 
 |         tup = (allow, dependent + extra_dependent_suffix, dependee) | 
 |         deps.add(tup) | 
 |  | 
 |     deps = set() | 
 |     if include_general_rules: | 
 |       AddDependencyTuplesImpl(deps, self._general_rules) | 
 |     if include_specific_rules: | 
 |       for regexp, rules in self._specific_rules.iteritems(): | 
 |         AddDependencyTuplesImpl(deps, rules, "/" + regexp) | 
 |     return deps | 
 |  | 
 |   def AddRule(self, rule_string, dependent_dir, source, dependee_regexp=None): | 
 |     """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. | 
 |       dependent_dir: The directory to which this rule applies. | 
 |       dependee_regexp: The rule will only be applied to dependee files | 
 |                        whose filename (last component of their path) | 
 |                        matches the expression. None to match all | 
 |                        dependee files. | 
 |     """ | 
 |     rule_type, rule_dir = ParseRuleString(rule_string, source) | 
 |  | 
 |     if not dependee_regexp: | 
 |       rules_to_update = self._general_rules | 
 |     else: | 
 |       if dependee_regexp in self._specific_rules: | 
 |         rules_to_update = self._specific_rules[dependee_regexp] | 
 |       else: | 
 |         rules_to_update = [] | 
 |  | 
 |     # 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". | 
 |     rules_to_update = [x for x in rules_to_update | 
 |                        if not x.ParentOrMatch(rule_dir)] | 
 |     rules_to_update.insert(0, Rule(rule_type, rule_dir, dependent_dir, source)) | 
 |  | 
 |     if not dependee_regexp: | 
 |       self._general_rules = rules_to_update | 
 |     else: | 
 |       self._specific_rules[dependee_regexp] = rules_to_update | 
 |  | 
 |   def RuleApplyingTo(self, include_path, dependee_path): | 
 |     """Returns the rule that applies to |include_path| for a dependee | 
 |     file located at |dependee_path|. | 
 |     """ | 
 |     dependee_filename = os.path.basename(dependee_path) | 
 |     for regexp, specific_rules in self._specific_rules.iteritems(): | 
 |       if re.match(regexp, dependee_filename): | 
 |         for rule in specific_rules: | 
 |           if rule.ChildOrMatch(include_path): | 
 |             return rule | 
 |     for rule in self._general_rules: | 
 |       if rule.ChildOrMatch(include_path): | 
 |         return rule | 
 |     return MessageRule('no rule applying.') |