blob: 2e33374f651dbd9f27583b0ab1e99d2565be4c48 [file] [log] [blame]
estade@chromium.orgae7af922012-01-27 14:51:13 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Generic presubmit checks that can be reused by other presubmit checks."""
6
Raul Tambre80ee78e2019-05-06 22:41:05 +00007from __future__ import print_function
8
maruel@chromium.orgff9a2172012-04-24 16:55:32 +00009import os as _os
10_HERE = _os.path.dirname(_os.path.abspath(__file__))
11
tfarina@chromium.orgb6795642014-12-12 00:03:49 +000012# Justifications for each filter:
13#
14# - build/include : Too many; fix in the future.
15# - build/include_order : Not happening; #ifdefed includes.
16# - build/namespace : I'm surprised by how often we violate this rule.
17# - readability/casting : Mistakes a whole bunch of function pointer.
18# - runtime/int : Can be fixed long term; volume of errors too high
19# - runtime/virtual : Broken now, but can be fixed in the future?
20# - whitespace/braces : We have a lot of explicit scoping in chrome code.
tfarina@chromium.orgb6795642014-12-12 00:03:49 +000021DEFAULT_LINT_FILTERS = [
22 '-build/include',
23 '-build/include_order',
24 '-build/namespace',
25 '-readability/casting',
26 '-runtime/int',
27 '-runtime/virtual',
28 '-whitespace/braces',
tfarina@chromium.orgb6795642014-12-12 00:03:49 +000029]
bradnelson@google.com56e48bc2011-03-24 20:51:21 +000030
danakj@chromium.org0ae71222016-01-11 19:37:11 +000031# These filters will always be removed, even if the caller specifies a filter
32# set, as they are problematic or broken in some way.
33#
34# Justifications for each filter:
35# - build/c++11 : Rvalue ref checks are unreliable (false positives),
36# include file and feature blacklists are
37# google3-specific.
38BLACKLIST_LINT_FILTERS = [
39 '-build/c++11',
40]
41
maruel@chromium.org3410d912009-06-09 20:56:16 +000042### Description checks
43
kuchhal@chromium.org00c41e42009-05-12 21:43:13 +000044def CheckChangeHasBugField(input_api, output_api):
Aaron Gablefc03e672017-05-15 14:09:42 -070045 """Requires that the changelist have a Bug: field."""
46 if input_api.change.BugsFromDescription():
kuchhal@chromium.org00c41e42009-05-12 21:43:13 +000047 return []
48 else:
49 return [output_api.PresubmitNotifyResult(
Aaron Gablefc03e672017-05-15 14:09:42 -070050 'If this change has an associated bug, add Bug: [bug number].')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000051
Dan Beamb2951052019-10-03 21:20:43 +000052def CheckChangeHasNoUnwantedTags(input_api, output_api):
53 UNWANTED_TAGS = {
54 'FIXED': {
55 'why': 'is not supported',
56 'instead': 'Use "Fixed:" instead.'
57 },
58 # TODO: BUG, ISSUE
59 }
60
61 errors = []
62 for tag, desc in UNWANTED_TAGS.items():
63 if tag in input_api.change.tags:
64 subs = tag, desc['why'], desc.get('instead', '')
65 errors.append(('%s= %s. %s' % subs).rstrip())
66
67 return [output_api.PresubmitError('\n'.join(errors))] if errors else []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000068
69def CheckDoNotSubmitInDescription(input_api, output_api):
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +000070 """Checks that the user didn't add 'DO NOT ''SUBMIT' to the CL description.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000071 """
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +000072 keyword = 'DO NOT ''SUBMIT'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000073 if keyword in input_api.change.DescriptionText():
74 return [output_api.PresubmitError(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000075 keyword + ' is present in the changelist description.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000076 else:
77 return []
78
79
maruel@chromium.orgbc50eb42009-06-10 18:22:47 +000080def CheckChangeHasDescription(input_api, output_api):
81 """Checks the CL description is not empty."""
82 text = input_api.change.DescriptionText()
83 if text.strip() == '':
84 if input_api.is_committing:
pgervais@chromium.orgbc3b3b52014-06-03 00:53:48 +000085 return [output_api.PresubmitError('Add a description to the CL.')]
maruel@chromium.orgbc50eb42009-06-10 18:22:47 +000086 else:
pgervais@chromium.orgbc3b3b52014-06-03 00:53:48 +000087 return [output_api.PresubmitNotifyResult('Add a description to the CL.')]
maruel@chromium.orgbc50eb42009-06-10 18:22:47 +000088 return []
89
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +000090
91def CheckChangeWasUploaded(input_api, output_api):
92 """Checks that the issue was uploaded before committing."""
maruel@chromium.orgd587f392011-07-26 00:41:18 +000093 if input_api.is_committing and not input_api.change.issue:
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +000094 return [output_api.PresubmitError(
95 'Issue wasn\'t uploaded. Please upload first.')]
96 return []
97
98
maruel@chromium.org3410d912009-06-09 20:56:16 +000099### Content checks
100
Sergiy Byelozyorov621fe6f2018-05-25 17:38:46 -0700101def CheckAuthorizedAuthor(input_api, output_api, bot_whitelist=None):
Michael Achenbachc850b962016-12-05 15:40:17 +0100102 """For non-googler/chromites committers, verify the author's email address is
103 in AUTHORS.
104 """
Michael Achenbach590420d2017-10-18 12:35:37 +0200105 if input_api.is_committing:
106 error_type = output_api.PresubmitError
107 else:
108 error_type = output_api.PresubmitPromptWarning
109
Michael Achenbachc850b962016-12-05 15:40:17 +0100110 author = input_api.change.author_email
111 if not author:
112 input_api.logging.info('No author, skipping AUTHOR check')
113 return []
Sergiy Byelozyorov621fe6f2018-05-25 17:38:46 -0700114
115 # This is used for CLs created by trusted robot accounts.
116 if bot_whitelist and author in bot_whitelist:
117 return []
118
Michael Achenbachc850b962016-12-05 15:40:17 +0100119 authors_path = input_api.os_path.join(
120 input_api.PresubmitLocalPath(), 'AUTHORS')
121 valid_authors = (
122 input_api.re.match(r'[^#]+\s+\<(.+?)\>\s*$', line)
123 for line in open(authors_path))
124 valid_authors = [item.group(1).lower() for item in valid_authors if item]
125 if not any(input_api.fnmatch.fnmatch(author.lower(), valid)
126 for valid in valid_authors):
127 input_api.logging.info('Valid authors are %s', ', '.join(valid_authors))
Michael Achenbach590420d2017-10-18 12:35:37 +0200128 return [error_type(
Michael Achenbachc850b962016-12-05 15:40:17 +0100129 ('%s is not in AUTHORS file. If you are a new contributor, please visit'
130 '\n'
Xiaoyin Liub5807972017-10-04 22:07:24 -0400131 'https://www.chromium.org/developers/contributing-code and read the '
Michael Achenbachc850b962016-12-05 15:40:17 +0100132 '"Legal" section\n'
133 'If you are a chromite, verify the contributor signed the CLA.') %
134 author)]
135 return []
136
137
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000138def CheckDoNotSubmitInFiles(input_api, output_api):
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +0000139 """Checks that the user didn't add 'DO NOT ''SUBMIT' to any files."""
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000140 # We want to check every text file, not just source files.
141 file_filter = lambda x : x
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +0000142 keyword = 'DO NOT ''SUBMIT'
Haiyang Pan5eb757a2019-10-29 16:49:17 +0000143 def DoNotSubmitRule(extension, line):
144 try:
145 return keyword not in line
146 # Fallback to True for non-text content
147 except UnicodeDecodeError:
148 return True
149
150 errors = _FindNewViolationsOfRule(DoNotSubmitRule, input_api, file_filter)
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000151 text = '\n'.join('Found %s in %s' % (keyword, loc) for loc in errors)
152 if text:
153 return [output_api.PresubmitError(text)]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000154 return []
155
156
tfarina@chromium.orgb6795642014-12-12 00:03:49 +0000157def CheckChangeLintsClean(input_api, output_api, source_file_filter=None,
tfarina@chromium.orga62208f2015-02-25 03:23:11 +0000158 lint_filters=None, verbose_level=None):
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000159 """Checks that all '.cc' and '.h' files pass cpplint.py."""
erg@google.com26970fa2009-11-17 18:07:32 +0000160 _RE_IS_TEST = input_api.re.compile(r'.*tests?.(cc|h)$')
161 result = []
162
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000163 cpplint = input_api.cpplint
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000164 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800165 # pylint: disable=protected-access
erg@google.com26970fa2009-11-17 18:07:32 +0000166 cpplint._cpplint_state.ResetErrorCounts()
167
tfarina@chromium.orgb6795642014-12-12 00:03:49 +0000168 lint_filters = lint_filters or DEFAULT_LINT_FILTERS
danakj@chromium.org0ae71222016-01-11 19:37:11 +0000169 lint_filters.extend(BLACKLIST_LINT_FILTERS)
tfarina@chromium.orgb6795642014-12-12 00:03:49 +0000170 cpplint._SetFilters(','.join(lint_filters))
erg@google.com26970fa2009-11-17 18:07:32 +0000171
172 # We currently are more strict with normal code than unit tests; 4 and 5 are
173 # the verbosity level that would normally be passed to cpplint.py through
174 # --verbose=#. Hopefully, in the future, we can be more verbose.
175 files = [f.AbsoluteLocalPath() for f in
176 input_api.AffectedSourceFiles(source_file_filter)]
177 for file_name in files:
178 if _RE_IS_TEST.match(file_name):
179 level = 5
180 else:
181 level = 4
182
tfarina@chromium.orga62208f2015-02-25 03:23:11 +0000183 verbose_level = verbose_level or level
184 cpplint.ProcessFile(file_name, verbose_level)
erg@google.com26970fa2009-11-17 18:07:32 +0000185
186 if cpplint._cpplint_state.error_count > 0:
187 if input_api.is_committing:
188 res_type = output_api.PresubmitError
189 else:
190 res_type = output_api.PresubmitPromptWarning
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000191 result = [res_type('Changelist failed cpplint.py check.')]
erg@google.com26970fa2009-11-17 18:07:32 +0000192
193 return result
194
195
maruel@chromium.org3410d912009-06-09 20:56:16 +0000196def CheckChangeHasNoCR(input_api, output_api, source_file_filter=None):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000197 """Checks no '\r' (CR) character is in any source files."""
198 cr_files = []
maruel@chromium.org3410d912009-06-09 20:56:16 +0000199 for f in input_api.AffectedSourceFiles(source_file_filter):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000200 if '\r' in input_api.ReadFile(f, 'rb'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000201 cr_files.append(f.LocalPath())
202 if cr_files:
203 return [output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000204 'Found a CR character in these files:', items=cr_files)]
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000205 return []
206
207
208def CheckChangeHasOnlyOneEol(input_api, output_api, source_file_filter=None):
209 """Checks the files ends with one and only one \n (LF)."""
210 eof_files = []
211 for f in input_api.AffectedSourceFiles(source_file_filter):
212 contents = input_api.ReadFile(f, 'rb')
213 # Check that the file ends in one and only one newline character.
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000214 if len(contents) > 1 and (contents[-1:] != '\n' or contents[-2:-1] == '\n'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000215 eof_files.append(f.LocalPath())
216
217 if eof_files:
218 return [output_api.PresubmitPromptWarning(
219 'These files should end in one (and only one) newline character:',
220 items=eof_files)]
221 return []
222
223
224def CheckChangeHasNoCrAndHasOnlyOneEol(input_api, output_api,
225 source_file_filter=None):
226 """Runs both CheckChangeHasNoCR and CheckChangeHasOnlyOneEOL in one pass.
227
228 It is faster because it is reading the file only once.
229 """
230 cr_files = []
231 eof_files = []
232 for f in input_api.AffectedSourceFiles(source_file_filter):
233 contents = input_api.ReadFile(f, 'rb')
234 if '\r' in contents:
235 cr_files.append(f.LocalPath())
236 # Check that the file ends in one and only one newline character.
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000237 if len(contents) > 1 and (contents[-1:] != '\n' or contents[-2:-1] == '\n'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000238 eof_files.append(f.LocalPath())
239 outputs = []
240 if cr_files:
241 outputs.append(output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000242 'Found a CR character in these files:', items=cr_files))
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000243 if eof_files:
244 outputs.append(output_api.PresubmitPromptWarning(
245 'These files should end in one (and only one) newline character:',
246 items=eof_files))
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000247 return outputs
248
seanmccullough0b670442016-06-07 10:45:58 -0700249def CheckGenderNeutral(input_api, output_api, source_file_filter=None):
250 """Checks that there are no gendered pronouns in any of the text files to be
251 submitted.
252 """
253 gendered_re = input_api.re.compile(
Edward Lemurb9830242019-10-30 22:19:20 +0000254 r'(^|\s|\(|\[)([Hh]e|[Hh]is|[Hh]ers?|[Hh]im|[Ss]he|[Gg]uys?)\\b')
seanmccullough0b670442016-06-07 10:45:58 -0700255
256 errors = []
257 for f in input_api.AffectedFiles(include_deletes=False,
258 file_filter=source_file_filter):
259 for line_num, line in f.ChangedContents():
260 if gendered_re.search(line):
261 errors.append('%s (%d): %s' % (f.LocalPath(), line_num, line))
262
263 if len(errors):
264 return [output_api.PresubmitPromptWarning('Found a gendered pronoun in:',
265 long_text='\n'.join(errors))]
266 return []
267
268
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000269
chrisha@google.com267d6592012-06-19 19:23:31 +0000270def _ReportErrorFileAndLine(filename, line_num, dummy_line):
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000271 """Default error formatter for _FindNewViolationsOfRule."""
danakj@chromium.orgc5965ba2013-08-14 00:27:24 +0000272 return '%s:%s' % (filename, line_num)
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000273
274
David 'Digit' Turner31a48642019-01-11 16:10:59 +0000275def _GenerateAffectedFileExtList(input_api, source_file_filter):
276 """Generate a list of (file, extension) tuples from affected files.
277
278 The result can be fed to _FindNewViolationsOfRule() directly, or
279 could be filtered before doing that.
280
281 Args:
282 input_api: object to enumerate the affected files.
283 source_file_filter: a filter to be passed to the input api.
284 Yields:
285 A list of (file, extension) tuples, where |file| is an affected
286 file, and |extension| its file path extension.
287 """
288 for f in input_api.AffectedFiles(
289 include_deletes=False, file_filter=source_file_filter):
290 extension = str(f.LocalPath()).rsplit('.', 1)[-1]
291 yield (f, extension)
292
293
294def _FindNewViolationsOfRuleForList(callable_rule,
295 file_ext_list,
296 error_formatter=_ReportErrorFileAndLine):
297 """Find all newly introduced violations of a per-line rule (a callable).
298
299 Prefer calling _FindNewViolationsOfRule() instead of this function, unless
300 the list of affected files need to be filtered in a special way.
301
302 Arguments:
303 callable_rule: a callable taking a file extension and line of input and
304 returning True if the rule is satisfied and False if there was a problem.
305 file_ext_list: a list of input (file, extension) tuples, as returned by
306 _GenerateAffectedFileExtList().
307 error_formatter: a callable taking (filename, line_number, line) and
308 returning a formatted error string.
309
310 Returns:
311 A list of the newly-introduced violations reported by the rule.
312 """
313 errors = []
314 for f, extension in file_ext_list:
315 # For speed, we do two passes, checking first the full file. Shelling out
316 # to the SCM to determine the changed region can be quite expensive on
317 # Win32. Assuming that most files will be kept problem-free, we can
318 # skip the SCM operations most of the time.
319 if all(callable_rule(extension, line) for line in f.NewContents()):
320 continue # No violation found in full text: can skip considering diff.
321
322 for line_num, line in f.ChangedContents():
323 if not callable_rule(extension, line):
324 errors.append(error_formatter(f.LocalPath(), line_num, line))
325
326 return errors
327
328
329def _FindNewViolationsOfRule(callable_rule,
330 input_api,
331 source_file_filter=None,
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000332 error_formatter=_ReportErrorFileAndLine):
333 """Find all newly introduced violations of a per-line rule (a callable).
334
335 Arguments:
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000336 callable_rule: a callable taking a file extension and line of input and
337 returning True if the rule is satisfied and False if there was a problem.
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000338 input_api: object to enumerate the affected files.
339 source_file_filter: a filter to be passed to the input api.
340 error_formatter: a callable taking (filename, line_number, line) and
341 returning a formatted error string.
342
343 Returns:
344 A list of the newly-introduced violations reported by the rule.
345 """
David 'Digit' Turner31a48642019-01-11 16:10:59 +0000346 return _FindNewViolationsOfRuleForList(
347 callable_rule, _GenerateAffectedFileExtList(
348 input_api, source_file_filter), error_formatter)
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000349
350
maruel@chromium.org3410d912009-06-09 20:56:16 +0000351def CheckChangeHasNoTabs(input_api, output_api, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000352 """Checks that there are no tab characters in any of the text files to be
353 submitted.
354 """
maruel@chromium.org115ae6c2010-06-18 17:11:43 +0000355 # In addition to the filter, make sure that makefiles are blacklisted.
356 if not source_file_filter:
357 # It's the default filter.
358 source_file_filter = input_api.FilterSourceFile
359 def filter_more(affected_file):
cmp@chromium.orgcb5e57c2012-04-06 19:50:15 +0000360 basename = input_api.os_path.basename(affected_file.LocalPath())
361 return (not (basename in ('Makefile', 'makefile') or
362 basename.endswith('.mk')) and
maruel@chromium.org115ae6c2010-06-18 17:11:43 +0000363 source_file_filter(affected_file))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000364
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000365 tabs = _FindNewViolationsOfRule(lambda _, line : '\t' not in line,
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000366 input_api, filter_more)
367
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000368 if tabs:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000369 return [output_api.PresubmitPromptWarning('Found a tab character in:',
370 long_text='\n'.join(tabs))]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000371 return []
372
373
estade@chromium.orgfdcc9f72011-02-07 22:25:07 +0000374def CheckChangeTodoHasOwner(input_api, output_api, source_file_filter=None):
375 """Checks that the user didn't add TODO(name) without an owner."""
376
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +0000377 unowned_todo = input_api.re.compile('TO''DO[^(]')
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000378 errors = _FindNewViolationsOfRule(lambda _, x : not unowned_todo.search(x),
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000379 input_api, source_file_filter)
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +0000380 errors = ['Found TO''DO with no owner in ' + x for x in errors]
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000381 if errors:
382 return [output_api.PresubmitPromptWarning('\n'.join(errors))]
estade@chromium.orgfdcc9f72011-02-07 22:25:07 +0000383 return []
384
385
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000386def CheckChangeHasNoStrayWhitespace(input_api, output_api,
387 source_file_filter=None):
388 """Checks that there is no stray whitespace at source lines end."""
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000389 errors = _FindNewViolationsOfRule(lambda _, line : line.rstrip() == line,
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000390 input_api, source_file_filter)
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000391 if errors:
392 return [output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000393 'Found line ending with white spaces in:',
394 long_text='\n'.join(errors))]
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000395 return []
396
397
jamesr@chromium.orgaf27f462013-04-04 21:44:22 +0000398def CheckLongLines(input_api, output_api, maxlen, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000399 """Checks that there aren't any lines longer than maxlen characters in any of
400 the text files to be submitted.
401 """
dpranke@chromium.orge3b1c3d2012-10-20 22:28:14 +0000402 maxlens = {
403 'java': 100,
torne@chromium.org0ecad9c2013-02-15 16:35:16 +0000404 # This is specifically for Android's handwritten makefiles (Android.mk).
405 'mk': 200,
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000406 '': maxlen,
407 }
maruel@chromium.orgeba64622011-06-20 18:26:48 +0000408
erikchen@google.com12816082013-12-03 02:04:20 +0000409 # Language specific exceptions to max line length.
410 # '.h' is considered an obj-c file extension, since OBJC_EXCEPTIONS are a
411 # superset of CPP_EXCEPTIONS.
412 CPP_FILE_EXTS = ('c', 'cc')
413 CPP_EXCEPTIONS = ('#define', '#endif', '#if', '#include', '#pragma')
Dave Schuyler6eea7c22017-03-03 18:46:25 -0800414 HTML_FILE_EXTS = ('html',)
Dave Schuyler9b9b94c2017-04-13 15:28:41 -0700415 HTML_EXCEPTIONS = ('<g ', '<link ', '<path ',)
erikchen@google.com12816082013-12-03 02:04:20 +0000416 JAVA_FILE_EXTS = ('java',)
417 JAVA_EXCEPTIONS = ('import ', 'package ')
dbeam@chromium.org5a2af6d2015-10-08 17:52:29 +0000418 JS_FILE_EXTS = ('js',)
419 JS_EXCEPTIONS = ("GEN('#include",)
erikchen@google.com12816082013-12-03 02:04:20 +0000420 OBJC_FILE_EXTS = ('h', 'm', 'mm')
421 OBJC_EXCEPTIONS = ('#define', '#endif', '#if', '#import', '#include',
422 '#pragma')
dbeam@chromium.org5a2af6d2015-10-08 17:52:29 +0000423 PY_FILE_EXTS = ('py',)
aiolos@chromium.org84033cc2015-07-16 01:27:15 +0000424 PY_EXCEPTIONS = ('import', 'from')
erikchen@google.com12816082013-12-03 02:04:20 +0000425
426 LANGUAGE_EXCEPTIONS = [
427 (CPP_FILE_EXTS, CPP_EXCEPTIONS),
Dave Schuyler6eea7c22017-03-03 18:46:25 -0800428 (HTML_FILE_EXTS, HTML_EXCEPTIONS),
erikchen@google.com12816082013-12-03 02:04:20 +0000429 (JAVA_FILE_EXTS, JAVA_EXCEPTIONS),
dbeam@chromium.org5a2af6d2015-10-08 17:52:29 +0000430 (JS_FILE_EXTS, JS_EXCEPTIONS),
erikchen@google.com12816082013-12-03 02:04:20 +0000431 (OBJC_FILE_EXTS, OBJC_EXCEPTIONS),
aiolos@chromium.org84033cc2015-07-16 01:27:15 +0000432 (PY_FILE_EXTS, PY_EXCEPTIONS),
erikchen@google.com12816082013-12-03 02:04:20 +0000433 ]
sivachandra@chromium.org270db5c2012-10-09 22:38:44 +0000434
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000435 def no_long_lines(file_extension, line):
erikchen@google.com12816082013-12-03 02:04:20 +0000436 # Check for language specific exceptions.
Dave Schuyler9b9b94c2017-04-13 15:28:41 -0700437 if any(file_extension in exts and line.lstrip().startswith(exceptions)
erikchen@google.com12816082013-12-03 02:04:20 +0000438 for exts, exceptions in LANGUAGE_EXCEPTIONS):
sivachandra@chromium.org270db5c2012-10-09 22:38:44 +0000439 return True
440
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000441 file_maxlen = maxlens.get(file_extension, maxlens[''])
442 # Stupidly long symbols that needs to be worked around if takes 66% of line.
443 long_symbol = file_maxlen * 2 / 3
444 # Hard line length limit at 50% more.
445 extra_maxlen = file_maxlen * 3 / 2
446
447 line_len = len(line)
448 if line_len <= file_maxlen:
maruel@chromium.orgeba64622011-06-20 18:26:48 +0000449 return True
450
dtu@chromium.org03f0c532015-03-27 22:22:07 +0000451 # Allow long URLs of any length.
treib@chromium.orgde3ee902014-07-28 14:23:11 +0000452 if any((url in line) for url in ('file://', 'http://', 'https://')):
453 return True
454
dtu@chromium.org03f0c532015-03-27 22:22:07 +0000455 if line_len > extra_maxlen:
456 return False
457
treib@chromium.orgde3ee902014-07-28 14:23:11 +0000458 if 'url(' in line and file_extension == 'css':
459 return True
460
dbeam@chromium.orgb5ccc9b2014-09-23 00:42:22 +0000461 if '<include' in line and file_extension in ('css', 'html', 'js'):
462 return True
463
treib@chromium.orgde3ee902014-07-28 14:23:11 +0000464 return input_api.re.match(
465 r'.*[A-Za-z][A-Za-z_0-9]{%d,}.*' % long_symbol, line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000466
David 'Digit' Turner31a48642019-01-11 16:10:59 +0000467 def is_global_pylint_directive(line, pos):
468 """True iff the pylint directive starting at line[pos] is global."""
469 # Any character before |pos| that is not whitespace or '#' indidcates
470 # this is a local directive.
471 return not any([c not in " \t#" for c in line[:pos]])
472
473 def check_python_long_lines(affected_files, error_formatter):
474 errors = []
475 global_check_enabled = True
476
477 for f in affected_files:
478 file_path = f.LocalPath()
479 for idx, line in enumerate(f.NewContents()):
480 line_num = idx + 1
481 line_is_short = no_long_lines(PY_FILE_EXTS[0], line)
482
483 pos = line.find('pylint: disable=line-too-long')
484 if pos >= 0:
485 if is_global_pylint_directive(line, pos):
486 global_check_enabled = False # Global disable
487 else:
488 continue # Local disable.
489
490 do_check = global_check_enabled
491
492 pos = line.find('pylint: enable=line-too-long')
493 if pos >= 0:
494 if is_global_pylint_directive(line, pos):
495 global_check_enabled = True # Global enable
496 do_check = True # Ensure it applies to current line as well.
497 else:
498 do_check = True # Local enable
499
500 if do_check and not line_is_short:
501 errors.append(error_formatter(file_path, line_num, line))
502
503 return errors
504
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000505 def format_error(filename, line_num, line):
506 return '%s, line %s, %s chars' % (filename, line_num, len(line))
507
David 'Digit' Turner31a48642019-01-11 16:10:59 +0000508 file_ext_list = list(
509 _GenerateAffectedFileExtList(input_api, source_file_filter))
510
511 errors = []
512
513 # For non-Python files, a simple line-based rule check is enough.
514 non_py_file_ext_list = [x for x in file_ext_list if x[1] not in PY_FILE_EXTS]
515 if non_py_file_ext_list:
516 errors += _FindNewViolationsOfRuleForList(
517 no_long_lines, non_py_file_ext_list, error_formatter=format_error)
518
519 # However, Python files need more sophisticated checks that need parsing
520 # the whole source file.
521 py_file_list = [x[0] for x in file_ext_list if x[1] in PY_FILE_EXTS]
522 if py_file_list:
523 errors += check_python_long_lines(
524 py_file_list, error_formatter=format_error)
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000525 if errors:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000526 msg = 'Found lines longer than %s characters (first 5 shown).' % maxlen
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000527 return [output_api.PresubmitPromptWarning(msg, items=errors[:5])]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000528 else:
529 return []
530
531
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000532def CheckLicense(input_api, output_api, license_re, source_file_filter=None,
maruel@chromium.org71626852010-11-03 13:14:25 +0000533 accept_empty_files=True):
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000534 """Verifies the license header.
535 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000536 license_re = input_api.re.compile(license_re, input_api.re.MULTILINE)
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000537 bad_files = []
538 for f in input_api.AffectedSourceFiles(source_file_filter):
539 contents = input_api.ReadFile(f, 'rb')
maruel@chromium.org71626852010-11-03 13:14:25 +0000540 if accept_empty_files and not contents:
541 continue
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000542 if not license_re.search(contents):
543 bad_files.append(f.LocalPath())
544 if bad_files:
phajdan.jr@chromium.orge27eb7e2015-11-16 12:47:53 +0000545 return [output_api.PresubmitPromptWarning(
bradnelson@google.com393460b2011-03-29 01:20:26 +0000546 'License must match:\n%s\n' % license_re.pattern +
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000547 'Found a bad license header in these files:', items=bad_files)]
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000548 return []
549
550
maruel@chromium.org3410d912009-06-09 20:56:16 +0000551### Other checks
552
553def CheckDoNotSubmit(input_api, output_api):
554 return (
555 CheckDoNotSubmitInDescription(input_api, output_api) +
556 CheckDoNotSubmitInFiles(input_api, output_api)
557 )
558
559
bradnelson@google.comc0b332a2010-08-26 00:30:37 +0000560def CheckTreeIsOpen(input_api, output_api,
561 url=None, closed=None, json_url=None):
562 """Check whether to allow commit without prompt.
563
564 Supports two styles:
565 1. Checks that an url's content doesn't match a regexp that would mean that
566 the tree is closed. (old)
567 2. Check the json_url to decide whether to allow commit without prompt.
568 Args:
569 input_api: input related apis.
570 output_api: output related apis.
571 url: url to use for regex based tree status.
572 closed: regex to match for closed status.
573 json_url: url to download json style status.
574 """
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000575 if not input_api.is_committing:
576 return []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000577 try:
bradnelson@google.comc0b332a2010-08-26 00:30:37 +0000578 if json_url:
Edward Lemurb9830242019-10-30 22:19:20 +0000579 connection = input_api.urllib_request.urlopen(json_url)
bradnelson@google.comc0b332a2010-08-26 00:30:37 +0000580 status = input_api.json.loads(connection.read())
581 connection.close()
582 if not status['can_commit_freely']:
583 short_text = 'Tree state is: ' + status['general_state']
584 long_text = status['message'] + '\n' + json_url
585 return [output_api.PresubmitError(short_text, long_text=long_text)]
586 else:
587 # TODO(bradnelson): drop this once all users are gone.
Edward Lemurb9830242019-10-30 22:19:20 +0000588 connection = input_api.urllib_request.urlopen(url)
bradnelson@google.comc0b332a2010-08-26 00:30:37 +0000589 status = connection.read()
590 connection.close()
591 if input_api.re.match(closed, status):
592 long_text = status + '\n' + url
593 return [output_api.PresubmitError('The tree is closed.',
594 long_text=long_text)]
rohitrao@chromium.orgd490ee82012-02-06 19:31:33 +0000595 except IOError as e:
596 return [output_api.PresubmitError('Error fetching tree status.',
597 long_text=str(e))]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000598 return []
maruel@chromium.org7b305e82009-05-19 18:24:20 +0000599
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000600def GetUnitTestsInDirectory(
Edward Lemur940c2822019-08-23 00:34:25 +0000601 input_api, output_api, directory, whitelist=None, blacklist=None, env=None,
602 run_on_python2=True, run_on_python3=True):
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000603 """Lists all files in a directory and runs them. Doesn't recurse.
604
nick@chromium.orgff526192013-06-10 19:30:26 +0000605 It's mainly a wrapper for RunUnitTests. Use whitelist and blacklist to filter
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000606 tests accordingly.
607 """
608 unit_tests = []
609 test_path = input_api.os_path.abspath(
610 input_api.os_path.join(input_api.PresubmitLocalPath(), directory))
611
612 def check(filename, filters):
613 return any(True for i in filters if input_api.re.match(i, filename))
614
maruel@chromium.orgfae707b2011-09-15 18:57:58 +0000615 to_run = found = 0
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000616 for filename in input_api.os_listdir(test_path):
maruel@chromium.orgfae707b2011-09-15 18:57:58 +0000617 found += 1
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000618 fullpath = input_api.os_path.join(test_path, filename)
619 if not input_api.os_path.isfile(fullpath):
620 continue
621 if whitelist and not check(filename, whitelist):
622 continue
623 if blacklist and check(filename, blacklist):
624 continue
625 unit_tests.append(input_api.os_path.join(directory, filename))
maruel@chromium.orgfae707b2011-09-15 18:57:58 +0000626 to_run += 1
anatoly techtonikbdcdc592017-03-29 17:00:13 +0300627 input_api.logging.debug('Found %d files, running %d unit tests'
628 % (found, to_run))
maruel@chromium.orgfae707b2011-09-15 18:57:58 +0000629 if not to_run:
630 return [
631 output_api.PresubmitPromptWarning(
632 'Out of %d files, found none that matched w=%r, b=%r in directory %s'
633 % (found, whitelist, blacklist, directory))
634 ]
Edward Lemur940c2822019-08-23 00:34:25 +0000635 return GetUnitTests(
636 input_api, output_api, unit_tests, env, run_on_python2, run_on_python3)
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000637
638
Edward Lemur940c2822019-08-23 00:34:25 +0000639def GetUnitTests(
640 input_api, output_api, unit_tests, env=None, run_on_python2=True,
641 run_on_python3=True):
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000642 """Runs all unit tests in a directory.
643
644 On Windows, sys.executable is used for unit tests ending with ".py".
645 """
Edward Lemur940c2822019-08-23 00:34:25 +0000646 assert run_on_python3 or run_on_python2, (
647 'At least one of "run_on_python2" or "run_on_python3" must be set.')
648 def has_py3_shebang(test):
649 with open(test) as f:
650 maybe_shebang = f.readline()
651 return maybe_shebang.startswith('#!') and 'python3' in maybe_shebang
652
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000653 # We don't want to hinder users from uploading incomplete patches.
654 if input_api.is_committing:
655 message_type = output_api.PresubmitError
656 else:
657 message_type = output_api.PresubmitPromptWarning
658
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000659 results = []
660 for unit_test in unit_tests:
Robert Iannucci50258932018-03-19 10:30:59 -0700661 cmd = [unit_test]
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000662 if input_api.verbose:
maruel@chromium.org6c7723e2011-04-12 19:04:55 +0000663 cmd.append('--verbose')
smut@google.comac296202014-04-24 21:47:17 +0000664 kwargs = {'cwd': input_api.PresubmitLocalPath()}
665 if env:
666 kwargs['env'] = env
Edward Lemur940c2822019-08-23 00:34:25 +0000667 if not unit_test.endswith('.py'):
668 results.append(input_api.Command(
669 name=unit_test,
670 cmd=cmd,
671 kwargs=kwargs,
672 message=message_type))
673 else:
674 if has_py3_shebang(unit_test) and run_on_python3:
675 results.append(input_api.Command(
676 name=unit_test,
677 cmd=cmd,
678 kwargs=kwargs,
679 message=message_type,
680 python3=True))
681 if run_on_python2:
682 results.append(input_api.Command(
683 name=unit_test,
684 cmd=cmd,
685 kwargs=kwargs,
686 message=message_type))
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000687 return results
688
nick@chromium.orgff526192013-06-10 19:30:26 +0000689
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000690def GetUnitTestsRecursively(input_api, output_api, directory,
691 whitelist, blacklist):
692 """Gets all files in the directory tree (git repo) that match the whitelist.
693
694 Restricts itself to only find files within the Change's source repo, not
695 dependencies.
696 """
697 def check(filename):
698 return (any(input_api.re.match(f, filename) for f in whitelist) and
699 not any(input_api.re.match(f, filename) for f in blacklist))
700
701 tests = []
702
703 to_run = found = 0
704 for filepath in input_api.change.AllFiles(directory):
705 found += 1
706 if check(filepath):
707 to_run += 1
708 tests.append(filepath)
709 input_api.logging.debug('Found %d files, running %d' % (found, to_run))
710 if not to_run:
711 return [
712 output_api.PresubmitPromptWarning(
713 'Out of %d files, found none that matched w=%r, b=%r in directory %s'
714 % (found, whitelist, blacklist, directory))
715 ]
716
717 return GetUnitTests(input_api, output_api, tests)
718
719
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000720def GetPythonUnitTests(input_api, output_api, unit_tests):
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000721 """Run the unit tests out of process, capture the output and use the result
722 code to determine success.
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000723
724 DEPRECATED.
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000725 """
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000726 # We don't want to hinder users from uploading incomplete patches.
727 if input_api.is_committing:
728 message_type = output_api.PresubmitError
729 else:
730 message_type = output_api.PresubmitNotifyResult
maruel@chromium.org0e766052011-04-06 13:32:51 +0000731 results = []
maruel@chromium.org7b305e82009-05-19 18:24:20 +0000732 for unit_test in unit_tests:
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000733 # Run the unit tests out of process. This is because some unit tests
734 # stub out base libraries and don't clean up their mess. It's too easy to
735 # get subtle bugs.
736 cwd = None
737 env = None
738 unit_test_name = unit_test
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000739 # 'python -m test.unit_test' doesn't work. We need to change to the right
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000740 # directory instead.
741 if '.' in unit_test:
742 # Tests imported in submodules (subdirectories) assume that the current
743 # directory is in the PYTHONPATH. Manually fix that.
744 unit_test = unit_test.replace('.', '/')
745 cwd = input_api.os_path.dirname(unit_test)
746 unit_test = input_api.os_path.basename(unit_test)
747 env = input_api.environ.copy()
kbr@google.comab318592009-09-04 00:54:55 +0000748 # At least on Windows, it seems '.' must explicitly be in PYTHONPATH
749 backpath = [
750 '.', input_api.os_path.pathsep.join(['..'] * (cwd.count('/') + 1))
751 ]
Edward Lesmes401b25c2019-10-23 03:34:12 +0000752 # We convert to str, since on Windows on Python 2 only strings are allowed
753 # as environment variables, but literals are unicode since we're importing
754 # unicode_literals from __future__.
Edward Lemurb9830242019-10-30 22:19:20 +0000755 if env.get('PYTHONPATH'):
756 backpath.append(env.get('PYTHONPATH'))
757 env['PYTHONPATH'] = input_api.os_path.pathsep.join((backpath))
758 env.pop('VPYTHON_CLEAR_PYTHONPATH', None)
maruel@chromium.org0e766052011-04-06 13:32:51 +0000759 cmd = [input_api.python_executable, '-m', '%s' % unit_test]
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000760 results.append(input_api.Command(
761 name=unit_test_name,
762 cmd=cmd,
763 kwargs={'env': env, 'cwd': cwd},
764 message=message_type))
maruel@chromium.org0e766052011-04-06 13:32:51 +0000765 return results
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000766
767
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000768def RunUnitTestsInDirectory(input_api, *args, **kwargs):
769 """Run tests in a directory serially.
770
771 For better performance, use GetUnitTestsInDirectory and then
772 pass to input_api.RunTests.
773 """
774 return input_api.RunTests(
775 GetUnitTestsInDirectory(input_api, *args, **kwargs), False)
776
777
778def RunUnitTests(input_api, *args, **kwargs):
779 """Run tests serially.
780
781 For better performance, use GetUnitTests and then pass to
782 input_api.RunTests.
783 """
784 return input_api.RunTests(GetUnitTests(input_api, *args, **kwargs), False)
785
786
787def RunPythonUnitTests(input_api, *args, **kwargs):
788 """Run python tests in a directory serially.
789
790 DEPRECATED
791 """
792 return input_api.RunTests(
793 GetPythonUnitTests(input_api, *args, **kwargs), False)
794
795
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000796def _FetchAllFiles(input_api, white_list, black_list):
797 """Hack to fetch all files."""
798 # We cannot use AffectedFiles here because we want to test every python
799 # file on each single python change. It's because a change in a python file
800 # can break another unmodified file.
801 # Use code similar to InputApi.FilterSourceFile()
802 def Find(filepath, filters):
anatoly techtonikbdcdc592017-03-29 17:00:13 +0300803 if input_api.platform == 'win32':
804 filepath = filepath.replace('\\', '/')
805
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000806 for item in filters:
807 if input_api.re.match(item, filepath):
808 return True
809 return False
810
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000811 files = []
812 path_len = len(input_api.PresubmitLocalPath())
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000813 for dirpath, dirnames, filenames in input_api.os_walk(
814 input_api.PresubmitLocalPath()):
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000815 # Passes dirnames in black list to speed up search.
816 for item in dirnames[:]:
817 filepath = input_api.os_path.join(dirpath, item)[path_len + 1:]
818 if Find(filepath, black_list):
819 dirnames.remove(item)
820 for item in filenames:
821 filepath = input_api.os_path.join(dirpath, item)[path_len + 1:]
822 if Find(filepath, white_list) and not Find(filepath, black_list):
823 files.append(filepath)
824 return files
825
826
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000827def GetPylint(input_api, output_api, white_list=None, black_list=None,
dtu@chromium.orgf23524d2016-01-27 20:08:49 +0000828 disabled_warnings=None, extra_paths_list=None, pylintrc=None):
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000829 """Run pylint on python files.
830
chrisha@google.com267d6592012-06-19 19:23:31 +0000831 The default white_list enforces looking only at *.py files.
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000832 """
Edward Lemurb9830242019-10-30 22:19:20 +0000833 white_list = tuple(white_list or (r'.*\.py$',))
maruel@chromium.org69eaecb2011-06-14 13:09:13 +0000834 black_list = tuple(black_list or input_api.DEFAULT_BLACK_LIST)
ilevy@chromium.org7b677f72013-01-07 18:49:26 +0000835 extra_paths_list = extra_paths_list or []
836
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000837 if input_api.is_committing:
838 error_type = output_api.PresubmitError
839 else:
840 error_type = output_api.PresubmitPromptWarning
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000841
842 # Only trigger if there is at least one python file affected.
ilevy@chromium.org36576332013-01-08 03:16:15 +0000843 def rel_path(regex):
844 """Modifies a regex for a subject to accept paths relative to root."""
chrisha@chromium.org40e5d3d2013-01-18 17:46:57 +0000845 def samefile(a, b):
846 # Default implementation for platforms lacking os.path.samefile
847 # (like Windows).
848 return input_api.os_path.abspath(a) == input_api.os_path.abspath(b)
849 samefile = getattr(input_api.os_path, 'samefile', samefile)
850 if samefile(input_api.PresubmitLocalPath(),
851 input_api.change.RepositoryRoot()):
ilevy@chromium.orgdd4e9312013-01-15 03:22:04 +0000852 return regex
chrisha@chromium.org40e5d3d2013-01-18 17:46:57 +0000853
ilevy@chromium.org36576332013-01-08 03:16:15 +0000854 prefix = input_api.os_path.join(input_api.os_path.relpath(
855 input_api.PresubmitLocalPath(), input_api.change.RepositoryRoot()), '')
856 return input_api.re.escape(prefix) + regex
ilevy@chromium.org7b677f72013-01-07 18:49:26 +0000857 src_filter = lambda x: input_api.FilterSourceFile(
858 x, map(rel_path, white_list), map(rel_path, black_list))
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000859 if not input_api.AffectedSourceFiles(src_filter):
ilevy@chromium.org7b677f72013-01-07 18:49:26 +0000860 input_api.logging.info('Skipping pylint: no matching changes.')
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000861 return []
862
agable@chromium.org327d72b2015-04-21 20:22:50 +0000863 if pylintrc is not None:
864 pylintrc = input_api.os_path.join(input_api.PresubmitLocalPath(), pylintrc)
865 else:
866 pylintrc = input_api.os_path.join(_HERE, 'pylintrc')
dtu@chromium.orgf23524d2016-01-27 20:08:49 +0000867 extra_args = ['--rcfile=%s' % pylintrc]
maruel@chromium.orgff9a2172012-04-24 16:55:32 +0000868 if disabled_warnings:
869 extra_args.extend(['-d', ','.join(disabled_warnings)])
870
chrisha@google.com267d6592012-06-19 19:23:31 +0000871 files = _FetchAllFiles(input_api, white_list, black_list)
872 if not files:
maruel@chromium.orgfca53392010-12-21 18:42:57 +0000873 return []
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000874 files.sort()
chrisha@google.com267d6592012-06-19 19:23:31 +0000875
csharp@chromium.org40395342013-02-21 14:57:23 +0000876 input_api.logging.info('Running pylint on %d files', len(files))
877 input_api.logging.debug('Running pylint on: %s', files)
chrisha@google.com267d6592012-06-19 19:23:31 +0000878 env = input_api.environ.copy()
Edward Lemurb9830242019-10-30 22:19:20 +0000879 env['PYTHONPATH'] = input_api.os_path.pathsep.join(extra_paths_list)
Edward Lemurd2a5a4c2019-10-23 22:55:26 +0000880 env.pop('VPYTHON_CLEAR_PYTHONPATH', None)
Robert Iannucci82a64802018-03-23 16:40:16 -0700881 input_api.logging.debug(' with extra PYTHONPATH: %r', extra_paths_list)
chrisha@google.com267d6592012-06-19 19:23:31 +0000882
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000883 def GetPylintCmd(flist, extra, parallel):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000884 # Windows needs help running python files so we explicitly specify
885 # the interpreter to use. It also has limitations on the size of
886 # the command-line, so we pass arguments via a pipe.
Mike Frysinger53297792019-08-20 21:32:33 +0000887 tool = input_api.os_path.join(_HERE, 'pylint')
888 if input_api.platform == 'win32':
889 tool += '.bat'
890 cmd = [tool, '--args-on-stdin']
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000891 if len(flist) == 1:
892 description = flist[0]
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000893 else:
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000894 description = '%s files' % len(flist)
chrisha@chromium.org1b129e52012-06-22 17:08:11 +0000895
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000896 args = extra_args[:]
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000897 if extra:
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000898 args.extend(extra)
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000899 description += ' using %s' % (extra,)
900 if parallel:
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000901 args.append('--jobs=%s' % input_api.cpu_count)
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000902 description += ' on %d cores' % input_api.cpu_count
903
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000904 return input_api.Command(
905 name='Pylint (%s)' % description,
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000906 cmd=cmd,
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000907 kwargs={'env': env, 'stdin': '\n'.join(args + flist)},
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000908 message=error_type)
chrisha@chromium.org1b129e52012-06-22 17:08:11 +0000909
sbc@chromium.org7fc75ca2012-09-21 20:14:21 +0000910 # Always run pylint and pass it all the py files at once.
911 # Passing py files one at time is slower and can produce
912 # different results. input_api.verbose used to be used
913 # to enable this behaviour but differing behaviour in
914 # verbose mode is not desirable.
ilevy@chromium.org7b677f72013-01-07 18:49:26 +0000915 # Leave this unreachable code in here so users can make
916 # a quick local edit to diagnose pylint issues more
917 # easily.
sbc@chromium.org7fc75ca2012-09-21 20:14:21 +0000918 if True:
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000919 # pylint's cycle detection doesn't work in parallel, so spawn a second,
920 # single-threaded job for just that check.
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000921
922 # Some PRESUBMITs explicitly mention cycle detection.
923 if not any('R0401' in a or 'cyclic-import' in a for a in extra_args):
924 return [
925 GetPylintCmd(files, ["--disable=cyclic-import"], True),
926 GetPylintCmd(files, ["--disable=all", "--enable=cyclic-import"], False)
927 ]
928 else:
929 return [ GetPylintCmd(files, [], True) ]
930
chrisha@google.com267d6592012-06-19 19:23:31 +0000931 else:
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000932 return map(lambda x: GetPylintCmd([x], [], 1), files)
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000933
934
935def RunPylint(input_api, *args, **kwargs):
936 """Legacy presubmit function.
937
938 For better performance, get all tests and then pass to
939 input_api.RunTests.
940 """
941 return input_api.RunTests(GetPylint(input_api, *args, **kwargs), False)
maruel@chromium.orge94aedc2010-12-13 21:11:30 +0000942
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000943
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000944def CheckBuildbotPendingBuilds(input_api, output_api, url, max_pendings,
945 ignored):
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000946 try:
Edward Lemurb9830242019-10-30 22:19:20 +0000947 connection = input_api.urllib_request.urlopen(url)
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000948 raw_data = connection.read()
949 connection.close()
950 except IOError:
951 return [output_api.PresubmitNotifyResult('%s is not accessible' % url)]
952
953 try:
954 data = input_api.json.loads(raw_data)
955 except ValueError:
956 return [output_api.PresubmitNotifyResult('Received malformed json while '
957 'looking up buildbot status')]
958
959 out = []
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000960 for (builder_name, builder) in data.items():
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000961 if builder_name in ignored:
962 continue
maruel@chromium.orgcf1982c2010-10-04 15:08:28 +0000963 if builder.get('state', '') == 'offline':
964 continue
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000965 pending_builds_len = len(builder.get('pending_builds', []))
966 if pending_builds_len > max_pendings:
967 out.append('%s has %d build(s) pending' %
968 (builder_name, pending_builds_len))
969 if out:
970 return [output_api.PresubmitPromptWarning(
971 'Build(s) pending. It is suggested to wait that no more than %d '
972 'builds are pending.' % max_pendings,
973 long_text='\n'.join(out))]
974 return []
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000975
976
Edward Lesmes067ef5d2018-04-19 17:54:45 -0400977def CheckOwnersFormat(input_api, output_api):
978 affected_files = set([
979 f.LocalPath()
980 for f in input_api.change.AffectedFiles()
981 if 'OWNERS' in f.LocalPath() and f.Action() != 'D'
982 ])
983 if not affected_files:
984 return []
985 try:
Edward Lemurdb0055d2018-12-21 19:02:23 +0000986 owners_db = input_api.owners_db
987 owners_db.override_files = {}
988 owners_db.load_data_needed_for(affected_files)
Edward Lesmes067ef5d2018-04-19 17:54:45 -0400989 return []
990 except Exception as e:
991 return [output_api.PresubmitError(
992 'Error parsing OWNERS files:\n%s' % e)]
993
994
dpranke@chromium.orgbce925d2015-01-16 22:38:38 +0000995def CheckOwners(input_api, output_api, source_file_filter=None):
Aaron Gable8678d322018-04-02 13:28:19 -0700996 affected_files = set([f.LocalPath() for f in
997 input_api.change.AffectedFiles(file_filter=source_file_filter)])
Alan Cutter3fcadbb2019-09-12 00:34:06 +0000998 owners_db = input_api.owners_db
999 owners_db.override_files = input_api.change.OriginalOwnersFiles()
1000 owner_email, reviewers = GetCodereviewOwnerAndReviewers(
1001 input_api,
1002 owners_db.email_regexp,
1003 approval_needed=input_api.is_committing)
1004
1005 owner_email = owner_email or input_api.change.author_email
1006
1007 finder = input_api.owners_finder(
1008 affected_files,
1009 input_api.change.RepositoryRoot(),
1010 owner_email,
1011 reviewers,
Edward Lemurb9830242019-10-30 22:19:20 +00001012 fopen=open,
Alan Cutter3fcadbb2019-09-12 00:34:06 +00001013 os_path=input_api.os_path,
1014 email_postfix='',
1015 disable_color=True,
1016 override_files=input_api.change.OriginalOwnersFiles())
1017 missing_files = finder.unreviewed_files
1018 affects_owners = any('OWNERS' in name for name in missing_files)
Aaron Gable8678d322018-04-02 13:28:19 -07001019
pam@chromium.orgf46aed92012-03-08 09:18:17 +00001020 if input_api.is_committing:
Jeremy Roman5ae86d22018-04-26 15:36:02 -04001021 if input_api.tbr and not affects_owners:
pam@chromium.orgf46aed92012-03-08 09:18:17 +00001022 return [output_api.PresubmitNotifyResult(
1023 '--tbr was specified, skipping OWNERS check')]
Clemens Hammacherd0c226a2017-01-16 14:09:52 +01001024 needed = 'LGTM from an OWNER'
1025 output_fn = output_api.PresubmitError
rmistry@google.com5fc6c8c2015-04-16 16:38:43 +00001026 if input_api.change.issue:
tandrii@chromium.org9dea2ac2016-04-28 06:26:20 +00001027 if input_api.dry_run:
Clemens Hammacherd0c226a2017-01-16 14:09:52 +01001028 output_fn = lambda text: output_api.PresubmitNotifyResult(
1029 'This is a dry run, but these failures would be reported on ' +
1030 'commit:\n' + text)
rmistry@google.com5fc6c8c2015-04-16 16:38:43 +00001031 else:
Andrii Shyshkalov63560ad2018-02-09 19:09:54 -08001032 return [output_api.PresubmitError(
1033 'OWNERS check failed: this CL has no Gerrit change number, '
1034 'so we can\'t check it for approvals.')]
pam@chromium.orgf46aed92012-03-08 09:18:17 +00001035 else:
1036 needed = 'OWNER reviewers'
Clemens Hammacherd0c226a2017-01-16 14:09:52 +01001037 output_fn = output_api.PresubmitNotifyResult
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +00001038
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +00001039 if missing_files:
zork@chromium.org046e1752012-05-07 05:56:12 +00001040 output_list = [
Clemens Hammacherd0c226a2017-01-16 14:09:52 +01001041 output_fn('Missing %s for these files:\n %s' %
1042 (needed, '\n '.join(sorted(missing_files))))]
Jeremy Roman5ae86d22018-04-26 15:36:02 -04001043 if input_api.tbr and affects_owners:
Alan Cutter3fcadbb2019-09-12 00:34:06 +00001044 output_list.append(output_fn('TBR for OWNERS files are ignored.'))
zork@chromium.org046e1752012-05-07 05:56:12 +00001045 if not input_api.is_committing:
dpranke@chromium.org31b42c02013-09-19 19:24:15 +00001046 suggested_owners = owners_db.reviewers_for(missing_files, owner_email)
Jochen Eisinger76f5fc62017-04-07 16:27:46 +02001047 owners_with_comments = []
1048 def RecordComments(text):
1049 owners_with_comments.append(finder.print_indent() + text)
1050 finder.writeln = RecordComments
1051 for owner in suggested_owners:
1052 finder.print_comments(owner)
Clemens Hammacherd0c226a2017-01-16 14:09:52 +01001053 output_list.append(output_fn('Suggested OWNERS: ' +
scheib@chromium.org93276ab2013-10-14 23:55:32 +00001054 '(Use "git-cl owners" to interactively select owners.)\n %s' %
Jochen Eisinger76f5fc62017-04-07 16:27:46 +02001055 ('\n '.join(owners_with_comments))))
zork@chromium.org046e1752012-05-07 05:56:12 +00001056 return output_list
dpranke@chromium.org2a009622011-03-01 02:43:31 +00001057
pam@chromium.orgf46aed92012-03-08 09:18:17 +00001058 if input_api.is_committing and not reviewers:
Clemens Hammacherd0c226a2017-01-16 14:09:52 +01001059 return [output_fn('Missing LGTM from someone other than %s' % owner_email)]
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +00001060 return []
dpranke@chromium.org627ea672011-03-11 23:29:03 +00001061
Aaron Gable668c1d82018-04-03 10:19:16 -07001062
tandrii@chromium.org830dc0b2016-05-09 06:26:34 +00001063def GetCodereviewOwnerAndReviewers(input_api, email_regexp, approval_needed):
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001064 """Return the owner and reviewers of a change, if any.
1065
1066 If approval_needed is True, only reviewers who have approved the change
1067 will be returned.
1068 """
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001069 issue = input_api.change.issue
1070 if not issue:
tandrii@chromium.org830dc0b2016-05-09 06:26:34 +00001071 return None, (set() if approval_needed else
1072 _ReviewersFromChange(input_api.change))
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001073
1074 owner_email = input_api.gerrit.GetChangeOwner(issue)
1075 reviewers = set(
1076 r for r in input_api.gerrit.GetChangeReviewers(issue, approval_needed)
1077 if _match_reviewer_email(r, owner_email, email_regexp))
tandrii81665dc2016-08-29 09:16:19 -07001078 input_api.logging.debug('owner: %s; approvals given by: %s',
1079 owner_email, ', '.join(sorted(reviewers)))
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001080 return owner_email, reviewers
1081
1082
Aaron Gable668c1d82018-04-03 10:19:16 -07001083def _ReviewersFromChange(change):
1084 """Return the reviewers specified in the |change|, if any."""
1085 reviewers = set()
1086 reviewers.update(change.ReviewersFromDescription())
1087 reviewers.update(change.TBRsFromDescription())
1088
1089 # Drop reviewers that aren't specified in email address format.
1090 return set(reviewer for reviewer in reviewers if '@' in reviewer)
1091
1092
1093def _match_reviewer_email(r, owner_email, email_regexp):
1094 return email_regexp.match(r) and r != owner_email
1095
1096
bauerb@chromium.org4cea01e2012-03-20 19:49:05 +00001097def CheckSingletonInHeaders(input_api, output_api, source_file_filter=None):
glider@chromium.orgc3617f32015-02-19 16:33:33 +00001098 """Deprecated, must be removed."""
1099 return [
1100 output_api.PresubmitNotifyResult(
1101 'CheckSingletonInHeaders is deprecated, please remove it.')
1102 ]
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001103
1104
1105def PanProjectChecks(input_api, output_api,
1106 excluded_paths=None, text_files=None,
bradnelson@google.comd57771d2011-03-31 19:18:32 +00001107 license_header=None, project_name=None,
jamesr@chromium.orgaf27f462013-04-04 21:44:22 +00001108 owners_check=True, maxlen=80):
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001109 """Checks that ALL chromium orbit projects should use.
1110
1111 These are checks to be run on all Chromium orbit project, including:
1112 Chromium
1113 Native Client
1114 V8
1115 When you update this function, please take this broad scope into account.
1116 Args:
1117 input_api: Bag of input related interfaces.
1118 output_api: Bag of output related interfaces.
1119 excluded_paths: Don't include these paths in common checks.
1120 text_files: Which file are to be treated as documentation text files.
1121 license_header: What license header should be on files.
1122 project_name: What is the name of the project as it appears in the license.
1123 Returns:
1124 A list of warning or error objects.
1125 """
maruel@chromium.org69eaecb2011-06-14 13:09:13 +00001126 excluded_paths = tuple(excluded_paths or [])
1127 text_files = tuple(text_files or (
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +00001128 r'.+\.txt$',
1129 r'.+\.json$',
maruel@chromium.org69eaecb2011-06-14 13:09:13 +00001130 ))
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001131 project_name = project_name or 'Chromium'
mark@chromium.org2cc66422012-08-07 21:22:32 +00001132
1133 # Accept any year number from 2006 to the current year, or the special
rvargas@chromium.org02652602014-03-14 19:43:20 +00001134 # 2006-20xx string used on the oldest files. 2006-20xx is deprecated, but
1135 # tolerated on old files.
mark@chromium.org2cc66422012-08-07 21:22:32 +00001136 current_year = int(input_api.time.strftime('%Y'))
Edward Lemurb9830242019-10-30 22:19:20 +00001137 allowed_years = (str(s) for s in reversed(range(2006, current_year + 1)))
rvargas@chromium.org02652602014-03-14 19:43:20 +00001138 years_re = '(' + '|'.join(allowed_years) + '|2006-2008|2006-2009|2006-2010)'
mark@chromium.org2cc66422012-08-07 21:22:32 +00001139
1140 # The (c) is deprecated, but tolerate it until it's removed from all files.
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001141 license_header = license_header or (
mark@chromium.org2cc66422012-08-07 21:22:32 +00001142 r'.*? Copyright (\(c\) )?%(year)s The %(project)s Authors\. '
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001143 r'All rights reserved\.\n'
1144 r'.*? Use of this source code is governed by a BSD-style license that '
1145 r'can be\n'
dbeam@chromium.orgb2312102012-02-15 02:01:55 +00001146 r'.*? found in the LICENSE file\.(?: \*/)?\n'
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001147 ) % {
mark@chromium.org2cc66422012-08-07 21:22:32 +00001148 'year': years_re,
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001149 'project': project_name,
1150 }
1151
1152 results = []
1153 # This code loads the default black list (e.g. third_party, experimental, etc)
1154 # and add our black list (breakpad, skia and v8 are still not following
1155 # google style and are not really living this repository).
1156 # See presubmit_support.py InputApi.FilterSourceFile for the (simple) usage.
1157 black_list = input_api.DEFAULT_BLACK_LIST + excluded_paths
1158 white_list = input_api.DEFAULT_WHITE_LIST + text_files
1159 sources = lambda x: input_api.FilterSourceFile(x, black_list=black_list)
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +00001160 text_files = lambda x: input_api.FilterSourceFile(
1161 x, black_list=black_list, white_list=white_list)
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001162
1163 snapshot_memory = []
1164 def snapshot(msg):
1165 """Measures & prints performance warning if a rule is running slow."""
Edward Lemurb9830242019-10-30 22:19:20 +00001166 if input_api.sys.version_info.major == 2:
1167 dt2 = input_api.time.clock()
1168 else:
1169 dt2 = input_api.time.process_time()
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001170 if snapshot_memory:
1171 delta_ms = int(1000*(dt2 - snapshot_memory[0]))
1172 if delta_ms > 500:
Raul Tambre80ee78e2019-05-06 22:41:05 +00001173 print(" %s took a long time: %dms" % (snapshot_memory[1], delta_ms))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001174 snapshot_memory[:] = (dt2, msg)
1175
Edward Lesmescb62e482018-04-19 18:29:35 -04001176 snapshot("checking owners files format")
1177 results.extend(input_api.canned_checks.CheckOwnersFormat(
1178 input_api, output_api))
1179
bradnelson@google.comd57771d2011-03-31 19:18:32 +00001180 if owners_check:
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001181 snapshot("checking owners")
bradnelson@google.comd57771d2011-03-31 19:18:32 +00001182 results.extend(input_api.canned_checks.CheckOwners(
Dirk Pranke3c86cee2017-01-23 22:02:06 -08001183 input_api, output_api, source_file_filter=None))
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001184
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001185 snapshot("checking long lines")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001186 results.extend(input_api.canned_checks.CheckLongLines(
jamesr@chromium.orgaf27f462013-04-04 21:44:22 +00001187 input_api, output_api, maxlen, source_file_filter=sources))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001188 snapshot( "checking tabs")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001189 results.extend(input_api.canned_checks.CheckChangeHasNoTabs(
1190 input_api, output_api, source_file_filter=sources))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001191 snapshot( "checking stray whitespace")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001192 results.extend(input_api.canned_checks.CheckChangeHasNoStrayWhitespace(
1193 input_api, output_api, source_file_filter=sources))
phajdan.jr@chromium.orgd965db32015-11-16 09:46:56 +00001194 snapshot("checking license")
1195 results.extend(input_api.canned_checks.CheckLicense(
1196 input_api, output_api, license_header, source_file_filter=sources))
maruel@chromium.orgb1ce7752011-05-08 13:50:16 +00001197
maruel@chromium.orgb1ce7752011-05-08 13:50:16 +00001198 if input_api.is_committing:
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001199 snapshot("checking was uploaded")
1200 results.extend(input_api.canned_checks.CheckChangeWasUploaded(
1201 input_api, output_api))
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +00001202 snapshot("checking description")
1203 results.extend(input_api.canned_checks.CheckChangeHasDescription(
1204 input_api, output_api))
1205 results.extend(input_api.canned_checks.CheckDoNotSubmitInDescription(
1206 input_api, output_api))
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +00001207 snapshot("checking do not submit in files")
1208 results.extend(input_api.canned_checks.CheckDoNotSubmitInFiles(
1209 input_api, output_api))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001210 snapshot("done")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001211 return results
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001212
1213
Aiden Benner99b0ccb2018-11-20 19:53:31 +00001214def CheckPatchFormatted(input_api,
1215 output_api,
Ryan Tsengf28ef982018-12-04 19:53:08 +00001216 bypass_warnings=True,
Aiden Benner99b0ccb2018-11-20 19:53:31 +00001217 check_js=False,
1218 check_python=None,
1219 result_factory=None):
Nodir Turakulov425d9ce2018-06-14 00:07:46 +00001220 result_factory = result_factory or output_api.PresubmitPromptWarning
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001221 import git_cl
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001222
1223 display_args = []
Christopher Lamc5ba6922017-01-24 11:19:14 +11001224 if check_js:
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001225 display_args.append('--js')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00001226
1227 # Explicitly setting check_python to will enable/disable python formatting
1228 # on all files. Leaving it as None will enable checking patch formatting
1229 # on files that have a .style.yapf file in a parent directory.
1230 if check_python is not None:
1231 if check_python:
1232 display_args.append('--python')
1233 else:
1234 display_args.append('--no-python')
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001235
1236 cmd = ['-C', input_api.change.RepositoryRoot(),
1237 'cl', 'format', '--dry-run', '--presubmit'] + display_args
Andrew Grieved86c8032017-09-26 14:40:36 -04001238 presubmit_subdir = input_api.os_path.relpath(
1239 input_api.PresubmitLocalPath(), input_api.change.RepositoryRoot())
1240 if presubmit_subdir.startswith('..') or presubmit_subdir == '.':
1241 presubmit_subdir = ''
1242 # If the PRESUBMIT.py is in a parent repository, then format the entire
1243 # subrepository. Otherwise, format only the code in the directory that
1244 # contains the PRESUBMIT.py.
1245 if presubmit_subdir:
1246 cmd.append(input_api.PresubmitLocalPath())
Ryan Tsengf28ef982018-12-04 19:53:08 +00001247 code, _ = git_cl.RunGitWithCode(cmd, suppress_stderr=bypass_warnings)
1248 # bypass_warnings? Only fail with code 2.
1249 # As this is just a warning, ignore all other errors if the user
1250 # happens to have a broken clang-format, doesn't use git, etc etc.
1251 if code == 2 or (code and not bypass_warnings):
Andrew Grieved3b25482017-10-13 16:06:25 -04001252 if presubmit_subdir:
1253 short_path = presubmit_subdir
1254 else:
1255 short_path = input_api.basename(input_api.change.RepositoryRoot())
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001256 display_args.append(presubmit_subdir)
Nodir Turakulov425d9ce2018-06-14 00:07:46 +00001257 return [result_factory(
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00001258 'The %s directory requires source formatting. '
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001259 'Please run: git cl format %s' %
1260 (short_path, ' '.join(display_args)))]
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001261 return []
scottmg@chromium.orgd05ab352014-12-05 17:24:26 +00001262
1263
1264def CheckGNFormatted(input_api, output_api):
1265 import gn
1266 affected_files = input_api.AffectedFiles(
1267 include_deletes=False,
1268 file_filter=lambda x: x.LocalPath().endswith('.gn') or
kylechar58edce22016-06-17 06:07:51 -07001269 x.LocalPath().endswith('.gni') or
1270 x.LocalPath().endswith('.typemap'))
scottmg@chromium.orgd05ab352014-12-05 17:24:26 +00001271 warnings = []
1272 for f in affected_files:
1273 cmd = ['gn', 'format', '--dry-run', f.AbsoluteLocalPath()]
1274 rc = gn.main(cmd)
1275 if rc == 2:
1276 warnings.append(output_api.PresubmitPromptWarning(
brettw4b8ed592016-08-05 16:19:12 -07001277 '%s requires formatting. Please run:\n gn format %s' % (
scottmg@chromium.orgd05ab352014-12-05 17:24:26 +00001278 f.AbsoluteLocalPath(), f.LocalPath())))
1279 # It's just a warning, so ignore other types of failures assuming they'll be
1280 # caught elsewhere.
1281 return warnings
Dan Jacques94652a32017-10-09 23:18:46 -04001282
1283
1284def CheckCIPDManifest(input_api, output_api, path=None, content=None):
1285 """Verifies that a CIPD ensure file manifest is valid against all platforms.
1286
1287 Exactly one of "path" or "content" must be provided. An assertion will occur
1288 if neither or both are provided.
1289
1290 Args:
1291 path (str): If provided, the filesystem path to the manifest to verify.
1292 content (str): If provided, the raw content of the manifest to veirfy.
1293 """
1294 cipd_bin = 'cipd' if not input_api.is_windows else 'cipd.bat'
1295 cmd = [cipd_bin, 'ensure-file-verify']
1296 kwargs = {}
1297
1298 if input_api.is_windows:
1299 # Needs to be able to resolve "cipd.bat".
1300 kwargs['shell'] = True
1301
1302 if input_api.verbose:
1303 cmd += ['-log-level', 'debug']
1304
1305 if path:
1306 assert content is None, 'Cannot provide both "path" and "content".'
1307 cmd += ['-ensure-file', path]
Robert Iannucci7d47eb52017-12-06 12:18:18 -08001308 name = 'Check CIPD manifest %r' % path
Dan Jacques94652a32017-10-09 23:18:46 -04001309 elif content:
1310 assert path is None, 'Cannot provide both "path" and "content".'
1311 cmd += ['-ensure-file=-']
1312 kwargs['stdin'] = content
Robert Iannucci7d47eb52017-12-06 12:18:18 -08001313 # quick and dirty parser to extract checked packages.
1314 packages = [
1315 l.split()[0] for l in (ll.strip() for ll in content.splitlines())
1316 if ' ' in l and not l.startswith('$')
1317 ]
1318 name = 'Check CIPD packages from string: %r' % (packages,)
Dan Jacques94652a32017-10-09 23:18:46 -04001319 else:
1320 raise Exception('Exactly one of "path" or "content" must be provided.')
1321
1322 return input_api.Command(
Robert Iannucci7d47eb52017-12-06 12:18:18 -08001323 name,
Dan Jacques94652a32017-10-09 23:18:46 -04001324 cmd,
1325 kwargs,
1326 output_api.PresubmitError)
1327
1328
1329def CheckCIPDPackages(input_api, output_api, platforms, packages):
1330 """Verifies that all named CIPD packages can be resolved against all supplied
1331 platforms.
1332
1333 Args:
1334 platforms (list): List of CIPD platforms to verify.
1335 packages (dict): Mapping of package name to version.
1336 """
1337 manifest = []
1338 for p in platforms:
1339 manifest.append('$VerifiedPlatform %s' % (p,))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001340 for k, v in packages.items():
Dan Jacques94652a32017-10-09 23:18:46 -04001341 manifest.append('%s %s' % (k, v))
1342 return CheckCIPDManifest(input_api, output_api, content='\n'.join(manifest))
Sergiy Byelozyoroveba83472017-10-27 02:08:21 +02001343
1344
Vadim Shtayura21741362018-09-14 22:17:02 +00001345def CheckCIPDClientDigests(input_api, output_api, client_version_file):
1346 """Verifies that *.digests file was correctly regenerated.
1347
1348 <client_version_file>.digests file contains pinned hashes of the CIPD client.
1349 It is consulted during CIPD client bootstrap and self-update. It should be
1350 regenerated each time CIPD client version file changes.
1351
1352 Args:
1353 client_version_file (str): Path to a text file with CIPD client version.
1354 """
1355 cmd = [
1356 'cipd' if not input_api.is_windows else 'cipd.bat',
1357 'selfupdate-roll', '-check', '-version-file', client_version_file,
1358 ]
1359 if input_api.verbose:
1360 cmd += ['-log-level', 'debug']
1361 return input_api.Command(
1362 'Check CIPD client_version_file.digests file',
1363 cmd,
1364 {'shell': True} if input_api.is_windows else {}, # to resolve cipd.bat
1365 output_api.PresubmitError)
1366
1367
Sergiy Byelozyoroveba83472017-10-27 02:08:21 +02001368def CheckVPythonSpec(input_api, output_api, file_filter=None):
1369 """Validates any changed .vpython files with vpython verification tool.
1370
1371 Args:
1372 input_api: Bag of input related interfaces.
1373 output_api: Bag of output related interfaces.
1374 file_filter: Custom function that takes a path (relative to client root) and
1375 returns boolean, which is used to filter files for which to apply the
1376 verification to. Defaults to any path ending with .vpython, which captures
1377 both global .vpython and <script>.vpython files.
1378
1379 Returns:
1380 A list of input_api.Command objects containing verification commands.
1381 """
1382 file_filter = file_filter or (lambda f: f.LocalPath().endswith('.vpython'))
John Budorick16162372018-04-18 10:39:53 -07001383 affected_files = input_api.AffectedTestableFiles(file_filter=file_filter)
Sergiy Byelozyoroveba83472017-10-27 02:08:21 +02001384 affected_files = map(lambda f: f.AbsoluteLocalPath(), affected_files)
1385
1386 commands = []
1387 for f in affected_files:
1388 commands.append(input_api.Command(
1389 'Verify %s' % f,
1390 ['vpython', '-vpython-spec', f, '-vpython-tool', 'verify'],
1391 {'stderr': input_api.subprocess.STDOUT},
1392 output_api.PresubmitError))
1393
1394 return commands
Mun Yong Jang17995a92017-12-01 16:00:31 -08001395
1396
1397def CheckChangedLUCIConfigs(input_api, output_api):
1398 import collections
1399 import base64
1400 import json
Mun Yong Jangcfb9a232017-12-18 10:39:34 -08001401 import logging
Mun Yong Jang17995a92017-12-01 16:00:31 -08001402
1403 import auth
1404 import git_cl
1405
1406 LUCI_CONFIG_HOST_NAME = 'luci-config.appspot.com'
1407
1408 cl = git_cl.Changelist()
Mun Yong Jang603d01e2017-12-19 16:38:30 -08001409 if input_api.change.issue and input_api.gerrit:
1410 remote_branch = input_api.gerrit.GetDestRef(input_api.change.issue)
1411 else:
1412 remote, remote_branch = cl.GetRemoteBranch()
1413 if remote_branch.startswith('refs/remotes/%s/' % remote):
1414 remote_branch = remote_branch.replace(
1415 'refs/remotes/%s/' % remote, 'refs/heads/', 1)
1416 if remote_branch.startswith('refs/remotes/branch-heads/'):
1417 remote_branch = remote_branch.replace(
1418 'refs/remotes/branch-heads/', 'refs/branch-heads/', 1)
Mun Yong Jang17995a92017-12-01 16:00:31 -08001419
1420 remote_host_url = cl.GetRemoteUrl()
1421 if not remote_host_url:
1422 return [output_api.PresubmitError(
1423 'Remote host url for git has not been defined')]
1424 remote_host_url = remote_host_url.rstrip('/')
1425 if remote_host_url.endswith('.git'):
1426 remote_host_url = remote_host_url[:-len('.git')]
1427
1428 # authentication
1429 try:
Edward Lemur5b929a42019-10-21 17:57:39 +00001430 acc_tkn = auth.Authenticator().get_access_token()
1431 except auth.LoginRequiredError as e:
Mun Yong Jang17995a92017-12-01 16:00:31 -08001432 return [output_api.PresubmitError(
1433 'Error in authenticating user.', long_text=str(e))]
1434
1435 def request(endpoint, body=None):
1436 api_url = ('https://%s/_ah/api/config/v1/%s'
1437 % (LUCI_CONFIG_HOST_NAME, endpoint))
Edward Lemurb9830242019-10-30 22:19:20 +00001438 req = input_api.urllib_request.Request(api_url)
Mun Yong Jang17995a92017-12-01 16:00:31 -08001439 req.add_header('Authorization', 'Bearer %s' % acc_tkn.token)
1440 if body is not None:
1441 req.add_header('Content-Type', 'application/json')
1442 req.add_data(json.dumps(body))
Edward Lemurb9830242019-10-30 22:19:20 +00001443 return json.load(input_api.urllib_request.urlopen(req))
Mun Yong Jang17995a92017-12-01 16:00:31 -08001444
1445 try:
1446 config_sets = request('config-sets').get('config_sets')
Edward Lemurb9830242019-10-30 22:19:20 +00001447 except input_api.urllib_error.HTTPError as e:
Mun Yong Jang17995a92017-12-01 16:00:31 -08001448 return [output_api.PresubmitError(
1449 'Config set request to luci-config failed', long_text=str(e))]
1450 if not config_sets:
John Budoricka1512652019-08-14 17:48:43 +00001451 return [output_api.PresubmitPromptWarning('No config_sets were returned')]
Mun Yong Jang17995a92017-12-01 16:00:31 -08001452 loc_pref = '%s/+/%s/' % (remote_host_url, remote_branch)
Mun Yong Jangcfb9a232017-12-18 10:39:34 -08001453 logging.debug('Derived location prefix: %s', loc_pref)
Mun Yong Jang17995a92017-12-01 16:00:31 -08001454 dir_to_config_set = {
1455 '%s/' % cs['location'][len(loc_pref):].rstrip('/'): cs['config_set']
1456 for cs in config_sets
1457 if cs['location'].startswith(loc_pref) or
1458 ('%s/' % cs['location']) == loc_pref
1459 }
John Budoricka1512652019-08-14 17:48:43 +00001460 if not dir_to_config_set:
1461 warning_long_text_lines = [
1462 'No config_set found for %s.' % loc_pref,
1463 'Found the following:',
1464 ]
1465 for loc in sorted(cs['location'] for cs in config_sets):
1466 warning_long_text_lines.append(' %s' % loc)
1467 warning_long_text_lines.append('')
1468 warning_long_text_lines.append(
1469 'If the requested location is internal,'
1470 ' the requester may not have access.')
1471
1472 return [output_api.PresubmitPromptWarning(
1473 warning_long_text_lines[0],
1474 long_text='\n'.join(warning_long_text_lines))]
Mun Yong Jang17995a92017-12-01 16:00:31 -08001475 cs_to_files = collections.defaultdict(list)
Nodir Turakulov390028f2019-01-04 20:59:02 +00001476 for f in input_api.AffectedFiles(include_deletes=False):
Mun Yong Jang17995a92017-12-01 16:00:31 -08001477 # windows
1478 file_path = f.LocalPath().replace(_os.sep, '/')
Mun Yong Jangcfb9a232017-12-18 10:39:34 -08001479 logging.debug('Affected file path: %s', file_path)
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001480 for dr, cs in dir_to_config_set.items():
Mun Yong Jang17995a92017-12-01 16:00:31 -08001481 if dr == '/' or file_path.startswith(dr):
1482 cs_to_files[cs].append({
1483 'path': file_path[len(dr):] if dr != '/' else file_path,
1484 'content': base64.b64encode(
Edward Lemurb9830242019-10-30 22:19:20 +00001485 '\n'.join(f.NewContents()).encode('utf-8')).decode('utf-8')
Mun Yong Jang17995a92017-12-01 16:00:31 -08001486 })
1487 outputs = []
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001488 for cs, f in cs_to_files.items():
Mun Yong Jang17995a92017-12-01 16:00:31 -08001489 try:
1490 # TODO(myjang): parallelize
1491 res = request(
1492 'validate-config', body={'config_set': cs, 'files': f})
Edward Lemurb9830242019-10-30 22:19:20 +00001493 except input_api.urllib_error.HTTPError as e:
Mun Yong Jang17995a92017-12-01 16:00:31 -08001494 return [output_api.PresubmitError(
1495 'Validation request to luci-config failed', long_text=str(e))]
1496 for msg in res.get('messages', []):
1497 sev = msg['severity']
1498 if sev == 'WARNING':
1499 out_f = output_api.PresubmitPromptWarning
1500 elif sev == 'ERROR' or sev == 'CRITICAL':
1501 out_f = output_api.PresubmitError
1502 else:
1503 out_f = output_api.PresubmitNotifyResult
1504 outputs.append(out_f('Config validation: %s' % msg['text']))
1505 return outputs
Vadim Shtayura39b0b8e2019-01-31 23:56:08 +00001506
1507
1508def CheckLucicfgGenOutput(input_api, output_api, entry_script):
1509 """Verifies configs produced by `lucicfg` are up-to-date and pass validation.
1510
1511 Runs the check unconditionally, regardless of what files are modified. Examine
1512 input_api.AffectedFiles() yourself before using CheckLucicfgGenOutput if this
1513 is a concern.
1514
1515 Assumes `lucicfg` binary is in PATH and the user is logged in.
1516
1517 Args:
1518 entry_script: path to the entry-point *.star script responsible for
1519 generating a single config set. Either absolute or relative to the
1520 currently running PRESUBMIT.py script.
1521
1522 Returns:
1523 A list of input_api.Command objects containing verification commands.
1524 """
1525 return [
1526 input_api.Command(
1527 'lucicfg validate "%s"' % entry_script,
1528 [
1529 'lucicfg' if not input_api.is_windows else 'lucicfg.bat',
1530 'validate', entry_script,
1531 '-log-level', 'debug' if input_api.verbose else 'warning',
1532 ],
1533 {
1534 'stderr': input_api.subprocess.STDOUT,
1535 'shell': input_api.is_windows, # to resolve *.bat
1536 'cwd': input_api.PresubmitLocalPath(),
1537 },
1538 output_api.PresubmitError)
1539 ]
Aaron Gableaca5b6a2019-05-21 19:32:17 +00001540
1541# TODO(agable): Add this to PanProjectChecks.
1542def CheckJsonParses(input_api, output_api):
1543 """Verifies that all JSON files at least parse as valid JSON."""
1544 import json
1545 affected_files = input_api.AffectedFiles(
1546 include_deletes=False,
1547 file_filter=lambda x: x.LocalPath().endswith('.json'))
1548 warnings = []
1549 for f in affected_files:
1550 with open(f.AbsoluteLocalPath()) as j:
1551 try:
1552 json.load(j)
1553 except ValueError:
1554 # Just a warning for now, in case people are using JSON5 somewhere.
1555 warnings.append(output_api.PresubmitPromptWarning(
1556 '%s does not appear to be valid JSON.' % f.LocalPath()))
1557 return warnings