OLD | NEW |
---|---|
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2014 The Chromium Authors. All rights reserved. |
rpaquay
2014/02/18 21:39:26
nit: remove the "(c"): see http://www.chromium.org
| |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """Chromium presubmit script for src/chrome/browser/extensions. | 5 class StrictEnumValueChecker(object): |
6 '''Verify that changes to enums are valid. | |
6 | 7 |
7 See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts | 8 This class is used to check enums where reordering or deletion is not allowed, |
8 for more details on the presubmit API built into gcl. | 9 and additions must be at the end of the enum, just prior to some "boundary" |
9 """ | 10 entry. See comments at the top of the "extension_function_histogram_value.h" |
11 file in chrome/browser/extensions for an example what are considered valid | |
12 changes. There are situations where this class gives false positive warnings, | |
13 i.e. it warns even though the edit is legitimate. Since the class warns using | |
14 prompt warnings, the user can always choose to continue. The main point is to | |
15 attract the attention to all (potentially or not) invalid edits. | |
10 | 16 |
11 def GetPreferredTrySlaves(): | 17 ''' |
12 return ['linux_chromeos'] | 18 def __init__(self, input_api, output_api, start_marker, end_marker, path): |
13 | |
14 class HistogramValueChecker(object): | |
15 """Verify that changes to "extension_function_histogram_value.h" are valid. | |
16 | |
17 See comments at the top of the "extension_function_histogram_value.h" file | |
18 for what are considered valid changes. There are situations where this script | |
19 gives false positive warnings, i.e. it warns even though the edit is | |
20 legitimate. Since the script warns using prompt warnings, the user can always | |
21 choose to continue. The main point is to attract the attention to all | |
22 (potentially or not) invalid edits. | |
23 | |
24 """ | |
25 | |
26 # The name of the file we want to check against | |
27 LOCAL_PATH = "chrome/browser/extensions/extension_function_histogram_value.h" | |
28 | |
29 # The markers we look for in the above source file as delimiters of the enum | |
30 # definition. | |
31 ENUM_START_MARKER = "enum HistogramValue {" | |
32 ENUM_END_MARKER = " ENUM_BOUNDARY" | |
33 | |
34 def __init__(self, input_api, output_api): | |
35 self.input_api = input_api | 19 self.input_api = input_api |
36 self.output_api = output_api | 20 self.output_api = output_api |
21 self.start_marker = start_marker | |
22 self.end_marker = end_marker | |
23 self.path = path | |
37 self.results = [] | 24 self.results = [] |
38 | 25 |
39 class EnumRange(object): | 26 class EnumRange(object): |
40 """Represents a range of line numbers (1-based)""" | 27 '''Represents a range of line numbers (1-based)''' |
rpaquay
2014/02/18 21:39:26
nit: is there a particular reason you use single q
ahernandez
2014/02/20 17:23:42
There really wasn't a reason other than a personal
| |
41 def __init__(self, first_line, last_line): | 28 def __init__(self, first_line, last_line): |
42 self.first_line = first_line | 29 self.first_line = first_line |
43 self.last_line = last_line | 30 self.last_line = last_line |
44 | 31 |
45 def Count(self): | 32 def Count(self): |
46 return self.last_line - self.first_line + 1 | 33 return self.last_line - self.first_line + 1 |
47 | 34 |
48 def Contains(self, line_num): | 35 def Contains(self, line_num): |
49 return self.first_line <= line_num and line_num <= self.last_line | 36 return self.first_line <= line_num and line_num <= self.last_line |
50 | 37 |
51 def LogInfo(self, message): | 38 def LogInfo(self, message): |
52 self.input_api.logging.info(message) | 39 self.input_api.logging.info(message) |
53 return | 40 return |
54 | 41 |
55 def LogDebug(self, message): | 42 def LogDebug(self, message): |
56 self.input_api.logging.debug(message) | 43 self.input_api.logging.debug(message) |
57 return | 44 return |
58 | 45 |
59 def ComputeEnumRangeInContents(self, contents): | 46 def ComputeEnumRangeInContents(self, contents): |
60 """Returns an |EnumRange| object representing the line extent of the | 47 '''Returns an |EnumRange| object representing the line extent of the |
61 HistogramValue enum members in |contents|. The line numbers are 1-based, | 48 HistogramValue enum members in |contents|. The line numbers are 1-based, |
62 compatible with line numbers returned by AffectedFile.ChangeContents(). | 49 compatible with line numbers returned by AffectedFile.ChangeContents(). |
63 |contents| is a list of strings reprenting the lines of a text file. | 50 |contents| is a list of strings reprenting the lines of a text file. |
64 | 51 |
65 If either ENUM_START_MARKER or ENUM_END_MARKER cannot be found in | 52 If either start_marker or end_marker cannot be found in |
66 |contents|, returns None and emits detailed warnings about the problem. | 53 |contents|, returns None and emits detailed warnings about the problem. |
67 | 54 |
68 """ | 55 ''' |
69 first_enum_line = 0 | 56 first_enum_line = 0 |
70 last_enum_line = 0 | 57 last_enum_line = 0 |
71 line_num = 1 # Line numbers are 1-based | 58 line_num = 1 # Line numbers are 1-based |
72 for line in contents: | 59 for line in contents: |
73 if line.startswith(self.ENUM_START_MARKER): | 60 if line.startswith(self.start_marker): |
74 first_enum_line = line_num + 1 | 61 first_enum_line = line_num + 1 |
75 elif line.startswith(self.ENUM_END_MARKER): | 62 elif line.startswith(self.end_marker): |
76 last_enum_line = line_num | 63 last_enum_line = line_num |
77 line_num += 1 | 64 line_num += 1 |
78 | 65 |
79 if first_enum_line == 0: | 66 if first_enum_line == 0: |
80 self.EmitWarning("The presubmit script could not find the start of the " | 67 self.EmitWarning('The presubmit script could not find the start of the ' |
81 "enum definition (\"%s\"). Did the enum definition " | 68 'enum definition ("%s"). Did the enum definition ' |
82 "change?" % self.ENUM_START_MARKER) | 69 'change?' % self.start_marker) |
83 return None | 70 return None |
84 | 71 |
85 if last_enum_line == 0: | 72 if last_enum_line == 0: |
86 self.EmitWarning("The presubmit script could not find the end of the " | 73 self.EmitWarning('The presubmit script could not find the end of the ' |
87 "enum definition (\"%s\"). Did the enum definition " | 74 'enum definition ("%s"). Did the enum definition ' |
88 "change?" % self.ENUM_END_MARKER) | 75 'change?' % self.end_marker) |
89 return None | 76 return None |
90 | 77 |
91 if first_enum_line >= last_enum_line: | 78 if first_enum_line >= last_enum_line: |
92 self.EmitWarning("The presubmit script located the start of the enum " | 79 self.EmitWarning('The presubmit script located the start of the enum ' |
93 "definition (\"%s\" at line %d) *after* its end " | 80 'definition ("%s" at line %d) *after* its end ' |
94 "(\"%s\" at line %d). Something is not quite right." | 81 '("%s" at line %d). Something is not quite right.' |
95 % (self.ENUM_START_MARKER, first_enum_line, | 82 % (self.start_marker, first_enum_line, |
96 self.ENUM_END_MARKER, last_enum_line)) | 83 self.end_marker, last_enum_line)) |
97 return None | 84 return None |
98 | 85 |
99 self.LogInfo("Line extent of |HistogramValue| enum definition: " | 86 self.LogInfo('Line extent of |HistogramValue| enum definition: ' |
100 "first_line=%d, last_line=%d." | 87 'first_line=%d, last_line=%d.' |
101 % (first_enum_line, last_enum_line)) | 88 % (first_enum_line, last_enum_line)) |
102 return self.EnumRange(first_enum_line, last_enum_line) | 89 return self.EnumRange(first_enum_line, last_enum_line) |
103 | 90 |
104 def ComputeEnumRangeInNewFile(self, affected_file): | 91 def ComputeEnumRangeInNewFile(self, affected_file): |
105 return self.ComputeEnumRangeInContents(affected_file.NewContents()) | 92 return self.ComputeEnumRangeInContents(affected_file.NewContents()) |
106 | 93 |
107 def GetLongMessage(self): | 94 def GetLongMessage(self, local_path): |
108 return str("The file \"%s\" contains the definition of the " | 95 return str('The file "%s" contains the definition of the ' |
109 "|HistogramValue| enum which should be edited in specific ways " | 96 '|HistogramValue| enum which should be edited in specific ways ' |
rpaquay
2014/02/18 21:39:26
"HistogramValue" is too specific in this file, as
| |
110 "only - *** read the comments at the top of the header file ***" | 97 'only - *** read the comments at the top of the header file ***' |
111 ". There are changes to the file that may be incorrect and " | 98 '. There are changes to the file that may be incorrect and ' |
112 "warrant manual confirmation after review. Note that this " | 99 'warrant manual confirmation after review. Note that this ' |
113 "presubmit script can not reliably report the nature of all " | 100 'presubmit script can not reliably report the nature of all ' |
114 "types of invalid changes, especially when the diffs are " | 101 'types of invalid changes, especially when the diffs are ' |
115 "complex. For example, an invalid deletion may be reported " | 102 'complex. For example, an invalid deletion may be reported ' |
116 "whereas the change contains a valid rename." | 103 'whereas the change contains a valid rename.' |
117 % self.LOCAL_PATH) | 104 % local_path) |
118 | 105 |
119 def EmitWarning(self, message, line_number=None, line_text=None): | 106 def EmitWarning(self, message, line_number=None, line_text=None): |
120 """Emits a presubmit prompt warning containing the short message | 107 '''Emits a presubmit prompt warning containing the short message |
121 |message|. |item| is |LOCAL_PATH| with optional |line_number| and | 108 |message|. |item| is |LOCAL_PATH| with optional |line_number| and |
122 |line_text|. | 109 |line_text|. |
123 | 110 |
124 """ | 111 ''' |
125 if line_number is not None and line_text is not None: | 112 if line_number is not None and line_text is not None: |
126 item = "%s(%d): %s" % (self.LOCAL_PATH, line_number, line_text) | 113 item = '%s(%d): %s' % (self.path, line_number, line_text) |
127 elif line_number is not None: | 114 elif line_number is not None: |
128 item = "%s(%d)" % (self.LOCAL_PATH, line_number) | 115 item = '%s(%d)' % (self.path, line_number) |
129 else: | 116 else: |
130 item = self.LOCAL_PATH | 117 item = self.path |
131 long_message = self.GetLongMessage() | 118 long_message = self.GetLongMessage(self.path) |
132 self.LogInfo(message) | 119 self.LogInfo(message) |
133 self.results.append( | 120 self.results.append( |
134 self.output_api.PresubmitPromptWarning(message, [item], long_message)) | 121 self.output_api.PresubmitPromptWarning(message, [item], long_message)) |
135 | 122 |
136 def CollectRangesInsideEnumDefinition(self, affected_file, | 123 def CollectRangesInsideEnumDefinition(self, affected_file, |
137 first_line, last_line): | 124 first_line, last_line): |
138 """Returns a list of triplet (line_start, line_end, line_text) of ranges of | 125 '''Returns a list of triplet (line_start, line_end, line_text) of ranges of |
139 edits changes. The |line_text| part is the text at line |line_start|. | 126 edits changes. The |line_text| part is the text at line |line_start|. |
140 Since it used only for reporting purposes, we do not need all the text | 127 Since it used only for reporting purposes, we do not need all the text |
141 lines in the range. | 128 lines in the range. |
142 | 129 |
143 """ | 130 ''' |
144 results = [] | 131 results = [] |
145 previous_line_number = 0 | 132 previous_line_number = 0 |
146 previous_range_start_line_number = 0 | 133 previous_range_start_line_number = 0 |
147 previous_range_start_text = "" | 134 previous_range_start_text = '' |
148 | 135 |
149 def addRange(): | 136 def addRange(): |
150 tuple = (previous_range_start_line_number, | 137 tuple = (previous_range_start_line_number, |
151 previous_line_number, | 138 previous_line_number, |
152 previous_range_start_text) | 139 previous_range_start_text) |
153 results.append(tuple) | 140 results.append(tuple) |
154 | 141 |
155 for line_number, line_text in affected_file.ChangedContents(): | 142 for line_number, line_text in affected_file.ChangedContents(): |
156 if first_line <= line_number and line_number <= last_line: | 143 if first_line <= line_number and line_number <= last_line: |
157 self.LogDebug("Line change at line number " + str(line_number) + ": " + | 144 self.LogDebug('Line change at line number ' + str(line_number) + ': ' + |
158 line_text) | 145 line_text) |
159 # Start a new interval if none started | 146 # Start a new interval if none started |
160 if previous_range_start_line_number == 0: | 147 if previous_range_start_line_number == 0: |
161 previous_range_start_line_number = line_number | 148 previous_range_start_line_number = line_number |
162 previous_range_start_text = line_text | 149 previous_range_start_text = line_text |
163 # Add new interval if we reached past the previous one | 150 # Add new interval if we reached past the previous one |
164 elif line_number != previous_line_number + 1: | 151 elif line_number != previous_line_number + 1: |
165 addRange() | 152 addRange() |
166 previous_range_start_line_number = line_number | 153 previous_range_start_line_number = line_number |
167 previous_range_start_text = line_text | 154 previous_range_start_text = line_text |
168 previous_line_number = line_number | 155 previous_line_number = line_number |
169 | 156 |
170 # Add a last interval if needed | 157 # Add a last interval if needed |
171 if previous_range_start_line_number != 0: | 158 if previous_range_start_line_number != 0: |
172 addRange() | 159 addRange() |
173 return results | 160 return results |
174 | 161 |
175 def CheckForFileDeletion(self, affected_file): | 162 def CheckForFileDeletion(self, affected_file): |
176 """Emits a warning notification if file has been deleted """ | 163 '''Emits a warning notification if file has been deleted ''' |
177 if not affected_file.NewContents(): | 164 if not affected_file.NewContents(): |
178 self.EmitWarning("The file seems to be deleted in the changelist. If " | 165 self.EmitWarning('The file seems to be deleted in the changelist. If ' |
179 "your intent is to really delete the file, the code in " | 166 'your intent is to really delete the file, the code in ' |
180 "PRESUBMIT.py should be updated to remove the " | 167 'PRESUBMIT.py should be updated to remove the ' |
181 "|HistogramValueChecker| class."); | 168 '|HistogramValueChecker| class.'); |
rpaquay
2014/02/18 21:39:26
nit: HistogramValueChecker is too specific.
| |
182 return False | 169 return False |
183 return True | 170 return True |
184 | 171 |
185 def GetDeletedLinesFromScmDiff(self, affected_file): | 172 def GetDeletedLinesFromScmDiff(self, affected_file): |
186 """Return a list of of line numbers (1-based) corresponding to lines | 173 '''Return a list of of line numbers (1-based) corresponding to lines |
187 deleted from the new source file (if they had been present in it). Note | 174 deleted from the new source file (if they had been present in it). Note |
188 that if multiple contiguous lines have been deleted, the returned list will | 175 that if multiple contiguous lines have been deleted, the returned list will |
189 contain contiguous line number entries. To prevent false positives, we | 176 contain contiguous line number entries. To prevent false positives, we |
190 return deleted line numbers *only* from diff chunks which decrease the size | 177 return deleted line numbers *only* from diff chunks which decrease the size |
191 of the new file. | 178 of the new file. |
192 | 179 |
193 Note: We need this method because we have access to neither the old file | 180 Note: We need this method because we have access to neither the old file |
194 content nor the list of "delete" changes from the current presubmit script | 181 content nor the list of "delete" changes from the current presubmit script |
195 API. | 182 API. |
196 | 183 |
197 """ | 184 ''' |
198 results = [] | 185 results = [] |
199 line_num = 0 | 186 line_num = 0 |
200 deleting_lines = False | 187 deleting_lines = False |
201 for line in affected_file.GenerateScmDiff().splitlines(): | 188 for line in affected_file.GenerateScmDiff().splitlines(): |
202 # Parse the unified diff chunk optional section heading, which looks like | 189 # Parse the unified diff chunk optional section heading, which looks like |
203 # @@ -l,s +l,s @@ optional section heading | 190 # @@ -l,s +l,s @@ optional section heading |
204 m = self.input_api.re.match( | 191 m = self.input_api.re.match( |
205 r'^@@ \-([0-9]+)\,([0-9]+) \+([0-9]+)\,([0-9]+) @@', line) | 192 r'^@@ \-([0-9]+)\,([0-9]+) \+([0-9]+)\,([0-9]+) @@', line) |
206 if m: | 193 if m: |
207 old_line_num = int(m.group(1)) | 194 old_line_num = int(m.group(1)) |
208 old_size = int(m.group(2)) | 195 old_size = int(m.group(2)) |
209 new_line_num = int(m.group(3)) | 196 new_line_num = int(m.group(3)) |
210 new_size = int(m.group(4)) | 197 new_size = int(m.group(4)) |
211 line_num = new_line_num | 198 line_num = new_line_num |
212 # Return line numbers only from diff chunks decreasing the size of the | 199 # Return line numbers only from diff chunks decreasing the size of the |
213 # new file | 200 # new file |
214 deleting_lines = old_size > new_size | 201 deleting_lines = old_size > new_size |
215 continue | 202 continue |
216 if not line.startswith('-'): | 203 if not line.startswith('-'): |
217 line_num += 1 | 204 line_num += 1 |
218 if deleting_lines and line.startswith('-') and not line.startswith('--'): | 205 if deleting_lines and line.startswith('-') and not line.startswith('--'): |
219 results.append(line_num) | 206 results.append(line_num) |
220 return results | 207 return results |
221 | 208 |
222 def CheckForEnumEntryDeletions(self, affected_file): | 209 def CheckForEnumEntryDeletions(self, affected_file): |
223 """Look for deletions inside the enum definition. We currently use a | 210 '''Look for deletions inside the enum definition. We currently use a |
224 simple heuristics (not 100% accurate): if there are deleted lines inside | 211 simple heuristics (not 100% accurate): if there are deleted lines inside |
225 the enum definition, this might be a deletion. | 212 the enum definition, this might be a deletion. |
226 | 213 |
227 """ | 214 ''' |
228 range_new = self.ComputeEnumRangeInNewFile(affected_file) | 215 range_new = self.ComputeEnumRangeInNewFile(affected_file) |
229 if not range_new: | 216 if not range_new: |
230 return False | 217 return False |
231 | 218 |
232 is_ok = True | 219 is_ok = True |
233 for line_num in self.GetDeletedLinesFromScmDiff(affected_file): | 220 for line_num in self.GetDeletedLinesFromScmDiff(affected_file): |
234 if range_new.Contains(line_num): | 221 if range_new.Contains(line_num): |
235 self.EmitWarning("It looks like you are deleting line(s) from the " | 222 self.EmitWarning('It looks like you are deleting line(s) from the ' |
236 "enum definition. This should never happen.", | 223 'enum definition. This should never happen.', |
237 line_num) | 224 line_num) |
238 is_ok = False | 225 is_ok = False |
239 return is_ok | 226 return is_ok |
240 | 227 |
241 def CheckForEnumEntryInsertions(self, affected_file): | 228 def CheckForEnumEntryInsertions(self, affected_file): |
242 range = self.ComputeEnumRangeInNewFile(affected_file) | 229 range = self.ComputeEnumRangeInNewFile(affected_file) |
243 if not range: | 230 if not range: |
244 return False | 231 return False |
245 | 232 |
246 first_line = range.first_line | 233 first_line = range.first_line |
247 last_line = range.last_line | 234 last_line = range.last_line |
248 | 235 |
249 # Collect the range of changes inside the enum definition range. | 236 # Collect the range of changes inside the enum definition range. |
250 is_ok = True | 237 is_ok = True |
251 for line_start, line_end, line_text in \ | 238 for line_start, line_end, line_text in \ |
252 self.CollectRangesInsideEnumDefinition(affected_file, | 239 self.CollectRangesInsideEnumDefinition(affected_file, |
253 first_line, | 240 first_line, |
254 last_line): | 241 last_line): |
255 # The only edit we consider valid is adding 1 or more entries *exactly* | 242 # The only edit we consider valid is adding 1 or more entries *exactly* |
256 # at the end of the enum definition. Every other edit inside the enum | 243 # at the end of the enum definition. Every other edit inside the enum |
257 # definition will result in a "warning confirmation" message. | 244 # definition will result in a "warning confirmation" message. |
258 # | 245 # |
259 # TODO(rpaquay): We currently cannot detect "renames" of existing entries | 246 # TODO(rpaquay): We currently cannot detect "renames" of existing entries |
260 # vs invalid insertions, so we sometimes will warn for valid edits. | 247 # vs invalid insertions, so we sometimes will warn for valid edits. |
261 is_valid_edit = (line_end == last_line - 1) | 248 is_valid_edit = (line_end == last_line - 1) |
262 | 249 |
263 self.LogDebug("Edit range in new file at starting at line number %d and " | 250 self.LogDebug('Edit range in new file at starting at line number %d and ' |
264 "ending at line number %d: valid=%s" | 251 'ending at line number %d: valid=%s' |
265 % (line_start, line_end, is_valid_edit)) | 252 % (line_start, line_end, is_valid_edit)) |
266 | 253 |
267 if not is_valid_edit: | 254 if not is_valid_edit: |
268 self.EmitWarning("The change starting at line %d and ending at line " | 255 self.EmitWarning('The change starting at line %d and ending at line ' |
269 "%d is *not* located *exactly* at the end of the " | 256 '%d is *not* located *exactly* at the end of the ' |
270 "enum definition. Unless you are renaming an " | 257 'enum definition. Unless you are renaming an ' |
271 "existing entry, this is not a valid changes, as new " | 258 'existing entry, this is not a valid changes, as new ' |
272 "entries should *always* be added at the end of the " | 259 'entries should *always* be added at the end of the ' |
273 "enum definition, right before the 'ENUM_BOUNDARY' " | 260 'enum definition, right before the "ENUM_BOUNDARY" ' |
274 "entry." % (line_start, line_end), | 261 'entry.' % (line_start, line_end), |
275 line_start, | 262 line_start, |
276 line_text) | 263 line_text) |
277 is_ok = False | 264 is_ok = False |
278 return is_ok | 265 return is_ok |
279 | 266 |
280 def PerformChecks(self, affected_file): | 267 def PerformChecks(self, affected_file): |
281 if not self.CheckForFileDeletion(affected_file): | 268 if not self.CheckForFileDeletion(affected_file): |
282 return | 269 return |
283 if not self.CheckForEnumEntryDeletions(affected_file): | 270 if not self.CheckForEnumEntryDeletions(affected_file): |
284 return | 271 return |
285 if not self.CheckForEnumEntryInsertions(affected_file): | 272 if not self.CheckForEnumEntryInsertions(affected_file): |
286 return | 273 return |
287 | 274 |
288 def ProcessHistogramValueFile(self, affected_file): | 275 def ProcessHistogramValueFile(self, affected_file): |
289 self.LogInfo("Start processing file \"%s\"" % affected_file.LocalPath()) | 276 self.LogInfo('Start processing file "%s"' % affected_file.LocalPath()) |
290 self.PerformChecks(affected_file) | 277 self.PerformChecks(affected_file) |
291 self.LogInfo("Done processing file \"%s\"" % affected_file.LocalPath()) | 278 self.LogInfo('Done processing file "%s"' % affected_file.LocalPath()) |
292 | 279 |
293 def Run(self): | 280 def Run(self): |
294 for file in self.input_api.AffectedFiles(include_deletes=True): | 281 for file in self.input_api.AffectedFiles(include_deletes=True): |
295 if file.LocalPath() == self.LOCAL_PATH: | 282 if file.LocalPath() == self.path: |
296 self.ProcessHistogramValueFile(file) | 283 self.ProcessHistogramValueFile(file) |
297 return self.results | 284 return self.results |
298 | |
299 def CheckChangeOnUpload(input_api, output_api): | |
300 results = [] | |
301 results += HistogramValueChecker(input_api, output_api).Run() | |
302 results += input_api.canned_checks.CheckPatchFormatted(input_api, output_api) | |
303 return results | |
304 | |
OLD | NEW |