• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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