1 # Copyright 2014 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 class StrictEnumValueChecker(object):
6 """Verify that changes to enums are valid.
8 This class is used to check enums where reordering or deletion is not allowed,
9 and additions must be at the end of the enum, just prior to some "boundary"
10 entry. See comments at the top of the "extension_function_histogram_value.h"
11 file in chrome/browser/extensions for an example what are considered valid
12 changes. There are situations where this class gives false positive warnings,
13 i.e. it warns even though the edit is legitimate. Since the class warns using
14 prompt warnings, the user can always choose to continue. The main point is to
15 attract the attention to all (potentially or not) invalid edits.
18 def __init__(self
, input_api
, output_api
, start_marker
, end_marker
, path
):
19 self
.input_api
= input_api
20 self
.output_api
= output_api
21 self
.start_marker
= start_marker
22 self
.end_marker
= end_marker
26 class EnumRange(object):
27 """Represents a range of line numbers (1-based)"""
28 def __init__(self
, first_line
, last_line
):
29 self
.first_line
= first_line
30 self
.last_line
= last_line
33 return self
.last_line
- self
.first_line
+ 1
35 def Contains(self
, line_num
):
36 return self
.first_line
<= line_num
and line_num
<= self
.last_line
38 def LogInfo(self
, message
):
39 self
.input_api
.logging
.info(message
)
42 def LogDebug(self
, message
):
43 self
.input_api
.logging
.debug(message
)
46 def ComputeEnumRangeInContents(self
, contents
):
47 """Returns an |EnumRange| object representing the line extent of the
48 enum members in |contents|. The line numbers are 1-based,
49 compatible with line numbers returned by AffectedFile.ChangeContents().
50 |contents| is a list of strings reprenting the lines of a text file.
52 If either start_marker or end_marker cannot be found in
53 |contents|, returns None and emits detailed warnings about the problem.
58 line_num
= 1 # Line numbers are 1-based
60 if line
.startswith(self
.start_marker
):
61 first_enum_line
= line_num
+ 1
62 elif line
.startswith(self
.end_marker
):
63 last_enum_line
= line_num
66 if first_enum_line
== 0:
67 self
.EmitWarning("The presubmit script could not find the start of the "
68 "enum definition (\"%s\"). Did the enum definition "
69 "change?" % self
.start_marker
)
72 if last_enum_line
== 0:
73 self
.EmitWarning("The presubmit script could not find the end of the "
74 "enum definition (\"%s\"). Did the enum definition "
75 "change?" % self
.end_marker
)
78 if first_enum_line
>= last_enum_line
:
79 self
.EmitWarning("The presubmit script located the start of the enum "
80 "definition (\"%s\" at line %d) *after* its end "
81 "(\"%s\" at line %d). Something is not quite right."
82 % (self
.start_marker
, first_enum_line
,
83 self
.end_marker
, last_enum_line
))
86 self
.LogInfo("Line extent of (\"%s\") enum definition: "
87 "first_line=%d, last_line=%d."
88 % (self
.start_marker
, first_enum_line
, last_enum_line
))
89 return self
.EnumRange(first_enum_line
, last_enum_line
)
91 def ComputeEnumRangeInNewFile(self
, affected_file
):
92 return self
.ComputeEnumRangeInContents(affected_file
.NewContents())
94 def GetLongMessage(self
, local_path
):
95 return str("The file \"%s\" contains the definition of the "
96 "(\"%s\") enum which should be edited in specific ways "
97 "only - *** read the comments at the top of the header file ***"
98 ". There are changes to the file that may be incorrect and "
99 "warrant manual confirmation after review. Note that this "
100 "presubmit script can not reliably report the nature of all "
101 "types of invalid changes, especially when the diffs are "
102 "complex. For example, an invalid deletion may be reported "
103 "whereas the change contains a valid rename."
104 % (local_path
, self
.start_marker
))
106 def EmitWarning(self
, message
, line_number
=None, line_text
=None):
107 """Emits a presubmit prompt warning containing the short message
108 |message|. |item| is |LOCAL_PATH| with optional |line_number| and
112 if line_number
is not None and line_text
is not None:
113 item
= "%s(%d): %s" % (self
.path
, line_number
, line_text
)
114 elif line_number
is not None:
115 item
= "%s(%d)" % (self
.path
, line_number
)
118 long_message
= self
.GetLongMessage(self
.path
)
119 self
.LogInfo(message
)
121 self
.output_api
.PresubmitPromptWarning(message
, [item
], long_message
))
123 def CollectRangesInsideEnumDefinition(self
, affected_file
,
124 first_line
, last_line
):
125 """Returns a list of triplet (line_start, line_end, line_text) of ranges of
126 edits changes. The |line_text| part is the text at line |line_start|.
127 Since it used only for reporting purposes, we do not need all the text
132 previous_line_number
= 0
133 previous_range_start_line_number
= 0
134 previous_range_start_text
= ""
137 tuple = (previous_range_start_line_number
,
138 previous_line_number
,
139 previous_range_start_text
)
140 results
.append(tuple)
142 for line_number
, line_text
in affected_file
.ChangedContents():
143 if first_line
<= line_number
and line_number
<= last_line
:
144 self
.LogDebug("Line change at line number " + str(line_number
) + ": " +
146 # Start a new interval if none started
147 if previous_range_start_line_number
== 0:
148 previous_range_start_line_number
= line_number
149 previous_range_start_text
= line_text
150 # Add new interval if we reached past the previous one
151 elif line_number
!= previous_line_number
+ 1:
153 previous_range_start_line_number
= line_number
154 previous_range_start_text
= line_text
155 previous_line_number
= line_number
157 # Add a last interval if needed
158 if previous_range_start_line_number
!= 0:
162 def CheckForFileDeletion(self
, affected_file
):
163 """Emits a warning notification if file has been deleted """
164 if not affected_file
.NewContents():
165 self
.EmitWarning("The file seems to be deleted in the changelist. If "
166 "your intent is to really delete the file, the code in "
167 "PRESUBMIT.py should be updated to remove the "
168 "|StrictEnumValueChecker| class.");
172 def GetDeletedLinesFromScmDiff(self
, affected_file
):
173 """Return a list of of line numbers (1-based) corresponding to lines
174 deleted from the new source file (if they had been present in it). Note
175 that if multiple contiguous lines have been deleted, the returned list will
176 contain contiguous line number entries. To prevent false positives, we
177 return deleted line numbers *only* from diff chunks which decrease the size
180 Note: We need this method because we have access to neither the old file
181 content nor the list of "delete" changes from the current presubmit script
187 deleting_lines
= False
188 for line
in affected_file
.GenerateScmDiff().splitlines():
189 # Parse the unified diff chunk optional section heading, which looks like
190 # @@ -l,s +l,s @@ optional section heading
191 m
= self
.input_api
.re
.match(
192 r
"^@@ \-([0-9]+)\,([0-9]+) \+([0-9]+)\,([0-9]+) @@", line
)
194 old_line_num
= int(m
.group(1))
195 old_size
= int(m
.group(2))
196 new_line_num
= int(m
.group(3))
197 new_size
= int(m
.group(4))
198 line_num
= new_line_num
199 # Return line numbers only from diff chunks decreasing the size of the
201 deleting_lines
= old_size
> new_size
203 if not line
.startswith("-"):
205 if deleting_lines
and line
.startswith("-") and not line
.startswith("--"):
206 results
.append(line_num
)
209 def CheckForEnumEntryDeletions(self
, affected_file
):
210 """Look for deletions inside the enum definition. We currently use a
211 simple heuristics (not 100% accurate): if there are deleted lines inside
212 the enum definition, this might be a deletion.
215 range_new
= self
.ComputeEnumRangeInNewFile(affected_file
)
220 for line_num
in self
.GetDeletedLinesFromScmDiff(affected_file
):
221 if range_new
.Contains(line_num
):
222 self
.EmitWarning("It looks like you are deleting line(s) from the "
223 "enum definition. This should never happen.",
228 def CheckForEnumEntryInsertions(self
, affected_file
):
229 range = self
.ComputeEnumRangeInNewFile(affected_file
)
233 first_line
= range.first_line
234 last_line
= range.last_line
236 # Collect the range of changes inside the enum definition range.
238 for line_start
, line_end
, line_text
in \
239 self
.CollectRangesInsideEnumDefinition(affected_file
,
242 # The only edit we consider valid is adding 1 or more entries *exactly*
243 # at the end of the enum definition. Every other edit inside the enum
244 # definition will result in a "warning confirmation" message.
246 # TODO(rpaquay): We currently cannot detect "renames" of existing entries
247 # vs invalid insertions, so we sometimes will warn for valid edits.
248 is_valid_edit
= (line_end
== last_line
- 1)
250 self
.LogDebug("Edit range in new file at starting at line number %d and "
251 "ending at line number %d: valid=%s"
252 % (line_start
, line_end
, is_valid_edit
))
254 if not is_valid_edit
:
255 self
.EmitWarning("The change starting at line %d and ending at line "
256 "%d is *not* located *exactly* at the end of the "
257 "enum definition. Unless you are renaming an "
258 "existing entry, this is not a valid change, as new "
259 "entries should *always* be added at the end of the "
260 "enum definition, right before the \"%s\" "
261 "entry." % (line_start
, line_end
, self
.end_marker
),
267 def PerformChecks(self
, affected_file
):
268 if not self
.CheckForFileDeletion(affected_file
):
270 if not self
.CheckForEnumEntryDeletions(affected_file
):
272 if not self
.CheckForEnumEntryInsertions(affected_file
):
275 def ProcessHistogramValueFile(self
, affected_file
):
276 self
.LogInfo("Start processing file \"%s\"" % affected_file
.LocalPath())
277 self
.PerformChecks(affected_file
)
278 self
.LogInfo("Done processing file \"%s\"" % affected_file
.LocalPath())
281 for file in self
.input_api
.AffectedFiles(include_deletes
=True):
282 if file.LocalPath() == self
.path
:
283 self
.ProcessHistogramValueFile(file)