xref: /aosp_15_r20/external/pytorch/tools/linter/adapters/shellcheck_linter.py (revision da0073e96a02ea20f0ac840b70461e3646d07c45)
1from __future__ import annotations
2
3import argparse
4import json
5import logging
6import shutil
7import subprocess
8import sys
9import time
10from enum import Enum
11from typing import NamedTuple
12
13
14LINTER_CODE = "SHELLCHECK"
15
16
17class LintSeverity(str, Enum):
18    ERROR = "error"
19    WARNING = "warning"
20    ADVICE = "advice"
21    DISABLED = "disabled"
22
23
24class LintMessage(NamedTuple):
25    path: str | None
26    line: int | None
27    char: int | None
28    code: str
29    severity: LintSeverity
30    name: str
31    original: str | None
32    replacement: str | None
33    description: str | None
34
35
36def run_command(
37    args: list[str],
38) -> subprocess.CompletedProcess[bytes]:
39    logging.debug("$ %s", " ".join(args))
40    start_time = time.monotonic()
41    try:
42        return subprocess.run(
43            args,
44            capture_output=True,
45        )
46    finally:
47        end_time = time.monotonic()
48        logging.debug("took %dms", (end_time - start_time) * 1000)
49
50
51def check_files(
52    files: list[str],
53) -> list[LintMessage]:
54    try:
55        proc = run_command(
56            ["shellcheck", "--external-sources", "--format=json1"] + files
57        )
58    except OSError as err:
59        return [
60            LintMessage(
61                path=None,
62                line=None,
63                char=None,
64                code=LINTER_CODE,
65                severity=LintSeverity.ERROR,
66                name="command-failed",
67                original=None,
68                replacement=None,
69                description=(f"Failed due to {err.__class__.__name__}:\n{err}"),
70            )
71        ]
72    stdout = str(proc.stdout, "utf-8").strip()
73    results = json.loads(stdout)["comments"]
74    return [
75        LintMessage(
76            path=result["file"],
77            name=f"SC{result['code']}",
78            description=result["message"],
79            line=result["line"],
80            char=result["column"],
81            code=LINTER_CODE,
82            severity=LintSeverity.ERROR,
83            original=None,
84            replacement=None,
85        )
86        for result in results
87    ]
88
89
90if __name__ == "__main__":
91    parser = argparse.ArgumentParser(
92        description="shellcheck runner",
93        fromfile_prefix_chars="@",
94    )
95    parser.add_argument(
96        "filenames",
97        nargs="+",
98        help="paths to lint",
99    )
100
101    if shutil.which("shellcheck") is None:
102        err_msg = LintMessage(
103            path="<none>",
104            line=None,
105            char=None,
106            code=LINTER_CODE,
107            severity=LintSeverity.ERROR,
108            name="command-failed",
109            original=None,
110            replacement=None,
111            description="shellcheck is not installed, did you forget to run `lintrunner init`?",
112        )
113        print(json.dumps(err_msg._asdict()), flush=True)
114        sys.exit(0)
115
116    args = parser.parse_args()
117
118    lint_messages = check_files(args.filenames)
119    for lint_message in lint_messages:
120        print(json.dumps(lint_message._asdict()), flush=True)
121