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