| # Copyright 2012 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Run lint checks on the specified files.""" |
| |
| import fnmatch |
| import functools |
| import importlib |
| import itertools |
| import json |
| import logging |
| import os |
| from pathlib import Path |
| import stat |
| from typing import Callable, Dict, List, Optional, Union |
| |
| from chromite.cli import command |
| from chromite.lib import commandline |
| from chromite.lib import constants |
| from chromite.lib import cros_build_lib |
| from chromite.lib import git |
| from chromite.lib import json_lib |
| from chromite.lib import osutils |
| from chromite.lib import parallel |
| from chromite.lib import path_util |
| from chromite.lint import linters |
| from chromite.utils import path_filter |
| from chromite.utils import timer |
| from chromite.utils.parser import shebang |
| |
| |
| def _GetProjectPath(path: Path) -> Path: |
| """Find the absolute path of the git checkout that contains |path|.""" |
| ret = git.FindGitTopLevel(path) |
| if ret: |
| return Path(ret) |
| else: |
| # Maybe they're running on a file outside of a checkout. |
| # e.g. cros lint ~/foo.py /tmp/test.py |
| return path.parent |
| |
| |
| def _get_file_data(path: Union[str, os.PathLike], commit: Optional[str]) -> str: |
| """Read the file data for |path| either from disk or git |commit|.""" |
| if commit: |
| return git.RunGit(None, ["show", f"{commit}:./{path}"]).stdout |
| else: |
| return Path(path).read_text(encoding="utf-8") |
| |
| |
| def _GetPylintrc(path: Path) -> Path: |
| """Locate pylintrc or .pylintrc file that applies to |path|. |
| |
| If not found - use the default. |
| """ |
| |
| def _test_func(pylintrc): |
| dotpylintrc = pylintrc.with_name(".pylintrc") |
| # Only allow one of these to exist to avoid confusing which one is used. |
| if pylintrc.exists() and dotpylintrc.exists(): |
| cros_build_lib.Die( |
| '%s: Only one of "pylintrc" or ".pylintrc" is allowed', |
| pylintrc.parent, |
| ) |
| return pylintrc.exists() or dotpylintrc.exists() |
| |
| end_path = _GetProjectPath(path.parent).parent |
| ret = osutils.FindInPathParents( |
| "pylintrc", path.parent, test_func=_test_func, end_path=end_path |
| ) |
| if ret: |
| return ret if ret.exists() else ret.with_name(".pylintrc") |
| return constants.CHROMITE_DIR / "pylintrc" |
| |
| |
| def _GetPythonPath(): |
| """Return the set of Python library paths to use.""" |
| # Carry through custom PYTHONPATH that the host env has set. |
| return os.environ.get("PYTHONPATH", "").split(os.pathsep) + [ |
| # Ideally we'd modify meta_path in pylint to handle our virtual chromite |
| # module, but that's not possible currently. We'll have to deal with |
| # that at some point if we want `cros lint` to work when the dir is not |
| # named 'chromite'. |
| constants.SOURCE_ROOT, |
| ] |
| |
| |
| # The mapping between the "cros lint" --output-format flag and cpplint.py |
| # --output flag. |
| CPPLINT_OUTPUT_FORMAT_MAP = { |
| "colorized": "emacs", |
| "msvs": "vs7", |
| "parseable": "emacs", |
| } |
| |
| # Default category filters to pass to cpplint.py when invoked via `cros lint`. |
| # |
| # `-foo/bar` means "don't show any lints from category foo/bar". |
| # See `cpplint.py --help` for more explanation of category filters. |
| CPPLINT_DEFAULT_FILTERS = ("-runtime/references",) |
| |
| |
| # The mapping between the "cros lint" --output-format flag and shellcheck |
| # flags. |
| # Note that the msvs mapping here isn't quite VS format, but it's closer than |
| # the default output. |
| SHLINT_OUTPUT_FORMAT_MAP = { |
| "colorized": ["--color=always"], |
| "msvs": ["--format=gcc"], |
| "parseable": ["--format=gcc"], |
| } |
| |
| |
| def _ToolRunCommand(cmd, debug, **kwargs): |
| """Run the linter with common run args set as higher levels expect.""" |
| return cros_build_lib.run( |
| cmd, check=False, print_cmd=debug, debug_level=logging.NOTICE, **kwargs |
| ) |
| |
| |
| def _ConfLintFile(path, output_format, debug, relaxed: bool, commit: str): |
| """Determine applicable .conf syntax and call the appropriate handler.""" |
| ret = cros_build_lib.CompletedProcess(f'cros lint "{path}"', returncode=0) |
| if not os.path.isfile(path): |
| return ret |
| |
| # .conf files are used by more than upstart, so use the parent dirname |
| # to filter them. |
| parent_name = os.path.basename(os.path.dirname(os.path.realpath(path))) |
| if parent_name in {"init", "upstart"}: |
| return _UpstartLintFile(path, output_format, debug, relaxed, commit) |
| |
| # Check for the description and author lines present in upstart configs. |
| with open(path, "rb") as file: |
| tokens_to_find = {b"author", b"description"} |
| for line in file: |
| try: |
| token = line.split()[0] |
| except IndexError: |
| continue |
| |
| try: |
| tokens_to_find.remove(token) |
| except KeyError: |
| continue |
| |
| if not tokens_to_find: |
| logging.warning( |
| "Found upstart .conf in a directory other than init or " |
| "upstart." |
| ) |
| return _UpstartLintFile( |
| path, output_format, debug, relaxed, commit |
| ) |
| return ret |
| |
| |
| @functools.lru_cache(maxsize=None) |
| def _cpplint_module(): |
| """Load the cpplint.py module. |
| |
| We can't import the module directly because it lives in a non-standard path |
| (depot_tools), and we don't want to add that to sys.path because it has a |
| lot of unrelated Python module we don't want polluting our normal search. |
| """ |
| modname = "cpplint" |
| cpplint = str(constants.DEPOT_TOOLS_DIR / "cpplint.py") |
| loader = importlib.machinery.SourceFileLoader(modname, cpplint) |
| spec = importlib.util.spec_from_loader(modname, loader) |
| module = importlib.util.module_from_spec(spec) |
| loader.exec_module(module) |
| return module |
| |
| |
| def _CpplintFile(path, output_format, _debug, _relaxed: bool, commit: str): |
| """Returns result of running cpplint on |path|.""" |
| result = cros_build_lib.CompletedProcess(f'cpplint "{path}"', returncode=0) |
| |
| # pylint: disable=protected-access |
| # The cpplint API doesn't expose state that we need. We're pinned to a |
| # specific version, so we don't exactly have to worry about it changing. |
| cpplint = _cpplint_module() |
| cpplint._SetFilters(",".join(CPPLINT_DEFAULT_FILTERS)) |
| cpplint._SetOutputFormat( |
| "emacs" |
| if output_format == "default" |
| else CPPLINT_OUTPUT_FORMAT_MAP[output_format] |
| ) |
| if cpplint.ProcessConfigOverrides(str(path)): |
| data = _get_file_data(path, commit) |
| lines = data.split("\n") |
| ext = path.suffix[1:] or str(path) |
| cpplint.ProcessFileData(str(path), ext, lines, cpplint.Error) |
| result.returncode = 1 if cpplint._cpplint_state.error_count else 0 |
| |
| return result |
| |
| |
| def _PylintFile(path, output_format, debug, _relaxed: bool, _commit: str): |
| """Returns result of running pylint on |path|.""" |
| pylint = constants.CHROMITE_SCRIPTS_DIR / "pylint" |
| pylintrc = _GetPylintrc(path) |
| cmd = [pylint, "--rcfile=%s" % pylintrc] |
| if output_format != "default": |
| cmd.append("--output-format=%s" % output_format) |
| cmd.append(path) |
| extra_env = { |
| "PYTHONPATH": ":".join(_GetPythonPath()), |
| } |
| return _ToolRunCommand(cmd, debug, extra_env=extra_env) |
| |
| |
| def _GnlintFile(path, _, _debug, _relaxed: bool, commit: str): |
| """Returns result of running gnlint on |path|.""" |
| result = cros_build_lib.CompletedProcess(f'gnlint "{path}"', returncode=0) |
| |
| data = _get_file_data(path, commit) |
| if linters.gnlint.Data(data, path): |
| result.returncode = 1 |
| |
| return result |
| |
| |
| def _GolintFile(path, _, debug, _relaxed: bool, _commit: str): |
| """Returns result of running golint on |path|.""" |
| # Try using golint if it exists. |
| try: |
| cmd = ["golint", "-set_exit_status", path] |
| return _ToolRunCommand(cmd, debug) |
| except cros_build_lib.RunCommandError: |
| logging.notice("Install golint for additional go linting.") |
| return cros_build_lib.CompletedProcess(f'gofmt "{path}"', returncode=0) |
| |
| |
| def _JsonLintFile(path, _output_format, _debug, _relaxed: bool, commit: str): |
| """Returns result of running json lint checks on |path|.""" |
| result = cros_build_lib.CompletedProcess( |
| f'python -mjson.tool "{path}"', returncode=0 |
| ) |
| |
| data = _get_file_data(path, commit) |
| |
| # See if it validates. |
| try: |
| json_lib.loads(data) |
| except ValueError as e: |
| result.returncode = 1 |
| logging.notice("%s: %s", path, e) |
| |
| return result |
| |
| |
| def _MarkdownLintFile( |
| path, _output_format, _debug, _relaxed: bool, commit: str |
| ): |
| """Returns result of running lint checks on |path|.""" |
| result = cros_build_lib.CompletedProcess( |
| f'mdlint(internal) "{path}"', returncode=0 |
| ) |
| |
| data = _get_file_data(path, commit) |
| |
| # Check whitespace. |
| if not linters.whitespace.Data(data, Path(path)): |
| result.returncode = 1 |
| |
| return result |
| |
| |
| def _ShellLintFile( |
| path, |
| output_format, |
| debug, |
| _relaxed: bool, |
| _commit: str, |
| gentoo_format=False, |
| ): |
| """Returns result of running lint checks on |path|. |
| |
| Args: |
| path: The path to the script on which to run the linter. |
| output_format: The format of the output that the linter should emit. See |
| |SHLINT_OUTPUT_FORMAT_MAP|. |
| debug: Whether to print out the linter command. |
| gentoo_format: Whether to treat this file as an ebuild style script. |
| |
| Returns: |
| A CompletedProcess object. |
| """ |
| # Instruct shellcheck to run itself from the shell script's dir. Note that |
| # 'SCRIPTDIR' is a special string that shellcheck rewrites to the dirname of |
| # the given path. |
| extra_checks = [ |
| "avoid-nullary-conditions", # SC2244 |
| "check-unassigned-uppercase", # Include uppercase in SC2154 |
| "require-variable-braces", # SC2250 |
| ] |
| if not gentoo_format: |
| extra_checks.append("quote-safe-variables") # SC2248 |
| |
| cmd = [ |
| # pylint: disable=protected-access |
| linters.shell._find_shellcheck(), |
| "--source-path=SCRIPTDIR", |
| "--enable=%s" % ",".join(extra_checks), |
| ] |
| if output_format != "default": |
| cmd.extend(SHLINT_OUTPUT_FORMAT_MAP[output_format]) |
| cmd.append("-x") |
| # No warning for using local with /bin/sh. |
| cmd.append("--exclude=SC3043") |
| if gentoo_format: |
| # ebuilds don't explicitly export variables or contain a shebang. |
| cmd.append("--exclude=SC2148") |
| # ebuilds always use bash. |
| cmd.append("--shell=bash") |
| cmd.append(path) |
| |
| lint_result = _ToolRunCommand(cmd, debug) |
| |
| # Check whitespace. |
| if not linters.whitespace.Data(osutils.ReadFile(path), Path(path)): |
| lint_result.returncode = 1 |
| |
| return lint_result |
| |
| |
| def _GentooShellLintFile( |
| path, output_format, debug, relaxed: bool, commit: str |
| ): |
| """Run shell checks with Gentoo rules.""" |
| return _ShellLintFile( |
| path, output_format, debug, relaxed, commit, gentoo_format=True |
| ) |
| |
| |
| def _SeccompPolicyLintFile( |
| path, _output_format, debug, _relaxed: bool, commit: str |
| ): |
| """Run the seccomp policy linter.""" |
| dangerous_syscalls = { |
| "bpf", |
| "setns", |
| "execveat", |
| "ptrace", |
| "swapoff", |
| "swapon", |
| } |
| if commit: |
| stdin = _get_file_data(path, commit) |
| path = "/dev/stdin" |
| else: |
| stdin = "" |
| return _ToolRunCommand( |
| [ |
| os.path.join( |
| constants.SOURCE_ROOT, |
| "src", |
| "platform", |
| "minijail", |
| "tools", |
| "seccomp_policy_lint.py", |
| ), |
| "--dangerous-syscalls", |
| ",".join(dangerous_syscalls), |
| path, |
| ], |
| debug, |
| input=stdin, |
| ) |
| |
| |
| def _UpstartLintFile(path, _output_format, _debug, relaxed: bool, commit: str): |
| """Run lints on upstart configs.""" |
| # Skip .conf files that aren't in an init parent directory. |
| ret = cros_build_lib.CompletedProcess(f'cros lint "{path}"', returncode=0) |
| data = _get_file_data(path, commit) |
| if not linters.upstart.Data(data, Path(path), relaxed): |
| ret.returncode = 1 |
| return ret |
| |
| |
| def _DirMdLintFile(path, _output_format, debug, _relaxed: bool, commit: str): |
| """Run the dirmd linter.""" |
| data = _get_file_data(path, commit) |
| ret = _ToolRunCommand( |
| [constants.DEPOT_TOOLS_DIR / "dirmd", "parse"], |
| debug, |
| input=data, |
| stdout=True, |
| ) |
| if ret.returncode: |
| results = json.loads(ret.stdout) |
| print(path, results["stdin"]["error"]) |
| return ret |
| |
| |
| def _OwnersLintFile(path, _output_format, _debug, _relaxed: bool, commit: str): |
| """Run lints on OWNERS files.""" |
| ret = cros_build_lib.CompletedProcess(f'cros lint "{path}"', returncode=0) |
| data = _get_file_data(path, commit) |
| if not linters.owners.lint_data(Path(path), data): |
| ret.returncode = 1 |
| return ret |
| |
| |
| def _TextprotoLintFile( |
| path, _output_format, _debug, _relaxed: bool, _commit: str |
| ) -> cros_build_lib.CompletedProcess: |
| """Run lints on OWNERS files.""" |
| ret = cros_build_lib.CompletedProcess(f'cros lint "{path}"', returncode=0) |
| # go/textformat-spec#text-format-files says to use .textproto. |
| if os.path.splitext(path)[1] != ".textproto": |
| logging.error( |
| "%s: use '.textproto' extension for text proto messages", path |
| ) |
| ret.returncode = 1 |
| # TODO(build): Assert file header has `proto-file:` and `proto-message:` |
| # keywords in it. Also allow `proto-import:`, but ban all other `proto-` |
| # directives (in case of typos). go/textformat-schema |
| return ret |
| |
| |
| def _WhitespaceLintFile( |
| path, _output_format, _debug, _relaxed: bool, commit: str |
| ): |
| """Returns result of running basic whitespace checks on |path|.""" |
| result = cros_build_lib.CompletedProcess( |
| f'whitespace(internal) "{path}"', returncode=0 |
| ) |
| |
| data = _get_file_data(path, commit) |
| |
| # Check whitespace. |
| if not linters.whitespace.Data(data, Path(path)): |
| result.returncode = 1 |
| |
| return result |
| |
| |
| def _NonExecLintFile(path, _output_format, _debug, _relaxed: bool, commit: str): |
| """Check file permissions on |path| are -x.""" |
| result = cros_build_lib.CompletedProcess( |
| f'stat(internal) "{path}"', returncode=0 |
| ) |
| |
| if commit: |
| entries = git.LsTree( |
| os.path.dirname(path) or None, commit, [os.path.basename(path)] |
| ) |
| if entries and entries[0].is_exec: |
| result.returncode = 1 |
| logging.notice( |
| "%s: file should not be executable; chmod -x to fix", path |
| ) |
| else: |
| # Ignore symlinks. |
| st = os.lstat(path) |
| if stat.S_ISREG(st.st_mode): |
| mode = stat.S_IMODE(st.st_mode) |
| if mode & 0o111: |
| result.returncode = 1 |
| logging.notice( |
| "%s: file should not be executable; chmod -x to fix", path |
| ) |
| |
| return result |
| |
| |
| def _BreakoutDataByTool(map_to_return, path): |
| """Maps a tool method to the content of the |path|.""" |
| # Detect by content of the file itself. |
| try: |
| with open(path, "rb") as fp: |
| # We read 128 bytes because that's the Linux kernel's current limit. |
| # Look for BINPRM_BUF_SIZE in fs/binfmt_script.c. |
| data = fp.read(128) |
| |
| try: |
| result = shebang.parse(data) |
| except ValueError: |
| # If the file doesn't have a shebang, nothing to do. |
| return |
| |
| basename = os.path.basename(result.real_command) |
| if basename.startswith("python") or basename.startswith("vpython"): |
| for tool in _EXT_TOOL_MAP[frozenset({".py"})]: |
| map_to_return.setdefault(tool, []).append(path) |
| elif basename in ("sh", "dash", "bash"): |
| for tool in _EXT_TOOL_MAP[frozenset({".sh"})]: |
| map_to_return.setdefault(tool, []).append(path) |
| except IOError as e: |
| logging.debug("%s: reading initial data failed: %s", path, e) |
| |
| |
| # Map file extensions to a tool function. |
| _EXT_TOOL_MAP = { |
| # Note these are defined to keep in line with cpplint.py. Technically, we |
| # could include additional ones, but cpplint.py would just filter them out. |
| frozenset({".c"}): (_WhitespaceLintFile, _NonExecLintFile), |
| frozenset({".cc", ".cpp", ".h"}): (_CpplintFile, _NonExecLintFile), |
| frozenset({".conf", ".conf.in"}): (_ConfLintFile, _NonExecLintFile), |
| frozenset({".gn", ".gni"}): (_GnlintFile, _NonExecLintFile), |
| frozenset({".json", ".jsonproto"}): (_JsonLintFile, _NonExecLintFile), |
| frozenset({".py"}): (_PylintFile,), |
| frozenset({".go"}): (_GolintFile, _NonExecLintFile), |
| frozenset({".sh"}): (_ShellLintFile,), |
| frozenset({".ebuild", ".eclass", ".bashrc"}): ( |
| _GentooShellLintFile, |
| _NonExecLintFile, |
| ), |
| frozenset({".md"}): (_MarkdownLintFile, _NonExecLintFile), |
| # Yes, there's a lot of variations here. We catch these specifically to |
| # throw errors and force people to use the single correct name. |
| frozenset( |
| { |
| ".pb", |
| ".pb.txt", |
| ".pb.text", |
| ".pbtxt", |
| ".pbtext", |
| ".protoascii", |
| ".prototxt", |
| ".prototext", |
| ".textpb", |
| ".txtpb", |
| ".textproto", |
| ".txtproto", |
| } |
| ): ( |
| _TextprotoLintFile, |
| _NonExecLintFile, |
| ), |
| frozenset({".policy"}): ( |
| _SeccompPolicyLintFile, |
| _WhitespaceLintFile, |
| _NonExecLintFile, |
| ), |
| frozenset({".te"}): (_WhitespaceLintFile, _NonExecLintFile), |
| frozenset( |
| { |
| ".bzl", |
| ".cfg", |
| ".config", |
| ".css", |
| ".grd", |
| ".gyp", |
| ".gypi", |
| ".htm", |
| ".html", |
| ".ini", |
| ".jpeg", |
| ".jpg", |
| ".js", |
| ".l", |
| ".mk", |
| ".patch", |
| ".png", |
| ".proto", |
| ".rules", |
| ".service", |
| ".star", |
| ".svg", |
| ".toml", |
| ".txt", |
| ".xml", |
| ".xtb", |
| ".y", |
| ".yaml", |
| ".yml", |
| } |
| ): (_NonExecLintFile,), |
| } |
| |
| # Map known filenames to a tool function. |
| _FILENAME_PATTERNS_TOOL_MAP = { |
| frozenset({".gn"}): (_GnlintFile, _NonExecLintFile), |
| frozenset({"DIR_METADATA"}): (_DirMdLintFile, _NonExecLintFile), |
| frozenset({"OWNERS*"}): (_OwnersLintFile, _NonExecLintFile), |
| frozenset({"Dockerfile", "Makefile"}): (_NonExecLintFile,), |
| } |
| |
| |
| def _BreakoutFilesByTool(files: List[Path]) -> Dict[Callable, List[Path]]: |
| """Maps a tool method to the list of files to process.""" |
| map_to_return = {} |
| |
| for f in files: |
| extension = f.suffix |
| for extensions, tools in _EXT_TOOL_MAP.items(): |
| if extension in extensions: |
| for tool in tools: |
| map_to_return.setdefault(tool, []).append(f) |
| break |
| else: |
| for patterns, tools in _FILENAME_PATTERNS_TOOL_MAP.items(): |
| if any(fnmatch.fnmatch(f.name, x) for x in patterns): |
| for tool in tools: |
| map_to_return.setdefault(tool, []).append(f) |
| break |
| else: |
| if f.is_file(): |
| _BreakoutDataByTool(map_to_return, f) |
| |
| return map_to_return |
| |
| |
| def _Dispatcher( |
| output_format, debug, relaxed: bool, commit: str, tool, path: Path |
| ) -> int: |
| """Call |tool| on |path| and take care of coalescing exit codes/output.""" |
| try: |
| result = tool(path, output_format, debug, relaxed, commit) |
| except UnicodeDecodeError: |
| logging.error("%s: file is not UTF-8 compatible", path) |
| return 1 |
| return 1 if result.returncode else 0 |
| |
| |
| @command.command_decorator("lint") |
| class LintCommand(command.CliCommand): |
| """Run lint checks on the specified files.""" |
| |
| EPILOG = """ |
| For some file formats, see the CrOS style guide: |
| https://chromium.googlesource.com/chromiumos/docs/+/HEAD/styleguide/ |
| |
| Supported file formats: %s |
| Supported file names: %s |
| """ % ( |
| " ".join(sorted(itertools.chain(*_EXT_TOOL_MAP))), |
| " ".join(sorted(itertools.chain(*_FILENAME_PATTERNS_TOOL_MAP))), |
| ) |
| |
| # The output formats supported by cros lint. |
| OUTPUT_FORMATS = ("default", "colorized", "msvs", "parseable") |
| |
| # Override base class property to use path filter options. |
| use_filter_options = True |
| |
| @classmethod |
| def AddParser(cls, parser: commandline.ArgumentParser): |
| super().AddParser(parser) |
| parser.add_argument( |
| "--commit", |
| type=str, |
| help="Use files from git commit instead of on disk. " |
| "NB: Not all linters work with this yet.", |
| ) |
| parser.add_argument("files", type=Path, help="Files to lint", nargs="*") |
| parser.add_argument( |
| "--output", |
| default="default", |
| choices=LintCommand.OUTPUT_FORMATS, |
| help="Output format to pass to the linters. Supported " |
| "formats are: default (no option is passed to the " |
| "linter), colorized, msvs (Visual Studio) and " |
| "parseable.", |
| ) |
| parser.add_argument( |
| "--relaxed", |
| default=False, |
| action="store_true", |
| help="Disable some strict checks. This is used for " |
| "cases like builds where a more permissive " |
| "behavior is desired.", |
| ) |
| |
| def _Run(self): |
| # Hack "pre-submit" to "HEAD" when being run by repohooks/pre-upload.py |
| # --pre-submit. We should drop support for this once we merge repohooks |
| # into `cros` with proper preupload/presubmit. |
| commit = ( |
| "HEAD" |
| if self.options.commit == "pre-submit" |
| else self.options.commit |
| ) |
| |
| # Ignore symlinks. |
| files = [] |
| syms = [] |
| if commit: |
| for f in git.LsTree(None, commit, self.options.files): |
| if f.is_symlink: |
| syms.append(f.name) |
| else: |
| files.append(f.name) |
| else: |
| for f in path_util.ExpandDirectories(self.options.files): |
| if f.is_symlink(): |
| syms.append(f) |
| else: |
| files.append(f) |
| if syms: |
| logging.info("Ignoring symlinks: %s", syms) |
| if not files: |
| # Running with no arguments is allowed to make the repo upload hook |
| # simple, but print a warning so that if someone runs this manually |
| # they are aware that nothing happened. |
| logging.warning("No files found to process. Doing nothing.") |
| return 0 |
| |
| # Ignore generated files. Some tools can do this for us, but not all, |
| # and it'd be faster if we just never spawned the tools in the first |
| # place. |
| # TODO(build): Move to a centralized configuration somewhere. |
| self.options.filter.rules.extend( |
| ( |
| # Compiled python protobuf bindings. |
| path_filter.exclude("*_pb2.py"), |
| ) |
| ) |
| |
| files = self.options.filter.filter(files) |
| if not files: |
| logging.warning("All files are excluded. Doing nothing.") |
| return 0 |
| |
| tool_map = _BreakoutFilesByTool(files) |
| dispatcher = functools.partial( |
| _Dispatcher, |
| self.options.output, |
| self.options.debug, |
| self.options.relaxed, |
| commit, |
| ) |
| |
| # If we filtered out all files, do nothing. |
| # Special case one file (or fewer) as it's common -- faster to avoid the |
| # parallel startup penalty. |
| tasks = [] |
| for tool, files in tool_map.items(): |
| tasks.extend([tool, x] for x in files) |
| if not tasks: |
| return 0 |
| elif len(tasks) == 1: |
| tool, files = next(iter(tool_map.items())) |
| return dispatcher(tool, files[0]) |
| else: |
| # Run the tool in parallel on the files. |
| return sum(parallel.RunTasksInProcessPool(dispatcher, tasks)) |
| |
| def Run(self): |
| with timer.Timer() as t: |
| ret = self._Run() |
| if ret: |
| logging.error("Found lint errors in %i files in %s.", ret, t) |
| |
| return 1 if ret else 0 |