| #!/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)) |