| Index: tools/strict_enum_value_checker/strict_enum_value_checker.py
|
| diff --git a/tools/strict_enum_value_checker/strict_enum_value_checker.py b/tools/strict_enum_value_checker/strict_enum_value_checker.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..22a0276ee09fd0b63ac90f1de4e4e3bc1ed5db27
|
| --- /dev/null
|
| +++ b/tools/strict_enum_value_checker/strict_enum_value_checker.py
|
| @@ -0,0 +1,284 @@
|
| +# Copyright 2014 The Chromium Authors. All rights reserved.
|
| +# 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
|
|
|