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.
5 """Chromium presubmit script for src/chrome/browser/extensions.
7 See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
8 for more details on the presubmit API built into gcl.
11 def GetPreferredTrySlaves():
12 return ['linux_chromeos']
14 class HistogramValueChecker(object):
15 """Verify that changes to "extension_function_histogram_value.h" are valid.
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.
26 # The name of the file we want to check against
27 LOCAL_PATH
= "chrome/browser/extensions/extension_function_histogram_value.h"
29 # The markers we look for in the above source file as delimiters of the enum
31 ENUM_START_MARKER
= "enum HistogramValue {"
32 ENUM_END_MARKER
= " ENUM_BOUNDARY"
34 def __init__(self
, input_api
, output_api
):
35 self
.input_api
= input_api
36 self
.output_api
= output_api
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
46 return self
.last_line
- self
.first_line
+ 1
48 def Contains(self
, line_num
):
49 return self
.first_line
<= line_num
and line_num
<= self
.last_line
51 def LogInfo(self
, message
):
52 self
.input_api
.logging
.info(message
)
55 def LogDebug(self
, message
):
56 self
.input_api
.logging
.debug(message
)
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.
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.
71 line_num
= 1 # Line numbers are 1-based
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
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
)
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
)
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
))
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
)
104 def ComputeEnumRangeInNewFile(self
, affected_file
):
105 return self
.ComputeEnumRangeInContents(affected_file
.NewContents())
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."
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
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
)
130 item
= self
.LOCAL_PATH
131 long_message
= self
.GetLongMessage()
132 self
.LogInfo(message
)
134 self
.output_api
.PresubmitPromptWarning(message
, [item
], long_message
))
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
145 previous_line_number
= 0
146 previous_range_start_line_number
= 0
147 previous_range_start_text
= ""
150 tuple = (previous_range_start_line_number
,
151 previous_line_number
,
152 previous_range_start_text
)
153 results
.append(tuple)
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
) + ": " +
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:
166 previous_range_start_line_number
= line_number
167 previous_range_start_text
= line_text
168 previous_line_number
= line_number
170 # Add a last interval if needed
171 if previous_range_start_line_number
!= 0:
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.");
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
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
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
)
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
214 deleting_lines
= old_size
> new_size
216 if not line
.startswith('-'):
218 if deleting_lines
and line
.startswith('-') and not line
.startswith('--'):
219 results
.append(line_num
)
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.
228 range_new
= self
.ComputeEnumRangeInNewFile(affected_file
)
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.",
241 def CheckForEnumEntryInsertions(self
, affected_file
):
242 range = self
.ComputeEnumRangeInNewFile(affected_file
)
246 first_line
= range.first_line
247 last_line
= range.last_line
249 # Collect the range of changes inside the enum definition range.
251 for line_start
, line_end
, line_text
in \
252 self
.CollectRangesInsideEnumDefinition(affected_file
,
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.
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)
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
))
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
),
280 def PerformChecks(self
, affected_file
):
281 if not self
.CheckForFileDeletion(affected_file
):
283 if not self
.CheckForEnumEntryDeletions(affected_file
):
285 if not self
.CheckForEnumEntryInsertions(affected_file
):
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())
294 for file in self
.input_api
.AffectedFiles(include_deletes
=True):
295 if file.LocalPath() == self
.LOCAL_PATH
:
296 self
.ProcessHistogramValueFile(file)
299 def CheckChangeOnUpload(input_api
, output_api
):
301 results
+= HistogramValueChecker(input_api
, output_api
).Run()
302 results
+= input_api
.canned_checks
.CheckPatchFormatted(input_api
, output_api
)