xref: /aosp_15_r20/external/pytorch/tools/linter/adapters/cmake_linter.py (revision da0073e96a02ea20f0ac840b70461e3646d07c45)
1from __future__ import annotations
2
3import argparse
4import concurrent.futures
5import json
6import logging
7import os
8import re
9import subprocess
10import time
11from enum import Enum
12from typing import NamedTuple
13
14
15LINTER_CODE = "CMAKE"
16
17
18class LintSeverity(str, Enum):
19    ERROR = "error"
20    WARNING = "warning"
21    ADVICE = "advice"
22    DISABLED = "disabled"
23
24
25class LintMessage(NamedTuple):
26    path: str | None
27    line: int | None
28    char: int | None
29    code: str
30    severity: LintSeverity
31    name: str
32    original: str | None
33    replacement: str | None
34    description: str | None
35
36
37# CMakeLists.txt:901: Lines should be <= 80 characters long [linelength]
38RESULTS_RE: re.Pattern[str] = re.compile(
39    r"""(?mx)
40    ^
41    (?P<file>.*?):
42    (?P<line>\d+):
43    \s(?P<message>.*)
44    \s(?P<code>\[.*\])
45    $
46    """
47)
48
49
50def run_command(
51    args: list[str],
52) -> subprocess.CompletedProcess[bytes]:
53    logging.debug("$ %s", " ".join(args))
54    start_time = time.monotonic()
55    try:
56        return subprocess.run(
57            args,
58            capture_output=True,
59        )
60    finally:
61        end_time = time.monotonic()
62        logging.debug("took %dms", (end_time - start_time) * 1000)
63
64
65def check_file(
66    filename: str,
67    config: str,
68) -> list[LintMessage]:
69    try:
70        proc = run_command(
71            ["cmakelint", f"--config={config}", filename],
72        )
73    except OSError as err:
74        return [
75            LintMessage(
76                path=None,
77                line=None,
78                char=None,
79                code=LINTER_CODE,
80                severity=LintSeverity.ERROR,
81                name="command-failed",
82                original=None,
83                replacement=None,
84                description=(f"Failed due to {err.__class__.__name__}:\n{err}"),
85            )
86        ]
87    stdout = str(proc.stdout, "utf-8").strip()
88    return [
89        LintMessage(
90            path=match["file"],
91            name=match["code"],
92            description=match["message"],
93            line=int(match["line"]),
94            char=None,
95            code=LINTER_CODE,
96            severity=LintSeverity.ERROR,
97            original=None,
98            replacement=None,
99        )
100        for match in RESULTS_RE.finditer(stdout)
101    ]
102
103
104if __name__ == "__main__":
105    parser = argparse.ArgumentParser(
106        description="cmakelint runner",
107        fromfile_prefix_chars="@",
108    )
109    parser.add_argument(
110        "--config",
111        required=True,
112        help="location of cmakelint config",
113    )
114    parser.add_argument(
115        "filenames",
116        nargs="+",
117        help="paths to lint",
118    )
119
120    args = parser.parse_args()
121
122    with concurrent.futures.ThreadPoolExecutor(
123        max_workers=os.cpu_count(),
124        thread_name_prefix="Thread",
125    ) as executor:
126        futures = {
127            executor.submit(
128                check_file,
129                filename,
130                args.config,
131            ): filename
132            for filename in args.filenames
133        }
134        for future in concurrent.futures.as_completed(futures):
135            try:
136                for lint_message in future.result():
137                    print(json.dumps(lint_message._asdict()), flush=True)
138            except Exception:
139                logging.critical('Failed at "%s".', futures[future])
140                raise
141