blob: e37ff882752e4201e948905dea3e652eb1b30013 [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
52
53def CheckDoNotSubmitInDescription(input_api, output_api):
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +000054 """Checks that the user didn't add 'DO NOT ''SUBMIT' to the CL description.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000055 """
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +000056 keyword = 'DO NOT ''SUBMIT'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000057 if keyword in input_api.change.DescriptionText():
58 return [output_api.PresubmitError(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +000059 keyword + ' is present in the changelist description.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000060 else:
61 return []
62
63
maruel@chromium.orgbc50eb42009-06-10 18:22:47 +000064def CheckChangeHasDescription(input_api, output_api):
65 """Checks the CL description is not empty."""
66 text = input_api.change.DescriptionText()
67 if text.strip() == '':
68 if input_api.is_committing:
pgervais@chromium.orgbc3b3b52014-06-03 00:53:48 +000069 return [output_api.PresubmitError('Add a description to the CL.')]
maruel@chromium.orgbc50eb42009-06-10 18:22:47 +000070 else:
pgervais@chromium.orgbc3b3b52014-06-03 00:53:48 +000071 return [output_api.PresubmitNotifyResult('Add a description to the CL.')]
maruel@chromium.orgbc50eb42009-06-10 18:22:47 +000072 return []
73
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +000074
75def CheckChangeWasUploaded(input_api, output_api):
76 """Checks that the issue was uploaded before committing."""
maruel@chromium.orgd587f392011-07-26 00:41:18 +000077 if input_api.is_committing and not input_api.change.issue:
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +000078 return [output_api.PresubmitError(
79 'Issue wasn\'t uploaded. Please upload first.')]
80 return []
81
82
maruel@chromium.org3410d912009-06-09 20:56:16 +000083### Content checks
84
Sergiy Byelozyorov621fe6f2018-05-25 17:38:46 -070085def CheckAuthorizedAuthor(input_api, output_api, bot_whitelist=None):
Michael Achenbachc850b962016-12-05 15:40:17 +010086 """For non-googler/chromites committers, verify the author's email address is
87 in AUTHORS.
88 """
Michael Achenbach590420d2017-10-18 12:35:37 +020089 if input_api.is_committing:
90 error_type = output_api.PresubmitError
91 else:
92 error_type = output_api.PresubmitPromptWarning
93
Michael Achenbachc850b962016-12-05 15:40:17 +010094 author = input_api.change.author_email
95 if not author:
96 input_api.logging.info('No author, skipping AUTHOR check')
97 return []
Sergiy Byelozyorov621fe6f2018-05-25 17:38:46 -070098
99 # This is used for CLs created by trusted robot accounts.
100 if bot_whitelist and author in bot_whitelist:
101 return []
102
Michael Achenbachc850b962016-12-05 15:40:17 +0100103 authors_path = input_api.os_path.join(
104 input_api.PresubmitLocalPath(), 'AUTHORS')
105 valid_authors = (
106 input_api.re.match(r'[^#]+\s+\<(.+?)\>\s*$', line)
107 for line in open(authors_path))
108 valid_authors = [item.group(1).lower() for item in valid_authors if item]
109 if not any(input_api.fnmatch.fnmatch(author.lower(), valid)
110 for valid in valid_authors):
111 input_api.logging.info('Valid authors are %s', ', '.join(valid_authors))
Michael Achenbach590420d2017-10-18 12:35:37 +0200112 return [error_type(
Michael Achenbachc850b962016-12-05 15:40:17 +0100113 ('%s is not in AUTHORS file. If you are a new contributor, please visit'
114 '\n'
Xiaoyin Liub5807972017-10-04 22:07:24 -0400115 'https://www.chromium.org/developers/contributing-code and read the '
Michael Achenbachc850b962016-12-05 15:40:17 +0100116 '"Legal" section\n'
117 'If you are a chromite, verify the contributor signed the CLA.') %
118 author)]
119 return []
120
121
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000122def CheckDoNotSubmitInFiles(input_api, output_api):
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +0000123 """Checks that the user didn't add 'DO NOT ''SUBMIT' to any files."""
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000124 # We want to check every text file, not just source files.
125 file_filter = lambda x : x
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +0000126 keyword = 'DO NOT ''SUBMIT'
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000127 errors = _FindNewViolationsOfRule(lambda _, line : keyword not in line,
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000128 input_api, file_filter)
129 text = '\n'.join('Found %s in %s' % (keyword, loc) for loc in errors)
130 if text:
131 return [output_api.PresubmitError(text)]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000132 return []
133
134
tfarina@chromium.orgb6795642014-12-12 00:03:49 +0000135def CheckChangeLintsClean(input_api, output_api, source_file_filter=None,
tfarina@chromium.orga62208f2015-02-25 03:23:11 +0000136 lint_filters=None, verbose_level=None):
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000137 """Checks that all '.cc' and '.h' files pass cpplint.py."""
erg@google.com26970fa2009-11-17 18:07:32 +0000138 _RE_IS_TEST = input_api.re.compile(r'.*tests?.(cc|h)$')
139 result = []
140
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000141 cpplint = input_api.cpplint
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000142 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800143 # pylint: disable=protected-access
erg@google.com26970fa2009-11-17 18:07:32 +0000144 cpplint._cpplint_state.ResetErrorCounts()
145
tfarina@chromium.orgb6795642014-12-12 00:03:49 +0000146 lint_filters = lint_filters or DEFAULT_LINT_FILTERS
danakj@chromium.org0ae71222016-01-11 19:37:11 +0000147 lint_filters.extend(BLACKLIST_LINT_FILTERS)
tfarina@chromium.orgb6795642014-12-12 00:03:49 +0000148 cpplint._SetFilters(','.join(lint_filters))
erg@google.com26970fa2009-11-17 18:07:32 +0000149
150 # We currently are more strict with normal code than unit tests; 4 and 5 are
151 # the verbosity level that would normally be passed to cpplint.py through
152 # --verbose=#. Hopefully, in the future, we can be more verbose.
153 files = [f.AbsoluteLocalPath() for f in
154 input_api.AffectedSourceFiles(source_file_filter)]
155 for file_name in files:
156 if _RE_IS_TEST.match(file_name):
157 level = 5
158 else:
159 level = 4
160
tfarina@chromium.orga62208f2015-02-25 03:23:11 +0000161 verbose_level = verbose_level or level
162 cpplint.ProcessFile(file_name, verbose_level)
erg@google.com26970fa2009-11-17 18:07:32 +0000163
164 if cpplint._cpplint_state.error_count > 0:
165 if input_api.is_committing:
166 res_type = output_api.PresubmitError
167 else:
168 res_type = output_api.PresubmitPromptWarning
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000169 result = [res_type('Changelist failed cpplint.py check.')]
erg@google.com26970fa2009-11-17 18:07:32 +0000170
171 return result
172
173
maruel@chromium.org3410d912009-06-09 20:56:16 +0000174def CheckChangeHasNoCR(input_api, output_api, source_file_filter=None):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000175 """Checks no '\r' (CR) character is in any source files."""
176 cr_files = []
maruel@chromium.org3410d912009-06-09 20:56:16 +0000177 for f in input_api.AffectedSourceFiles(source_file_filter):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000178 if '\r' in input_api.ReadFile(f, 'rb'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000179 cr_files.append(f.LocalPath())
180 if cr_files:
181 return [output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000182 'Found a CR character in these files:', items=cr_files)]
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000183 return []
184
185
186def CheckChangeHasOnlyOneEol(input_api, output_api, source_file_filter=None):
187 """Checks the files ends with one and only one \n (LF)."""
188 eof_files = []
189 for f in input_api.AffectedSourceFiles(source_file_filter):
190 contents = input_api.ReadFile(f, 'rb')
191 # Check that the file ends in one and only one newline character.
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000192 if len(contents) > 1 and (contents[-1:] != '\n' or contents[-2:-1] == '\n'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000193 eof_files.append(f.LocalPath())
194
195 if eof_files:
196 return [output_api.PresubmitPromptWarning(
197 'These files should end in one (and only one) newline character:',
198 items=eof_files)]
199 return []
200
201
202def CheckChangeHasNoCrAndHasOnlyOneEol(input_api, output_api,
203 source_file_filter=None):
204 """Runs both CheckChangeHasNoCR and CheckChangeHasOnlyOneEOL in one pass.
205
206 It is faster because it is reading the file only once.
207 """
208 cr_files = []
209 eof_files = []
210 for f in input_api.AffectedSourceFiles(source_file_filter):
211 contents = input_api.ReadFile(f, 'rb')
212 if '\r' in contents:
213 cr_files.append(f.LocalPath())
214 # Check that the file ends in one and only one newline character.
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000215 if len(contents) > 1 and (contents[-1:] != '\n' or contents[-2:-1] == '\n'):
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000216 eof_files.append(f.LocalPath())
217 outputs = []
218 if cr_files:
219 outputs.append(output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000220 'Found a CR character in these files:', items=cr_files))
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000221 if eof_files:
222 outputs.append(output_api.PresubmitPromptWarning(
223 'These files should end in one (and only one) newline character:',
224 items=eof_files))
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000225 return outputs
226
seanmccullough0b670442016-06-07 10:45:58 -0700227def CheckGenderNeutral(input_api, output_api, source_file_filter=None):
228 """Checks that there are no gendered pronouns in any of the text files to be
229 submitted.
230 """
231 gendered_re = input_api.re.compile(
232 '(^|\s|\(|\[)([Hh]e|[Hh]is|[Hh]ers?|[Hh]im|[Ss]he|[Gg]uys?)\\b')
233
234 errors = []
235 for f in input_api.AffectedFiles(include_deletes=False,
236 file_filter=source_file_filter):
237 for line_num, line in f.ChangedContents():
238 if gendered_re.search(line):
239 errors.append('%s (%d): %s' % (f.LocalPath(), line_num, line))
240
241 if len(errors):
242 return [output_api.PresubmitPromptWarning('Found a gendered pronoun in:',
243 long_text='\n'.join(errors))]
244 return []
245
246
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000247
chrisha@google.com267d6592012-06-19 19:23:31 +0000248def _ReportErrorFileAndLine(filename, line_num, dummy_line):
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000249 """Default error formatter for _FindNewViolationsOfRule."""
danakj@chromium.orgc5965ba2013-08-14 00:27:24 +0000250 return '%s:%s' % (filename, line_num)
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000251
252
David 'Digit' Turner31a48642019-01-11 16:10:59 +0000253def _GenerateAffectedFileExtList(input_api, source_file_filter):
254 """Generate a list of (file, extension) tuples from affected files.
255
256 The result can be fed to _FindNewViolationsOfRule() directly, or
257 could be filtered before doing that.
258
259 Args:
260 input_api: object to enumerate the affected files.
261 source_file_filter: a filter to be passed to the input api.
262 Yields:
263 A list of (file, extension) tuples, where |file| is an affected
264 file, and |extension| its file path extension.
265 """
266 for f in input_api.AffectedFiles(
267 include_deletes=False, file_filter=source_file_filter):
268 extension = str(f.LocalPath()).rsplit('.', 1)[-1]
269 yield (f, extension)
270
271
272def _FindNewViolationsOfRuleForList(callable_rule,
273 file_ext_list,
274 error_formatter=_ReportErrorFileAndLine):
275 """Find all newly introduced violations of a per-line rule (a callable).
276
277 Prefer calling _FindNewViolationsOfRule() instead of this function, unless
278 the list of affected files need to be filtered in a special way.
279
280 Arguments:
281 callable_rule: a callable taking a file extension and line of input and
282 returning True if the rule is satisfied and False if there was a problem.
283 file_ext_list: a list of input (file, extension) tuples, as returned by
284 _GenerateAffectedFileExtList().
285 error_formatter: a callable taking (filename, line_number, line) and
286 returning a formatted error string.
287
288 Returns:
289 A list of the newly-introduced violations reported by the rule.
290 """
291 errors = []
292 for f, extension in file_ext_list:
293 # For speed, we do two passes, checking first the full file. Shelling out
294 # to the SCM to determine the changed region can be quite expensive on
295 # Win32. Assuming that most files will be kept problem-free, we can
296 # skip the SCM operations most of the time.
297 if all(callable_rule(extension, line) for line in f.NewContents()):
298 continue # No violation found in full text: can skip considering diff.
299
300 for line_num, line in f.ChangedContents():
301 if not callable_rule(extension, line):
302 errors.append(error_formatter(f.LocalPath(), line_num, line))
303
304 return errors
305
306
307def _FindNewViolationsOfRule(callable_rule,
308 input_api,
309 source_file_filter=None,
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000310 error_formatter=_ReportErrorFileAndLine):
311 """Find all newly introduced violations of a per-line rule (a callable).
312
313 Arguments:
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000314 callable_rule: a callable taking a file extension and line of input and
315 returning True if the rule is satisfied and False if there was a problem.
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000316 input_api: object to enumerate the affected files.
317 source_file_filter: a filter to be passed to the input api.
318 error_formatter: a callable taking (filename, line_number, line) and
319 returning a formatted error string.
320
321 Returns:
322 A list of the newly-introduced violations reported by the rule.
323 """
David 'Digit' Turner31a48642019-01-11 16:10:59 +0000324 return _FindNewViolationsOfRuleForList(
325 callable_rule, _GenerateAffectedFileExtList(
326 input_api, source_file_filter), error_formatter)
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000327
328
maruel@chromium.org3410d912009-06-09 20:56:16 +0000329def CheckChangeHasNoTabs(input_api, output_api, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000330 """Checks that there are no tab characters in any of the text files to be
331 submitted.
332 """
maruel@chromium.org115ae6c2010-06-18 17:11:43 +0000333 # In addition to the filter, make sure that makefiles are blacklisted.
334 if not source_file_filter:
335 # It's the default filter.
336 source_file_filter = input_api.FilterSourceFile
337 def filter_more(affected_file):
cmp@chromium.orgcb5e57c2012-04-06 19:50:15 +0000338 basename = input_api.os_path.basename(affected_file.LocalPath())
339 return (not (basename in ('Makefile', 'makefile') or
340 basename.endswith('.mk')) and
maruel@chromium.org115ae6c2010-06-18 17:11:43 +0000341 source_file_filter(affected_file))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000342
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000343 tabs = _FindNewViolationsOfRule(lambda _, line : '\t' not in line,
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000344 input_api, filter_more)
345
maruel@chromium.orge9b71c92009-06-10 18:10:01 +0000346 if tabs:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000347 return [output_api.PresubmitPromptWarning('Found a tab character in:',
348 long_text='\n'.join(tabs))]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000349 return []
350
351
estade@chromium.orgfdcc9f72011-02-07 22:25:07 +0000352def CheckChangeTodoHasOwner(input_api, output_api, source_file_filter=None):
353 """Checks that the user didn't add TODO(name) without an owner."""
354
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +0000355 unowned_todo = input_api.re.compile('TO''DO[^(]')
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000356 errors = _FindNewViolationsOfRule(lambda _, x : not unowned_todo.search(x),
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000357 input_api, source_file_filter)
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +0000358 errors = ['Found TO''DO with no owner in ' + x for x in errors]
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000359 if errors:
360 return [output_api.PresubmitPromptWarning('\n'.join(errors))]
estade@chromium.orgfdcc9f72011-02-07 22:25:07 +0000361 return []
362
363
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000364def CheckChangeHasNoStrayWhitespace(input_api, output_api,
365 source_file_filter=None):
366 """Checks that there is no stray whitespace at source lines end."""
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000367 errors = _FindNewViolationsOfRule(lambda _, line : line.rstrip() == line,
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000368 input_api, source_file_filter)
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000369 if errors:
370 return [output_api.PresubmitPromptWarning(
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000371 'Found line ending with white spaces in:',
372 long_text='\n'.join(errors))]
maruel@chromium.orgf5888bb2009-06-10 20:26:37 +0000373 return []
374
375
jamesr@chromium.orgaf27f462013-04-04 21:44:22 +0000376def CheckLongLines(input_api, output_api, maxlen, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000377 """Checks that there aren't any lines longer than maxlen characters in any of
378 the text files to be submitted.
379 """
dpranke@chromium.orge3b1c3d2012-10-20 22:28:14 +0000380 maxlens = {
381 'java': 100,
torne@chromium.org0ecad9c2013-02-15 16:35:16 +0000382 # This is specifically for Android's handwritten makefiles (Android.mk).
383 'mk': 200,
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000384 '': maxlen,
385 }
maruel@chromium.orgeba64622011-06-20 18:26:48 +0000386
erikchen@google.com12816082013-12-03 02:04:20 +0000387 # Language specific exceptions to max line length.
388 # '.h' is considered an obj-c file extension, since OBJC_EXCEPTIONS are a
389 # superset of CPP_EXCEPTIONS.
390 CPP_FILE_EXTS = ('c', 'cc')
391 CPP_EXCEPTIONS = ('#define', '#endif', '#if', '#include', '#pragma')
Dave Schuyler6eea7c22017-03-03 18:46:25 -0800392 HTML_FILE_EXTS = ('html',)
Dave Schuyler9b9b94c2017-04-13 15:28:41 -0700393 HTML_EXCEPTIONS = ('<g ', '<link ', '<path ',)
erikchen@google.com12816082013-12-03 02:04:20 +0000394 JAVA_FILE_EXTS = ('java',)
395 JAVA_EXCEPTIONS = ('import ', 'package ')
dbeam@chromium.org5a2af6d2015-10-08 17:52:29 +0000396 JS_FILE_EXTS = ('js',)
397 JS_EXCEPTIONS = ("GEN('#include",)
erikchen@google.com12816082013-12-03 02:04:20 +0000398 OBJC_FILE_EXTS = ('h', 'm', 'mm')
399 OBJC_EXCEPTIONS = ('#define', '#endif', '#if', '#import', '#include',
400 '#pragma')
dbeam@chromium.org5a2af6d2015-10-08 17:52:29 +0000401 PY_FILE_EXTS = ('py',)
aiolos@chromium.org84033cc2015-07-16 01:27:15 +0000402 PY_EXCEPTIONS = ('import', 'from')
erikchen@google.com12816082013-12-03 02:04:20 +0000403
404 LANGUAGE_EXCEPTIONS = [
405 (CPP_FILE_EXTS, CPP_EXCEPTIONS),
Dave Schuyler6eea7c22017-03-03 18:46:25 -0800406 (HTML_FILE_EXTS, HTML_EXCEPTIONS),
erikchen@google.com12816082013-12-03 02:04:20 +0000407 (JAVA_FILE_EXTS, JAVA_EXCEPTIONS),
dbeam@chromium.org5a2af6d2015-10-08 17:52:29 +0000408 (JS_FILE_EXTS, JS_EXCEPTIONS),
erikchen@google.com12816082013-12-03 02:04:20 +0000409 (OBJC_FILE_EXTS, OBJC_EXCEPTIONS),
aiolos@chromium.org84033cc2015-07-16 01:27:15 +0000410 (PY_FILE_EXTS, PY_EXCEPTIONS),
erikchen@google.com12816082013-12-03 02:04:20 +0000411 ]
sivachandra@chromium.org270db5c2012-10-09 22:38:44 +0000412
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000413 def no_long_lines(file_extension, line):
erikchen@google.com12816082013-12-03 02:04:20 +0000414 # Check for language specific exceptions.
Dave Schuyler9b9b94c2017-04-13 15:28:41 -0700415 if any(file_extension in exts and line.lstrip().startswith(exceptions)
erikchen@google.com12816082013-12-03 02:04:20 +0000416 for exts, exceptions in LANGUAGE_EXCEPTIONS):
sivachandra@chromium.org270db5c2012-10-09 22:38:44 +0000417 return True
418
bulach@chromium.orgbfffd452012-02-22 01:13:29 +0000419 file_maxlen = maxlens.get(file_extension, maxlens[''])
420 # Stupidly long symbols that needs to be worked around if takes 66% of line.
421 long_symbol = file_maxlen * 2 / 3
422 # Hard line length limit at 50% more.
423 extra_maxlen = file_maxlen * 3 / 2
424
425 line_len = len(line)
426 if line_len <= file_maxlen:
maruel@chromium.orgeba64622011-06-20 18:26:48 +0000427 return True
428
dtu@chromium.org03f0c532015-03-27 22:22:07 +0000429 # Allow long URLs of any length.
treib@chromium.orgde3ee902014-07-28 14:23:11 +0000430 if any((url in line) for url in ('file://', 'http://', 'https://')):
431 return True
432
dtu@chromium.org03f0c532015-03-27 22:22:07 +0000433 if line_len > extra_maxlen:
434 return False
435
treib@chromium.orgde3ee902014-07-28 14:23:11 +0000436 if 'url(' in line and file_extension == 'css':
437 return True
438
dbeam@chromium.orgb5ccc9b2014-09-23 00:42:22 +0000439 if '<include' in line and file_extension in ('css', 'html', 'js'):
440 return True
441
treib@chromium.orgde3ee902014-07-28 14:23:11 +0000442 return input_api.re.match(
443 r'.*[A-Za-z][A-Za-z_0-9]{%d,}.*' % long_symbol, line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000444
David 'Digit' Turner31a48642019-01-11 16:10:59 +0000445 def is_global_pylint_directive(line, pos):
446 """True iff the pylint directive starting at line[pos] is global."""
447 # Any character before |pos| that is not whitespace or '#' indidcates
448 # this is a local directive.
449 return not any([c not in " \t#" for c in line[:pos]])
450
451 def check_python_long_lines(affected_files, error_formatter):
452 errors = []
453 global_check_enabled = True
454
455 for f in affected_files:
456 file_path = f.LocalPath()
457 for idx, line in enumerate(f.NewContents()):
458 line_num = idx + 1
459 line_is_short = no_long_lines(PY_FILE_EXTS[0], line)
460
461 pos = line.find('pylint: disable=line-too-long')
462 if pos >= 0:
463 if is_global_pylint_directive(line, pos):
464 global_check_enabled = False # Global disable
465 else:
466 continue # Local disable.
467
468 do_check = global_check_enabled
469
470 pos = line.find('pylint: enable=line-too-long')
471 if pos >= 0:
472 if is_global_pylint_directive(line, pos):
473 global_check_enabled = True # Global enable
474 do_check = True # Ensure it applies to current line as well.
475 else:
476 do_check = True # Local enable
477
478 if do_check and not line_is_short:
479 errors.append(error_formatter(file_path, line_num, line))
480
481 return errors
482
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000483 def format_error(filename, line_num, line):
484 return '%s, line %s, %s chars' % (filename, line_num, len(line))
485
David 'Digit' Turner31a48642019-01-11 16:10:59 +0000486 file_ext_list = list(
487 _GenerateAffectedFileExtList(input_api, source_file_filter))
488
489 errors = []
490
491 # For non-Python files, a simple line-based rule check is enough.
492 non_py_file_ext_list = [x for x in file_ext_list if x[1] not in PY_FILE_EXTS]
493 if non_py_file_ext_list:
494 errors += _FindNewViolationsOfRuleForList(
495 no_long_lines, non_py_file_ext_list, error_formatter=format_error)
496
497 # However, Python files need more sophisticated checks that need parsing
498 # the whole source file.
499 py_file_list = [x[0] for x in file_ext_list if x[1] in PY_FILE_EXTS]
500 if py_file_list:
501 errors += check_python_long_lines(
502 py_file_list, error_formatter=format_error)
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000503 if errors:
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000504 msg = 'Found lines longer than %s characters (first 5 shown).' % maxlen
nick@chromium.orge06cb4e2011-04-23 01:20:38 +0000505 return [output_api.PresubmitPromptWarning(msg, items=errors[:5])]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000506 else:
507 return []
508
509
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000510def CheckLicense(input_api, output_api, license_re, source_file_filter=None,
maruel@chromium.org71626852010-11-03 13:14:25 +0000511 accept_empty_files=True):
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000512 """Verifies the license header.
513 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000514 license_re = input_api.re.compile(license_re, input_api.re.MULTILINE)
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000515 bad_files = []
516 for f in input_api.AffectedSourceFiles(source_file_filter):
517 contents = input_api.ReadFile(f, 'rb')
maruel@chromium.org71626852010-11-03 13:14:25 +0000518 if accept_empty_files and not contents:
519 continue
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000520 if not license_re.search(contents):
521 bad_files.append(f.LocalPath())
522 if bad_files:
phajdan.jr@chromium.orge27eb7e2015-11-16 12:47:53 +0000523 return [output_api.PresubmitPromptWarning(
bradnelson@google.com393460b2011-03-29 01:20:26 +0000524 'License must match:\n%s\n' % license_re.pattern +
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000525 'Found a bad license header in these files:', items=bad_files)]
maruel@chromium.orgb9e7ada2010-01-27 23:12:39 +0000526 return []
527
528
maruel@chromium.org3410d912009-06-09 20:56:16 +0000529### Other checks
530
531def CheckDoNotSubmit(input_api, output_api):
532 return (
533 CheckDoNotSubmitInDescription(input_api, output_api) +
534 CheckDoNotSubmitInFiles(input_api, output_api)
535 )
536
537
bradnelson@google.comc0b332a2010-08-26 00:30:37 +0000538def CheckTreeIsOpen(input_api, output_api,
539 url=None, closed=None, json_url=None):
540 """Check whether to allow commit without prompt.
541
542 Supports two styles:
543 1. Checks that an url's content doesn't match a regexp that would mean that
544 the tree is closed. (old)
545 2. Check the json_url to decide whether to allow commit without prompt.
546 Args:
547 input_api: input related apis.
548 output_api: output related apis.
549 url: url to use for regex based tree status.
550 closed: regex to match for closed status.
551 json_url: url to download json style status.
552 """
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000553 if not input_api.is_committing:
554 return []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000555 try:
bradnelson@google.comc0b332a2010-08-26 00:30:37 +0000556 if json_url:
557 connection = input_api.urllib2.urlopen(json_url)
558 status = input_api.json.loads(connection.read())
559 connection.close()
560 if not status['can_commit_freely']:
561 short_text = 'Tree state is: ' + status['general_state']
562 long_text = status['message'] + '\n' + json_url
563 return [output_api.PresubmitError(short_text, long_text=long_text)]
564 else:
565 # TODO(bradnelson): drop this once all users are gone.
566 connection = input_api.urllib2.urlopen(url)
567 status = connection.read()
568 connection.close()
569 if input_api.re.match(closed, status):
570 long_text = status + '\n' + url
571 return [output_api.PresubmitError('The tree is closed.',
572 long_text=long_text)]
rohitrao@chromium.orgd490ee82012-02-06 19:31:33 +0000573 except IOError as e:
574 return [output_api.PresubmitError('Error fetching tree status.',
575 long_text=str(e))]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000576 return []
maruel@chromium.org7b305e82009-05-19 18:24:20 +0000577
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000578def GetUnitTestsInDirectory(
smut@google.comac296202014-04-24 21:47:17 +0000579 input_api, output_api, directory, whitelist=None, blacklist=None, env=None):
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000580 """Lists all files in a directory and runs them. Doesn't recurse.
581
nick@chromium.orgff526192013-06-10 19:30:26 +0000582 It's mainly a wrapper for RunUnitTests. Use whitelist and blacklist to filter
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000583 tests accordingly.
584 """
585 unit_tests = []
586 test_path = input_api.os_path.abspath(
587 input_api.os_path.join(input_api.PresubmitLocalPath(), directory))
588
589 def check(filename, filters):
590 return any(True for i in filters if input_api.re.match(i, filename))
591
maruel@chromium.orgfae707b2011-09-15 18:57:58 +0000592 to_run = found = 0
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000593 for filename in input_api.os_listdir(test_path):
maruel@chromium.orgfae707b2011-09-15 18:57:58 +0000594 found += 1
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000595 fullpath = input_api.os_path.join(test_path, filename)
596 if not input_api.os_path.isfile(fullpath):
597 continue
598 if whitelist and not check(filename, whitelist):
599 continue
600 if blacklist and check(filename, blacklist):
601 continue
602 unit_tests.append(input_api.os_path.join(directory, filename))
maruel@chromium.orgfae707b2011-09-15 18:57:58 +0000603 to_run += 1
anatoly techtonikbdcdc592017-03-29 17:00:13 +0300604 input_api.logging.debug('Found %d files, running %d unit tests'
605 % (found, to_run))
maruel@chromium.orgfae707b2011-09-15 18:57:58 +0000606 if not to_run:
607 return [
608 output_api.PresubmitPromptWarning(
609 'Out of %d files, found none that matched w=%r, b=%r in directory %s'
610 % (found, whitelist, blacklist, directory))
611 ]
smut@google.comac296202014-04-24 21:47:17 +0000612 return GetUnitTests(input_api, output_api, unit_tests, env)
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000613
614
smut@google.comac296202014-04-24 21:47:17 +0000615def GetUnitTests(input_api, output_api, unit_tests, env=None):
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000616 """Runs all unit tests in a directory.
617
618 On Windows, sys.executable is used for unit tests ending with ".py".
619 """
620 # We don't want to hinder users from uploading incomplete patches.
621 if input_api.is_committing:
622 message_type = output_api.PresubmitError
623 else:
624 message_type = output_api.PresubmitPromptWarning
625
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000626 results = []
627 for unit_test in unit_tests:
Robert Iannucci50258932018-03-19 10:30:59 -0700628 cmd = [unit_test]
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000629 if input_api.verbose:
maruel@chromium.org6c7723e2011-04-12 19:04:55 +0000630 cmd.append('--verbose')
smut@google.comac296202014-04-24 21:47:17 +0000631 kwargs = {'cwd': input_api.PresubmitLocalPath()}
632 if env:
633 kwargs['env'] = env
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000634 results.append(input_api.Command(
635 name=unit_test,
636 cmd=cmd,
smut@google.comac296202014-04-24 21:47:17 +0000637 kwargs=kwargs,
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000638 message=message_type))
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000639 return results
640
nick@chromium.orgff526192013-06-10 19:30:26 +0000641
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000642def GetUnitTestsRecursively(input_api, output_api, directory,
643 whitelist, blacklist):
644 """Gets all files in the directory tree (git repo) that match the whitelist.
645
646 Restricts itself to only find files within the Change's source repo, not
647 dependencies.
648 """
649 def check(filename):
650 return (any(input_api.re.match(f, filename) for f in whitelist) and
651 not any(input_api.re.match(f, filename) for f in blacklist))
652
653 tests = []
654
655 to_run = found = 0
656 for filepath in input_api.change.AllFiles(directory):
657 found += 1
658 if check(filepath):
659 to_run += 1
660 tests.append(filepath)
661 input_api.logging.debug('Found %d files, running %d' % (found, to_run))
662 if not to_run:
663 return [
664 output_api.PresubmitPromptWarning(
665 'Out of %d files, found none that matched w=%r, b=%r in directory %s'
666 % (found, whitelist, blacklist, directory))
667 ]
668
669 return GetUnitTests(input_api, output_api, tests)
670
671
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000672def GetPythonUnitTests(input_api, output_api, unit_tests):
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000673 """Run the unit tests out of process, capture the output and use the result
674 code to determine success.
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000675
676 DEPRECATED.
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000677 """
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000678 # We don't want to hinder users from uploading incomplete patches.
679 if input_api.is_committing:
680 message_type = output_api.PresubmitError
681 else:
682 message_type = output_api.PresubmitNotifyResult
maruel@chromium.org0e766052011-04-06 13:32:51 +0000683 results = []
maruel@chromium.org7b305e82009-05-19 18:24:20 +0000684 for unit_test in unit_tests:
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000685 # Run the unit tests out of process. This is because some unit tests
686 # stub out base libraries and don't clean up their mess. It's too easy to
687 # get subtle bugs.
688 cwd = None
689 env = None
690 unit_test_name = unit_test
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000691 # 'python -m test.unit_test' doesn't work. We need to change to the right
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000692 # directory instead.
693 if '.' in unit_test:
694 # Tests imported in submodules (subdirectories) assume that the current
695 # directory is in the PYTHONPATH. Manually fix that.
696 unit_test = unit_test.replace('.', '/')
697 cwd = input_api.os_path.dirname(unit_test)
698 unit_test = input_api.os_path.basename(unit_test)
699 env = input_api.environ.copy()
kbr@google.comab318592009-09-04 00:54:55 +0000700 # At least on Windows, it seems '.' must explicitly be in PYTHONPATH
701 backpath = [
702 '.', input_api.os_path.pathsep.join(['..'] * (cwd.count('/') + 1))
703 ]
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000704 if env.get('PYTHONPATH'):
705 backpath.append(env.get('PYTHONPATH'))
ukai@chromium.orga301f1f2009-08-05 10:37:33 +0000706 env['PYTHONPATH'] = input_api.os_path.pathsep.join((backpath))
Robert Iannucci23bd7352018-03-23 11:51:40 -0700707 env.pop('VPYTHON_CLEAR_PYTHONPATH', None)
maruel@chromium.org0e766052011-04-06 13:32:51 +0000708 cmd = [input_api.python_executable, '-m', '%s' % unit_test]
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000709 results.append(input_api.Command(
710 name=unit_test_name,
711 cmd=cmd,
712 kwargs={'env': env, 'cwd': cwd},
713 message=message_type))
maruel@chromium.org0e766052011-04-06 13:32:51 +0000714 return results
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000715
716
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000717def RunUnitTestsInDirectory(input_api, *args, **kwargs):
718 """Run tests in a directory serially.
719
720 For better performance, use GetUnitTestsInDirectory and then
721 pass to input_api.RunTests.
722 """
723 return input_api.RunTests(
724 GetUnitTestsInDirectory(input_api, *args, **kwargs), False)
725
726
727def RunUnitTests(input_api, *args, **kwargs):
728 """Run tests serially.
729
730 For better performance, use GetUnitTests and then pass to
731 input_api.RunTests.
732 """
733 return input_api.RunTests(GetUnitTests(input_api, *args, **kwargs), False)
734
735
736def RunPythonUnitTests(input_api, *args, **kwargs):
737 """Run python tests in a directory serially.
738
739 DEPRECATED
740 """
741 return input_api.RunTests(
742 GetPythonUnitTests(input_api, *args, **kwargs), False)
743
744
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000745def _FetchAllFiles(input_api, white_list, black_list):
746 """Hack to fetch all files."""
747 # We cannot use AffectedFiles here because we want to test every python
748 # file on each single python change. It's because a change in a python file
749 # can break another unmodified file.
750 # Use code similar to InputApi.FilterSourceFile()
751 def Find(filepath, filters):
anatoly techtonikbdcdc592017-03-29 17:00:13 +0300752 if input_api.platform == 'win32':
753 filepath = filepath.replace('\\', '/')
754
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000755 for item in filters:
756 if input_api.re.match(item, filepath):
757 return True
758 return False
759
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000760 files = []
761 path_len = len(input_api.PresubmitLocalPath())
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000762 for dirpath, dirnames, filenames in input_api.os_walk(
763 input_api.PresubmitLocalPath()):
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000764 # Passes dirnames in black list to speed up search.
765 for item in dirnames[:]:
766 filepath = input_api.os_path.join(dirpath, item)[path_len + 1:]
767 if Find(filepath, black_list):
768 dirnames.remove(item)
769 for item in filenames:
770 filepath = input_api.os_path.join(dirpath, item)[path_len + 1:]
771 if Find(filepath, white_list) and not Find(filepath, black_list):
772 files.append(filepath)
773 return files
774
775
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000776def GetPylint(input_api, output_api, white_list=None, black_list=None,
dtu@chromium.orgf23524d2016-01-27 20:08:49 +0000777 disabled_warnings=None, extra_paths_list=None, pylintrc=None):
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000778 """Run pylint on python files.
779
chrisha@google.com267d6592012-06-19 19:23:31 +0000780 The default white_list enforces looking only at *.py files.
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000781 """
maruel@chromium.org69eaecb2011-06-14 13:09:13 +0000782 white_list = tuple(white_list or ('.*\.py$',))
783 black_list = tuple(black_list or input_api.DEFAULT_BLACK_LIST)
ilevy@chromium.org7b677f72013-01-07 18:49:26 +0000784 extra_paths_list = extra_paths_list or []
785
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000786 if input_api.is_committing:
787 error_type = output_api.PresubmitError
788 else:
789 error_type = output_api.PresubmitPromptWarning
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000790
791 # Only trigger if there is at least one python file affected.
ilevy@chromium.org36576332013-01-08 03:16:15 +0000792 def rel_path(regex):
793 """Modifies a regex for a subject to accept paths relative to root."""
chrisha@chromium.org40e5d3d2013-01-18 17:46:57 +0000794 def samefile(a, b):
795 # Default implementation for platforms lacking os.path.samefile
796 # (like Windows).
797 return input_api.os_path.abspath(a) == input_api.os_path.abspath(b)
798 samefile = getattr(input_api.os_path, 'samefile', samefile)
799 if samefile(input_api.PresubmitLocalPath(),
800 input_api.change.RepositoryRoot()):
ilevy@chromium.orgdd4e9312013-01-15 03:22:04 +0000801 return regex
chrisha@chromium.org40e5d3d2013-01-18 17:46:57 +0000802
ilevy@chromium.org36576332013-01-08 03:16:15 +0000803 prefix = input_api.os_path.join(input_api.os_path.relpath(
804 input_api.PresubmitLocalPath(), input_api.change.RepositoryRoot()), '')
805 return input_api.re.escape(prefix) + regex
ilevy@chromium.org7b677f72013-01-07 18:49:26 +0000806 src_filter = lambda x: input_api.FilterSourceFile(
807 x, map(rel_path, white_list), map(rel_path, black_list))
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000808 if not input_api.AffectedSourceFiles(src_filter):
ilevy@chromium.org7b677f72013-01-07 18:49:26 +0000809 input_api.logging.info('Skipping pylint: no matching changes.')
maruel@chromium.orgbf38a7e2010-12-14 18:15:54 +0000810 return []
811
agable@chromium.org327d72b2015-04-21 20:22:50 +0000812 if pylintrc is not None:
813 pylintrc = input_api.os_path.join(input_api.PresubmitLocalPath(), pylintrc)
814 else:
815 pylintrc = input_api.os_path.join(_HERE, 'pylintrc')
dtu@chromium.orgf23524d2016-01-27 20:08:49 +0000816 extra_args = ['--rcfile=%s' % pylintrc]
maruel@chromium.orgff9a2172012-04-24 16:55:32 +0000817 if disabled_warnings:
818 extra_args.extend(['-d', ','.join(disabled_warnings)])
819
chrisha@google.com267d6592012-06-19 19:23:31 +0000820 files = _FetchAllFiles(input_api, white_list, black_list)
821 if not files:
maruel@chromium.orgfca53392010-12-21 18:42:57 +0000822 return []
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000823 files.sort()
chrisha@google.com267d6592012-06-19 19:23:31 +0000824
csharp@chromium.org40395342013-02-21 14:57:23 +0000825 input_api.logging.info('Running pylint on %d files', len(files))
826 input_api.logging.debug('Running pylint on: %s', files)
chrisha@google.com267d6592012-06-19 19:23:31 +0000827 env = input_api.environ.copy()
ilevy@chromium.org7b677f72013-01-07 18:49:26 +0000828 env['PYTHONPATH'] = input_api.os_path.pathsep.join(
Robert Iannucci82a64802018-03-23 16:40:16 -0700829 extra_paths_list).encode('utf8')
Robert Iannucci23bd7352018-03-23 11:51:40 -0700830 env.pop('VPYTHON_CLEAR_PYTHONPATH', None)
Robert Iannucci82a64802018-03-23 16:40:16 -0700831 input_api.logging.debug(' with extra PYTHONPATH: %r', extra_paths_list)
chrisha@google.com267d6592012-06-19 19:23:31 +0000832
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000833 def GetPylintCmd(flist, extra, parallel):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000834 # Windows needs help running python files so we explicitly specify
835 # the interpreter to use. It also has limitations on the size of
836 # the command-line, so we pass arguments via a pipe.
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000837 cmd = [input_api.python_executable,
838 input_api.os_path.join(_HERE, 'third_party', 'pylint.py'),
839 '--args-on-stdin']
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000840 if len(flist) == 1:
841 description = flist[0]
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000842 else:
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000843 description = '%s files' % len(flist)
chrisha@chromium.org1b129e52012-06-22 17:08:11 +0000844
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000845 args = extra_args[:]
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000846 if extra:
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000847 args.extend(extra)
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000848 description += ' using %s' % (extra,)
849 if parallel:
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000850 args.append('--jobs=%s' % input_api.cpu_count)
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000851 description += ' on %d cores' % input_api.cpu_count
852
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000853 return input_api.Command(
854 name='Pylint (%s)' % description,
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000855 cmd=cmd,
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000856 kwargs={'env': env, 'stdin': '\n'.join(args + flist)},
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000857 message=error_type)
chrisha@chromium.org1b129e52012-06-22 17:08:11 +0000858
sbc@chromium.org7fc75ca2012-09-21 20:14:21 +0000859 # Always run pylint and pass it all the py files at once.
860 # Passing py files one at time is slower and can produce
861 # different results. input_api.verbose used to be used
862 # to enable this behaviour but differing behaviour in
863 # verbose mode is not desirable.
ilevy@chromium.org7b677f72013-01-07 18:49:26 +0000864 # Leave this unreachable code in here so users can make
865 # a quick local edit to diagnose pylint issues more
866 # easily.
sbc@chromium.org7fc75ca2012-09-21 20:14:21 +0000867 if True:
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000868 # pylint's cycle detection doesn't work in parallel, so spawn a second,
869 # single-threaded job for just that check.
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000870
871 # Some PRESUBMITs explicitly mention cycle detection.
872 if not any('R0401' in a or 'cyclic-import' in a for a in extra_args):
873 return [
874 GetPylintCmd(files, ["--disable=cyclic-import"], True),
875 GetPylintCmd(files, ["--disable=all", "--enable=cyclic-import"], False)
876 ]
877 else:
878 return [ GetPylintCmd(files, [], True) ]
879
chrisha@google.com267d6592012-06-19 19:23:31 +0000880 else:
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000881 return map(lambda x: GetPylintCmd([x], [], 1), files)
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000882
883
884def RunPylint(input_api, *args, **kwargs):
885 """Legacy presubmit function.
886
887 For better performance, get all tests and then pass to
888 input_api.RunTests.
889 """
890 return input_api.RunTests(GetPylint(input_api, *args, **kwargs), False)
maruel@chromium.orge94aedc2010-12-13 21:11:30 +0000891
maruel@chromium.orgade9c592011-04-07 15:59:11 +0000892
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000893def CheckBuildbotPendingBuilds(input_api, output_api, url, max_pendings,
894 ignored):
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000895 try:
896 connection = input_api.urllib2.urlopen(url)
897 raw_data = connection.read()
898 connection.close()
899 except IOError:
900 return [output_api.PresubmitNotifyResult('%s is not accessible' % url)]
901
902 try:
903 data = input_api.json.loads(raw_data)
904 except ValueError:
905 return [output_api.PresubmitNotifyResult('Received malformed json while '
906 'looking up buildbot status')]
907
908 out = []
909 for (builder_name, builder) in data.iteritems():
910 if builder_name in ignored:
911 continue
maruel@chromium.orgcf1982c2010-10-04 15:08:28 +0000912 if builder.get('state', '') == 'offline':
913 continue
maruel@chromium.org3fbcb082010-03-19 14:03:28 +0000914 pending_builds_len = len(builder.get('pending_builds', []))
915 if pending_builds_len > max_pendings:
916 out.append('%s has %d build(s) pending' %
917 (builder_name, pending_builds_len))
918 if out:
919 return [output_api.PresubmitPromptWarning(
920 'Build(s) pending. It is suggested to wait that no more than %d '
921 'builds are pending.' % max_pendings,
922 long_text='\n'.join(out))]
923 return []
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000924
925
Edward Lesmes067ef5d2018-04-19 17:54:45 -0400926def CheckOwnersFormat(input_api, output_api):
927 affected_files = set([
928 f.LocalPath()
929 for f in input_api.change.AffectedFiles()
930 if 'OWNERS' in f.LocalPath() and f.Action() != 'D'
931 ])
932 if not affected_files:
933 return []
934 try:
Edward Lemurdb0055d2018-12-21 19:02:23 +0000935 owners_db = input_api.owners_db
936 owners_db.override_files = {}
937 owners_db.load_data_needed_for(affected_files)
Edward Lesmes067ef5d2018-04-19 17:54:45 -0400938 return []
939 except Exception as e:
940 return [output_api.PresubmitError(
941 'Error parsing OWNERS files:\n%s' % e)]
942
943
dpranke@chromium.orgbce925d2015-01-16 22:38:38 +0000944def CheckOwners(input_api, output_api, source_file_filter=None):
Aaron Gable8678d322018-04-02 13:28:19 -0700945 affected_files = set([f.LocalPath() for f in
946 input_api.change.AffectedFiles(file_filter=source_file_filter)])
Jeremy Roman5ae86d22018-04-26 15:36:02 -0400947 affects_owners = any('OWNERS' in name for name in affected_files)
Aaron Gable8678d322018-04-02 13:28:19 -0700948
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000949 if input_api.is_committing:
Jeremy Roman5ae86d22018-04-26 15:36:02 -0400950 if input_api.tbr and not affects_owners:
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000951 return [output_api.PresubmitNotifyResult(
952 '--tbr was specified, skipping OWNERS check')]
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100953 needed = 'LGTM from an OWNER'
954 output_fn = output_api.PresubmitError
rmistry@google.com5fc6c8c2015-04-16 16:38:43 +0000955 if input_api.change.issue:
tandrii@chromium.org9dea2ac2016-04-28 06:26:20 +0000956 if input_api.dry_run:
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100957 output_fn = lambda text: output_api.PresubmitNotifyResult(
958 'This is a dry run, but these failures would be reported on ' +
959 'commit:\n' + text)
rmistry@google.com5fc6c8c2015-04-16 16:38:43 +0000960 else:
Andrii Shyshkalov63560ad2018-02-09 19:09:54 -0800961 return [output_api.PresubmitError(
962 'OWNERS check failed: this CL has no Gerrit change number, '
963 'so we can\'t check it for approvals.')]
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000964 else:
965 needed = 'OWNER reviewers'
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100966 output_fn = output_api.PresubmitNotifyResult
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +0000967
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000968 owners_db = input_api.owners_db
Jochen Eisingerd0573ec2017-04-13 10:55:06 +0200969 owners_db.override_files = input_api.change.OriginalOwnersFiles()
tandrii@chromium.org830dc0b2016-05-09 06:26:34 +0000970 owner_email, reviewers = GetCodereviewOwnerAndReviewers(
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000971 input_api,
972 owners_db.email_regexp,
973 approval_needed=input_api.is_committing)
974
dpranke@chromium.orgf6ddfa42013-03-05 21:06:03 +0000975 owner_email = owner_email or input_api.change.author_email
976
Edward Lemur707d70b2018-02-07 00:50:14 +0100977 finder = input_api.owners_finder(
978 affected_files, input_api.change.RepositoryRoot(),
979 owner_email, reviewers, fopen=file, os_path=input_api.os_path,
980 email_postfix='', disable_color=True,
981 override_files=input_api.change.OriginalOwnersFiles())
982 missing_files = finder.unreviewed_files
maruel@chromium.orge4067ab2011-06-03 01:07:35 +0000983
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000984 if missing_files:
zork@chromium.org046e1752012-05-07 05:56:12 +0000985 output_list = [
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100986 output_fn('Missing %s for these files:\n %s' %
987 (needed, '\n '.join(sorted(missing_files))))]
Jeremy Roman5ae86d22018-04-26 15:36:02 -0400988 if input_api.tbr and affects_owners:
Robert Sesek16a396e2018-08-15 21:11:29 +0000989 output_list.append(output_fn('The CL affects an OWNERS file, so TBR will '
990 'be ignored.'))
zork@chromium.org046e1752012-05-07 05:56:12 +0000991 if not input_api.is_committing:
dpranke@chromium.org31b42c02013-09-19 19:24:15 +0000992 suggested_owners = owners_db.reviewers_for(missing_files, owner_email)
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200993 owners_with_comments = []
994 def RecordComments(text):
995 owners_with_comments.append(finder.print_indent() + text)
996 finder.writeln = RecordComments
997 for owner in suggested_owners:
998 finder.print_comments(owner)
Clemens Hammacherd0c226a2017-01-16 14:09:52 +0100999 output_list.append(output_fn('Suggested OWNERS: ' +
scheib@chromium.org93276ab2013-10-14 23:55:32 +00001000 '(Use "git-cl owners" to interactively select owners.)\n %s' %
Jochen Eisinger76f5fc62017-04-07 16:27:46 +02001001 ('\n '.join(owners_with_comments))))
zork@chromium.org046e1752012-05-07 05:56:12 +00001002 return output_list
dpranke@chromium.org2a009622011-03-01 02:43:31 +00001003
pam@chromium.orgf46aed92012-03-08 09:18:17 +00001004 if input_api.is_committing and not reviewers:
Clemens Hammacherd0c226a2017-01-16 14:09:52 +01001005 return [output_fn('Missing LGTM from someone other than %s' % owner_email)]
dpranke@chromium.org3e331bd2011-03-24 23:13:04 +00001006 return []
dpranke@chromium.org627ea672011-03-11 23:29:03 +00001007
Aaron Gable668c1d82018-04-03 10:19:16 -07001008
tandrii@chromium.org830dc0b2016-05-09 06:26:34 +00001009def GetCodereviewOwnerAndReviewers(input_api, email_regexp, approval_needed):
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001010 """Return the owner and reviewers of a change, if any.
1011
1012 If approval_needed is True, only reviewers who have approved the change
1013 will be returned.
1014 """
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001015 issue = input_api.change.issue
1016 if not issue:
tandrii@chromium.org830dc0b2016-05-09 06:26:34 +00001017 return None, (set() if approval_needed else
1018 _ReviewersFromChange(input_api.change))
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001019
1020 owner_email = input_api.gerrit.GetChangeOwner(issue)
1021 reviewers = set(
1022 r for r in input_api.gerrit.GetChangeReviewers(issue, approval_needed)
1023 if _match_reviewer_email(r, owner_email, email_regexp))
tandrii81665dc2016-08-29 09:16:19 -07001024 input_api.logging.debug('owner: %s; approvals given by: %s',
1025 owner_email, ', '.join(sorted(reviewers)))
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001026 return owner_email, reviewers
1027
1028
Aaron Gable668c1d82018-04-03 10:19:16 -07001029def _ReviewersFromChange(change):
1030 """Return the reviewers specified in the |change|, if any."""
1031 reviewers = set()
1032 reviewers.update(change.ReviewersFromDescription())
1033 reviewers.update(change.TBRsFromDescription())
1034
1035 # Drop reviewers that aren't specified in email address format.
1036 return set(reviewer for reviewer in reviewers if '@' in reviewer)
1037
1038
1039def _match_reviewer_email(r, owner_email, email_regexp):
1040 return email_regexp.match(r) and r != owner_email
1041
1042
bauerb@chromium.org4cea01e2012-03-20 19:49:05 +00001043def CheckSingletonInHeaders(input_api, output_api, source_file_filter=None):
glider@chromium.orgc3617f32015-02-19 16:33:33 +00001044 """Deprecated, must be removed."""
1045 return [
1046 output_api.PresubmitNotifyResult(
1047 'CheckSingletonInHeaders is deprecated, please remove it.')
1048 ]
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001049
1050
1051def PanProjectChecks(input_api, output_api,
1052 excluded_paths=None, text_files=None,
bradnelson@google.comd57771d2011-03-31 19:18:32 +00001053 license_header=None, project_name=None,
jamesr@chromium.orgaf27f462013-04-04 21:44:22 +00001054 owners_check=True, maxlen=80):
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001055 """Checks that ALL chromium orbit projects should use.
1056
1057 These are checks to be run on all Chromium orbit project, including:
1058 Chromium
1059 Native Client
1060 V8
1061 When you update this function, please take this broad scope into account.
1062 Args:
1063 input_api: Bag of input related interfaces.
1064 output_api: Bag of output related interfaces.
1065 excluded_paths: Don't include these paths in common checks.
1066 text_files: Which file are to be treated as documentation text files.
1067 license_header: What license header should be on files.
1068 project_name: What is the name of the project as it appears in the license.
1069 Returns:
1070 A list of warning or error objects.
1071 """
maruel@chromium.org69eaecb2011-06-14 13:09:13 +00001072 excluded_paths = tuple(excluded_paths or [])
1073 text_files = tuple(text_files or (
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +00001074 r'.+\.txt$',
1075 r'.+\.json$',
maruel@chromium.org69eaecb2011-06-14 13:09:13 +00001076 ))
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001077 project_name = project_name or 'Chromium'
mark@chromium.org2cc66422012-08-07 21:22:32 +00001078
1079 # Accept any year number from 2006 to the current year, or the special
rvargas@chromium.org02652602014-03-14 19:43:20 +00001080 # 2006-20xx string used on the oldest files. 2006-20xx is deprecated, but
1081 # tolerated on old files.
mark@chromium.org2cc66422012-08-07 21:22:32 +00001082 current_year = int(input_api.time.strftime('%Y'))
1083 allowed_years = (str(s) for s in reversed(xrange(2006, current_year + 1)))
rvargas@chromium.org02652602014-03-14 19:43:20 +00001084 years_re = '(' + '|'.join(allowed_years) + '|2006-2008|2006-2009|2006-2010)'
mark@chromium.org2cc66422012-08-07 21:22:32 +00001085
1086 # The (c) is deprecated, but tolerate it until it's removed from all files.
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001087 license_header = license_header or (
mark@chromium.org2cc66422012-08-07 21:22:32 +00001088 r'.*? Copyright (\(c\) )?%(year)s The %(project)s Authors\. '
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001089 r'All rights reserved\.\n'
1090 r'.*? Use of this source code is governed by a BSD-style license that '
1091 r'can be\n'
dbeam@chromium.orgb2312102012-02-15 02:01:55 +00001092 r'.*? found in the LICENSE file\.(?: \*/)?\n'
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001093 ) % {
mark@chromium.org2cc66422012-08-07 21:22:32 +00001094 'year': years_re,
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001095 'project': project_name,
1096 }
1097
1098 results = []
1099 # This code loads the default black list (e.g. third_party, experimental, etc)
1100 # and add our black list (breakpad, skia and v8 are still not following
1101 # google style and are not really living this repository).
1102 # See presubmit_support.py InputApi.FilterSourceFile for the (simple) usage.
1103 black_list = input_api.DEFAULT_BLACK_LIST + excluded_paths
1104 white_list = input_api.DEFAULT_WHITE_LIST + text_files
1105 sources = lambda x: input_api.FilterSourceFile(x, black_list=black_list)
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +00001106 text_files = lambda x: input_api.FilterSourceFile(
1107 x, black_list=black_list, white_list=white_list)
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001108
1109 snapshot_memory = []
1110 def snapshot(msg):
1111 """Measures & prints performance warning if a rule is running slow."""
1112 dt2 = input_api.time.clock()
1113 if snapshot_memory:
1114 delta_ms = int(1000*(dt2 - snapshot_memory[0]))
1115 if delta_ms > 500:
Raul Tambre80ee78e2019-05-06 22:41:05 +00001116 print(" %s took a long time: %dms" % (snapshot_memory[1], delta_ms))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001117 snapshot_memory[:] = (dt2, msg)
1118
Edward Lesmescb62e482018-04-19 18:29:35 -04001119 snapshot("checking owners files format")
1120 results.extend(input_api.canned_checks.CheckOwnersFormat(
1121 input_api, output_api))
1122
bradnelson@google.comd57771d2011-03-31 19:18:32 +00001123 if owners_check:
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001124 snapshot("checking owners")
bradnelson@google.comd57771d2011-03-31 19:18:32 +00001125 results.extend(input_api.canned_checks.CheckOwners(
Dirk Pranke3c86cee2017-01-23 22:02:06 -08001126 input_api, output_api, source_file_filter=None))
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001127
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001128 snapshot("checking long lines")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001129 results.extend(input_api.canned_checks.CheckLongLines(
jamesr@chromium.orgaf27f462013-04-04 21:44:22 +00001130 input_api, output_api, maxlen, source_file_filter=sources))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001131 snapshot( "checking tabs")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001132 results.extend(input_api.canned_checks.CheckChangeHasNoTabs(
1133 input_api, output_api, source_file_filter=sources))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001134 snapshot( "checking stray whitespace")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001135 results.extend(input_api.canned_checks.CheckChangeHasNoStrayWhitespace(
1136 input_api, output_api, source_file_filter=sources))
phajdan.jr@chromium.orgd965db32015-11-16 09:46:56 +00001137 snapshot("checking license")
1138 results.extend(input_api.canned_checks.CheckLicense(
1139 input_api, output_api, license_header, source_file_filter=sources))
maruel@chromium.orgb1ce7752011-05-08 13:50:16 +00001140
maruel@chromium.orgb1ce7752011-05-08 13:50:16 +00001141 if input_api.is_committing:
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001142 snapshot("checking was uploaded")
1143 results.extend(input_api.canned_checks.CheckChangeWasUploaded(
1144 input_api, output_api))
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +00001145 snapshot("checking description")
1146 results.extend(input_api.canned_checks.CheckChangeHasDescription(
1147 input_api, output_api))
1148 results.extend(input_api.canned_checks.CheckDoNotSubmitInDescription(
1149 input_api, output_api))
ilevy@chromium.orgc50d7612012-12-05 04:25:14 +00001150 snapshot("checking do not submit in files")
1151 results.extend(input_api.canned_checks.CheckDoNotSubmitInFiles(
1152 input_api, output_api))
nick@chromium.orge06cb4e2011-04-23 01:20:38 +00001153 snapshot("done")
bradnelson@google.com56e48bc2011-03-24 20:51:21 +00001154 return results
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001155
1156
Aiden Benner99b0ccb2018-11-20 19:53:31 +00001157def CheckPatchFormatted(input_api,
1158 output_api,
Ryan Tsengf28ef982018-12-04 19:53:08 +00001159 bypass_warnings=True,
Aiden Benner99b0ccb2018-11-20 19:53:31 +00001160 check_js=False,
1161 check_python=None,
1162 result_factory=None):
Nodir Turakulov425d9ce2018-06-14 00:07:46 +00001163 result_factory = result_factory or output_api.PresubmitPromptWarning
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001164 import git_cl
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001165
1166 display_args = []
Christopher Lamc5ba6922017-01-24 11:19:14 +11001167 if check_js:
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001168 display_args.append('--js')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00001169
1170 # Explicitly setting check_python to will enable/disable python formatting
1171 # on all files. Leaving it as None will enable checking patch formatting
1172 # on files that have a .style.yapf file in a parent directory.
1173 if check_python is not None:
1174 if check_python:
1175 display_args.append('--python')
1176 else:
1177 display_args.append('--no-python')
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001178
1179 cmd = ['-C', input_api.change.RepositoryRoot(),
1180 'cl', 'format', '--dry-run', '--presubmit'] + display_args
Andrew Grieved86c8032017-09-26 14:40:36 -04001181 presubmit_subdir = input_api.os_path.relpath(
1182 input_api.PresubmitLocalPath(), input_api.change.RepositoryRoot())
1183 if presubmit_subdir.startswith('..') or presubmit_subdir == '.':
1184 presubmit_subdir = ''
1185 # If the PRESUBMIT.py is in a parent repository, then format the entire
1186 # subrepository. Otherwise, format only the code in the directory that
1187 # contains the PRESUBMIT.py.
1188 if presubmit_subdir:
1189 cmd.append(input_api.PresubmitLocalPath())
Ryan Tsengf28ef982018-12-04 19:53:08 +00001190 code, _ = git_cl.RunGitWithCode(cmd, suppress_stderr=bypass_warnings)
1191 # bypass_warnings? Only fail with code 2.
1192 # As this is just a warning, ignore all other errors if the user
1193 # happens to have a broken clang-format, doesn't use git, etc etc.
1194 if code == 2 or (code and not bypass_warnings):
Andrew Grieved3b25482017-10-13 16:06:25 -04001195 if presubmit_subdir:
1196 short_path = presubmit_subdir
1197 else:
1198 short_path = input_api.basename(input_api.change.RepositoryRoot())
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001199 display_args.append(presubmit_subdir)
Nodir Turakulov425d9ce2018-06-14 00:07:46 +00001200 return [result_factory(
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00001201 'The %s directory requires source formatting. '
Nodir Turakulov0ae14e92018-05-31 14:53:53 -07001202 'Please run: git cl format %s' %
1203 (short_path, ' '.join(display_args)))]
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00001204 return []
scottmg@chromium.orgd05ab352014-12-05 17:24:26 +00001205
1206
1207def CheckGNFormatted(input_api, output_api):
1208 import gn
1209 affected_files = input_api.AffectedFiles(
1210 include_deletes=False,
1211 file_filter=lambda x: x.LocalPath().endswith('.gn') or
kylechar58edce22016-06-17 06:07:51 -07001212 x.LocalPath().endswith('.gni') or
1213 x.LocalPath().endswith('.typemap'))
scottmg@chromium.orgd05ab352014-12-05 17:24:26 +00001214 warnings = []
1215 for f in affected_files:
1216 cmd = ['gn', 'format', '--dry-run', f.AbsoluteLocalPath()]
1217 rc = gn.main(cmd)
1218 if rc == 2:
1219 warnings.append(output_api.PresubmitPromptWarning(
brettw4b8ed592016-08-05 16:19:12 -07001220 '%s requires formatting. Please run:\n gn format %s' % (
scottmg@chromium.orgd05ab352014-12-05 17:24:26 +00001221 f.AbsoluteLocalPath(), f.LocalPath())))
1222 # It's just a warning, so ignore other types of failures assuming they'll be
1223 # caught elsewhere.
1224 return warnings
Dan Jacques94652a32017-10-09 23:18:46 -04001225
1226
1227def CheckCIPDManifest(input_api, output_api, path=None, content=None):
1228 """Verifies that a CIPD ensure file manifest is valid against all platforms.
1229
1230 Exactly one of "path" or "content" must be provided. An assertion will occur
1231 if neither or both are provided.
1232
1233 Args:
1234 path (str): If provided, the filesystem path to the manifest to verify.
1235 content (str): If provided, the raw content of the manifest to veirfy.
1236 """
1237 cipd_bin = 'cipd' if not input_api.is_windows else 'cipd.bat'
1238 cmd = [cipd_bin, 'ensure-file-verify']
1239 kwargs = {}
1240
1241 if input_api.is_windows:
1242 # Needs to be able to resolve "cipd.bat".
1243 kwargs['shell'] = True
1244
1245 if input_api.verbose:
1246 cmd += ['-log-level', 'debug']
1247
1248 if path:
1249 assert content is None, 'Cannot provide both "path" and "content".'
1250 cmd += ['-ensure-file', path]
Robert Iannucci7d47eb52017-12-06 12:18:18 -08001251 name = 'Check CIPD manifest %r' % path
Dan Jacques94652a32017-10-09 23:18:46 -04001252 elif content:
1253 assert path is None, 'Cannot provide both "path" and "content".'
1254 cmd += ['-ensure-file=-']
1255 kwargs['stdin'] = content
Robert Iannucci7d47eb52017-12-06 12:18:18 -08001256 # quick and dirty parser to extract checked packages.
1257 packages = [
1258 l.split()[0] for l in (ll.strip() for ll in content.splitlines())
1259 if ' ' in l and not l.startswith('$')
1260 ]
1261 name = 'Check CIPD packages from string: %r' % (packages,)
Dan Jacques94652a32017-10-09 23:18:46 -04001262 else:
1263 raise Exception('Exactly one of "path" or "content" must be provided.')
1264
1265 return input_api.Command(
Robert Iannucci7d47eb52017-12-06 12:18:18 -08001266 name,
Dan Jacques94652a32017-10-09 23:18:46 -04001267 cmd,
1268 kwargs,
1269 output_api.PresubmitError)
1270
1271
1272def CheckCIPDPackages(input_api, output_api, platforms, packages):
1273 """Verifies that all named CIPD packages can be resolved against all supplied
1274 platforms.
1275
1276 Args:
1277 platforms (list): List of CIPD platforms to verify.
1278 packages (dict): Mapping of package name to version.
1279 """
1280 manifest = []
1281 for p in platforms:
1282 manifest.append('$VerifiedPlatform %s' % (p,))
1283 for k, v in packages.iteritems():
1284 manifest.append('%s %s' % (k, v))
1285 return CheckCIPDManifest(input_api, output_api, content='\n'.join(manifest))
Sergiy Byelozyoroveba83472017-10-27 02:08:21 +02001286
1287
Vadim Shtayura21741362018-09-14 22:17:02 +00001288def CheckCIPDClientDigests(input_api, output_api, client_version_file):
1289 """Verifies that *.digests file was correctly regenerated.
1290
1291 <client_version_file>.digests file contains pinned hashes of the CIPD client.
1292 It is consulted during CIPD client bootstrap and self-update. It should be
1293 regenerated each time CIPD client version file changes.
1294
1295 Args:
1296 client_version_file (str): Path to a text file with CIPD client version.
1297 """
1298 cmd = [
1299 'cipd' if not input_api.is_windows else 'cipd.bat',
1300 'selfupdate-roll', '-check', '-version-file', client_version_file,
1301 ]
1302 if input_api.verbose:
1303 cmd += ['-log-level', 'debug']
1304 return input_api.Command(
1305 'Check CIPD client_version_file.digests file',
1306 cmd,
1307 {'shell': True} if input_api.is_windows else {}, # to resolve cipd.bat
1308 output_api.PresubmitError)
1309
1310
Sergiy Byelozyoroveba83472017-10-27 02:08:21 +02001311def CheckVPythonSpec(input_api, output_api, file_filter=None):
1312 """Validates any changed .vpython files with vpython verification tool.
1313
1314 Args:
1315 input_api: Bag of input related interfaces.
1316 output_api: Bag of output related interfaces.
1317 file_filter: Custom function that takes a path (relative to client root) and
1318 returns boolean, which is used to filter files for which to apply the
1319 verification to. Defaults to any path ending with .vpython, which captures
1320 both global .vpython and <script>.vpython files.
1321
1322 Returns:
1323 A list of input_api.Command objects containing verification commands.
1324 """
1325 file_filter = file_filter or (lambda f: f.LocalPath().endswith('.vpython'))
John Budorick16162372018-04-18 10:39:53 -07001326 affected_files = input_api.AffectedTestableFiles(file_filter=file_filter)
Sergiy Byelozyoroveba83472017-10-27 02:08:21 +02001327 affected_files = map(lambda f: f.AbsoluteLocalPath(), affected_files)
1328
1329 commands = []
1330 for f in affected_files:
1331 commands.append(input_api.Command(
1332 'Verify %s' % f,
1333 ['vpython', '-vpython-spec', f, '-vpython-tool', 'verify'],
1334 {'stderr': input_api.subprocess.STDOUT},
1335 output_api.PresubmitError))
1336
1337 return commands
Mun Yong Jang17995a92017-12-01 16:00:31 -08001338
1339
1340def CheckChangedLUCIConfigs(input_api, output_api):
1341 import collections
1342 import base64
1343 import json
Mun Yong Jangcfb9a232017-12-18 10:39:34 -08001344 import logging
Mun Yong Jang17995a92017-12-01 16:00:31 -08001345 import urllib2
1346
1347 import auth
1348 import git_cl
1349
1350 LUCI_CONFIG_HOST_NAME = 'luci-config.appspot.com'
1351
1352 cl = git_cl.Changelist()
Mun Yong Jang603d01e2017-12-19 16:38:30 -08001353 if input_api.change.issue and input_api.gerrit:
1354 remote_branch = input_api.gerrit.GetDestRef(input_api.change.issue)
1355 else:
1356 remote, remote_branch = cl.GetRemoteBranch()
1357 if remote_branch.startswith('refs/remotes/%s/' % remote):
1358 remote_branch = remote_branch.replace(
1359 'refs/remotes/%s/' % remote, 'refs/heads/', 1)
1360 if remote_branch.startswith('refs/remotes/branch-heads/'):
1361 remote_branch = remote_branch.replace(
1362 'refs/remotes/branch-heads/', 'refs/branch-heads/', 1)
Mun Yong Jang17995a92017-12-01 16:00:31 -08001363
1364 remote_host_url = cl.GetRemoteUrl()
1365 if not remote_host_url:
1366 return [output_api.PresubmitError(
1367 'Remote host url for git has not been defined')]
1368 remote_host_url = remote_host_url.rstrip('/')
1369 if remote_host_url.endswith('.git'):
1370 remote_host_url = remote_host_url[:-len('.git')]
1371
1372 # authentication
1373 try:
1374 authenticator = auth.get_authenticator_for_host(
1375 LUCI_CONFIG_HOST_NAME, auth.make_auth_config())
1376 acc_tkn = authenticator.get_access_token()
1377 except auth.AuthenticationError as e:
1378 return [output_api.PresubmitError(
1379 'Error in authenticating user.', long_text=str(e))]
1380
1381 def request(endpoint, body=None):
1382 api_url = ('https://%s/_ah/api/config/v1/%s'
1383 % (LUCI_CONFIG_HOST_NAME, endpoint))
1384 req = urllib2.Request(api_url)
1385 req.add_header('Authorization', 'Bearer %s' % acc_tkn.token)
1386 if body is not None:
1387 req.add_header('Content-Type', 'application/json')
1388 req.add_data(json.dumps(body))
1389 return json.load(urllib2.urlopen(req))
1390
1391 try:
1392 config_sets = request('config-sets').get('config_sets')
1393 except urllib2.HTTPError as e:
1394 return [output_api.PresubmitError(
1395 'Config set request to luci-config failed', long_text=str(e))]
1396 if not config_sets:
John Budoricka1512652019-08-14 17:48:43 +00001397 return [output_api.PresubmitPromptWarning('No config_sets were returned')]
Mun Yong Jang17995a92017-12-01 16:00:31 -08001398 loc_pref = '%s/+/%s/' % (remote_host_url, remote_branch)
Mun Yong Jangcfb9a232017-12-18 10:39:34 -08001399 logging.debug('Derived location prefix: %s', loc_pref)
Mun Yong Jang17995a92017-12-01 16:00:31 -08001400 dir_to_config_set = {
1401 '%s/' % cs['location'][len(loc_pref):].rstrip('/'): cs['config_set']
1402 for cs in config_sets
1403 if cs['location'].startswith(loc_pref) or
1404 ('%s/' % cs['location']) == loc_pref
1405 }
John Budoricka1512652019-08-14 17:48:43 +00001406 if not dir_to_config_set:
1407 warning_long_text_lines = [
1408 'No config_set found for %s.' % loc_pref,
1409 'Found the following:',
1410 ]
1411 for loc in sorted(cs['location'] for cs in config_sets):
1412 warning_long_text_lines.append(' %s' % loc)
1413 warning_long_text_lines.append('')
1414 warning_long_text_lines.append(
1415 'If the requested location is internal,'
1416 ' the requester may not have access.')
1417
1418 return [output_api.PresubmitPromptWarning(
1419 warning_long_text_lines[0],
1420 long_text='\n'.join(warning_long_text_lines))]
Mun Yong Jang17995a92017-12-01 16:00:31 -08001421 cs_to_files = collections.defaultdict(list)
Nodir Turakulov390028f2019-01-04 20:59:02 +00001422 for f in input_api.AffectedFiles(include_deletes=False):
Mun Yong Jang17995a92017-12-01 16:00:31 -08001423 # windows
1424 file_path = f.LocalPath().replace(_os.sep, '/')
Mun Yong Jangcfb9a232017-12-18 10:39:34 -08001425 logging.debug('Affected file path: %s', file_path)
Mun Yong Jang17995a92017-12-01 16:00:31 -08001426 for dr, cs in dir_to_config_set.iteritems():
1427 if dr == '/' or file_path.startswith(dr):
1428 cs_to_files[cs].append({
1429 'path': file_path[len(dr):] if dr != '/' else file_path,
1430 'content': base64.b64encode(
1431 '\n'.join(f.NewContents()).encode('utf-8'))
1432 })
1433 outputs = []
1434 for cs, f in cs_to_files.iteritems():
1435 try:
1436 # TODO(myjang): parallelize
1437 res = request(
1438 'validate-config', body={'config_set': cs, 'files': f})
1439 except urllib2.HTTPError as e:
1440 return [output_api.PresubmitError(
1441 'Validation request to luci-config failed', long_text=str(e))]
1442 for msg in res.get('messages', []):
1443 sev = msg['severity']
1444 if sev == 'WARNING':
1445 out_f = output_api.PresubmitPromptWarning
1446 elif sev == 'ERROR' or sev == 'CRITICAL':
1447 out_f = output_api.PresubmitError
1448 else:
1449 out_f = output_api.PresubmitNotifyResult
1450 outputs.append(out_f('Config validation: %s' % msg['text']))
1451 return outputs
Vadim Shtayura39b0b8e2019-01-31 23:56:08 +00001452
1453
1454def CheckLucicfgGenOutput(input_api, output_api, entry_script):
1455 """Verifies configs produced by `lucicfg` are up-to-date and pass validation.
1456
1457 Runs the check unconditionally, regardless of what files are modified. Examine
1458 input_api.AffectedFiles() yourself before using CheckLucicfgGenOutput if this
1459 is a concern.
1460
1461 Assumes `lucicfg` binary is in PATH and the user is logged in.
1462
1463 Args:
1464 entry_script: path to the entry-point *.star script responsible for
1465 generating a single config set. Either absolute or relative to the
1466 currently running PRESUBMIT.py script.
1467
1468 Returns:
1469 A list of input_api.Command objects containing verification commands.
1470 """
1471 return [
1472 input_api.Command(
1473 'lucicfg validate "%s"' % entry_script,
1474 [
1475 'lucicfg' if not input_api.is_windows else 'lucicfg.bat',
1476 'validate', entry_script,
1477 '-log-level', 'debug' if input_api.verbose else 'warning',
1478 ],
1479 {
1480 'stderr': input_api.subprocess.STDOUT,
1481 'shell': input_api.is_windows, # to resolve *.bat
1482 'cwd': input_api.PresubmitLocalPath(),
1483 },
1484 output_api.PresubmitError)
1485 ]
Aaron Gableaca5b6a2019-05-21 19:32:17 +00001486
1487# TODO(agable): Add this to PanProjectChecks.
1488def CheckJsonParses(input_api, output_api):
1489 """Verifies that all JSON files at least parse as valid JSON."""
1490 import json
1491 affected_files = input_api.AffectedFiles(
1492 include_deletes=False,
1493 file_filter=lambda x: x.LocalPath().endswith('.json'))
1494 warnings = []
1495 for f in affected_files:
1496 with open(f.AbsoluteLocalPath()) as j:
1497 try:
1498 json.load(j)
1499 except ValueError:
1500 # Just a warning for now, in case people are using JSON5 somewhere.
1501 warnings.append(output_api.PresubmitPromptWarning(
1502 '%s does not appear to be valid JSON.' % f.LocalPath()))
1503 return warnings