• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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