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
7See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
8for more details on the presubmit API built into gcl.
9"""
10
11def GetPreferredTrySlaves():
12  return ['linux_chromeos']
13
14class 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
299def CheckChangeOnUpload(input_api, output_api):
300    results = []
301    results += HistogramValueChecker(input_api, output_api).Run()
302    return results
303
304