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