blob: c394e9b8fcf7fba4847af1dc39ddf5cc16a66244 [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(
Trent Apted4a0812b2023-05-15 15:33:55 +1000131 # Use relpath because Path.relative_to requires that `self` is
132 # rooted at `where`.
Alex Klein1699fab2022-09-08 08:46:06 -0600133 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
Trent Apted4a0812b2023-05-15 15:33:55 +1000166 # complicated around the first and last lines of a file. Adding
167 # boundaries here removes some complexity from those implementations.
Alex Klein1699fab2022-09-08 08:46:06 -0600168 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
Trent Apted4a0812b2023-05-15 15:33:55 +1000226 # Sometimes tidy will give us empty file names; they don't map to any
227 # file, and are generally issues it has with CFLAGS, etc. File offsets
228 # don't 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
Trent Apted4a0812b2023-05-15 15:33:55 +1000267 # hicpp-member-init. Supporting it should be simple, but I'd
268 # like to get the basics running first.
Alex Klein1699fab2022-09-08 08:46:06 -0600269 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):
Trent Apted3abd3d22023-05-17 11:40:51 +1000334 """Builds instance; only intended to be called from `except` blocks."""
Alex Klein1699fab2022-09-08 08:46:06 -0600335 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")
Trent Apted4a0812b2023-05-15 15:33:55 +1000390 # If there is no yaml file, clang-tidy was either killed or found no
391 # lints.
Alex Klein1699fab2022-09-08 08:46:06 -0600392 if not yaml_file.exists():
393 if meta.exit_code:
394 raise RuntimeError(
395 "clang-tidy didn't produce an output file for "
396 f"{json_file}. Output:\n{meta.stdstreams}"
397 )
398 else:
399 return meta, []
George Burgess IV853d65b2020-02-25 13:13:15 -0800400
Alex Klein1699fab2022-09-08 08:46:06 -0600401 with yaml_file.open("rb") as f:
402 yaml_data = yaml.safe_load(f)
403 return meta, list(parse_tidy_fixes_file(Path(meta.wd), yaml_data))
404 except Exception:
405 return ExceptionData()
George Burgess IV853d65b2020-02-25 13:13:15 -0800406
407
408def generate_lints(board: str, ebuild_path: str) -> Path:
Alex Klein1699fab2022-09-08 08:46:06 -0600409 """Collects the lints for a given package on a given board.
George Burgess IV853d65b2020-02-25 13:13:15 -0800410
Alex Klein1699fab2022-09-08 08:46:06 -0600411 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000412 board: the board to collect lints for.
413 ebuild_path: the path to the ebuild to collect lints for.
George Burgess IV853d65b2020-02-25 13:13:15 -0800414
Alex Klein1699fab2022-09-08 08:46:06 -0600415 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000416 The path to a tmpdir that all of the lint YAML files (if any) will be
417 in. This will also be populated by JSON files containing
418 InvocationMetadata. The generation of this is handled by our compiler
419 wrapper.
Alex Klein1699fab2022-09-08 08:46:06 -0600420 """
421 logging.info("Running lints for %r on board %r", ebuild_path, board)
George Burgess IV853d65b2020-02-25 13:13:15 -0800422
Alex Klein1699fab2022-09-08 08:46:06 -0600423 osutils.RmDir(LINT_BASE, ignore_missing=True, sudo=True)
424 osutils.SafeMakedirs(LINT_BASE, 0o777, sudo=True)
George Burgess IV853d65b2020-02-25 13:13:15 -0800425
Alex Klein1699fab2022-09-08 08:46:06 -0600426 # FIXME(gbiv): |test| might be better here?
427 result = cros_build_lib.run(
428 [f"ebuild-{board}", ebuild_path, "clean", "compile"],
429 check=False,
430 print_cmd=True,
431 extra_env={"WITH_TIDY": "tricium"},
432 capture_output=True,
433 encoding="utf-8",
434 errors="replace",
435 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800436
Alex Klein1699fab2022-09-08 08:46:06 -0600437 if result.returncode:
438 status = (
439 f"failed with code {result.returncode}; output:\n{result.stdout}"
440 )
441 log_fn = logging.warning
442 else:
443 status = "succeeded"
444 log_fn = logging.info
George Burgess IV853d65b2020-02-25 13:13:15 -0800445
Alex Klein1699fab2022-09-08 08:46:06 -0600446 log_fn("Running |ebuild| on %s %s", ebuild_path, status)
447 lint_tmpdir = tempfile.mkdtemp(prefix="tricium_tidy")
448 osutils.CopyDirContents(LINT_BASE, lint_tmpdir)
449 return Path(lint_tmpdir)
George Burgess IV853d65b2020-02-25 13:13:15 -0800450
451
Alex Klein1699fab2022-09-08 08:46:06 -0600452def collect_lints(
453 lint_tmpdir: Path, yaml_pool: multiprocessing.Pool
454) -> Set[TidyDiagnostic]:
Trent Apted3abd3d22023-05-17 11:40:51 +1000455 """Collects lints for a given directory filled with linting artifacts."""
Alex Klein1699fab2022-09-08 08:46:06 -0600456 json_files = list(lint_tmpdir.glob("*.json"))
457 pending_parses = yaml_pool.imap(parse_tidy_invocation, json_files)
George Burgess IV853d65b2020-02-25 13:13:15 -0800458
Alex Klein1699fab2022-09-08 08:46:06 -0600459 parses_failed = 0
460 all_complaints = set()
461 for path, parse in zip(json_files, pending_parses):
462 if isinstance(parse, ExceptionData):
463 parses_failed += 1
464 logging.error(
465 "Parsing %r failed with an exception\n%s", path, parse
466 )
467 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800468
Alex Klein1699fab2022-09-08 08:46:06 -0600469 meta, complaints = parse
470 if meta.exit_code:
471 logging.warning(
472 "Invoking clang-tidy on %r with flags %r exited with code %d; "
473 "output:\n%s",
474 meta.lint_target,
475 meta.invocation,
476 meta.exit_code,
477 meta.stdstreams,
478 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800479
Alex Klein1699fab2022-09-08 08:46:06 -0600480 all_complaints.update(complaints)
George Burgess IV853d65b2020-02-25 13:13:15 -0800481
Alex Klein1699fab2022-09-08 08:46:06 -0600482 if parses_failed:
483 raise ClangTidyParseError(parses_failed, len(json_files))
George Burgess IV853d65b2020-02-25 13:13:15 -0800484
Alex Klein1699fab2022-09-08 08:46:06 -0600485 return all_complaints
George Burgess IV853d65b2020-02-25 13:13:15 -0800486
487
488def setup_tidy(board: str, ebuild_list: List[portage_util.EBuild]):
Alex Klein1699fab2022-09-08 08:46:06 -0600489 """Sets up to run clang-tidy on the given ebuilds for the given board."""
490 packages = [x.package for x in ebuild_list]
491 logging.info("Setting up to lint %r", packages)
George Burgess IV853d65b2020-02-25 13:13:15 -0800492
Alex Klein1699fab2022-09-08 08:46:06 -0600493 workon = workon_helper.WorkonHelper(
494 build_target_lib.get_default_sysroot_path(board)
495 )
496 workon.StopWorkingOnPackages(packages=[], use_all=True)
497 workon.StartWorkingOnPackages(packages)
George Burgess IV853d65b2020-02-25 13:13:15 -0800498
Alex Klein1699fab2022-09-08 08:46:06 -0600499 # We're going to be hacking with |ebuild| later on, so having all
500 # dependencies in place is necessary so one |ebuild| won't stomp on another.
501 cmd = [
502 f"emerge-{board}",
503 "--onlydeps",
504 # Since each `emerge` may eat up to `ncpu` cores, limit the maximum
505 # concurrency we can get here to (arbitrarily) 8 jobs. Having
506 # `configure`s and such run in parallel is nice.
507 f"-j{min(8, multiprocessing.cpu_count())}",
508 ]
509 cmd += packages
510 result = cros_build_lib.run(cmd, print_cmd=True, check=False)
511 if result.returncode:
512 logging.error(
513 "Setup failed with exit code %d; some lints may fail.",
514 result.returncode,
515 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800516
517
Alex Klein1699fab2022-09-08 08:46:06 -0600518def run_tidy(
519 board: str,
520 ebuild_list: List[portage_util.EBuild],
521 keep_dirs: bool,
522 parse_errors_are_nonfatal: bool,
523) -> Set[TidyDiagnostic]:
524 """Runs clang-tidy on the given ebuilds for the given board.
George Burgess IV853d65b2020-02-25 13:13:15 -0800525
Alex Klein1699fab2022-09-08 08:46:06 -0600526 Returns the set of |TidyDiagnostic|s produced by doing so.
527 """
528 # Since we rely on build actions _actually_ running, we can't live with a
529 # cache.
530 osutils.RmDir(
531 Path(build_target_lib.get_default_sysroot_path(board))
532 / "var"
533 / "cache"
534 / "portage",
535 ignore_missing=True,
536 sudo=True,
537 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800538
Alex Klein1699fab2022-09-08 08:46:06 -0600539 results = set()
540 # If clang-tidy dumps a lot of diags, it can take 1-10secs of CPU while
541 # holding the GIL to |yaml.safe_load| on my otherwise-idle dev box.
542 # |yaml_pool| lets us do this in parallel.
543 with multiprocessing.pool.Pool() as yaml_pool:
544 for ebuild in ebuild_list:
545 lint_tmpdir = generate_lints(board, ebuild.ebuild_path)
546 try:
547 results |= collect_lints(lint_tmpdir, yaml_pool)
548 except ClangTidyParseError:
549 if not parse_errors_are_nonfatal:
550 raise
551 logging.exception("Working on %r", ebuild)
552 finally:
553 if keep_dirs:
554 logging.info(
555 "Lints for %r are in %r",
556 ebuild.ebuild_path,
557 lint_tmpdir,
558 )
559 else:
560 osutils.RmDir(lint_tmpdir, ignore_missing=True, sudo=True)
561 return results
George Burgess IV853d65b2020-02-25 13:13:15 -0800562
563
Alex Klein1699fab2022-09-08 08:46:06 -0600564def resolve_package_ebuilds(
565 board: str, package_names: Iterable[str]
566) -> List[str]:
567 """Figures out ebuild paths for the given package names."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800568
Alex Klein1699fab2022-09-08 08:46:06 -0600569 def resolve_package(package_name_or_ebuild):
570 """Resolves a single package name an ebuild path."""
571 if package_name_or_ebuild.endswith(".ebuild"):
572 return package_name_or_ebuild
573 return cros_build_lib.run(
574 [f"equery-{board}", "w", package_name_or_ebuild],
575 check=True,
576 stdout=subprocess.PIPE,
577 encoding="utf-8",
578 ).stdout.strip()
George Burgess IV853d65b2020-02-25 13:13:15 -0800579
Trent Apted4a0812b2023-05-15 15:33:55 +1000580 # Resolving ebuilds takes time. If we get more than one (like when I'm
581 # tesing on 50 of them), parallelism speeds things up quite a bit.
Alex Klein1699fab2022-09-08 08:46:06 -0600582 with multiprocessing.pool.ThreadPool() as pool:
583 return pool.map(resolve_package, package_names)
George Burgess IV853d65b2020-02-25 13:13:15 -0800584
585
Alex Klein1699fab2022-09-08 08:46:06 -0600586def filter_tidy_lints(
587 only_files: Optional[Set[Path]],
588 git_repo_base: Optional[Path],
589 diags: Iterable[TidyDiagnostic],
590) -> List[TidyDiagnostic]:
591 """Transforms and filters the given TidyDiagnostics.
George Burgess IV853d65b2020-02-25 13:13:15 -0800592
Alex Klein1699fab2022-09-08 08:46:06 -0600593 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000594 only_files: a set of file paths, or None; if this is not None, only
595 |TidyDiagnostic|s in these files will be kept.
596 git_repo_base: if not None, only files in the given directory will be
597 kept. All paths of the returned diagnostics will be made relative to
598 |git_repo_base|.
599 diags: diagnostics to transform/filter.
George Burgess IV853d65b2020-02-25 13:13:15 -0800600
Alex Klein1699fab2022-09-08 08:46:06 -0600601 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000602 A sorted list of |TidyDiagnostic|s.
Alex Klein1699fab2022-09-08 08:46:06 -0600603 """
604 result_diags = []
605 total_diags = 0
George Burgess IV853d65b2020-02-25 13:13:15 -0800606
Alex Klein1699fab2022-09-08 08:46:06 -0600607 for diag in diags:
608 total_diags += 1
George Burgess IV853d65b2020-02-25 13:13:15 -0800609
Alex Klein1699fab2022-09-08 08:46:06 -0600610 if not diag.file_path:
Trent Apted4a0812b2023-05-15 15:33:55 +1000611 # Things like |-DFOO=1 -DFOO=2| can trigger diagnostics ("oh no
612 # you're redefining |FOO| with a different value") in 'virtual'
613 # files; these receive no name in clang.
Alex Klein1699fab2022-09-08 08:46:06 -0600614 logging.info(
615 "Dropping diagnostic %r, since it has no associated file", diag
616 )
617 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800618
Alex Klein1699fab2022-09-08 08:46:06 -0600619 file_path = Path(diag.file_path)
620 if only_files and file_path not in only_files:
621 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800622
Alex Klein1699fab2022-09-08 08:46:06 -0600623 if git_repo_base:
624 if git_repo_base not in file_path.parents:
625 continue
626 diag = diag.normalize_paths_to(git_repo_base)
George Burgess IV853d65b2020-02-25 13:13:15 -0800627
Alex Klein1699fab2022-09-08 08:46:06 -0600628 result_diags.append(diag)
George Burgess IV853d65b2020-02-25 13:13:15 -0800629
Alex Klein1699fab2022-09-08 08:46:06 -0600630 logging.info(
631 "Dropped %d/%d diags", total_diags - len(result_diags), total_diags
632 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800633
Alex Klein1699fab2022-09-08 08:46:06 -0600634 result_diags.sort()
635 return result_diags
George Burgess IV853d65b2020-02-25 13:13:15 -0800636
637
638def get_parser() -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -0600639 """Creates an argument parser for this script."""
640 parser = commandline.ArgumentParser(description=__doc__)
641 parser.add_argument(
642 "--output", required=True, type="path", help="File to write results to."
643 )
644 parser.add_argument(
645 "--git-repo-base",
646 type="path",
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000647 help=(
648 "Base directory of the git repo we're looking at. If specified, "
649 "only diagnostics in files in this directory will be emitted. All "
650 "diagnostic file paths will be made relative to this directory."
651 ),
Alex Klein1699fab2022-09-08 08:46:06 -0600652 )
653 parser.add_argument("--board", required=True, help="Board to run under.")
654 parser.add_argument(
655 "--package",
656 action="append",
657 required=True,
658 help="Package(s) to build and lint. Required.",
659 )
660 parser.add_argument(
661 "--keep-lint-dirs",
662 action="store_true",
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000663 help=(
664 "Keep directories with tidy lints around; meant primarily for "
665 "debugging."
666 ),
Alex Klein1699fab2022-09-08 08:46:06 -0600667 )
668 parser.add_argument(
669 "--nonfatal-parse-errors",
670 action="store_true",
671 help="Keep going even if clang-tidy's output is impossible to parse.",
672 )
673 parser.add_argument(
674 "file",
675 nargs="*",
676 type="path",
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000677 help=(
678 "File(s) to output lints for. If none are specified, this tool "
679 "outputs all lints that clang-tidy emits after applying filtering "
680 "from |--git-repo-base|, if applicable."
681 ),
Alex Klein1699fab2022-09-08 08:46:06 -0600682 )
683 return parser
George Burgess IV853d65b2020-02-25 13:13:15 -0800684
685
686def main(argv: List[str]) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600687 cros_build_lib.AssertInsideChroot()
688 parser = get_parser()
689 opts = parser.parse_args(argv)
690 opts.Freeze()
George Burgess IV853d65b2020-02-25 13:13:15 -0800691
Alex Klein1699fab2022-09-08 08:46:06 -0600692 only_files = {Path(f).resolve() for f in opts.file}
George Burgess IV853d65b2020-02-25 13:13:15 -0800693
Alex Klein1699fab2022-09-08 08:46:06 -0600694 git_repo_base = opts.git_repo_base
695 if git_repo_base:
696 git_repo_base = Path(opts.git_repo_base)
697 if not (git_repo_base / ".git").exists():
Trent Apted4a0812b2023-05-15 15:33:55 +1000698 # This script doesn't strictly care if there's a .git dir there;
699 # more of a smoke check.
Alex Klein1699fab2022-09-08 08:46:06 -0600700 parser.error(
701 f"Given git repo base ({git_repo_base}) has no .git dir"
702 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800703
Alex Klein1699fab2022-09-08 08:46:06 -0600704 package_ebuilds = [
705 portage_util.EBuild(x)
706 for x in resolve_package_ebuilds(opts.board, opts.package)
707 ]
George Burgess IV853d65b2020-02-25 13:13:15 -0800708
Alex Klein1699fab2022-09-08 08:46:06 -0600709 setup_tidy(opts.board, package_ebuilds)
710 lints = filter_tidy_lints(
711 only_files,
712 git_repo_base,
713 diags=run_tidy(
714 opts.board,
715 package_ebuilds,
716 opts.keep_lint_dirs,
717 opts.nonfatal_parse_errors,
718 ),
719 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800720
Alex Klein1699fab2022-09-08 08:46:06 -0600721 osutils.WriteFile(
722 opts.output,
723 json.dumps({"tidy_diagnostics": [x.to_dict() for x in lints]}),
724 atomic=True,
725 )