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  return (rule_string[0], rule_string[1:])
78
79
80class Rules(object):
81  """Sets of rules for files in a directory.
82
83  By default, rules are added to the set of rules applicable to all
84  dependee files in the directory.  Rules may also be added that apply
85  only to dependee files whose filename (last component of their path)
86  matches a given regular expression; hence there is one additional
87  set of rules per unique regular expression.
88  """
89
90  def __init__(self):
91    """Initializes the current rules with an empty rule list for all
92    files.
93    """
94    # We keep the general rules out of the specific rules dictionary,
95    # as we need to always process them last.
96    self._general_rules = []
97
98    # Keys are regular expression strings, values are arrays of rules
99    # that apply to dependee files whose basename matches the regular
100    # expression.  These are applied before the general rules, but
101    # their internal order is arbitrary.
102    self._specific_rules = {}
103
104  def __str__(self):
105    result = ['Rules = {\n    (apply to all files): [\n%s\n    ],' % '\n'.join(
106        '      %s' % x for x in self._general_rules)]
107    for regexp, rules in self._specific_rules.iteritems():
108      result.append('    (limited to files matching %s): [\n%s\n    ]' % (
109          regexp, '\n'.join('      %s' % x for x in rules)))
110    result.append('  }')
111    return '\n'.join(result)
112
113  def AsDependencyTuples(self, include_general_rules, include_specific_rules):
114    """Returns a list of tuples (allow, dependent dir, dependee dir) for the
115    specified rules (general/specific). Currently only general rules are
116    supported."""
117    def AddDependencyTuplesImpl(deps, rules, extra_dependent_suffix=""):
118      for rule in rules:
119        (allow, dependent, dependee) = rule.AsDependencyTuple()
120        tup = (allow, dependent + extra_dependent_suffix, dependee)
121        deps.add(tup)
122
123    deps = set()
124    if include_general_rules:
125      AddDependencyTuplesImpl(deps, self._general_rules)
126    if include_specific_rules:
127      for regexp, rules in self._specific_rules.iteritems():
128        AddDependencyTuplesImpl(deps, rules, "/" + regexp)
129    return deps
130
131  def AddRule(self, rule_string, dependent_dir, source, dependee_regexp=None):
132    """Adds a rule for the given rule string.
133
134    Args:
135      rule_string: The include_rule string read from the DEPS file to apply.
136      source: A string representing the location of that string (filename, etc.)
137              so that we can give meaningful errors.
138      dependent_dir: The directory to which this rule applies.
139      dependee_regexp: The rule will only be applied to dependee files
140                       whose filename (last component of their path)
141                       matches the expression. None to match all
142                       dependee files.
143    """
144    (rule_type, rule_dir) = ParseRuleString(rule_string, source)
145
146    if not dependee_regexp:
147      rules_to_update = self._general_rules
148    else:
149      if dependee_regexp in self._specific_rules:
150        rules_to_update = self._specific_rules[dependee_regexp]
151      else:
152        rules_to_update = []
153
154    # Remove any existing rules or sub-rules that apply. For example, if we're
155    # passed "foo", we should remove "foo", "foo/bar", but not "foobar".
156    rules_to_update = [x for x in rules_to_update
157                       if not x.ParentOrMatch(rule_dir)]
158    rules_to_update.insert(0, Rule(rule_type, rule_dir, dependent_dir, source))
159
160    if not dependee_regexp:
161      self._general_rules = rules_to_update
162    else:
163      self._specific_rules[dependee_regexp] = rules_to_update
164
165  def RuleApplyingTo(self, include_path, dependee_path):
166    """Returns the rule that applies to |include_path| for a dependee
167    file located at |dependee_path|.
168    """
169    dependee_filename = os.path.basename(dependee_path)
170    for regexp, specific_rules in self._specific_rules.iteritems():
171      if re.match(regexp, dependee_filename):
172        for rule in specific_rules:
173          if rule.ChildOrMatch(include_path):
174            return rule
175    for rule in self._general_rules:
176      if rule.ChildOrMatch(include_path):
177        return rule
178    return MessageRule('no rule applying.')
179