| #!/usr/bin/env python3 |
| # Copyright 2020 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Run eslint with our settings.""" |
| |
| import json |
| import os |
| import sys |
| |
| import libdot |
| |
| |
| def convert_to_kokoro(data): |
| """Take eslint JSON output and convert it to kokoro comment format. |
| |
| The |data| input will look like: |
| [ |
| { |
| "errorCount": <total number of errors>, |
| "filePath": ".../libapps/libdot/js/lib_f.js", |
| "fixableErrorCount": 1, |
| "fixableWarningCount": 0, |
| "messages": [ // This contains errors & warnings. |
| { |
| "column": 2, |
| "endColumn": 57, |
| "endLine": 1, |
| "line": 1, |
| "message": "Missing semicolon.", |
| "nodeType": "ExpressionStatement", |
| "ruleId": "semi", |
| "severity": <1=warning 2=error> |
| } |
| ], |
| "source": "...the entire source file...", |
| "warningCount": <total number of warnings> |
| }, |
| { |
| "errorCount": <total number of errors>, |
| "filePath": ".../libapps/libdot/js/lib_fs.js", |
| "fixableErrorCount": 2, |
| "fixableWarningCount": 0, |
| "messages": [ // This contains errors & warnings. |
| { |
| "column": 2, |
| "fix": { |
| "range": [8180, 8180], |
| "text": ";" |
| }, |
| "line": 262, |
| "message": "Missing semicolon.", |
| "nodeType": "ExpressionStatement", |
| "ruleId": "semi", |
| "severity": <1=warning 2=error> |
| }, |
| { |
| "column": 10, |
| "fix": { |
| "range": [8436, 8436], |
| "text": ";" |
| }, |
| "line": 271, |
| "message": "Missing semicolon.", |
| "nodeType": "VariableDeclaration", |
| "ruleId": "semi", |
| "severity": <1=warning 2=error> |
| } |
| ], |
| "source": "...the entire source file...", |
| "warningCount": <total number of warnings> |
| } |
| ] |
| |
| We want to return a list of results like: |
| [ |
| { |
| path: "relative/path/to/source/file", |
| message: "Content of the inline comment." |
| startLine: <integer line number>, |
| startCharacter: <integer column number>, |
| endLine: <integer line number>, |
| endCharacter: <integer column number> |
| }, |
| { ... more results above ... } |
| ] |
| """ |
| RULES_URI_BASE = "https://eslint.org/docs/rules/" |
| SEVERITY_TO_HEADER = {1: "Warning", 2: "Error"} |
| |
| for file_results in data: |
| # Ignore generated files not in git. |
| if libdot.lint.is_generated_path(file_results["filePath"]): |
| continue |
| |
| for message in file_results["messages"]: |
| msg = ( |
| "[eslint] " |
| + SEVERITY_TO_HEADER[message["severity"]] |
| + ": " |
| + message["message"] |
| ) |
| |
| # For syntax errors, this will be omitted. |
| ruleId = message.get("ruleId") |
| if ruleId: |
| msg += "\n" + RULES_URI_BASE + ruleId |
| |
| # Gerrit comments appear to count columns from 0 while eslint counts |
| # from 1. Adjust them down by one to handle. |
| yield { |
| "path": os.path.relpath( |
| file_results["filePath"], libdot.LIBAPPS_DIR |
| ), |
| "message": msg, |
| "startLine": message["line"], |
| "endLine": message.get("endLine", message["line"]), |
| "startCharacter": message["column"] - 1, |
| "endCharacter": message.get("endColumn", message["column"]), |
| } |
| |
| |
| def setup(): |
| """Initialize the tool settings.""" |
| libdot.node_and_npm_setup() |
| |
| |
| def run(argv=(), **kwargs): |
| """Run the tool directly.""" |
| setup() |
| return libdot.node.run(["eslint"] + list(argv), **kwargs) |
| |
| |
| def perform(argv=(), paths=(), fix=False, gerrit_comments_file=None): |
| """Run high level tool logic.""" |
| ret = True |
| argv = list(argv) |
| paths = list(paths) |
| |
| comments_path = libdot.lint.kokoro_comments_path( |
| gerrit_comments_file, "eslint" |
| ) |
| |
| if comments_path: |
| argv += ["--max-warnings=0"] |
| if fix: |
| argv += ["--fix"] |
| |
| result = run(argv + paths, check=False) |
| if result.returncode: |
| ret = False |
| |
| # Rerun for Gerrit. |
| if comments_path: |
| # Handle relative paths like "foo.json". |
| dirname = os.path.dirname(comments_path) |
| if dirname: |
| os.makedirs(dirname, exist_ok=True) |
| |
| argv += ["--format=json"] |
| result = run(argv + paths, check=False, capture_output=True) |
| |
| # Save a copy for debugging later. |
| with open(comments_path + ".in", "wb") as fp: |
| fp.write(result.stdout) |
| |
| data = json.loads(result.stdout.decode("utf-8")) |
| comments = list(convert_to_kokoro(data)) |
| with open(comments_path, "w", encoding="utf-8") as fp: |
| json.dump(comments, fp, sort_keys=True) |
| elif comments_path: |
| # If there were no failures, clear the files to avoid leaving previous |
| # results laying around & confuse devs. |
| libdot.unlink(comments_path) |
| libdot.unlink(comments_path + ".in") |
| |
| return ret |
| |
| |
| def get_parser(): |
| """Get a command line parser.""" |
| parser = libdot.ArgumentParser(description=__doc__, short_options=False) |
| parser.add_argument( |
| "--gerrit-comments-file", |
| help="Save errors for posting files to Gerrit.", |
| ) |
| parser.add_argument("paths", nargs="*", help="Paths to lint.") |
| return parser |
| |
| |
| def main(argv): |
| """The main func!""" |
| parser = get_parser() |
| opts, args = parser.parse_known_args(argv) |
| |
| return ( |
| 0 |
| if perform( |
| argv=args, |
| paths=opts.paths, |
| gerrit_comments_file=opts.gerrit_comments_file, |
| ) |
| else 1 |
| ) |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main(sys.argv[1:])) |