Update scripts to reference contributors.json instead of committers.py in messaging
[WebKit-https.git] / Tools / Scripts / webkitpy / common / watchlist / watchlistparser.py
1 # Copyright (C) 2011 Google Inc. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions are
5 # met:
6 #
7 #     * Redistributions of source code must retain the above copyright
8 # notice, this list of conditions and the following disclaimer.
9 #     * Redistributions in binary form must reproduce the above
10 # copyright notice, this list of conditions and the following disclaimer
11 # in the documentation and/or other materials provided with the
12 # distribution.
13 #     * Neither the name of Google Inc. nor the names of its
14 # contributors may be used to endorse or promote products derived from
15 # this software without specific prior written permission.
16 #
17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29
30 import difflib
31 import logging
32 import re
33
34 from webkitpy.common.watchlist.amountchangedpattern import AmountChangedPattern
35 from webkitpy.common.watchlist.changedlinepattern import ChangedLinePattern
36 from webkitpy.common.watchlist.filenamepattern import FilenamePattern
37 from webkitpy.common.watchlist.watchlist import WatchList
38 from webkitpy.common.watchlist.watchlistrule import WatchListRule
39 from webkitpy.common.config.committers import CommitterList
40
41
42 _log = logging.getLogger(__name__)
43
44
45 class WatchListParser(object):
46     _DEFINITIONS = 'DEFINITIONS'
47     _CC_RULES = 'CC_RULES'
48     _MESSAGE_RULES = 'MESSAGE_RULES'
49     _INVALID_DEFINITION_NAME_REGEX = r'\|'
50
51     def __init__(self, log_error=None):
52         self._log_error = log_error or _log.error
53         self._section_parsers = {
54             self._DEFINITIONS: self._parse_definition_section,
55             self._CC_RULES: self._parse_cc_rules,
56             self._MESSAGE_RULES: self._parse_message_rules,
57         }
58         self._definition_pattern_parsers = {
59             'filename': FilenamePattern,
60             'in_added_lines': (lambda compiled_regex: ChangedLinePattern(compiled_regex, 0)),
61             'in_deleted_lines': (lambda compiled_regex: ChangedLinePattern(compiled_regex, 1)),
62             'less': (lambda compiled_regex: AmountChangedPattern(compiled_regex, 1)),
63             'more': (lambda compiled_regex: AmountChangedPattern(compiled_regex, 0)),
64         }
65
66     def parse(self, watch_list_contents):
67         watch_list = WatchList()
68
69         # Change the watch list text into a dictionary.
70         dictionary = self._eval_watch_list(watch_list_contents)
71
72         # Parse the top level sections in the watch list.
73         for section in dictionary:
74             parser = self._section_parsers.get(section)
75             if not parser:
76                 self._log_error(('Unknown section "%s" in watch list.'
77                                 + self._suggest_words(section, self._section_parsers.keys()))
78                                % section)
79                 continue
80             parser(dictionary[section], watch_list)
81
82         self._validate(watch_list)
83         return watch_list
84
85     def _eval_watch_list(self, watch_list_contents):
86         return eval(watch_list_contents, {'__builtins__': None}, None)
87
88     def _suggest_words(self, invalid_word, valid_words):
89         close_matches = difflib.get_close_matches(invalid_word, valid_words)
90         if not close_matches:
91             return ''
92         return '\n\nPerhaps it should be %s.' % (' or '.join(close_matches))
93
94     def _parse_definition_section(self, definition_section, watch_list):
95         definitions = {}
96         for name in definition_section:
97             invalid_character = re.search(self._INVALID_DEFINITION_NAME_REGEX, name)
98             if invalid_character:
99                 self._log_error('Invalid character "%s" in definition "%s".' % (invalid_character.group(0), name))
100                 continue
101
102             definition = definition_section[name]
103             definitions[name] = []
104             for pattern_type in definition:
105                 pattern_parser = self._definition_pattern_parsers.get(pattern_type)
106                 if not pattern_parser:
107                     self._log_error(('Unknown pattern type "%s" in definition "%s".'
108                                      + self._suggest_words(pattern_type, self._definition_pattern_parsers.keys()))
109                                     % (pattern_type, name))
110                     continue
111
112                 try:
113                     compiled_regex = re.compile(definition[pattern_type])
114                 except Exception, e:
115                     self._log_error('The regex "%s" is invalid due to "%s".' % (definition[pattern_type], str(e)))
116                     continue
117
118                 pattern = pattern_parser(compiled_regex)
119                 definitions[name].append(pattern)
120             if not definitions[name]:
121                 self._log_error('The definition "%s" has no patterns, so it should be deleted.' % name)
122                 continue
123         watch_list.definitions = definitions
124
125     def _parse_rules(self, rules_section):
126         rules = []
127         for complex_definition in rules_section:
128             instructions = rules_section[complex_definition]
129             if not instructions:
130                 self._log_error('A rule for definition "%s" is empty, so it should be deleted.' % complex_definition)
131                 continue
132             rules.append(WatchListRule(complex_definition, instructions))
133         return rules
134
135     def _parse_cc_rules(self, cc_section, watch_list):
136         watch_list.cc_rules = self._parse_rules(cc_section)
137
138     def _parse_message_rules(self, message_section, watch_list):
139         watch_list.message_rules = self._parse_rules(message_section)
140
141     def _validate(self, watch_list):
142         cc_definitions_set = self._rule_definitions_as_set(watch_list.cc_rules)
143         messages_definitions_set = self._rule_definitions_as_set(watch_list.message_rules)
144         self._verify_all_definitions_are_used(watch_list, cc_definitions_set.union(messages_definitions_set))
145
146         self._validate_definitions(cc_definitions_set, self._CC_RULES, watch_list)
147         self._validate_definitions(messages_definitions_set, self._MESSAGE_RULES, watch_list)
148
149         accounts = CommitterList()
150         for cc_rule in watch_list.cc_rules:
151             # Copy the instructions since we'll be remove items from the original list and
152             # modifying a list while iterating through it leads to undefined behavior.
153             intructions_copy = cc_rule.instructions()[:]
154             for email in intructions_copy:
155                 if not accounts.contributor_by_email(email):
156                     cc_rule.remove_instruction(email)
157                     self._log_error("The email alias %s which is in the watchlist is not listed as a contributor in contributors.json" % email)
158                     continue
159
160     def _verify_all_definitions_are_used(self, watch_list, used_definitions):
161         definitions_not_used = set(watch_list.definitions.keys())
162         definitions_not_used.difference_update(used_definitions)
163         if definitions_not_used:
164             self._log_error('The following definitions are not used and should be removed: %s' % (', '.join(definitions_not_used)))
165
166     def _validate_definitions(self, definitions, rules_section_name, watch_list):
167         declared_definitions = watch_list.definitions.keys()
168         definition_set = set(definitions)
169         definition_set.difference_update(declared_definitions)
170
171         if definition_set:
172             suggestions = ''
173             if len(definition_set) == 1:
174                 suggestions = self._suggest_words(set().union(definition_set).pop(), declared_definitions)
175             self._log_error('In section "%s", the following definitions are not used and should be removed: %s%s' % (rules_section_name, ', '.join(definition_set), suggestions))
176
177     def _rule_definitions_as_set(self, rules):
178         definition_set = set()
179         for rule in rules:
180             definition_set = definition_set.union(rule.definitions_to_match)
181         return definition_set