Home | History | Annotate | Download | only in extensions
      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