blob: c6b9eb908a712dd8a879865cebef290faf91de1e [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2019 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Lint our source files."""
import fnmatch
import logging
import os
from pathlib import Path
import sys
from typing import List
import libdot
# All checks except strictMissingRequire.
DEFAULT_CLOSURE_ARGS = (
'--jscomp_error=*', '--jscomp_off=strictMissingRequire')
# Options passed when invoking eslint.
ESLINT_ARGS = (
'--cache',
'--cache-location', libdot.LIBAPPS_DIR / '.eslintcache',
)
def kokoro_comments_path(path, tool):
"""Expand % markers that might exist in |path|."""
if not path:
return None
else:
return path.replace('%(tool)', tool)
def is_generated_path(path):
"""Return True if |path| is generated.
Useful for filtering out comments for files not in the tree.
"""
return (
fnmatch.fnmatch(path, '*.concat.js') or
fnmatch.fnmatch(path, '*.rollup.js')
)
def get_known_files(basedir: Path) -> List[Path]:
"""Return list of files committed to the tree."""
# -z appends \0 to each path, it doesn't delimit them. So strip off the
# trailing \0 to avoid a spurious '' entry at the end.
all_files = libdot.run(
['git', 'ls-tree', '--name-only', '-r', '-z', 'HEAD'], cwd=basedir,
capture_output=True, encoding='utf-8').stdout[:-1].split('\0')
return [basedir / x for x in all_files if (basedir / x).exists()]
def get_known_sources(basedir: Path) -> List[Path]:
"""Get list of committed files that we could possibly lint."""
return (x for x in get_known_files(basedir)
if x.suffix not in {
'.log', '.jpg', '.ogg', '.patch', '.png', '.webp',
})
def lint_whitespace_data(path: Path, data: str) -> bool:
"""Basic whitespace checks on text files."""
ret = True
if '\r' in data:
ret = False
logging.error(r'%s: \r not allowed in text files', path)
if data.startswith('\n'):
ret = False
logging.error('%s: No leading blank lines allowed', path)
if not data.endswith('\n'):
ret = False
logging.error('%s: Files must end with a newline', path)
return ret
def lint_whitespace_files(paths: List[Path]) -> bool:
"""Basic whitespace checks on text files."""
ret = True
for path in paths:
with open(path, mode='r', encoding='utf-8') as fp:
data = fp.read()
ret &= lint_whitespace_data(path, data)
return ret
def lint_html_files(paths: List[Path]) -> bool:
"""Basic lint checks on HTML files."""
logging.info('Linting HTML files %s', libdot.cmdstr(paths))
ret = True
META_CHARSET_TAG = "<meta charset='utf-8'/>"
for path in paths:
with open(path, mode='r', encoding='utf-8') as fp:
data = fp.read()
if META_CHARSET_TAG not in data:
ret = False
logging.error('%s: <head>: missing %s tag', path, META_CHARSET_TAG)
ret &= lint_whitespace_data(path, data)
return ret
def lint_text_files(paths: List[Path]) -> bool:
"""Basic lint checks on HTML files."""
logging.info('Linting text files %s', libdot.cmdstr(paths))
return lint_whitespace_files(paths)
def _get_default_paths(basedir: Path) -> List[Path]:
"""Get list of paths to lint by default."""
most_files = sorted(x for x in get_known_sources(basedir)
if x.suffix not in {'.js'})
# All files in js/*.js excluding generated files.
# Use relpath for nicer default output.
# Sort to ensure lib.js comes before lib_array.js, etc.
# Filter out the generated libdot.js/libdot.min.js/etc...
js_files = (
list((libdot.DIR / 'html').glob('*.html')) +
list((libdot.DIR / 'js').glob('*.js')) +
list((libdot.DIR / 'third_party').glob('*/*.js')))
js_files = sorted(x for x in js_files
if not x.name.startswith('libdot.'))
return [os.path.relpath(x) for x in most_files + js_files]
def get_parser():
"""Get a command line parser."""
parser = libdot.ArgumentParser(description=__doc__)
parser.add_argument('--version', action='store_true',
help='Show linter versions.')
parser.add_argument('--fix', action='store_true',
help='Run linters with --fix setting if possible.')
parser.add_argument('--gerrit-comments-file',
help='Save errors for posting files to Gerrit.')
parser.add_argument('--skip-mkdeps', dest='run_mkdeps',
action='store_false', default=True,
help='Skip (re)building of dependencies.')
group = parser.add_argument_group(
'File selection/filtering',
description='All files are linted by default')
group.add_argument('--cpp', action='store_true',
help='Lint C/C++ files.')
group.add_argument('--js', action='store_true',
help='Lint JavaScript files.')
group.add_argument('--json', action='store_true',
help='Lint JSON files.')
group.add_argument('--md', action='store_true',
help='Lint Markdown files.')
group.add_argument('--py', action='store_true',
help='Lint Python files.')
group.add_argument('--txt', action='store_true',
help='Lint text files.')
parser.add_argument('paths', nargs='*',
help='Paths to lint.')
return parser
def main(argv, get_default_paths=get_known_sources, basedir=None, mkdeps=None,
closure_args=DEFAULT_CLOSURE_ARGS):
"""The common main func for all linters.
Args:
argv: The command line to process.
paths: The default set of files to lint. If the user doesn't specify
any, we'll use these instead.
basedir: The base source tree to search for default set of files to lint.
mkdeps: Callback to build dependencies after we've initialized.
closure_args: Extra arguments to pass to closure-compiler.
"""
parser = get_parser()
opts = parser.parse_args(argv)
libdot.node_and_npm_setup()
if opts.version:
for func in (
libdot.eslint.run,
libdot.closure_compiler.run,
libdot.pylint.run,
):
func(['--version'])
return 0
if not opts.paths:
opts.paths = [str(x) for x in get_default_paths(basedir)]
if not opts.paths:
print('No files to lint.')
return 0
if mkdeps:
if opts.run_mkdeps:
mkdeps(opts)
else:
logging.info('Skipping building dependencies due to --skip-mkdeps')
lint_groups = {'cpp', 'js', 'json', 'md', 'py', 'txt'}
lint_all = all(not getattr(opts, x) for x in lint_groups)
for group in lint_groups:
setattr(opts, group, lint_all or getattr(opts, group))
tolint_paths = set(opts.paths)
kwargs = {
'fix': opts.fix,
'gerrit_comments_file': opts.gerrit_comments_file,
}
checks = []
html_files = [x for x in opts.paths
if x.endswith('.html') or x.endswith('.html.in')]
js_files = [x for x in opts.paths if x.endswith('.js')]
tolint_paths -= set(js_files)
if js_files and opts.js:
js_kwargs = {'paths': js_files, **kwargs}
html_js_kwargs = {'paths': html_files + js_files, **kwargs}
checks += [
libdot.eslint.perform(argv=ESLINT_ARGS, **html_js_kwargs),
libdot.closure_compiler.perform(argv=closure_args, **js_kwargs),
]
py_files = libdot.pylint.filter_known_files(opts.paths)
tolint_paths -= set(py_files)
if py_files and opts.py:
py_kwargs = {'paths': py_files, **kwargs}
checks += [libdot.pylint.perform(**py_kwargs)]
cpp_files = libdot.cpplint.filter_known_files(opts.paths)
tolint_paths -= set(cpp_files)
if cpp_files and opts.cpp:
cpp_kwargs = {'paths': cpp_files, **kwargs}
checks += [libdot.cpplint.perform(**cpp_kwargs)]
tolint_paths -= set(html_files)
if html_files and opts.txt:
checks += [lint_html_files(html_files)]
md_files = libdot.mdlint.filter_known_files(opts.paths)
tolint_paths -= set(md_files)
if md_files and opts.md:
md_kwargs = {'paths': md_files, **kwargs}
checks += [
lint_whitespace_files(md_files),
libdot.mdlint.perform(**md_kwargs),
]
json_files = libdot.jsonlint.filter_known_files(opts.paths)
tolint_paths -= set(json_files)
if json_files and opts.json:
json_kwargs = {'paths': json_files, **kwargs}
checks += [
lint_whitespace_files(json_files),
libdot.jsonlint.perform(**json_kwargs),
]
TEXT_FILENAMES = {
'.clang-format', '.eslintrc.js', '.gitignore', '.markdownlintrc',
'.npmrc', '.pylintrc',
'Dockerfile', 'LICENSE', 'Makefile', 'METADATA', 'OWNERS',
}
TEXT_EXTENSIONS = {
'.cfg', '.concat', '.css', '.el', '.sh', '.svg', '.txt', '.vim', '.xml',
}
text_files = [x for x in opts.paths
if (os.path.basename(x) in TEXT_FILENAMES or
os.path.splitext(x)[1] in TEXT_EXTENSIONS)]
tolint_paths -= set(text_files)
if text_files and opts.txt:
checks += [lint_text_files(text_files)]
if tolint_paths:
logging.warning('Linting skipped:\n%s', ' '.join(sorted(tolint_paths)))
return 0 if all(checks) else 1
if __name__ == '__main__':
sys.exit(main(sys.argv[1:], basedir=libdot.DIR,
get_default_paths=_get_default_paths))