|  | # Copyright 2014 The Chromium Authors | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  |  | 
|  | class StrictEnumValueChecker(object): | 
|  | """Verify that changes to enums are valid. | 
|  |  | 
|  | This class is used to check enums where reordering or deletion is not allowed, | 
|  | and additions must be at the end of the enum, just prior to some "boundary" | 
|  | entry. See comments at the top of the "extension_function_histogram_value.h" | 
|  | file in chrome/browser/extensions for an example what are considered valid | 
|  | changes. There are situations where this class gives false positive warnings, | 
|  | i.e. it warns even though the edit is legitimate. Since the class warns using | 
|  | prompt warnings, the user can always choose to continue. The main point is to | 
|  | attract the attention to all (potentially or not) invalid edits. | 
|  |  | 
|  | """ | 
|  | def __init__(self, input_api, output_api, start_marker, end_marker, path): | 
|  | self.input_api = input_api | 
|  | self.output_api = output_api | 
|  | self.start_marker = start_marker | 
|  | self.end_marker = end_marker | 
|  | self.path = path | 
|  | self.results = [] | 
|  |  | 
|  | class EnumRange(object): | 
|  | """Represents a range of line numbers (1-based)""" | 
|  | def __init__(self, first_line, last_line): | 
|  | self.first_line = first_line | 
|  | self.last_line = last_line | 
|  |  | 
|  | def Count(self): | 
|  | return self.last_line - self.first_line + 1 | 
|  |  | 
|  | def Contains(self, line_num): | 
|  | return self.first_line <= line_num and line_num <= self.last_line | 
|  |  | 
|  | def LogInfo(self, message): | 
|  | self.input_api.logging.info(message) | 
|  | return | 
|  |  | 
|  | def LogDebug(self, message): | 
|  | self.input_api.logging.debug(message) | 
|  | return | 
|  |  | 
|  | def ComputeEnumRangeInContents(self, contents): | 
|  | """Returns an |EnumRange| object representing the line extent of the | 
|  | enum members in |contents|. The line numbers are 1-based, | 
|  | compatible with line numbers returned by AffectedFile.ChangeContents(). | 
|  | |contents| is a list of strings reprenting the lines of a text file. | 
|  |  | 
|  | If either start_marker or end_marker cannot be found in | 
|  | |contents|, returns None and emits detailed warnings about the problem. | 
|  |  | 
|  | """ | 
|  | first_enum_line = 0 | 
|  | last_enum_line = 0 | 
|  | line_num = 1  # Line numbers are 1-based | 
|  | for line in contents: | 
|  | if line.startswith(self.start_marker): | 
|  | first_enum_line = line_num + 1 | 
|  | elif line.startswith(self.end_marker): | 
|  | last_enum_line = line_num | 
|  | line_num += 1 | 
|  |  | 
|  | if first_enum_line == 0: | 
|  | self.EmitWarning("The presubmit script could not find the start of the " | 
|  | "enum definition (\"%s\"). Did the enum definition " | 
|  | "change?" % self.start_marker) | 
|  | return None | 
|  |  | 
|  | if last_enum_line == 0: | 
|  | self.EmitWarning("The presubmit script could not find the end of the " | 
|  | "enum definition (\"%s\"). Did the enum definition " | 
|  | "change?" % self.end_marker) | 
|  | return None | 
|  |  | 
|  | if first_enum_line >= last_enum_line: | 
|  | self.EmitWarning("The presubmit script located the start of the enum " | 
|  | "definition (\"%s\" at line %d) *after* its end " | 
|  | "(\"%s\" at line %d). Something is not quite right." | 
|  | % (self.start_marker, first_enum_line, | 
|  | self.end_marker, last_enum_line)) | 
|  | return None | 
|  |  | 
|  | self.LogInfo("Line extent of (\"%s\") enum definition: " | 
|  | "first_line=%d, last_line=%d." | 
|  | % (self.start_marker, first_enum_line, last_enum_line)) | 
|  | return self.EnumRange(first_enum_line, last_enum_line) | 
|  |  | 
|  | def ComputeEnumRangeInNewFile(self, affected_file): | 
|  | return self.ComputeEnumRangeInContents(affected_file.NewContents()) | 
|  |  | 
|  | def GetLongMessage(self, local_path): | 
|  | return str("The file \"%s\" contains the definition of the " | 
|  | "(\"%s\") enum which should be edited in specific ways " | 
|  | "only - *** read the comments at the top of the header file ***" | 
|  | ". There are changes to the file that may be incorrect and " | 
|  | "warrant manual confirmation after review. Note that this " | 
|  | "presubmit script can not reliably report the nature of all " | 
|  | "types of invalid changes, especially when the diffs are " | 
|  | "complex. For example, an invalid deletion may be reported " | 
|  | "whereas the change contains a valid rename." | 
|  | % (local_path, self.start_marker)) | 
|  |  | 
|  | def EmitWarning(self, message, line_number=None, line_text=None): | 
|  | """Emits a presubmit prompt warning containing the short message | 
|  | |message|. |item| is |LOCAL_PATH| with optional |line_number| and | 
|  | |line_text|. | 
|  |  | 
|  | """ | 
|  | if line_number is not None and line_text is not None: | 
|  | item = "%s(%d): %s" % (self.path, line_number, line_text) | 
|  | elif line_number is not None: | 
|  | item = "%s(%d)" % (self.path, line_number) | 
|  | else: | 
|  | item = self.path | 
|  | long_message = self.GetLongMessage(self.path) | 
|  | self.LogInfo(message) | 
|  | self.results.append( | 
|  | self.output_api.PresubmitPromptWarning(message, [item], long_message)) | 
|  |  | 
|  | def CollectRangesInsideEnumDefinition(self, affected_file, | 
|  | first_line, last_line): | 
|  | """Returns a list of triplet (line_start, line_end, line_text) of ranges of | 
|  | edits changes. The |line_text| part is the text at line |line_start|. | 
|  | Since it used only for reporting purposes, we do not need all the text | 
|  | lines in the range. | 
|  |  | 
|  | """ | 
|  | results = [] | 
|  | previous_line_number = 0 | 
|  | previous_range_start_line_number = 0 | 
|  | previous_range_start_text = "" | 
|  |  | 
|  | def addRange(): | 
|  | tuple = (previous_range_start_line_number, | 
|  | previous_line_number, | 
|  | previous_range_start_text) | 
|  | results.append(tuple) | 
|  |  | 
|  | for line_number, line_text in affected_file.ChangedContents(): | 
|  | if first_line <= line_number and line_number <= last_line: | 
|  | self.LogDebug("Line change at line number " + str(line_number) + ": " + | 
|  | line_text) | 
|  | # Start a new interval if none started | 
|  | if previous_range_start_line_number == 0: | 
|  | previous_range_start_line_number = line_number | 
|  | previous_range_start_text = line_text | 
|  | # Add new interval if we reached past the previous one | 
|  | elif line_number != previous_line_number + 1: | 
|  | addRange() | 
|  | previous_range_start_line_number = line_number | 
|  | previous_range_start_text = line_text | 
|  | previous_line_number = line_number | 
|  |  | 
|  | # Add a last interval if needed | 
|  | if previous_range_start_line_number != 0: | 
|  | addRange() | 
|  | return results | 
|  |  | 
|  | def CheckForFileDeletion(self, affected_file): | 
|  | """Emits a warning notification if file has been deleted """ | 
|  | if not affected_file.NewContents(): | 
|  | self.EmitWarning("The file seems to be deleted in the changelist. If " | 
|  | "your intent is to really delete the file, the code in " | 
|  | "PRESUBMIT.py should be updated to remove the " | 
|  | "|StrictEnumValueChecker| class."); | 
|  | return False | 
|  | return True | 
|  |  | 
|  | def GetDeletedLinesFromScmDiff(self, affected_file): | 
|  | """Return a list of of line numbers (1-based) corresponding to lines | 
|  | deleted from the new source file (if they had been present in it). Note | 
|  | that if multiple contiguous lines have been deleted, the returned list will | 
|  | contain contiguous line number entries. To prevent false positives, we | 
|  | return deleted line numbers *only* from diff chunks which decrease the size | 
|  | of the new file. | 
|  |  | 
|  | Note: We need this method because we have access to neither the old file | 
|  | content nor the list of "delete" changes from the current presubmit script | 
|  | API. | 
|  |  | 
|  | """ | 
|  | results = [] | 
|  | line_num = 0 | 
|  | deleting_lines = False | 
|  | for line in affected_file.GenerateScmDiff().splitlines(): | 
|  | # Parse the unified diff chunk optional section heading, which looks like | 
|  | # @@ -l,s +l,s @@ optional section heading | 
|  | m = self.input_api.re.match( | 
|  | r"^@@ \-([0-9]+)\,([0-9]+) \+([0-9]+)\,([0-9]+) @@", line) | 
|  | if m: | 
|  | old_line_num = int(m.group(1)) | 
|  | old_size = int(m.group(2)) | 
|  | new_line_num = int(m.group(3)) | 
|  | new_size = int(m.group(4)) | 
|  | line_num = new_line_num | 
|  | # Return line numbers only from diff chunks decreasing the size of the | 
|  | # new file | 
|  | deleting_lines = old_size > new_size | 
|  | continue | 
|  | if not line.startswith("-"): | 
|  | line_num += 1 | 
|  | if deleting_lines and line.startswith("-") and not line.startswith("--"): | 
|  | results.append(line_num) | 
|  | return results | 
|  |  | 
|  | def CheckForEnumEntryDeletions(self, affected_file): | 
|  | """Look for deletions inside the enum definition. We currently use a | 
|  | simple heuristics (not 100% accurate): if there are deleted lines inside | 
|  | the enum definition, this might be a deletion. | 
|  |  | 
|  | """ | 
|  | range_new = self.ComputeEnumRangeInNewFile(affected_file) | 
|  | if not range_new: | 
|  | return False | 
|  |  | 
|  | is_ok = True | 
|  | for line_num in self.GetDeletedLinesFromScmDiff(affected_file): | 
|  | if range_new.Contains(line_num): | 
|  | self.EmitWarning("It looks like you are deleting line(s) from the " | 
|  | "enum definition. This should never happen.", | 
|  | line_num) | 
|  | is_ok = False | 
|  | return is_ok | 
|  |  | 
|  | def CheckForEnumEntryInsertions(self, affected_file): | 
|  | range = self.ComputeEnumRangeInNewFile(affected_file) | 
|  | if not range: | 
|  | return False | 
|  |  | 
|  | first_line = range.first_line | 
|  | last_line = range.last_line | 
|  |  | 
|  | # Collect the range of changes inside the enum definition range. | 
|  | is_ok = True | 
|  | for line_start, line_end, line_text in \ | 
|  | self.CollectRangesInsideEnumDefinition(affected_file, | 
|  | first_line, | 
|  | last_line): | 
|  | # The only edit we consider valid is adding 1 or more entries *exactly* | 
|  | # at the end of the enum definition. Every other edit inside the enum | 
|  | # definition will result in a "warning confirmation" message. | 
|  | # | 
|  | # TODO(rpaquay): We currently cannot detect "renames" of existing entries | 
|  | # vs invalid insertions, so we sometimes will warn for valid edits. | 
|  | is_valid_edit = (line_end == last_line - 1) | 
|  |  | 
|  | self.LogDebug("Edit range in new file at starting at line number %d and " | 
|  | "ending at line number %d: valid=%s" | 
|  | % (line_start, line_end, is_valid_edit)) | 
|  |  | 
|  | if not is_valid_edit: | 
|  | self.EmitWarning("The change starting at line %d and ending at line " | 
|  | "%d is *not* located *exactly* at the end of the " | 
|  | "enum definition. Unless you are renaming an " | 
|  | "existing entry, this is not a valid change, as new " | 
|  | "entries should *always* be added at the end of the " | 
|  | "enum definition, right before the \"%s\" " | 
|  | "entry." % (line_start, line_end, self.end_marker), | 
|  | line_start, | 
|  | line_text) | 
|  | is_ok = False | 
|  | return is_ok | 
|  |  | 
|  | def PerformChecks(self, affected_file): | 
|  | if not self.CheckForFileDeletion(affected_file): | 
|  | return | 
|  | if not self.CheckForEnumEntryDeletions(affected_file): | 
|  | return | 
|  | if not self.CheckForEnumEntryInsertions(affected_file): | 
|  | return | 
|  |  | 
|  | def ProcessHistogramValueFile(self, affected_file): | 
|  | self.LogInfo("Start processing file \"%s\"" % affected_file.LocalPath()) | 
|  | self.PerformChecks(affected_file) | 
|  | self.LogInfo("Done processing file \"%s\"" % affected_file.LocalPath()) | 
|  |  | 
|  | def Run(self): | 
|  | for file in self.input_api.AffectedFiles(include_deletes=True): | 
|  | if file.LocalPath() == self.path: | 
|  | self.ProcessHistogramValueFile(file) | 
|  | return self.results |