blob: c2b8149f8a6168513d85f1466fb7e7252ac88d16 [file] [log] [blame]
Mike Frysinger0e2cb7a2019-08-20 17:04:52 -04001#!/usr/bin/python2 -u
beeps48fc6f52013-01-07 14:36:40 -08002"""
3Wrapper to patch pylint library functions to suit autotest.
4
5This script is invoked as part of the presubmit checks for autotest python
6files. It runs pylint on a list of files that it obtains either through
7the command line or from an environment variable set in pre-upload.py.
8
9Example:
10run_pylint.py filename.py
11"""
mbligh99d2ded2008-06-23 16:17:36 +000012
Prashanth Balasubramanianbaabef22014-11-04 12:38:44 -080013import fnmatch
14import logging
15import os
16import re
17import sys
mbligh99d2ded2008-06-23 16:17:36 +000018
beeps98365d82013-02-20 20:08:07 -080019import common
20from autotest_lib.client.common_lib import autotemp, revision_control
beeps48fc6f52013-01-07 14:36:40 -080021
22# Do a basic check to see if pylint is even installed.
mbligh65e06b12008-08-22 18:12:49 +000023try:
24 import pylint
Eric Li861b2d52011-02-04 14:50:35 -080025 from pylint.__pkginfo__ import version as pylint_version
mbligh65e06b12008-08-22 18:12:49 +000026except ImportError:
beeps48fc6f52013-01-07 14:36:40 -080027 print ("Unable to import pylint, it may need to be installed."
28 " Run 'sudo aptitude install pylint' if you haven't already.")
mbligh65e06b12008-08-22 18:12:49 +000029 sys.exit(1)
30
Kazuhiro Inaba0e7bd162019-06-07 16:24:45 +090031pylint_version_parsed = tuple(map(int, pylint_version.split('.')))
mbligh99d2ded2008-06-23 16:17:36 +000032
beeps48fc6f52013-01-07 14:36:40 -080033# some files make pylint blow up, so make sure we ignore them
Prathmesh Prabhu5dab08c2017-01-12 11:16:56 -080034BLACKLIST = ['/site-packages/*', '/contrib/*', '/frontend/afe/management.py']
jadmanski94a64932008-07-22 14:03:10 +000035
Mike Frysingercb574632019-09-20 10:43:45 -040036import astroid
mbligh99d2ded2008-06-23 16:17:36 +000037import pylint.lint
beeps2c669642013-01-14 18:30:57 -080038from pylint.checkers import base, imports, variables
mbligh99d2ded2008-06-23 16:17:36 +000039
40# need to put autotest root dir on sys.path so pylint will be happy
41autotest_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
42sys.path.insert(0, autotest_root)
43
44# patch up pylint import checker to handle our importing magic
beeps48fc6f52013-01-07 14:36:40 -080045ROOT_MODULE = 'autotest_lib.'
beepse19d3032013-05-30 09:22:07 -070046
47# A list of modules for pylint to ignore, specifically, these modules
48# are imported for their side-effects and are not meant to be used.
Prashanth B2d8047e2014-04-27 18:54:47 -070049_IGNORE_MODULES=['common', 'frontend_test_utils',
50 'setup_django_environment',
Keyar Hoodf9a36512013-06-13 18:39:56 -070051 'setup_django_lite_environment',
Prashanth B2d8047e2014-04-27 18:54:47 -070052 'setup_django_readonly_environment', 'setup_test_environment',]
beeps48fc6f52013-01-07 14:36:40 -080053
54
beeps98365d82013-02-20 20:08:07 -080055class pylint_error(Exception):
56 """
57 Error raised when pylint complains about a file.
58 """
59
60
61class run_pylint_error(pylint_error):
62 """
63 Error raised when an assumption made in this file is violated.
64 """
65
66
beeps48fc6f52013-01-07 14:36:40 -080067def patch_modname(modname):
68 """
69 Patches modname so we can make sense of autotest_lib modules.
70
71 @param modname: name of a module, contains '.'
72 @return modified modname string.
73 """
74 if modname.startswith(ROOT_MODULE) or modname.startswith(ROOT_MODULE[:-1]):
75 modname = modname[len(ROOT_MODULE):]
76 return modname
77
78
79def patch_consumed_list(to_consume=None, consumed=None):
80 """
beepse19d3032013-05-30 09:22:07 -070081 Patches the consumed modules list to ignore modules with side effects.
beeps48fc6f52013-01-07 14:36:40 -080082
beepse19d3032013-05-30 09:22:07 -070083 Autotest relies on importing certain modules solely for their side
84 effects. Pylint doesn't understand this and flags them as unused, since
85 they're not referenced anywhere in the code. To overcome this we need
86 to transplant said modules into the dictionary of modules pylint has
87 already seen, before pylint checks it.
beeps48fc6f52013-01-07 14:36:40 -080088
beeps48fc6f52013-01-07 14:36:40 -080089 @param to_consume: a dictionary of names pylint needs to see referenced.
90 @param consumed: a dictionary of names that pylint has seen referenced.
beeps48fc6f52013-01-07 14:36:40 -080091 """
beepse19d3032013-05-30 09:22:07 -070092 ignore_modules = []
93 if (to_consume is not None and consumed is not None):
94 ignore_modules = [module_name for module_name in _IGNORE_MODULES
95 if module_name in to_consume]
96
97 for module_name in ignore_modules:
98 consumed[module_name] = to_consume[module_name]
99 del to_consume[module_name]
beeps48fc6f52013-01-07 14:36:40 -0800100
mbligh99d2ded2008-06-23 16:17:36 +0000101
102class CustomImportsChecker(imports.ImportsChecker):
beeps48fc6f52013-01-07 14:36:40 -0800103 """Modifies stock imports checker to suit autotest."""
Ryo Hashimotod35d5632018-08-01 15:53:42 +0900104 def visit_importfrom(self, node):
Allen Li4c275d22017-07-19 11:56:24 -0700105 """Patches modnames so pylints understands autotest_lib."""
beeps48fc6f52013-01-07 14:36:40 -0800106 node.modname = patch_modname(node.modname)
Ryo Hashimotod35d5632018-08-01 15:53:42 +0900107 return super(CustomImportsChecker, self).visit_importfrom(node)
beeps48fc6f52013-01-07 14:36:40 -0800108
109
110class CustomVariablesChecker(variables.VariablesChecker):
111 """Modifies stock variables checker to suit autotest."""
112
113 def visit_module(self, node):
114 """
115 Unflag 'import common'.
116
117 _to_consume eg: [({to reference}, {referenced}, 'scope type')]
118 Enteries are appended to this list as we drill deeper in scope.
beepse19d3032013-05-30 09:22:07 -0700119 If we ever come across a module to ignore, we immediately move it
beeps48fc6f52013-01-07 14:36:40 -0800120 to the consumed list.
121
122 @param node: node of the ast we're currently checking.
123 """
124 super(CustomVariablesChecker, self).visit_module(node)
125 scoped_names = self._to_consume.pop()
Kazuhiro Inaba0e7bd162019-06-07 16:24:45 +0900126 # The type of the object has changed in pylint 1.8.2
127 if pylint_version_parsed >= (1, 8, 2):
128 patch_consumed_list(scoped_names.to_consume,scoped_names.consumed)
129 else:
130 patch_consumed_list(scoped_names[0],scoped_names[1])
beeps48fc6f52013-01-07 14:36:40 -0800131 self._to_consume.append(scoped_names)
132
Ryo Hashimotod35d5632018-08-01 15:53:42 +0900133 def visit_importfrom(self, node):
beeps48fc6f52013-01-07 14:36:40 -0800134 """Patches modnames so pylints understands autotest_lib."""
135 node.modname = patch_modname(node.modname)
Ryo Hashimotod35d5632018-08-01 15:53:42 +0900136 return super(CustomVariablesChecker, self).visit_importfrom(node)
mbligh99d2ded2008-06-23 16:17:36 +0000137
Mike Frysingercb574632019-09-20 10:43:45 -0400138 def visit_expr(self, node):
139 """
140 Flag exceptions instantiated but not used.
141
142 https://crbug.com/1005893
143 """
144 if not isinstance(node.value, astroid.Call):
145 return
146 func = node.value.func
147 try:
148 cls = next(func.infer())
149 except astroid.InferenceError:
150 return
151 if not isinstance(cls, astroid.ClassDef):
152 return
153 if any(x for x in cls.ancestors() if x.name == 'BaseException'):
154 self.add_message('W0104', node=node, line=node.fromlineno)
155
beeps2c669642013-01-14 18:30:57 -0800156
157class CustomDocStringChecker(base.DocStringChecker):
158 """Modifies stock docstring checker to suit Autotest doxygen style."""
159
beeps1b6433b2013-01-31 17:46:50 -0800160 def visit_module(self, node):
161 """
162 Don't visit imported modules when checking for docstrings.
163
164 @param node: the node we're visiting.
165 """
166 pass
167
168
Ryo Hashimotod35d5632018-08-01 15:53:42 +0900169 def visit_functiondef(self, node):
beeps98365d82013-02-20 20:08:07 -0800170 """
171 Don't request docstrings for commonly overridden autotest functions.
172
173 @param node: node of the ast we're currently checking.
174 """
beepsc38decc2013-04-29 19:42:06 -0700175
176 # Even plain functions will have a parent, which is the
177 # module they're in, and a frame, which is the context
178 # of said module; They need not however, always have
179 # ancestors.
beeps98365d82013-02-20 20:08:07 -0800180 if (node.name in ('run_once', 'initialize', 'cleanup') and
beepsc38decc2013-04-29 19:42:06 -0700181 hasattr(node.parent.frame(), 'ancestors') and
beeps98365d82013-02-20 20:08:07 -0800182 any(ancestor.name == 'base_test' for ancestor in
183 node.parent.frame().ancestors())):
184 return
185
Prathmesh Prabhua9980672017-07-13 15:52:02 -0700186 if _is_test_case_method(node):
187 return
188
Ryo Hashimotod35d5632018-08-01 15:53:42 +0900189 super(CustomDocStringChecker, self).visit_functiondef(node)
beeps98365d82013-02-20 20:08:07 -0800190
191
Chris Sosaae6acd92013-02-06 15:08:04 -0800192 @staticmethod
193 def _should_skip_arg(arg):
beeps98365d82013-02-20 20:08:07 -0800194 """
195 @return: True if the argument given by arg is whitelisted, and does
196 not require a "@param" docstring.
197 """
Chris Sosaae6acd92013-02-06 15:08:04 -0800198 return arg in ('self', 'cls', 'args', 'kwargs', 'dargs')
199
beeps2c669642013-01-14 18:30:57 -0800200base.DocStringChecker = CustomDocStringChecker
mbligh99d2ded2008-06-23 16:17:36 +0000201imports.ImportsChecker = CustomImportsChecker
beeps48fc6f52013-01-07 14:36:40 -0800202variables.VariablesChecker = CustomVariablesChecker
mbligh99d2ded2008-06-23 16:17:36 +0000203
204
beeps98365d82013-02-20 20:08:07 -0800205def batch_check_files(file_paths, base_opts):
206 """
207 Run pylint on a list of files so we get consolidated errors.
208
209 @param file_paths: a list of file paths.
210 @param base_opts: a list of pylint config options.
211
Raul E Rangel67d9cfc2019-01-30 16:28:16 -0700212 @returns pylint return code
213
beeps98365d82013-02-20 20:08:07 -0800214 @raises: pylint_error if pylint finds problems with a file
215 in this commit.
216 """
beeps12a3c882013-04-22 13:42:04 -0700217 if not file_paths:
Raul E Rangel67d9cfc2019-01-30 16:28:16 -0700218 return 0
beeps12a3c882013-04-22 13:42:04 -0700219
beeps98365d82013-02-20 20:08:07 -0800220 pylint_runner = pylint.lint.Run(list(base_opts) + list(file_paths),
221 exit=False)
Raul E Rangel67d9cfc2019-01-30 16:28:16 -0700222 return pylint_runner.linter.msg_status
beeps98365d82013-02-20 20:08:07 -0800223
224
225def should_check_file(file_path):
226 """
227 Don't check blacklisted or non .py files.
228
229 @param file_path: abs path of file to check.
230 @return: True if this file is a non-blacklisted python file.
231 """
232 file_path = os.path.abspath(file_path)
233 if file_path.endswith('.py'):
234 return all(not fnmatch.fnmatch(file_path, '*' + pattern)
235 for pattern in BLACKLIST)
236 return False
237
238
beeps48fc6f52013-01-07 14:36:40 -0800239def check_file(file_path, base_opts):
240 """
241 Invokes pylint on files after confirming that they're not black listed.
242
beeps2c669642013-01-14 18:30:57 -0800243 @param base_opts: pylint base options.
beeps48fc6f52013-01-07 14:36:40 -0800244 @param file_path: path to the file we need to run pylint on.
Raul E Rangel67d9cfc2019-01-30 16:28:16 -0700245
246 @returns pylint return code
beeps48fc6f52013-01-07 14:36:40 -0800247 """
beeps98365d82013-02-20 20:08:07 -0800248 if not isinstance(file_path, basestring):
249 raise TypeError('expected a string as filepath, got %s'%
250 type(file_path))
251
252 if should_check_file(file_path):
253 pylint_runner = pylint.lint.Run(base_opts + [file_path], exit=False)
Raul E Rangel67d9cfc2019-01-30 16:28:16 -0700254
255 return pylint_runner.linter.msg_status
256
257 return 0
mbligh99d2ded2008-06-23 16:17:36 +0000258
259
260def visit(arg, dirname, filenames):
beeps48fc6f52013-01-07 14:36:40 -0800261 """
262 Visit function invoked in check_dir.
263
264 @param arg: arg from os.walk.path
265 @param dirname: dir from os.walk.path
266 @param filenames: files in dir from os.walk.path
267 """
mbligh99d2ded2008-06-23 16:17:36 +0000268 for filename in filenames:
Raul E Rangel67d9cfc2019-01-30 16:28:16 -0700269 arg.append(os.path.join(dirname, filename))
mbligh99d2ded2008-06-23 16:17:36 +0000270
271
beeps48fc6f52013-01-07 14:36:40 -0800272def check_dir(dir_path, base_opts):
273 """
274 Calls visit on files in dir_path.
275
beeps2c669642013-01-14 18:30:57 -0800276 @param base_opts: pylint base options.
beeps48fc6f52013-01-07 14:36:40 -0800277 @param dir_path: path to directory.
Raul E Rangel67d9cfc2019-01-30 16:28:16 -0700278
279 @returns pylint return code
beeps48fc6f52013-01-07 14:36:40 -0800280 """
Raul E Rangel67d9cfc2019-01-30 16:28:16 -0700281 files = []
282
283 os.path.walk(dir_path, visit, files)
284
285 return batch_check_files(files, base_opts)
mbligh99d2ded2008-06-23 16:17:36 +0000286
287
beeps48fc6f52013-01-07 14:36:40 -0800288def extend_baseopts(base_opts, new_opt):
289 """
290 Replaces an argument in base_opts with a cmd line argument.
291
292 @param base_opts: original pylint_base_opts.
293 @param new_opt: new cmd line option.
294 """
295 for args in base_opts:
296 if new_opt in args:
297 base_opts.remove(args)
298 base_opts.append(new_opt)
299
300
301def get_cmdline_options(args_list, pylint_base_opts, rcfile):
302 """
303 Parses args_list and extends pylint_base_opts.
304
305 Command line arguments might include options mixed with files.
306 Go through this list and filter out the options, if the options are
307 specified in the pylintrc file we cannot replace them and the file
308 needs to be edited. If the options are already a part of
309 pylint_base_opts we replace them, and if not we append to
310 pylint_base_opts.
311
312 @param args_list: list of files/pylint args passed in through argv.
313 @param pylint_base_opts: default pylint options.
314 @param rcfile: text from pylint_rc.
315 """
316 for args in args_list:
317 if args.startswith('--'):
318 opt_name = args[2:].split('=')[0]
Raul E Rangeld214c092019-01-30 16:48:43 -0700319 extend_baseopts(pylint_base_opts, args)
320 args_list.remove(args)
beeps48fc6f52013-01-07 14:36:40 -0800321
beeps98365d82013-02-20 20:08:07 -0800322
323def git_show_to_temp_file(commit, original_file, new_temp_file):
324 """
325 'Git shows' the file in original_file to a tmp file with
326 the name new_temp_file. We need to preserve the filename
327 as it gets reflected in pylints error report.
328
329 @param commit: commit hash of the commit we're running repo upload on.
330 @param original_file: the path to the original file we'd like to run
331 'git show' on.
332 @param new_temp_file: new_temp_file is the path to a temp file we write the
333 output of 'git show' into.
334 """
335 git_repo = revision_control.GitRepo(common.autotest_dir, None, None,
336 common.autotest_dir)
337
338 with open(new_temp_file, 'w') as f:
339 output = git_repo.gitcmd('show --no-ext-diff %s:%s'
340 % (commit, original_file),
341 ignore_status=False).stdout
342 f.write(output)
343
344
345def check_committed_files(work_tree_files, commit, pylint_base_opts):
346 """
347 Get a list of files corresponding to the commit hash.
348
349 The contents of a file in the git work tree can differ from the contents
350 of a file in the commit we mean to upload. To work around this we run
351 pylint on a temp file into which we've 'git show'n the committed version
352 of each file.
353
354 @param work_tree_files: list of files in this commit specified by their
355 absolute path.
356 @param commit: hash of the commit this upload applies to.
357 @param pylint_base_opts: a list of pylint config options.
Raul E Rangel67d9cfc2019-01-30 16:28:16 -0700358
359 @returns pylint return code
beeps98365d82013-02-20 20:08:07 -0800360 """
361 files_to_check = filter(should_check_file, work_tree_files)
362
363 # Map the absolute path of each file so it's relative to the autotest repo.
364 # All files that are a part of this commit should have an abs path within
365 # the autotest repo, so this regex should never fail.
366 work_tree_files = [re.search(r'%s/(.*)' % common.autotest_dir, f).group(1)
367 for f in files_to_check]
368
369 tempdir = None
370 try:
371 tempdir = autotemp.tempdir()
372 temp_files = [os.path.join(tempdir.name, file_path.split('/')[-1:][0])
373 for file_path in work_tree_files]
374
375 for file_tuple in zip(work_tree_files, temp_files):
376 git_show_to_temp_file(commit, *file_tuple)
377 # Only check if we successfully git showed all files in the commit.
Raul E Rangel67d9cfc2019-01-30 16:28:16 -0700378 return batch_check_files(temp_files, pylint_base_opts)
beeps98365d82013-02-20 20:08:07 -0800379 finally:
380 if tempdir:
381 tempdir.clean()
382
383
Prathmesh Prabhua9980672017-07-13 15:52:02 -0700384def _is_test_case_method(node):
385 """Determine if the given function node is a method of a TestCase.
386
387 We simply check for 'TestCase' being one of the parent classes in the mro of
388 the containing class.
389
390 @params node: A function node.
391 """
392 if not hasattr(node.parent.frame(), 'ancestors'):
393 return False
394
395 parent_class_names = {x.name for x in node.parent.frame().ancestors()}
396 return 'TestCase' in parent_class_names
397
398
beeps48fc6f52013-01-07 14:36:40 -0800399def main():
400 """Main function checks each file in a commit for pylint violations."""
401
beeps1b6433b2013-01-31 17:46:50 -0800402 # For now all error/warning/refactor/convention exceptions except those in
403 # the enable string are disabled.
404 # W0611: All imported modules (except common) need to be used.
405 # W1201: Logging methods should take the form
406 # logging.<loggingmethod>(format_string, format_args...); and not
407 # logging.<loggingmethod>(format_string % (format_args...))
408 # C0111: Docstring needed. Also checks @param for each arg.
409 # C0112: Non-empty Docstring needed.
beeps48fc6f52013-01-07 14:36:40 -0800410 # Ideally we would like to enable as much as we can, but if we did so at
411 # this stage anyone who makes a tiny change to a file will be tasked with
412 # cleaning all the lint in it. See chromium-os:37364.
413
beepse19d3032013-05-30 09:22:07 -0700414 # Note:
415 # 1. There are three major sources of E1101/E1103/E1120 false positives:
416 # * common_lib.enum.Enum objects
417 # * DB model objects (scheduler models are the worst, but Django models
418 # also generate some errors)
419 # 2. Docstrings are optional on private methods, and any methods that begin
420 # with either 'set_' or 'get_'.
beeps48fc6f52013-01-07 14:36:40 -0800421 pylint_rc = os.path.join(os.path.dirname(os.path.abspath(__file__)),
422 'pylintrc')
beepse19d3032013-05-30 09:22:07 -0700423
424 no_docstring_rgx = r'((_.*)|(set_.*)|(get_.*))'
Kazuhiro Inaba0e7bd162019-06-07 16:24:45 +0900425 if pylint_version_parsed >= (0, 21):
beeps48fc6f52013-01-07 14:36:40 -0800426 pylint_base_opts = ['--rcfile=%s' % pylint_rc,
427 '--reports=no',
beeps55532562013-01-16 12:13:46 -0800428 '--disable=W,R,E,C,F',
Mike Frysingercb574632019-09-20 10:43:45 -0400429 '--enable=W0104,W0611,W1201,C0111,C0112,E0602,'
430 'W0601,E0633',
beepse19d3032013-05-30 09:22:07 -0700431 '--no-docstring-rgx=%s' % no_docstring_rgx,]
beeps48fc6f52013-01-07 14:36:40 -0800432 else:
433 all_failures = 'error,warning,refactor,convention'
434 pylint_base_opts = ['--disable-msg-cat=%s' % all_failures,
435 '--reports=no',
436 '--include-ids=y',
beeps1b6433b2013-01-31 17:46:50 -0800437 '--ignore-docstrings=n',
beepse19d3032013-05-30 09:22:07 -0700438 '--no-docstring-rgx=%s' % no_docstring_rgx,]
beeps48fc6f52013-01-07 14:36:40 -0800439
440 # run_pylint can be invoked directly with command line arguments,
441 # or through a presubmit hook which uses the arguments in pylintrc. In the
442 # latter case no command line arguments are passed. If it is invoked
443 # directly without any arguments, it should check all files in the cwd.
444 args_list = sys.argv[1:]
445 if args_list:
446 get_cmdline_options(args_list,
447 pylint_base_opts,
448 open(pylint_rc).read())
Raul E Rangel67d9cfc2019-01-30 16:28:16 -0700449 return batch_check_files(args_list, pylint_base_opts)
beeps98365d82013-02-20 20:08:07 -0800450 elif os.environ.get('PRESUBMIT_FILES') is not None:
Raul E Rangel67d9cfc2019-01-30 16:28:16 -0700451 return check_committed_files(
beeps98365d82013-02-20 20:08:07 -0800452 os.environ.get('PRESUBMIT_FILES').split('\n'),
453 os.environ.get('PRESUBMIT_COMMIT'),
454 pylint_base_opts)
beeps48fc6f52013-01-07 14:36:40 -0800455 else:
Raul E Rangel67d9cfc2019-01-30 16:28:16 -0700456 return check_dir('.', pylint_base_opts)
beeps48fc6f52013-01-07 14:36:40 -0800457
458
459if __name__ == '__main__':
Prashanth Balasubramanianbaabef22014-11-04 12:38:44 -0800460 try:
Raul E Rangel67d9cfc2019-01-30 16:28:16 -0700461 ret = main()
462
463 sys.exit(ret)
Prathmesh Prabhu7e107082017-01-11 17:16:41 -0800464 except pylint_error as e:
Prashanth Balasubramanianbaabef22014-11-04 12:38:44 -0800465 logging.error(e)
466 sys.exit(1)