Files

219 lines
5.3 KiB
Python
Executable File

#!/usr/bin/env python
# Copyright 2019 Google
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Lints source files for conformance with the style guide that applies.
Currently supports linting Objective-C, Objective-C++, C++, and Python source.
"""
import argparse
import logging
import os
import subprocess
import sys
import textwrap
from lib import checker
from lib import command_trace
from lib import git
from lib import source
_logger = logging.getLogger('lint')
_dry_run = False
_CPPLINT_OBJC_FILTERS = [
# Objective-C uses #import and does not use header guards
'-build/header_guard',
# Inline definitions of Objective-C blocks confuse
'-readability/braces',
# C-style casts are acceptable in Objective-C++
'-readability/casting',
# Objective-C needs use type 'long' for interop between types like NSInteger
# and printf-style functions.
'-runtime/int',
# cpplint is generally confused by Objective-C mixing with C++.
# * Objective-C method invocations in a for loop make it think its a
# range-for
# * Objective-C dictionary literals confuse brace spacing
# * Empty category declarations ("@interface Foo ()") look like function
# invocations
'-whitespace',
]
_CPPLINT_OBJC_OPTIONS = [
# cpplint normally excludes Objective-C++
'--extensions=h,m,mm',
# Objective-C style allows longer lines
'--linelength=100',
'--filter=' + ','.join(_CPPLINT_OBJC_FILTERS),
]
def main():
global _dry_run
parser = argparse.ArgumentParser(description='Lint source files.')
parser.add_argument('--dry-run', '-n', action='store_true',
help='Show what the linter would do without doing it')
parser.add_argument('--all', action='store_true',
help='run the linter over all known sources')
parser.add_argument('rev_or_files', nargs='*',
help='A single revision that specifies a point in time '
'from which to look for changes. Defaults to '
'origin/main. Alternatively, a list of specific '
'files or git pathspecs to lint.')
args = command_trace.parse_args(parser)
if args.dry_run:
_dry_run = True
command_trace.enable_tracing()
pool = checker.Pool()
sources = _unique(source.CC_DIRS + source.OBJC_DIRS + source.PYTHON_DIRS)
patterns = git.make_patterns(sources)
files = git.find_changed_or_files(args.all, args.rev_or_files, patterns)
check(pool, files)
pool.exit()
def check(pool, files):
group = source.categorize_files(files)
for kind, files in group.kinds.items():
for chunk in checker.shard(files):
if not chunk:
continue
linter = _linters[kind]
pool.submit(linter, chunk)
def lint_cc(files):
return _run_cpplint([], files)
def lint_objc(files):
return _run_cpplint(_CPPLINT_OBJC_OPTIONS, files)
def _run_cpplint(options, files):
scripts_dir = os.path.dirname(os.path.abspath(__file__))
cpplint = os.path.join(scripts_dir, 'cpplint.py')
command = [sys.executable, cpplint, '--quiet']
command.extend(options)
command.extend(files)
return _read_output(command)
_flake8_warned = False
def lint_py(files):
flake8 = which('flake8')
if flake8 is None:
global _flake8_warned
if not _flake8_warned:
_flake8_warned = True
_logger.warn(textwrap.dedent(
"""
Could not find flake8 on $PATH; skipping python lint.
Install with:
pip install --user flake8
"""))
return
command = [flake8]
command.extend(files)
return _read_output(command)
def _read_output(command):
command_trace.log(command)
if _dry_run:
return checker.Result(0, '')
proc = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output = proc.communicate('')[0]
sc = proc.wait()
return checker.Result(sc, output)
_linters = {
'cc': lint_cc,
'objc': lint_objc,
'py': lint_py,
}
def _unique(items):
return list(set(items))
def which(executable):
"""Finds the executable with the given name.
Returns:
The fully qualified path to the executable or None if the executable isn't
found.
"""
if executable.startswith('/'):
return executable
path = os.environ['PATH'].split(os.pathsep)
for executable_with_ext in _executable_names(executable):
for entry in path:
joined = os.path.join(entry, executable_with_ext)
if os.path.isfile(joined) and os.access(joined, os.X_OK):
return joined
return None
def _executable_names(executable):
"""Yields a sequence of all possible executable names."""
if os.name == 'nt':
pathext = os.environ.get('PATHEXT', '').split(os.pathsep)
for ext in pathext:
yield executable + ext
else:
yield executable
if __name__ == '__main__':
main()