1#!/usr/bin/env vpython 2 3# [VPYTHON:BEGIN] 4# # Third party dependencies. These are only listed because pylint itself needs 5# # them. Feel free to add/remove anything here. 6# 7# wheel: < 8# name: "infra/python/wheels/configparser-py2_py3" 9# version: "version:3.5.0" 10# > 11# wheel: < 12# name: "infra/python/wheels/futures-py2_py3" 13# version: "version:3.1.1" 14# > 15# wheel: < 16# name: "infra/python/wheels/isort-py2_py3" 17# version: "version:4.3.4" 18# > 19# wheel: < 20# name: "infra/python/wheels/wrapt/${vpython_platform}" 21# version: "version:1.10.11" 22# > 23# wheel: < 24# name: "infra/python/wheels/backports_functools_lru_cache-py2_py3" 25# version: "version:1.5" 26# > 27# wheel: < 28# name: "infra/python/wheels/lazy-object-proxy/${vpython_platform}" 29# version: "version:1.3.1" 30# > 31# wheel: < 32# name: "infra/python/wheels/singledispatch-py2_py3" 33# version: "version:3.4.0.3" 34# > 35# wheel: < 36# name: "infra/python/wheels/enum34-py2" 37# version: "version:1.1.6" 38# > 39# wheel: < 40# name: "infra/python/wheels/mccabe-py2_py3" 41# version: "version:0.6.1" 42# > 43# wheel: < 44# name: "infra/python/wheels/six-py2_py3" 45# version: "version:1.10.0" 46# > 47# 48# # Pylint dependencies. 49# 50# wheel: < 51# name: "infra/python/wheels/astroid-py2_py3" 52# version: "version:1.6.6" 53# > 54# 55# wheel: < 56# name: "infra/python/wheels/pylint-py2_py3" 57# version: "version:1.9.5-45a720817e4de1df2f173c7e4029e176" 58# > 59# [VPYTHON:END] 60 61""" 62Wrapper to patch pylint library functions to suit autotest. 63 64This script is invoked as part of the presubmit checks for autotest python 65files. It runs pylint on a list of files that it obtains either through 66the command line or from an environment variable set in pre-upload.py. 67 68Example: 69run_pylint.py filename.py 70""" 71 72import fnmatch 73import logging 74import os 75import re 76import sys 77 78import common 79from autotest_lib.client.common_lib import autotemp, revision_control 80 81# Do a basic check to see if pylint is even installed. 82try: 83 import pylint 84 from pylint.__pkginfo__ import version as pylint_version 85except ImportError: 86 print ("Unable to import pylint, it may need to be installed." 87 " Run 'sudo aptitude install pylint' if you haven't already.") 88 sys.exit(1) 89 90pylint_version_parsed = tuple(map(int, pylint_version.split('.'))) 91 92# some files make pylint blow up, so make sure we ignore them 93SKIPLIST = ['/site-packages/*', '/contrib/*', '/frontend/afe/management.py'] 94 95import astroid 96import pylint.lint 97from pylint.checkers import base, imports, variables 98 99# need to put autotest root dir on sys.path so pylint will be happy 100autotest_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 101sys.path.insert(0, autotest_root) 102 103# patch up pylint import checker to handle our importing magic 104ROOT_MODULE = 'autotest_lib.' 105 106# A list of modules for pylint to ignore, specifically, these modules 107# are imported for their side-effects and are not meant to be used. 108_IGNORE_MODULES=['common', 'frontend_test_utils', 109 'setup_django_environment', 110 'setup_django_lite_environment', 111 'setup_django_readonly_environment', 'setup_test_environment',] 112 113 114class pylint_error(Exception): 115 """ 116 Error raised when pylint complains about a file. 117 """ 118 119 120class run_pylint_error(pylint_error): 121 """ 122 Error raised when an assumption made in this file is violated. 123 """ 124 125 126def patch_modname(modname): 127 """ 128 Patches modname so we can make sense of autotest_lib modules. 129 130 @param modname: name of a module, contains '.' 131 @return modified modname string. 132 """ 133 if modname.startswith(ROOT_MODULE) or modname.startswith(ROOT_MODULE[:-1]): 134 modname = modname[len(ROOT_MODULE):] 135 return modname 136 137 138def patch_consumed_list(to_consume=None, consumed=None): 139 """ 140 Patches the consumed modules list to ignore modules with side effects. 141 142 Autotest relies on importing certain modules solely for their side 143 effects. Pylint doesn't understand this and flags them as unused, since 144 they're not referenced anywhere in the code. To overcome this we need 145 to transplant said modules into the dictionary of modules pylint has 146 already seen, before pylint checks it. 147 148 @param to_consume: a dictionary of names pylint needs to see referenced. 149 @param consumed: a dictionary of names that pylint has seen referenced. 150 """ 151 ignore_modules = [] 152 if (to_consume is not None and consumed is not None): 153 ignore_modules = [module_name for module_name in _IGNORE_MODULES 154 if module_name in to_consume] 155 156 for module_name in ignore_modules: 157 consumed[module_name] = to_consume[module_name] 158 del to_consume[module_name] 159 160 161class CustomImportsChecker(imports.ImportsChecker): 162 """Modifies stock imports checker to suit autotest.""" 163 def visit_importfrom(self, node): 164 """Patches modnames so pylints understands autotest_lib.""" 165 node.modname = patch_modname(node.modname) 166 return super(CustomImportsChecker, self).visit_importfrom(node) 167 168 169class CustomVariablesChecker(variables.VariablesChecker): 170 """Modifies stock variables checker to suit autotest.""" 171 172 def visit_module(self, node): 173 """ 174 Unflag 'import common'. 175 176 _to_consume eg: [({to reference}, {referenced}, 'scope type')] 177 Enteries are appended to this list as we drill deeper in scope. 178 If we ever come across a module to ignore, we immediately move it 179 to the consumed list. 180 181 @param node: node of the ast we're currently checking. 182 """ 183 super(CustomVariablesChecker, self).visit_module(node) 184 scoped_names = self._to_consume.pop() 185 # The type of the object has changed in pylint 1.8.2 186 if pylint_version_parsed >= (1, 8, 2): 187 patch_consumed_list(scoped_names.to_consume,scoped_names.consumed) 188 else: 189 patch_consumed_list(scoped_names[0],scoped_names[1]) 190 self._to_consume.append(scoped_names) 191 192 def visit_importfrom(self, node): 193 """Patches modnames so pylints understands autotest_lib.""" 194 node.modname = patch_modname(node.modname) 195 return super(CustomVariablesChecker, self).visit_importfrom(node) 196 197 def visit_expr(self, node): 198 """ 199 Flag exceptions instantiated but not used. 200 201 https://crbug.com/1005893 202 """ 203 if not isinstance(node.value, astroid.Call): 204 return 205 func = node.value.func 206 try: 207 cls = next(func.infer()) 208 except astroid.InferenceError: 209 return 210 if not isinstance(cls, astroid.ClassDef): 211 return 212 if any(x for x in cls.ancestors() if x.name == 'BaseException'): 213 self.add_message('W0104', node=node, line=node.fromlineno) 214 215 216class CustomDocStringChecker(base.DocStringChecker): 217 """Modifies stock docstring checker to suit Autotest doxygen style.""" 218 219 def visit_module(self, node): 220 """ 221 Don't visit imported modules when checking for docstrings. 222 223 @param node: the node we're visiting. 224 """ 225 pass 226 227 228 def visit_functiondef(self, node): 229 """ 230 Don't request docstrings for commonly overridden autotest functions. 231 232 @param node: node of the ast we're currently checking. 233 """ 234 235 # Even plain functions will have a parent, which is the 236 # module they're in, and a frame, which is the context 237 # of said module; They need not however, always have 238 # ancestors. 239 if (node.name in ('run_once', 'initialize', 'cleanup') and 240 hasattr(node.parent.frame(), 'ancestors') and 241 any(ancestor.name == 'base_test' for ancestor in 242 node.parent.frame().ancestors())): 243 return 244 245 if _is_test_case_method(node): 246 return 247 248 super(CustomDocStringChecker, self).visit_functiondef(node) 249 250 251 @staticmethod 252 def _should_skip_arg(arg): 253 """ 254 @return: True if the argument given by arg is allowlisted, and does 255 not require a "@param" docstring. 256 """ 257 return arg in ('self', 'cls', 'args', 'kwargs', 'dargs') 258 259base.DocStringChecker = CustomDocStringChecker 260imports.ImportsChecker = CustomImportsChecker 261variables.VariablesChecker = CustomVariablesChecker 262 263 264def batch_check_files(file_paths, base_opts): 265 """ 266 Run pylint on a list of files so we get consolidated errors. 267 268 @param file_paths: a list of file paths. 269 @param base_opts: a list of pylint config options. 270 271 @returns pylint return code 272 273 @raises: pylint_error if pylint finds problems with a file 274 in this commit. 275 """ 276 if not file_paths: 277 return 0 278 279 pylint_runner = pylint.lint.Run(list(base_opts) + list(file_paths), 280 exit=False) 281 return pylint_runner.linter.msg_status 282 283 284def should_check_file(file_path): 285 """ 286 Don't check skiplisted or non .py files. 287 288 @param file_path: abs path of file to check. 289 @return: True if this file is a non-skiplisted python file. 290 """ 291 file_path = os.path.abspath(file_path) 292 if file_path.endswith('.py'): 293 return all(not fnmatch.fnmatch(file_path, '*' + pattern) 294 for pattern in SKIPLIST) 295 return False 296 297 298def check_file(file_path, base_opts): 299 """ 300 Invokes pylint on files after confirming that they're not black listed. 301 302 @param base_opts: pylint base options. 303 @param file_path: path to the file we need to run pylint on. 304 305 @returns pylint return code 306 """ 307 if not isinstance(file_path, basestring): 308 raise TypeError('expected a string as filepath, got %s'% 309 type(file_path)) 310 311 if should_check_file(file_path): 312 pylint_runner = pylint.lint.Run(base_opts + [file_path], exit=False) 313 314 return pylint_runner.linter.msg_status 315 316 return 0 317 318 319def visit(arg, dirname, filenames): 320 """ 321 Visit function invoked in check_dir. 322 323 @param arg: arg from os.walk.path 324 @param dirname: dir from os.walk.path 325 @param filenames: files in dir from os.walk.path 326 """ 327 for filename in filenames: 328 arg.append(os.path.join(dirname, filename)) 329 330 331def check_dir(dir_path, base_opts): 332 """ 333 Calls visit on files in dir_path. 334 335 @param base_opts: pylint base options. 336 @param dir_path: path to directory. 337 338 @returns pylint return code 339 """ 340 files = [] 341 342 os.path.walk(dir_path, visit, files) 343 344 return batch_check_files(files, base_opts) 345 346 347def extend_baseopts(base_opts, new_opt): 348 """ 349 Replaces an argument in base_opts with a cmd line argument. 350 351 @param base_opts: original pylint_base_opts. 352 @param new_opt: new cmd line option. 353 """ 354 for args in base_opts: 355 if new_opt in args: 356 base_opts.remove(args) 357 base_opts.append(new_opt) 358 359 360def get_cmdline_options(args_list, pylint_base_opts, rcfile): 361 """ 362 Parses args_list and extends pylint_base_opts. 363 364 Command line arguments might include options mixed with files. 365 Go through this list and filter out the options, if the options are 366 specified in the pylintrc file we cannot replace them and the file 367 needs to be edited. If the options are already a part of 368 pylint_base_opts we replace them, and if not we append to 369 pylint_base_opts. 370 371 @param args_list: list of files/pylint args passed in through argv. 372 @param pylint_base_opts: default pylint options. 373 @param rcfile: text from pylint_rc. 374 """ 375 for args in args_list: 376 if args.startswith('--'): 377 opt_name = args[2:].split('=')[0] 378 extend_baseopts(pylint_base_opts, args) 379 args_list.remove(args) 380 381 382def git_show_to_temp_file(commit, original_file, new_temp_file): 383 """ 384 'Git shows' the file in original_file to a tmp file with 385 the name new_temp_file. We need to preserve the filename 386 as it gets reflected in pylints error report. 387 388 @param commit: commit hash of the commit we're running repo upload on. 389 @param original_file: the path to the original file we'd like to run 390 'git show' on. 391 @param new_temp_file: new_temp_file is the path to a temp file we write the 392 output of 'git show' into. 393 """ 394 git_repo = revision_control.GitRepo(common.autotest_dir, None, None, 395 common.autotest_dir) 396 397 with open(new_temp_file, 'w') as f: 398 output = git_repo.gitcmd('show --no-ext-diff %s:%s' 399 % (commit, original_file), 400 ignore_status=False).stdout 401 f.write(output) 402 403 404def check_committed_files(work_tree_files, commit, pylint_base_opts): 405 """ 406 Get a list of files corresponding to the commit hash. 407 408 The contents of a file in the git work tree can differ from the contents 409 of a file in the commit we mean to upload. To work around this we run 410 pylint on a temp file into which we've 'git show'n the committed version 411 of each file. 412 413 @param work_tree_files: list of files in this commit specified by their 414 absolute path. 415 @param commit: hash of the commit this upload applies to. 416 @param pylint_base_opts: a list of pylint config options. 417 418 @returns pylint return code 419 """ 420 files_to_check = filter(should_check_file, work_tree_files) 421 422 # Map the absolute path of each file so it's relative to the autotest repo. 423 # All files that are a part of this commit should have an abs path within 424 # the autotest repo, so this regex should never fail. 425 work_tree_files = [re.search(r'%s/(.*)' % common.autotest_dir, f).group(1) 426 for f in files_to_check] 427 428 tempdir = None 429 try: 430 tempdir = autotemp.tempdir() 431 temp_files = [os.path.join(tempdir.name, file_path.split('/')[-1:][0]) 432 for file_path in work_tree_files] 433 434 for file_tuple in zip(work_tree_files, temp_files): 435 git_show_to_temp_file(commit, *file_tuple) 436 # Only check if we successfully git showed all files in the commit. 437 return batch_check_files(temp_files, pylint_base_opts) 438 finally: 439 if tempdir: 440 tempdir.clean() 441 442 443def _is_test_case_method(node): 444 """Determine if the given function node is a method of a TestCase. 445 446 We simply check for 'TestCase' being one of the parent classes in the mro of 447 the containing class. 448 449 @params node: A function node. 450 """ 451 if not hasattr(node.parent.frame(), 'ancestors'): 452 return False 453 454 parent_class_names = {x.name for x in node.parent.frame().ancestors()} 455 return 'TestCase' in parent_class_names 456 457 458def main(): 459 """Main function checks each file in a commit for pylint violations.""" 460 461 # For now all error/warning/refactor/convention exceptions except those in 462 # the enable string are disabled. 463 # W0611: All imported modules (except common) need to be used. 464 # W1201: Logging methods should take the form 465 # logging.<loggingmethod>(format_string, format_args...); and not 466 # logging.<loggingmethod>(format_string % (format_args...)) 467 # C0111: Docstring needed. Also checks @param for each arg. 468 # C0112: Non-empty Docstring needed. 469 # Ideally we would like to enable as much as we can, but if we did so at 470 # this stage anyone who makes a tiny change to a file will be tasked with 471 # cleaning all the lint in it. See chromium-os:37364. 472 473 # Note: 474 # 1. There are three major sources of E1101/E1103/E1120 false positives: 475 # * common_lib.enum.Enum objects 476 # * DB model objects (scheduler models are the worst, but Django models 477 # also generate some errors) 478 # 2. Docstrings are optional on private methods, and any methods that begin 479 # with either 'set_' or 'get_'. 480 pylint_rc = os.path.join(os.path.dirname(os.path.abspath(__file__)), 481 'pylintrc') 482 483 no_docstring_rgx = r'((_.*)|(set_.*)|(get_.*))' 484 if pylint_version_parsed >= (0, 21): 485 pylint_base_opts = ['--rcfile=%s' % pylint_rc, 486 '--reports=no', 487 '--disable=W,R,E,C,F', 488 '--enable=W0104,W0611,W1201,C0111,C0112,E0602,' 489 'W0601,E0633', 490 '--no-docstring-rgx=%s' % no_docstring_rgx,] 491 else: 492 all_failures = 'error,warning,refactor,convention' 493 pylint_base_opts = ['--disable-msg-cat=%s' % all_failures, 494 '--reports=no', 495 '--include-ids=y', 496 '--ignore-docstrings=n', 497 '--no-docstring-rgx=%s' % no_docstring_rgx,] 498 499 # run_pylint can be invoked directly with command line arguments, 500 # or through a presubmit hook which uses the arguments in pylintrc. In the 501 # latter case no command line arguments are passed. If it is invoked 502 # directly without any arguments, it should check all files in the cwd. 503 args_list = sys.argv[1:] 504 if args_list: 505 get_cmdline_options(args_list, 506 pylint_base_opts, 507 open(pylint_rc).read()) 508 return batch_check_files(args_list, pylint_base_opts) 509 elif os.environ.get('PRESUBMIT_FILES') is not None: 510 return check_committed_files( 511 os.environ.get('PRESUBMIT_FILES').split('\n'), 512 os.environ.get('PRESUBMIT_COMMIT'), 513 pylint_base_opts) 514 else: 515 return check_dir('.', pylint_base_opts) 516 517 518if __name__ == '__main__': 519 try: 520 ret = main() 521 522 sys.exit(ret) 523 except pylint_error as e: 524 logging.error(e) 525 sys.exit(1) 526