rules.py 6.9 KB

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