Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(196)

Side by Side Diff: tools/extra_imports/strict_enum_value_checker.py

Issue 170233008: Add presubmit check and automatic update script for ExtensionPermission enum. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 6 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698