blob: 59d4729777b702708424e2ab21f52281081cdd57 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2020 The ChromiumOS Authors
George Burgess IV853d65b2020-02-25 13:13:15 -08002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Runs clang-tidy across the given files, dumping diagnostics to a JSON file.
6
7This script is intended specifically for use with Tricium (go/tricium).
8"""
9
10# From an implementation perspective, it's good to note that this script
11# cooperates with the toolchain's compiler wrapper. In particular,
12# ${cros}/src/third_party/toolchain-utils/compiler_wrapper/clang_tidy_flag.go.
13#
14# When |WITH_TIDY=tricium| is set and the wrapper (which is already $CC/$CXX)
15# is invoked, $CC will invoke clang-tidy _as well_ as the regular compiler.
16# This clang-tidy invocation will result in a few files being dumped to
17# |LINT_BASE| (below):
18# - "${LINT_BASE}/some-prefix.yaml" -- a YAML file that represents
19# clang-tidy's diagnostics for the file the compiler was asked to build
20# - "${LINT_BASE}/some-prefix.json" -- metadata about how the above YAML file
21# was generated, including clang-tidy's exit code, stdout, etc. See
22# |InvocationMetadata| below.
23#
24# As one might expect, the compiler wrapper writes the JSON file only after
25# clang-tidy is done executing.
26#
27# This directory might contain other files, as well; these are ignored by this
28# script.
29
30import bisect
31import json
Chris McDonald59650c32021-07-20 15:29:28 -060032import logging
George Burgess IV853d65b2020-02-25 13:13:15 -080033import multiprocessing
34import os
35from pathlib import Path
36import re
37import subprocess
38import sys
39import tempfile
40import traceback
Mike Frysinger807d8282022-04-28 22:45:17 -040041from typing import (
42 Any,
43 Dict,
44 Iterable,
45 List,
46 NamedTuple,
47 Optional,
48 Set,
49 Tuple,
50 Union,
51)
George Burgess IV853d65b2020-02-25 13:13:15 -080052
53import yaml # pylint: disable=import-error
Mike Frysinger06a51c82021-04-06 11:39:17 -040054
55from chromite.lib import build_target_lib
George Burgess IV853d65b2020-02-25 13:13:15 -080056from chromite.lib import commandline
57from chromite.lib import cros_build_lib
George Burgess IV853d65b2020-02-25 13:13:15 -080058from chromite.lib import osutils
59from chromite.lib import portage_util
60from chromite.lib import workon_helper
61
George Burgess IV853d65b2020-02-25 13:13:15 -080062
63# The directory under which the compiler wrapper stores clang-tidy reports.
Alex Klein1699fab2022-09-08 08:46:06 -060064LINT_BASE = Path("/tmp/linting_output/clang-tidy")
George Burgess IV853d65b2020-02-25 13:13:15 -080065
66
67class TidyReplacement(NamedTuple):
Alex Klein1699fab2022-09-08 08:46:06 -060068 """Represents a replacement emitted by clang-tidy.
George Burgess IV853d65b2020-02-25 13:13:15 -080069
Alex Klein1699fab2022-09-08 08:46:06 -060070 File path is omitted, since these are intended to be associated with
71 TidyDiagnostics with identical paths.
72 """
73
74 new_text: str
75 start_line: int
76 end_line: int
77 start_char: int
78 end_char: int
George Burgess IV853d65b2020-02-25 13:13:15 -080079
80
81class TidyExpandedFrom(NamedTuple):
Alex Klein1699fab2022-09-08 08:46:06 -060082 """Represents a macro expansion.
George Burgess IV853d65b2020-02-25 13:13:15 -080083
Alex Klein1699fab2022-09-08 08:46:06 -060084 When a diagnostic is inside of a macro expansion, clang-tidy emits
85 information about where said macro was expanded from. |TidyDiagnostic|s will
86 have one |TidyExpandedFrom| for each level of this expansion.
87 """
George Burgess IV853d65b2020-02-25 13:13:15 -080088
Alex Klein1699fab2022-09-08 08:46:06 -060089 file_path: Path
90 line_number: int
91
92 def to_dict(self) -> Dict[str, Any]:
93 """Converts this |TidyExpandedFrom| to a dict serializeable as JSON."""
94 return {
95 "file_path": self.file_path.as_posix(),
96 "line_number": self.line_number,
97 }
George Burgess IV853d65b2020-02-25 13:13:15 -080098
99
100class Error(Exception):
Alex Klein1699fab2022-09-08 08:46:06 -0600101 """Base error class for tricium-clang-tidy."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800102
103
104class ClangTidyParseError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -0600105 """Raised when clang-tidy parsing jobs fail."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800106
Alex Klein1699fab2022-09-08 08:46:06 -0600107 def __init__(self, failed_jobs: int, total_jobs: int):
108 super().__init__(f"{failed_jobs}/{total_jobs} parse jobs failed")
109 self.failed_jobs = failed_jobs
110 self.total_jobs = total_jobs
George Burgess IV853d65b2020-02-25 13:13:15 -0800111
112
113class TidyDiagnostic(NamedTuple):
Alex Klein1699fab2022-09-08 08:46:06 -0600114 """A diagnostic emitted by clang-tidy.
George Burgess IV853d65b2020-02-25 13:13:15 -0800115
Alex Klein1699fab2022-09-08 08:46:06 -0600116 Note that we shove these in a set for cheap deduplication, and we sort based
117 on the natural element order here. Sorting is mostly just for
118 deterministic/pretty output.
119 """
George Burgess IV853d65b2020-02-25 13:13:15 -0800120
Alex Klein1699fab2022-09-08 08:46:06 -0600121 file_path: Path
122 line_number: int
123 diag_name: str
124 message: str
125 replacements: Tuple[TidyReplacement]
126 expansion_locs: Tuple[TidyExpandedFrom]
George Burgess IV853d65b2020-02-25 13:13:15 -0800127
Alex Klein1699fab2022-09-08 08:46:06 -0600128 def normalize_paths_to(self, where: str) -> "TidyDiagnostic":
129 """Creates a new TidyDiagnostic with all paths relative to |where|."""
130 return self._replace(
131 # Use relpath because Path.relative_to requires that `self` is rooted
132 # at `where`.
133 file_path=Path(os.path.relpath(self.file_path, where)),
134 expansion_locs=tuple(
135 x._replace(file_path=Path(os.path.relpath(x.file_path, where)))
136 for x in self.expansion_locs
137 ),
138 )
139
140 def to_dict(self) -> Dict[str, Any]:
141 """Converts this |TidyDiagnostic| to a dict serializeable as JSON."""
142 return {
143 "file_path": self.file_path.as_posix(),
144 "line_number": self.line_number,
145 "diag_name": self.diag_name,
146 "message": self.message,
147 "replacements": [x._asdict() for x in self.replacements],
148 "expansion_locs": [x.to_dict() for x in self.expansion_locs],
149 }
George Burgess IV853d65b2020-02-25 13:13:15 -0800150
151
152class ClangTidySchemaError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -0600153 """Raised when we encounter malformed YAML."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800154
Alex Klein1699fab2022-09-08 08:46:06 -0600155 def __init__(self, err_msg: str):
156 super().__init__(err_msg)
157 self.err_msg = err_msg
George Burgess IV853d65b2020-02-25 13:13:15 -0800158
159
160class LineOffsetMap:
Alex Klein1699fab2022-09-08 08:46:06 -0600161 """Convenient API to turn offsets in a file into line numbers."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800162
Alex Klein1699fab2022-09-08 08:46:06 -0600163 def __init__(self, newline_locations: Iterable[int]):
164 line_starts = [x + 1 for x in newline_locations]
165 # The |bisect| logic in |get_line_number|/|get_line_offset| gets a bit
166 # complicated around the first and last lines of a file. Adding boundaries
167 # here removes some complexity from those implementations.
168 line_starts.append(0)
169 line_starts.append(sys.maxsize)
170 line_starts.sort()
George Burgess IV853d65b2020-02-25 13:13:15 -0800171
Alex Klein1699fab2022-09-08 08:46:06 -0600172 assert line_starts[0] == 0, line_starts[0]
173 assert line_starts[1] != 0, line_starts[1]
174 assert line_starts[-2] < sys.maxsize, line_starts[-2]
175 assert line_starts[-1] == sys.maxsize, line_starts[-1]
George Burgess IV853d65b2020-02-25 13:13:15 -0800176
Alex Klein1699fab2022-09-08 08:46:06 -0600177 self._line_starts = line_starts
George Burgess IV853d65b2020-02-25 13:13:15 -0800178
Alex Klein1699fab2022-09-08 08:46:06 -0600179 def get_line_number(self, char_number: int) -> int:
180 """Given a char offset into a file, returns its line number."""
181 assert 0 <= char_number < sys.maxsize, char_number
182 return bisect.bisect_right(self._line_starts, char_number)
George Burgess IV853d65b2020-02-25 13:13:15 -0800183
Alex Klein1699fab2022-09-08 08:46:06 -0600184 def get_line_offset(self, char_number: int) -> int:
185 """Given a char offset into a file, returns its column number."""
186 assert 0 <= char_number < sys.maxsize, char_number
187 line_start_index = (
188 bisect.bisect_right(self._line_starts, char_number) - 1
189 )
190 return char_number - self._line_starts[line_start_index]
George Burgess IV853d65b2020-02-25 13:13:15 -0800191
Alex Klein1699fab2022-09-08 08:46:06 -0600192 @staticmethod
193 def for_text(data: str) -> "LineOffsetMap":
194 """Creates a LineOffsetMap for the given string."""
195 return LineOffsetMap(m.start() for m in re.finditer(r"\n", data))
George Burgess IV853d65b2020-02-25 13:13:15 -0800196
197
Alex Klein1699fab2022-09-08 08:46:06 -0600198def parse_tidy_fixes_file(
199 tidy_invocation_dir: Path, yaml_data: Any
200) -> Iterable[TidyDiagnostic]:
201 """Parses a clang-tidy YAML file.
George Burgess IV853d65b2020-02-25 13:13:15 -0800202
Alex Klein1699fab2022-09-08 08:46:06 -0600203 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000204 yaml_data: The parsed YAML data from clang-tidy's fixits file.
205 tidy_invocation_dir: The directory clang-tidy was run in.
George Burgess IV853d65b2020-02-25 13:13:15 -0800206
Alex Klein1699fab2022-09-08 08:46:06 -0600207 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000208 A generator of |TidyDiagnostic|s.
Alex Klein1699fab2022-09-08 08:46:06 -0600209 """
210 assert tidy_invocation_dir.is_absolute(), tidy_invocation_dir
George Burgess IV853d65b2020-02-25 13:13:15 -0800211
Alex Klein1699fab2022-09-08 08:46:06 -0600212 if yaml_data is None:
213 return
George Burgess IV853d65b2020-02-25 13:13:15 -0800214
Alex Klein1699fab2022-09-08 08:46:06 -0600215 # A cache of file_path => LineOffsetMap so we only need to load offsets once
216 # per file per |parse_tidy_fixes_file| invocation.
217 cached_line_offsets = {}
George Burgess IV853d65b2020-02-25 13:13:15 -0800218
Alex Klein1699fab2022-09-08 08:46:06 -0600219 def get_line_offsets(file_path: Optional[Path]) -> LineOffsetMap:
220 """Gets a LineOffsetMap for the given |file_path|."""
221 assert not file_path or file_path.is_absolute(), file_path
George Burgess IV853d65b2020-02-25 13:13:15 -0800222
Alex Klein1699fab2022-09-08 08:46:06 -0600223 if file_path in cached_line_offsets:
224 return cached_line_offsets[file_path]
George Burgess IV853d65b2020-02-25 13:13:15 -0800225
Alex Klein1699fab2022-09-08 08:46:06 -0600226 # Sometimes tidy will give us empty file names; they don't map to any file,
227 # and are generally issues it has with CFLAGS, etc. File offsets don't
228 # matter in those, so use an empty map.
Ryan Beltranc9063352023-04-26 20:48:43 +0000229 offsets = LineOffsetMap(())
Alex Klein1699fab2022-09-08 08:46:06 -0600230 if file_path:
Ryan Beltranc9063352023-04-26 20:48:43 +0000231 try:
232 offsets = LineOffsetMap.for_text(
233 file_path.read_text(encoding="utf-8")
234 )
235 except FileNotFoundError:
236 logging.warning(
237 "Cannot get offsets for %r since file does not exist.",
238 file_path,
239 )
Alex Klein1699fab2022-09-08 08:46:06 -0600240 cached_line_offsets[file_path] = offsets
241 return offsets
George Burgess IV853d65b2020-02-25 13:13:15 -0800242
Alex Klein1699fab2022-09-08 08:46:06 -0600243 # Rarely (e.g., in the case of missing |#include|s, clang will emit relative
244 # file paths for diagnostics. This fixes those.
245 def makeabs(file_path: str) -> Path:
246 """Resolves a |file_path| emitted by clang-tidy to an absolute path."""
247 if not file_path:
248 return None
249 path = Path(file_path)
250 if not path.is_absolute():
251 path = tidy_invocation_dir / path
252 return path.resolve()
George Burgess IV853d65b2020-02-25 13:13:15 -0800253
Alex Klein1699fab2022-09-08 08:46:06 -0600254 try:
255 for diag in yaml_data["Diagnostics"]:
256 message = diag["DiagnosticMessage"]
257 file_path = message["FilePath"]
George Burgess IV853d65b2020-02-25 13:13:15 -0800258
Alex Klein1699fab2022-09-08 08:46:06 -0600259 absolute_file_path = makeabs(file_path)
260 line_offsets = get_line_offsets(absolute_file_path)
George Burgess IV853d65b2020-02-25 13:13:15 -0800261
Alex Klein1699fab2022-09-08 08:46:06 -0600262 replacements = []
263 for replacement in message.get("Replacements", ()):
264 replacement_file_path = makeabs(replacement["FilePath"])
George Burgess IV853d65b2020-02-25 13:13:15 -0800265
Alex Klein1699fab2022-09-08 08:46:06 -0600266 # FIXME(gbiv): This happens in practice with things like
267 # hicpp-member-init. Supporting it should be simple, but I'd like to
268 # get the basics running first.
269 if replacement_file_path != absolute_file_path:
270 logging.warning(
271 "Replacement %r wasn't in original file %r (diag: %r)",
272 replacement_file_path,
273 file_path,
274 diag,
275 )
276 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800277
Alex Klein1699fab2022-09-08 08:46:06 -0600278 start_offset = replacement["Offset"]
279 end_offset = start_offset + replacement["Length"]
280 replacements.append(
281 TidyReplacement(
282 new_text=replacement["ReplacementText"],
283 start_line=line_offsets.get_line_number(start_offset),
284 end_line=line_offsets.get_line_number(end_offset),
285 start_char=line_offsets.get_line_offset(start_offset),
286 end_char=line_offsets.get_line_offset(end_offset),
287 )
288 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800289
Alex Klein1699fab2022-09-08 08:46:06 -0600290 expansion_locs = []
291 for note in diag.get("Notes", ()):
292 if not note["Message"].startswith("expanded from macro "):
293 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800294
Alex Klein1699fab2022-09-08 08:46:06 -0600295 absolute_note_path = makeabs(note["FilePath"])
296 note_offsets = get_line_offsets(absolute_note_path)
297 expansion_locs.append(
298 TidyExpandedFrom(
299 file_path=absolute_note_path,
300 line_number=note_offsets.get_line_number(
301 note["FileOffset"]
302 ),
303 )
304 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800305
Alex Klein1699fab2022-09-08 08:46:06 -0600306 yield TidyDiagnostic(
307 diag_name=diag["DiagnosticName"],
308 message=message["Message"],
309 file_path=absolute_file_path,
310 line_number=line_offsets.get_line_number(message["FileOffset"]),
311 replacements=tuple(replacements),
312 expansion_locs=tuple(expansion_locs),
313 )
314 except KeyError as k:
315 key_name = k.args[0]
316 raise ClangTidySchemaError(f"Broken yaml: missing key {key_name!r}")
George Burgess IV853d65b2020-02-25 13:13:15 -0800317
318
319# Represents metadata about a clang-tidy invocation.
320class InvocationMetadata(NamedTuple):
Alex Klein1699fab2022-09-08 08:46:06 -0600321 """Metadata describing a singular invocation of clang-tidy."""
322
323 exit_code: int
324 invocation: List[str]
325 lint_target: str
326 stdstreams: str
327 wd: str
George Burgess IV853d65b2020-02-25 13:13:15 -0800328
329
330class ExceptionData:
Alex Klein1699fab2022-09-08 08:46:06 -0600331 """Info about an exception that can be sent across processes."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800332
Alex Klein1699fab2022-09-08 08:46:06 -0600333 def __init__(self):
334 """Builds an instance; only intended to be called from `except` blocks."""
335 self._str = traceback.format_exc()
George Burgess IV853d65b2020-02-25 13:13:15 -0800336
Alex Klein1699fab2022-09-08 08:46:06 -0600337 def __str__(self):
338 return self._str
George Burgess IV853d65b2020-02-25 13:13:15 -0800339
340
341def parse_tidy_invocation(
342 json_file: Path,
343) -> Union[ExceptionData, Tuple[InvocationMetadata, List[TidyDiagnostic]]]:
Alex Klein1699fab2022-09-08 08:46:06 -0600344 """Parses a clang-tidy invocation result based on a JSON file.
George Burgess IV853d65b2020-02-25 13:13:15 -0800345
Alex Klein1699fab2022-09-08 08:46:06 -0600346 This is intended to be run in a separate process, which Exceptions and
347 locking and such work notoriously poorly over, so it's never intended to
348 |raise| (except under a KeyboardInterrupt or similar).
George Burgess IV853d65b2020-02-25 13:13:15 -0800349
Alex Klein1699fab2022-09-08 08:46:06 -0600350 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000351 json_file: The JSON invocation metadata file to parse.
George Burgess IV853d65b2020-02-25 13:13:15 -0800352
Alex Klein1699fab2022-09-08 08:46:06 -0600353 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000354 An |ExceptionData| instance on failure. On success, it returns a
355 (InvocationMetadata, [TidyLint]).
Alex Klein1699fab2022-09-08 08:46:06 -0600356 """
357 try:
358 assert json_file.suffix == ".json", json_file
George Burgess IV853d65b2020-02-25 13:13:15 -0800359
Alex Klein1699fab2022-09-08 08:46:06 -0600360 with json_file.open(encoding="utf-8") as f:
361 raw_meta = json.load(f)
George Burgess IV853d65b2020-02-25 13:13:15 -0800362
Alex Klein1699fab2022-09-08 08:46:06 -0600363 meta = InvocationMetadata(
364 exit_code=raw_meta["exit_code"],
365 invocation=[raw_meta["executable"]] + raw_meta["args"],
366 lint_target=raw_meta["lint_target"],
367 stdstreams=raw_meta["stdstreams"],
368 wd=raw_meta["wd"],
369 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800370
Alex Klein1699fab2022-09-08 08:46:06 -0600371 raw_crash_output = raw_meta.get("crash_output")
372 if raw_crash_output:
373 crash_reproducer_path = raw_crash_output["crash_reproducer_path"]
374 output = raw_crash_output["stdstreams"]
375 raise RuntimeError(
376 f"""\
George Burgess IV853d65b2020-02-25 13:13:15 -0800377Clang-tidy apparently crashed; dumping lots of invocation info:
378## Tidy JSON file target: {json_file}
379## Invocation: {meta.invocation}
380## Target: {meta.lint_target}
381## Crash reproducer is at: {crash_reproducer_path}
382## Output producing reproducer:
383{output}
384## Output from the crashing invocation:
385{meta.stdstreams}
Alex Klein1699fab2022-09-08 08:46:06 -0600386"""
387 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800388
Alex Klein1699fab2022-09-08 08:46:06 -0600389 yaml_file = json_file.with_suffix(".yaml")
390 # If there is no yaml file, clang-tidy was either killed or found no lints.
391 if not yaml_file.exists():
392 if meta.exit_code:
393 raise RuntimeError(
394 "clang-tidy didn't produce an output file for "
395 f"{json_file}. Output:\n{meta.stdstreams}"
396 )
397 else:
398 return meta, []
George Burgess IV853d65b2020-02-25 13:13:15 -0800399
Alex Klein1699fab2022-09-08 08:46:06 -0600400 with yaml_file.open("rb") as f:
401 yaml_data = yaml.safe_load(f)
402 return meta, list(parse_tidy_fixes_file(Path(meta.wd), yaml_data))
403 except Exception:
404 return ExceptionData()
George Burgess IV853d65b2020-02-25 13:13:15 -0800405
406
407def generate_lints(board: str, ebuild_path: str) -> Path:
Alex Klein1699fab2022-09-08 08:46:06 -0600408 """Collects the lints for a given package on a given board.
George Burgess IV853d65b2020-02-25 13:13:15 -0800409
Alex Klein1699fab2022-09-08 08:46:06 -0600410 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000411 board: the board to collect lints for.
412 ebuild_path: the path to the ebuild to collect lints for.
George Burgess IV853d65b2020-02-25 13:13:15 -0800413
Alex Klein1699fab2022-09-08 08:46:06 -0600414 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000415 The path to a tmpdir that all of the lint YAML files (if any) will be
416 in. This will also be populated by JSON files containing
417 InvocationMetadata. The generation of this is handled by our compiler
418 wrapper.
Alex Klein1699fab2022-09-08 08:46:06 -0600419 """
420 logging.info("Running lints for %r on board %r", ebuild_path, board)
George Burgess IV853d65b2020-02-25 13:13:15 -0800421
Alex Klein1699fab2022-09-08 08:46:06 -0600422 osutils.RmDir(LINT_BASE, ignore_missing=True, sudo=True)
423 osutils.SafeMakedirs(LINT_BASE, 0o777, sudo=True)
George Burgess IV853d65b2020-02-25 13:13:15 -0800424
Alex Klein1699fab2022-09-08 08:46:06 -0600425 # FIXME(gbiv): |test| might be better here?
426 result = cros_build_lib.run(
427 [f"ebuild-{board}", ebuild_path, "clean", "compile"],
428 check=False,
429 print_cmd=True,
430 extra_env={"WITH_TIDY": "tricium"},
431 capture_output=True,
432 encoding="utf-8",
433 errors="replace",
434 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800435
Alex Klein1699fab2022-09-08 08:46:06 -0600436 if result.returncode:
437 status = (
438 f"failed with code {result.returncode}; output:\n{result.stdout}"
439 )
440 log_fn = logging.warning
441 else:
442 status = "succeeded"
443 log_fn = logging.info
George Burgess IV853d65b2020-02-25 13:13:15 -0800444
Alex Klein1699fab2022-09-08 08:46:06 -0600445 log_fn("Running |ebuild| on %s %s", ebuild_path, status)
446 lint_tmpdir = tempfile.mkdtemp(prefix="tricium_tidy")
447 osutils.CopyDirContents(LINT_BASE, lint_tmpdir)
448 return Path(lint_tmpdir)
George Burgess IV853d65b2020-02-25 13:13:15 -0800449
450
Alex Klein1699fab2022-09-08 08:46:06 -0600451def collect_lints(
452 lint_tmpdir: Path, yaml_pool: multiprocessing.Pool
453) -> Set[TidyDiagnostic]:
454 """Collects the lints for a given directory filled with linting artifacts."""
455 json_files = list(lint_tmpdir.glob("*.json"))
456 pending_parses = yaml_pool.imap(parse_tidy_invocation, json_files)
George Burgess IV853d65b2020-02-25 13:13:15 -0800457
Alex Klein1699fab2022-09-08 08:46:06 -0600458 parses_failed = 0
459 all_complaints = set()
460 for path, parse in zip(json_files, pending_parses):
461 if isinstance(parse, ExceptionData):
462 parses_failed += 1
463 logging.error(
464 "Parsing %r failed with an exception\n%s", path, parse
465 )
466 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800467
Alex Klein1699fab2022-09-08 08:46:06 -0600468 meta, complaints = parse
469 if meta.exit_code:
470 logging.warning(
471 "Invoking clang-tidy on %r with flags %r exited with code %d; "
472 "output:\n%s",
473 meta.lint_target,
474 meta.invocation,
475 meta.exit_code,
476 meta.stdstreams,
477 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800478
Alex Klein1699fab2022-09-08 08:46:06 -0600479 all_complaints.update(complaints)
George Burgess IV853d65b2020-02-25 13:13:15 -0800480
Alex Klein1699fab2022-09-08 08:46:06 -0600481 if parses_failed:
482 raise ClangTidyParseError(parses_failed, len(json_files))
George Burgess IV853d65b2020-02-25 13:13:15 -0800483
Alex Klein1699fab2022-09-08 08:46:06 -0600484 return all_complaints
George Burgess IV853d65b2020-02-25 13:13:15 -0800485
486
487def setup_tidy(board: str, ebuild_list: List[portage_util.EBuild]):
Alex Klein1699fab2022-09-08 08:46:06 -0600488 """Sets up to run clang-tidy on the given ebuilds for the given board."""
489 packages = [x.package for x in ebuild_list]
490 logging.info("Setting up to lint %r", packages)
George Burgess IV853d65b2020-02-25 13:13:15 -0800491
Alex Klein1699fab2022-09-08 08:46:06 -0600492 workon = workon_helper.WorkonHelper(
493 build_target_lib.get_default_sysroot_path(board)
494 )
495 workon.StopWorkingOnPackages(packages=[], use_all=True)
496 workon.StartWorkingOnPackages(packages)
George Burgess IV853d65b2020-02-25 13:13:15 -0800497
Alex Klein1699fab2022-09-08 08:46:06 -0600498 # We're going to be hacking with |ebuild| later on, so having all
499 # dependencies in place is necessary so one |ebuild| won't stomp on another.
500 cmd = [
501 f"emerge-{board}",
502 "--onlydeps",
503 # Since each `emerge` may eat up to `ncpu` cores, limit the maximum
504 # concurrency we can get here to (arbitrarily) 8 jobs. Having
505 # `configure`s and such run in parallel is nice.
506 f"-j{min(8, multiprocessing.cpu_count())}",
507 ]
508 cmd += packages
509 result = cros_build_lib.run(cmd, print_cmd=True, check=False)
510 if result.returncode:
511 logging.error(
512 "Setup failed with exit code %d; some lints may fail.",
513 result.returncode,
514 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800515
516
Alex Klein1699fab2022-09-08 08:46:06 -0600517def run_tidy(
518 board: str,
519 ebuild_list: List[portage_util.EBuild],
520 keep_dirs: bool,
521 parse_errors_are_nonfatal: bool,
522) -> Set[TidyDiagnostic]:
523 """Runs clang-tidy on the given ebuilds for the given board.
George Burgess IV853d65b2020-02-25 13:13:15 -0800524
Alex Klein1699fab2022-09-08 08:46:06 -0600525 Returns the set of |TidyDiagnostic|s produced by doing so.
526 """
527 # Since we rely on build actions _actually_ running, we can't live with a
528 # cache.
529 osutils.RmDir(
530 Path(build_target_lib.get_default_sysroot_path(board))
531 / "var"
532 / "cache"
533 / "portage",
534 ignore_missing=True,
535 sudo=True,
536 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800537
Alex Klein1699fab2022-09-08 08:46:06 -0600538 results = set()
539 # If clang-tidy dumps a lot of diags, it can take 1-10secs of CPU while
540 # holding the GIL to |yaml.safe_load| on my otherwise-idle dev box.
541 # |yaml_pool| lets us do this in parallel.
542 with multiprocessing.pool.Pool() as yaml_pool:
543 for ebuild in ebuild_list:
544 lint_tmpdir = generate_lints(board, ebuild.ebuild_path)
545 try:
546 results |= collect_lints(lint_tmpdir, yaml_pool)
547 except ClangTidyParseError:
548 if not parse_errors_are_nonfatal:
549 raise
550 logging.exception("Working on %r", ebuild)
551 finally:
552 if keep_dirs:
553 logging.info(
554 "Lints for %r are in %r",
555 ebuild.ebuild_path,
556 lint_tmpdir,
557 )
558 else:
559 osutils.RmDir(lint_tmpdir, ignore_missing=True, sudo=True)
560 return results
George Burgess IV853d65b2020-02-25 13:13:15 -0800561
562
Alex Klein1699fab2022-09-08 08:46:06 -0600563def resolve_package_ebuilds(
564 board: str, package_names: Iterable[str]
565) -> List[str]:
566 """Figures out ebuild paths for the given package names."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800567
Alex Klein1699fab2022-09-08 08:46:06 -0600568 def resolve_package(package_name_or_ebuild):
569 """Resolves a single package name an ebuild path."""
570 if package_name_or_ebuild.endswith(".ebuild"):
571 return package_name_or_ebuild
572 return cros_build_lib.run(
573 [f"equery-{board}", "w", package_name_or_ebuild],
574 check=True,
575 stdout=subprocess.PIPE,
576 encoding="utf-8",
577 ).stdout.strip()
George Burgess IV853d65b2020-02-25 13:13:15 -0800578
Alex Klein1699fab2022-09-08 08:46:06 -0600579 # Resolving ebuilds takes time. If we get more than one (like when I'm tesing
580 # on 50 of them), parallelism speeds things up quite a bit.
581 with multiprocessing.pool.ThreadPool() as pool:
582 return pool.map(resolve_package, package_names)
George Burgess IV853d65b2020-02-25 13:13:15 -0800583
584
Alex Klein1699fab2022-09-08 08:46:06 -0600585def filter_tidy_lints(
586 only_files: Optional[Set[Path]],
587 git_repo_base: Optional[Path],
588 diags: Iterable[TidyDiagnostic],
589) -> List[TidyDiagnostic]:
590 """Transforms and filters the given TidyDiagnostics.
George Burgess IV853d65b2020-02-25 13:13:15 -0800591
Alex Klein1699fab2022-09-08 08:46:06 -0600592 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000593 only_files: a set of file paths, or None; if this is not None, only
594 |TidyDiagnostic|s in these files will be kept.
595 git_repo_base: if not None, only files in the given directory will be
596 kept. All paths of the returned diagnostics will be made relative to
597 |git_repo_base|.
598 diags: diagnostics to transform/filter.
George Burgess IV853d65b2020-02-25 13:13:15 -0800599
Alex Klein1699fab2022-09-08 08:46:06 -0600600 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000601 A sorted list of |TidyDiagnostic|s.
Alex Klein1699fab2022-09-08 08:46:06 -0600602 """
603 result_diags = []
604 total_diags = 0
George Burgess IV853d65b2020-02-25 13:13:15 -0800605
Alex Klein1699fab2022-09-08 08:46:06 -0600606 for diag in diags:
607 total_diags += 1
George Burgess IV853d65b2020-02-25 13:13:15 -0800608
Alex Klein1699fab2022-09-08 08:46:06 -0600609 if not diag.file_path:
610 # Things like |-DFOO=1 -DFOO=2| can trigger diagnostics ("oh no you're
611 # redefining |FOO| with a different value") in 'virtual' files; these
612 # receive no name in clang.
613 logging.info(
614 "Dropping diagnostic %r, since it has no associated file", diag
615 )
616 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800617
Alex Klein1699fab2022-09-08 08:46:06 -0600618 file_path = Path(diag.file_path)
619 if only_files and file_path not in only_files:
620 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800621
Alex Klein1699fab2022-09-08 08:46:06 -0600622 if git_repo_base:
623 if git_repo_base not in file_path.parents:
624 continue
625 diag = diag.normalize_paths_to(git_repo_base)
George Burgess IV853d65b2020-02-25 13:13:15 -0800626
Alex Klein1699fab2022-09-08 08:46:06 -0600627 result_diags.append(diag)
George Burgess IV853d65b2020-02-25 13:13:15 -0800628
Alex Klein1699fab2022-09-08 08:46:06 -0600629 logging.info(
630 "Dropped %d/%d diags", total_diags - len(result_diags), total_diags
631 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800632
Alex Klein1699fab2022-09-08 08:46:06 -0600633 result_diags.sort()
634 return result_diags
George Burgess IV853d65b2020-02-25 13:13:15 -0800635
636
637def get_parser() -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -0600638 """Creates an argument parser for this script."""
639 parser = commandline.ArgumentParser(description=__doc__)
640 parser.add_argument(
641 "--output", required=True, type="path", help="File to write results to."
642 )
643 parser.add_argument(
644 "--git-repo-base",
645 type="path",
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000646 help=(
647 "Base directory of the git repo we're looking at. If specified, "
648 "only diagnostics in files in this directory will be emitted. All "
649 "diagnostic file paths will be made relative to this directory."
650 ),
Alex Klein1699fab2022-09-08 08:46:06 -0600651 )
652 parser.add_argument("--board", required=True, help="Board to run under.")
653 parser.add_argument(
654 "--package",
655 action="append",
656 required=True,
657 help="Package(s) to build and lint. Required.",
658 )
659 parser.add_argument(
660 "--keep-lint-dirs",
661 action="store_true",
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000662 help=(
663 "Keep directories with tidy lints around; meant primarily for "
664 "debugging."
665 ),
Alex Klein1699fab2022-09-08 08:46:06 -0600666 )
667 parser.add_argument(
668 "--nonfatal-parse-errors",
669 action="store_true",
670 help="Keep going even if clang-tidy's output is impossible to parse.",
671 )
672 parser.add_argument(
673 "file",
674 nargs="*",
675 type="path",
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000676 help=(
677 "File(s) to output lints for. If none are specified, this tool "
678 "outputs all lints that clang-tidy emits after applying filtering "
679 "from |--git-repo-base|, if applicable."
680 ),
Alex Klein1699fab2022-09-08 08:46:06 -0600681 )
682 return parser
George Burgess IV853d65b2020-02-25 13:13:15 -0800683
684
685def main(argv: List[str]) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600686 cros_build_lib.AssertInsideChroot()
687 parser = get_parser()
688 opts = parser.parse_args(argv)
689 opts.Freeze()
George Burgess IV853d65b2020-02-25 13:13:15 -0800690
Alex Klein1699fab2022-09-08 08:46:06 -0600691 only_files = {Path(f).resolve() for f in opts.file}
George Burgess IV853d65b2020-02-25 13:13:15 -0800692
Alex Klein1699fab2022-09-08 08:46:06 -0600693 git_repo_base = opts.git_repo_base
694 if git_repo_base:
695 git_repo_base = Path(opts.git_repo_base)
696 if not (git_repo_base / ".git").exists():
697 # This script doesn't strictly care if there's a .git dir there; more of
698 # a smoke check.
699 parser.error(
700 f"Given git repo base ({git_repo_base}) has no .git dir"
701 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800702
Alex Klein1699fab2022-09-08 08:46:06 -0600703 package_ebuilds = [
704 portage_util.EBuild(x)
705 for x in resolve_package_ebuilds(opts.board, opts.package)
706 ]
George Burgess IV853d65b2020-02-25 13:13:15 -0800707
Alex Klein1699fab2022-09-08 08:46:06 -0600708 setup_tidy(opts.board, package_ebuilds)
709 lints = filter_tidy_lints(
710 only_files,
711 git_repo_base,
712 diags=run_tidy(
713 opts.board,
714 package_ebuilds,
715 opts.keep_lint_dirs,
716 opts.nonfatal_parse_errors,
717 ),
718 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800719
Alex Klein1699fab2022-09-08 08:46:06 -0600720 osutils.WriteFile(
721 opts.output,
722 json.dumps({"tidy_diagnostics": [x.to_dict() for x in lints]}),
723 atomic=True,
724 )