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