| OLD | NEW |
| 1 # Copyright 2014 The Chromium Authors. All rights reserved. | 1 # Copyright 2014 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/extensions/browser. | 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" |
| 10 | 11 file in chrome/browser/extensions for an example what are considered valid |
| 11 def GetPreferredTryMasters(project, change): | 12 changes. There are situations where this class gives false positive warnings, |
| 12 return { | 13 i.e. it warns even though the edit is legitimate. Since the class warns using |
| 13 'tryserver.chromium': { | 14 prompt warnings, the user can always choose to continue. The main point is to |
| 14 'linux_chromium_chromeos_rel': set(['defaulttests']), | 15 attract the attention to all (potentially or not) invalid edits. |
| 15 } | |
| 16 } | |
| 17 | |
| 18 class HistogramValueChecker(object): | |
| 19 """Verify that changes to "extension_function_histogram_value.h" are valid. | |
| 20 | |
| 21 See comments at the top of the "extension_function_histogram_value.h" file | |
| 22 for what are considered valid changes. There are situations where this script | |
| 23 gives false positive warnings, i.e. it warns even though the edit is | |
| 24 legitimate. Since the script warns using prompt warnings, the user can always | |
| 25 choose to continue. The main point is to attract the attention to all | |
| 26 (potentially or not) invalid edits. | |
| 27 | 16 |
| 28 """ | 17 """ |
| 29 | 18 def __init__(self, input_api, output_api, start_marker, end_marker, path): |
| 30 # The name of the file we want to check against | |
| 31 LOCAL_PATH = "extensions/browser/extension_function_histogram_value.h" | |
| 32 | |
| 33 # The markers we look for in the above source file as delimiters of the enum | |
| 34 # definition. | |
| 35 ENUM_START_MARKER = "enum HistogramValue {" | |
| 36 ENUM_END_MARKER = " // Last entry:" | |
| 37 | |
| 38 def __init__(self, input_api, output_api): | |
| 39 self.input_api = input_api | 19 self.input_api = input_api |
| 40 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 |
| 41 self.results = [] | 24 self.results = [] |
| 42 | 25 |
| 43 class EnumRange(object): | 26 class EnumRange(object): |
| 44 """Represents a range of line numbers (1-based)""" | 27 """Represents a range of line numbers (1-based)""" |
| 45 def __init__(self, first_line, last_line): | 28 def __init__(self, first_line, last_line): |
| 46 self.first_line = first_line | 29 self.first_line = first_line |
| 47 self.last_line = last_line | 30 self.last_line = last_line |
| 48 | 31 |
| 49 def Count(self): | 32 def Count(self): |
| 50 return self.last_line - self.first_line + 1 | 33 return self.last_line - self.first_line + 1 |
| 51 | 34 |
| 52 def Contains(self, line_num): | 35 def Contains(self, line_num): |
| 53 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 |
| 54 | 37 |
| 55 def LogInfo(self, message): | 38 def LogInfo(self, message): |
| 56 self.input_api.logging.info(message) | 39 self.input_api.logging.info(message) |
| 57 return | 40 return |
| 58 | 41 |
| 59 def LogDebug(self, message): | 42 def LogDebug(self, message): |
| 60 self.input_api.logging.debug(message) | 43 self.input_api.logging.debug(message) |
| 61 return | 44 return |
| 62 | 45 |
| 63 def ComputeEnumRangeInContents(self, contents): | 46 def ComputeEnumRangeInContents(self, contents): |
| 64 """Returns an |EnumRange| object representing the line extent of the | 47 """Returns an |EnumRange| object representing the line extent of the |
| 65 HistogramValue enum members in |contents|. The line numbers are 1-based, | 48 enum members in |contents|. The line numbers are 1-based, |
| 66 compatible with line numbers returned by AffectedFile.ChangeContents(). | 49 compatible with line numbers returned by AffectedFile.ChangeContents(). |
| 67 |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. |
| 68 | 51 |
| 69 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 |
| 70 |contents|, returns None and emits detailed warnings about the problem. | 53 |contents|, returns None and emits detailed warnings about the problem. |
| 71 | 54 |
| 72 """ | 55 """ |
| 73 first_enum_line = 0 | 56 first_enum_line = 0 |
| 74 last_enum_line = 0 | 57 last_enum_line = 0 |
| 75 line_num = 1 # Line numbers are 1-based | 58 line_num = 1 # Line numbers are 1-based |
| 76 for line in contents: | 59 for line in contents: |
| 77 if line.startswith(self.ENUM_START_MARKER): | 60 if line.startswith(self.start_marker): |
| 78 first_enum_line = line_num + 1 | 61 first_enum_line = line_num + 1 |
| 79 elif line.startswith(self.ENUM_END_MARKER): | 62 elif line.startswith(self.end_marker): |
| 80 last_enum_line = line_num | 63 last_enum_line = line_num |
| 81 line_num += 1 | 64 line_num += 1 |
| 82 | 65 |
| 83 if first_enum_line == 0: | 66 if first_enum_line == 0: |
| 84 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 " |
| 85 "enum definition (\"%s\"). Did the enum definition " | 68 "enum definition (\"%s\"). Did the enum definition " |
| 86 "change?" % self.ENUM_START_MARKER) | 69 "change?" % self.start_marker) |
| 87 return None | 70 return None |
| 88 | 71 |
| 89 if last_enum_line == 0: | 72 if last_enum_line == 0: |
| 90 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 " |
| 91 "enum definition (\"%s\"). Did the enum definition " | 74 "enum definition (\"%s\"). Did the enum definition " |
| 92 "change?" % self.ENUM_END_MARKER) | 75 "change?" % self.end_marker) |
| 93 return None | 76 return None |
| 94 | 77 |
| 95 if first_enum_line >= last_enum_line: | 78 if first_enum_line >= last_enum_line: |
| 96 self.EmitWarning("The presubmit script located the start of the enum " | 79 self.EmitWarning("The presubmit script located the start of the enum " |
| 97 "definition (\"%s\" at line %d) *after* its end " | 80 "definition (\"%s\" at line %d) *after* its end " |
| 98 "(\"%s\" at line %d). Something is not quite right." | 81 "(\"%s\" at line %d). Something is not quite right." |
| 99 % (self.ENUM_START_MARKER, first_enum_line, | 82 % (self.start_marker, first_enum_line, |
| 100 self.ENUM_END_MARKER, last_enum_line)) | 83 self.end_marker, last_enum_line)) |
| 101 return None | 84 return None |
| 102 | 85 |
| 103 self.LogInfo("Line extent of |HistogramValue| enum definition: " | 86 self.LogInfo("Line extent of (\"%s\") enum definition: " |
| 104 "first_line=%d, last_line=%d." | 87 "first_line=%d, last_line=%d." |
| 105 % (first_enum_line, last_enum_line)) | 88 % (self.start_marker, first_enum_line, last_enum_line)) |
| 106 return self.EnumRange(first_enum_line, last_enum_line) | 89 return self.EnumRange(first_enum_line, last_enum_line) |
| 107 | 90 |
| 108 def ComputeEnumRangeInNewFile(self, affected_file): | 91 def ComputeEnumRangeInNewFile(self, affected_file): |
| 109 return self.ComputeEnumRangeInContents(affected_file.NewContents()) | 92 return self.ComputeEnumRangeInContents(affected_file.NewContents()) |
| 110 | 93 |
| 111 def GetLongMessage(self): | 94 def GetLongMessage(self, local_path): |
| 112 return str("The file \"%s\" contains the definition of the " | 95 return str("The file \"%s\" contains the definition of the " |
| 113 "|HistogramValue| enum which should be edited in specific ways " | 96 "(\"%s\") enum which should be edited in specific ways " |
| 114 "only - *** read the comments at the top of the header file ***" | 97 "only - *** read the comments at the top of the header file ***" |
| 115 ". There are changes to the file that may be incorrect and " | 98 ". There are changes to the file that may be incorrect and " |
| 116 "warrant manual confirmation after review. Note that this " | 99 "warrant manual confirmation after review. Note that this " |
| 117 "presubmit script can not reliably report the nature of all " | 100 "presubmit script can not reliably report the nature of all " |
| 118 "types of invalid changes, especially when the diffs are " | 101 "types of invalid changes, especially when the diffs are " |
| 119 "complex. For example, an invalid deletion may be reported " | 102 "complex. For example, an invalid deletion may be reported " |
| 120 "whereas the change contains a valid rename." | 103 "whereas the change contains a valid rename." |
| 121 % self.LOCAL_PATH) | 104 % local_path, self.start_marker) |
| 122 | 105 |
| 123 def EmitWarning(self, message, line_number=None, line_text=None): | 106 def EmitWarning(self, message, line_number=None, line_text=None): |
| 124 """Emits a presubmit prompt warning containing the short message | 107 """Emits a presubmit prompt warning containing the short message |
| 125 |message|. |item| is |LOCAL_PATH| with optional |line_number| and | 108 |message|. |item| is |LOCAL_PATH| with optional |line_number| and |
| 126 |line_text|. | 109 |line_text|. |
| 127 | 110 |
| 128 """ | 111 """ |
| 129 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: |
| 130 item = "%s(%d): %s" % (self.LOCAL_PATH, line_number, line_text) | 113 item = "%s(%d): %s" % (self.path, line_number, line_text) |
| 131 elif line_number is not None: | 114 elif line_number is not None: |
| 132 item = "%s(%d)" % (self.LOCAL_PATH, line_number) | 115 item = "%s(%d)" % (self.path, line_number) |
| 133 else: | 116 else: |
| 134 item = self.LOCAL_PATH | 117 item = self.path |
| 135 long_message = self.GetLongMessage() | 118 long_message = self.GetLongMessage(self.path) |
| 136 self.LogInfo(message) | 119 self.LogInfo(message) |
| 137 self.results.append( | 120 self.results.append( |
| 138 self.output_api.PresubmitPromptWarning(message, [item], long_message)) | 121 self.output_api.PresubmitPromptWarning(message, [item], long_message)) |
| 139 | 122 |
| 140 def CollectRangesInsideEnumDefinition(self, affected_file, | 123 def CollectRangesInsideEnumDefinition(self, affected_file, |
| 141 first_line, last_line): | 124 first_line, last_line): |
| 142 """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 |
| 143 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|. |
| 144 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 |
| 145 lines in the range. | 128 lines in the range. |
| (...skipping 29 matching lines...) Expand all Loading... |
| 175 if previous_range_start_line_number != 0: | 158 if previous_range_start_line_number != 0: |
| 176 addRange() | 159 addRange() |
| 177 return results | 160 return results |
| 178 | 161 |
| 179 def CheckForFileDeletion(self, affected_file): | 162 def CheckForFileDeletion(self, affected_file): |
| 180 """Emits a warning notification if file has been deleted """ | 163 """Emits a warning notification if file has been deleted """ |
| 181 if not affected_file.NewContents(): | 164 if not affected_file.NewContents(): |
| 182 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 " |
| 183 "your intent is to really delete the file, the code in " | 166 "your intent is to really delete the file, the code in " |
| 184 "PRESUBMIT.py should be updated to remove the " | 167 "PRESUBMIT.py should be updated to remove the " |
| 185 "|HistogramValueChecker| class."); | 168 "|StrictEnumValueChecker| class."); |
| 186 return False | 169 return False |
| 187 return True | 170 return True |
| 188 | 171 |
| 189 def GetDeletedLinesFromScmDiff(self, affected_file): | 172 def GetDeletedLinesFromScmDiff(self, affected_file): |
| 190 """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 |
| 191 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 |
| 192 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 |
| 193 contain contiguous line number entries. To prevent false positives, we | 176 contain contiguous line number entries. To prevent false positives, we |
| 194 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 |
| 195 of the new file. | 178 of the new file. |
| 196 | 179 |
| 197 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 |
| 198 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 |
| 199 API. | 182 API. |
| 200 | 183 |
| 201 """ | 184 """ |
| 202 results = [] | 185 results = [] |
| 203 line_num = 0 | 186 line_num = 0 |
| 204 deleting_lines = False | 187 deleting_lines = False |
| 205 for line in affected_file.GenerateScmDiff().splitlines(): | 188 for line in affected_file.GenerateScmDiff().splitlines(): |
| 206 # Parse the unified diff chunk optional section heading, which looks like | 189 # Parse the unified diff chunk optional section heading, which looks like |
| 207 # @@ -l,s +l,s @@ optional section heading | 190 # @@ -l,s +l,s @@ optional section heading |
| 208 m = self.input_api.re.match( | 191 m = self.input_api.re.match( |
| 209 r'^@@ \-([0-9]+)\,([0-9]+) \+([0-9]+)\,([0-9]+) @@', line) | 192 r"^@@ \-([0-9]+)\,([0-9]+) \+([0-9]+)\,([0-9]+) @@", line) |
| 210 if m: | 193 if m: |
| 211 old_line_num = int(m.group(1)) | 194 old_line_num = int(m.group(1)) |
| 212 old_size = int(m.group(2)) | 195 old_size = int(m.group(2)) |
| 213 new_line_num = int(m.group(3)) | 196 new_line_num = int(m.group(3)) |
| 214 new_size = int(m.group(4)) | 197 new_size = int(m.group(4)) |
| 215 line_num = new_line_num | 198 line_num = new_line_num |
| 216 # 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 |
| 217 # new file | 200 # new file |
| 218 deleting_lines = old_size > new_size | 201 deleting_lines = old_size > new_size |
| 219 continue | 202 continue |
| 220 if not line.startswith('-'): | 203 if not line.startswith("-"): |
| 221 line_num += 1 | 204 line_num += 1 |
| 222 if deleting_lines and line.startswith('-') and not line.startswith('--'): | 205 if deleting_lines and line.startswith("-") and not line.startswith("--"): |
| 223 results.append(line_num) | 206 results.append(line_num) |
| 224 return results | 207 return results |
| 225 | 208 |
| 226 def CheckForEnumEntryDeletions(self, affected_file): | 209 def CheckForEnumEntryDeletions(self, affected_file): |
| 227 """Look for deletions inside the enum definition. We currently use a | 210 """Look for deletions inside the enum definition. We currently use a |
| 228 simple heuristics (not 100% accurate): if there are deleted lines inside | 211 simple heuristics (not 100% accurate): if there are deleted lines inside |
| 229 the enum definition, this might be a deletion. | 212 the enum definition, this might be a deletion. |
| 230 | 213 |
| 231 """ | 214 """ |
| 232 range_new = self.ComputeEnumRangeInNewFile(affected_file) | 215 range_new = self.ComputeEnumRangeInNewFile(affected_file) |
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 265 is_valid_edit = (line_end == last_line - 1) | 248 is_valid_edit = (line_end == last_line - 1) |
| 266 | 249 |
| 267 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 " |
| 268 "ending at line number %d: valid=%s" | 251 "ending at line number %d: valid=%s" |
| 269 % (line_start, line_end, is_valid_edit)) | 252 % (line_start, line_end, is_valid_edit)) |
| 270 | 253 |
| 271 if not is_valid_edit: | 254 if not is_valid_edit: |
| 272 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 " |
| 273 "%d is *not* located *exactly* at the end of the " | 256 "%d is *not* located *exactly* at the end of the " |
| 274 "enum definition. Unless you are renaming an " | 257 "enum definition. Unless you are renaming an " |
| 275 "existing entry, this is not a valid changes, as new " | 258 "existing entry, this is not a valid change, as new " |
| 276 "entries should *always* be added at the end of the " | 259 "entries should *always* be added at the end of the " |
| 277 "enum definition, right before the 'ENUM_BOUNDARY' " | 260 "enum definition, right before the \"%s\" " |
| 278 "entry." % (line_start, line_end), | 261 "entry." % (line_start, line_end, self.end_marker), |
| 279 line_start, | 262 line_start, |
| 280 line_text) | 263 line_text) |
| 281 is_ok = False | 264 is_ok = False |
| 282 return is_ok | 265 return is_ok |
| 283 | 266 |
| 284 def PerformChecks(self, affected_file): | 267 def PerformChecks(self, affected_file): |
| 285 if not self.CheckForFileDeletion(affected_file): | 268 if not self.CheckForFileDeletion(affected_file): |
| 286 return | 269 return |
| 287 if not self.CheckForEnumEntryDeletions(affected_file): | 270 if not self.CheckForEnumEntryDeletions(affected_file): |
| 288 return | 271 return |
| 289 if not self.CheckForEnumEntryInsertions(affected_file): | 272 if not self.CheckForEnumEntryInsertions(affected_file): |
| 290 return | 273 return |
| 291 | 274 |
| 292 def ProcessHistogramValueFile(self, affected_file): | 275 def ProcessHistogramValueFile(self, affected_file): |
| 293 self.LogInfo("Start processing file \"%s\"" % affected_file.LocalPath()) | 276 self.LogInfo("Start processing file \"%s\"" % affected_file.LocalPath()) |
| 294 self.PerformChecks(affected_file) | 277 self.PerformChecks(affected_file) |
| 295 self.LogInfo("Done processing file \"%s\"" % affected_file.LocalPath()) | 278 self.LogInfo("Done processing file \"%s\"" % affected_file.LocalPath()) |
| 296 | 279 |
| 297 def Run(self): | 280 def Run(self): |
| 298 for file in self.input_api.AffectedFiles(include_deletes=True): | 281 for file in self.input_api.AffectedFiles(include_deletes=True): |
| 299 if file.LocalPath() == self.LOCAL_PATH: | 282 if file.LocalPath() == self.path: |
| 300 self.ProcessHistogramValueFile(file) | 283 self.ProcessHistogramValueFile(file) |
| 301 return self.results | 284 return self.results |
| 302 | |
| 303 def CheckChangeOnUpload(input_api, output_api): | |
| 304 results = [] | |
| 305 results += HistogramValueChecker(input_api, output_api).Run() | |
| 306 results += input_api.canned_checks.CheckPatchFormatted(input_api, output_api) | |
| 307 return results | |
| 308 | |
| OLD | NEW |