1# Copyright 2012 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""Base classes to represent dependency rules, used by checkdeps.py""" 6 7 8import os 9import re 10 11 12class Rule(object): 13 """Specifies a single rule for an include, which can be one of 14 ALLOW, DISALLOW and TEMP_ALLOW. 15 """ 16 17 # These are the prefixes used to indicate each type of rule. These 18 # are also used as values for self.allow to indicate which type of 19 # rule this is. 20 ALLOW = '+' 21 DISALLOW = '-' 22 TEMP_ALLOW = '!' 23 24 def __init__(self, allow, directory, dependent_directory, source): 25 self.allow = allow 26 self._dir = directory 27 self._dependent_dir = dependent_directory 28 self._source = source 29 30 def __str__(self): 31 return '"%s%s" from %s.' % (self.allow, self._dir, self._source) 32 33 def AsDependencyTuple(self): 34 """Returns a tuple (allow, dependent dir, dependee dir) for this rule, 35 which is fully self-sufficient to answer the question whether the dependent 36 is allowed to depend on the dependee, without knowing the external 37 context.""" 38 return self.allow, self._dependent_dir or '.', self._dir or '.' 39 40 def ParentOrMatch(self, other): 41 """Returns true if the input string is an exact match or is a parent 42 of the current rule. For example, the input "foo" would match "foo/bar".""" 43 return self._dir == other or self._dir.startswith(other + '/') 44 45 def ChildOrMatch(self, other): 46 """Returns true if the input string would be covered by this rule. For 47 example, the input "foo/bar" would match the rule "foo".""" 48 return self._dir == other or other.startswith(self._dir + '/') 49 50 51class MessageRule(Rule): 52 """A rule that has a simple message as the reason for failing, 53 unrelated to directory or source. 54 """ 55 56 def __init__(self, reason): 57 super(MessageRule, self).__init__(Rule.DISALLOW, '', '', '') 58 self._reason = reason 59 60 def __str__(self): 61 return self._reason 62 63 64def ParseRuleString(rule_string, source): 65 """Returns a tuple of a character indicating what type of rule this 66 is, and a string holding the path the rule applies to. 67 """ 68 if not rule_string: 69 raise Exception('The rule string "%s" is empty\nin %s' % 70 (rule_string, source)) 71 72 if not rule_string[0] in [Rule.ALLOW, Rule.DISALLOW, Rule.TEMP_ALLOW]: 73 raise Exception( 74 'The rule string "%s" does not begin with a "+", "-" or "!".' % 75 rule_string) 76 77 # If a directory is specified in a DEPS file with a trailing slash, then it 78 # will not match as a parent directory in Rule's [Parent|Child]OrMatch above. 79 # Ban them. 80 if rule_string[-1] == '/': 81 raise Exception( 82 'The rule string "%s" ends with a "/" which is not allowed.' 83 ' Please remove the trailing "/".' % rule_string) 84 85 return rule_string[0], rule_string[1:] 86 87 88class Rules(object): 89 """Sets of rules for files in a directory. 90 91 By default, rules are added to the set of rules applicable to all 92 dependee files in the directory. Rules may also be added that apply 93 only to dependee files whose filename (last component of their path) 94 matches a given regular expression; hence there is one additional 95 set of rules per unique regular expression. 96 """ 97 98 def __init__(self): 99 """Initializes the current rules with an empty rule list for all 100 files. 101 """ 102 # We keep the general rules out of the specific rules dictionary, 103 # as we need to always process them last. 104 self._general_rules = [] 105 106 # Keys are regular expression strings, values are arrays of rules 107 # that apply to dependee files whose basename matches the regular 108 # expression. These are applied before the general rules, but 109 # their internal order is arbitrary. 110 self._specific_rules = {} 111 112 def __str__(self): 113 result = ['Rules = {\n (apply to all files): [\n%s\n ],' % '\n'.join( 114 ' %s' % x for x in self._general_rules)] 115 for regexp, rules in list(self._specific_rules.items()): 116 result.append(' (limited to files matching %s): [\n%s\n ]' % ( 117 regexp, '\n'.join(' %s' % x for x in rules))) 118 result.append(' }') 119 return '\n'.join(result) 120 121 def AsDependencyTuples(self, include_general_rules, include_specific_rules): 122 """Returns a list of tuples (allow, dependent dir, dependee dir) for the 123 specified rules (general/specific). Currently only general rules are 124 supported.""" 125 def AddDependencyTuplesImpl(deps, rules, extra_dependent_suffix=""): 126 for rule in rules: 127 (allow, dependent, dependee) = rule.AsDependencyTuple() 128 tup = (allow, dependent + extra_dependent_suffix, dependee) 129 deps.add(tup) 130 131 deps = set() 132 if include_general_rules: 133 AddDependencyTuplesImpl(deps, self._general_rules) 134 if include_specific_rules: 135 for regexp, rules in list(self._specific_rules.items()): 136 AddDependencyTuplesImpl(deps, rules, "/" + regexp) 137 return deps 138 139 def AddRule(self, rule_string, dependent_dir, source, dependee_regexp=None): 140 """Adds a rule for the given rule string. 141 142 Args: 143 rule_string: The include_rule string read from the DEPS file to apply. 144 source: A string representing the location of that string (filename, etc.) 145 so that we can give meaningful errors. 146 dependent_dir: The directory to which this rule applies. 147 dependee_regexp: The rule will only be applied to dependee files 148 whose filename (last component of their path) 149 matches the expression. None to match all 150 dependee files. 151 """ 152 rule_type, rule_dir = ParseRuleString(rule_string, source) 153 154 if not dependee_regexp: 155 rules_to_update = self._general_rules 156 else: 157 if dependee_regexp in self._specific_rules: 158 rules_to_update = self._specific_rules[dependee_regexp] 159 else: 160 rules_to_update = [] 161 162 # Remove any existing rules or sub-rules that apply. For example, if we're 163 # passed "foo", we should remove "foo", "foo/bar", but not "foobar". 164 rules_to_update = [x for x in rules_to_update 165 if not x.ParentOrMatch(rule_dir)] 166 rules_to_update.insert(0, Rule(rule_type, rule_dir, dependent_dir, source)) 167 168 if not dependee_regexp: 169 self._general_rules = rules_to_update 170 else: 171 self._specific_rules[dependee_regexp] = rules_to_update 172 173 def RuleApplyingTo(self, include_path, dependee_path): 174 """Returns the rule that applies to |include_path| for a dependee 175 file located at |dependee_path|. 176 """ 177 dependee_filename = os.path.basename(dependee_path) 178 for regexp, specific_rules in list(self._specific_rules.items()): 179 if re.match(regexp, dependee_filename): 180 for rule in specific_rules: 181 if rule.ChildOrMatch(include_path): 182 return rule 183 for rule in self._general_rules: 184 if rule.ChildOrMatch(include_path): 185 return rule 186 return MessageRule('no rule applying.') 187