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 3 # found in the LICENSE file. 4 5 """Chromium presubmit script for src/chrome/browser/extensions. 6 7 See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts 8 for more details on the presubmit API built into gcl. 9 """ 10 11 def GetPreferredTrySlaves(): 12 return ['linux_chromeos'] 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 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 299 def CheckChangeOnUpload(input_api, output_api): 300 results = [] 301 results += HistogramValueChecker(input_api, output_api).Run() 302 return results 303 304