blob: 150e612bbb77d4095f3697122bb004ff246a91dd [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
Ryan Beltran4706f342023-08-29 01:16:22 +000057from chromite.lib import constants
George Burgess IV853d65b2020-02-25 13:13:15 -080058from chromite.lib import cros_build_lib
George Burgess IV853d65b2020-02-25 13:13:15 -080059from chromite.lib import osutils
60from chromite.lib import portage_util
61from chromite.lib import workon_helper
62
George Burgess IV853d65b2020-02-25 13:13:15 -080063
64# The directory under which the compiler wrapper stores clang-tidy reports.
Alex Klein1699fab2022-09-08 08:46:06 -060065LINT_BASE = Path("/tmp/linting_output/clang-tidy")
George Burgess IV853d65b2020-02-25 13:13:15 -080066
Ryan Beltran4706f342023-08-29 01:16:22 +000067PLATFORM_PATH = constants.CHROOT_SOURCE_ROOT / "src/platform"
68PLATFORM2_PATH = constants.CHROOT_SOURCE_ROOT / "src/platform2"
69
George Burgess IV853d65b2020-02-25 13:13:15 -080070
71class TidyReplacement(NamedTuple):
Alex Klein1699fab2022-09-08 08:46:06 -060072 """Represents a replacement emitted by clang-tidy.
George Burgess IV853d65b2020-02-25 13:13:15 -080073
Alex Klein1699fab2022-09-08 08:46:06 -060074 File path is omitted, since these are intended to be associated with
75 TidyDiagnostics with identical paths.
76 """
77
78 new_text: str
79 start_line: int
80 end_line: int
81 start_char: int
82 end_char: int
Ryan Beltran179d6bb2023-09-14 23:37:33 +000083 start_offset: int
84 end_offset: int
George Burgess IV853d65b2020-02-25 13:13:15 -080085
86
87class TidyExpandedFrom(NamedTuple):
Alex Klein1699fab2022-09-08 08:46:06 -060088 """Represents a macro expansion.
George Burgess IV853d65b2020-02-25 13:13:15 -080089
Alex Klein1699fab2022-09-08 08:46:06 -060090 When a diagnostic is inside of a macro expansion, clang-tidy emits
91 information about where said macro was expanded from. |TidyDiagnostic|s will
92 have one |TidyExpandedFrom| for each level of this expansion.
93 """
George Burgess IV853d65b2020-02-25 13:13:15 -080094
Alex Klein1699fab2022-09-08 08:46:06 -060095 file_path: Path
96 line_number: int
97
98 def to_dict(self) -> Dict[str, Any]:
99 """Converts this |TidyExpandedFrom| to a dict serializeable as JSON."""
100 return {
101 "file_path": self.file_path.as_posix(),
102 "line_number": self.line_number,
103 }
George Burgess IV853d65b2020-02-25 13:13:15 -0800104
105
106class Error(Exception):
Alex Klein1699fab2022-09-08 08:46:06 -0600107 """Base error class for tricium-clang-tidy."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800108
109
110class ClangTidyParseError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -0600111 """Raised when clang-tidy parsing jobs fail."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800112
Alex Klein1699fab2022-09-08 08:46:06 -0600113 def __init__(self, failed_jobs: int, total_jobs: int):
114 super().__init__(f"{failed_jobs}/{total_jobs} parse jobs failed")
115 self.failed_jobs = failed_jobs
116 self.total_jobs = total_jobs
George Burgess IV853d65b2020-02-25 13:13:15 -0800117
118
119class TidyDiagnostic(NamedTuple):
Alex Klein1699fab2022-09-08 08:46:06 -0600120 """A diagnostic emitted by clang-tidy.
George Burgess IV853d65b2020-02-25 13:13:15 -0800121
Alex Klein1699fab2022-09-08 08:46:06 -0600122 Note that we shove these in a set for cheap deduplication, and we sort based
123 on the natural element order here. Sorting is mostly just for
124 deterministic/pretty output.
125 """
George Burgess IV853d65b2020-02-25 13:13:15 -0800126
Alex Klein1699fab2022-09-08 08:46:06 -0600127 file_path: Path
128 line_number: int
129 diag_name: str
130 message: str
131 replacements: Tuple[TidyReplacement]
132 expansion_locs: Tuple[TidyExpandedFrom]
George Burgess IV853d65b2020-02-25 13:13:15 -0800133
Alex Klein1699fab2022-09-08 08:46:06 -0600134 def normalize_paths_to(self, where: str) -> "TidyDiagnostic":
135 """Creates a new TidyDiagnostic with all paths relative to |where|."""
136 return self._replace(
Trent Apted4a0812b2023-05-15 15:33:55 +1000137 # Use relpath because Path.relative_to requires that `self` is
138 # rooted at `where`.
Alex Klein1699fab2022-09-08 08:46:06 -0600139 file_path=Path(os.path.relpath(self.file_path, where)),
140 expansion_locs=tuple(
141 x._replace(file_path=Path(os.path.relpath(x.file_path, where)))
142 for x in self.expansion_locs
143 ),
144 )
145
146 def to_dict(self) -> Dict[str, Any]:
147 """Converts this |TidyDiagnostic| to a dict serializeable as JSON."""
148 return {
149 "file_path": self.file_path.as_posix(),
150 "line_number": self.line_number,
151 "diag_name": self.diag_name,
152 "message": self.message,
153 "replacements": [x._asdict() for x in self.replacements],
154 "expansion_locs": [x.to_dict() for x in self.expansion_locs],
155 }
George Burgess IV853d65b2020-02-25 13:13:15 -0800156
157
158class ClangTidySchemaError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -0600159 """Raised when we encounter malformed YAML."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800160
Alex Klein1699fab2022-09-08 08:46:06 -0600161 def __init__(self, err_msg: str):
162 super().__init__(err_msg)
163 self.err_msg = err_msg
George Burgess IV853d65b2020-02-25 13:13:15 -0800164
165
166class LineOffsetMap:
Alex Klein1699fab2022-09-08 08:46:06 -0600167 """Convenient API to turn offsets in a file into line numbers."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800168
Alex Klein1699fab2022-09-08 08:46:06 -0600169 def __init__(self, newline_locations: Iterable[int]):
170 line_starts = [x + 1 for x in newline_locations]
171 # The |bisect| logic in |get_line_number|/|get_line_offset| gets a bit
Trent Apted4a0812b2023-05-15 15:33:55 +1000172 # complicated around the first and last lines of a file. Adding
173 # boundaries here removes some complexity from those implementations.
Alex Klein1699fab2022-09-08 08:46:06 -0600174 line_starts.append(0)
175 line_starts.append(sys.maxsize)
176 line_starts.sort()
George Burgess IV853d65b2020-02-25 13:13:15 -0800177
Alex Klein1699fab2022-09-08 08:46:06 -0600178 assert line_starts[0] == 0, line_starts[0]
179 assert line_starts[1] != 0, line_starts[1]
180 assert line_starts[-2] < sys.maxsize, line_starts[-2]
181 assert line_starts[-1] == sys.maxsize, line_starts[-1]
George Burgess IV853d65b2020-02-25 13:13:15 -0800182
Alex Klein1699fab2022-09-08 08:46:06 -0600183 self._line_starts = line_starts
George Burgess IV853d65b2020-02-25 13:13:15 -0800184
Alex Klein1699fab2022-09-08 08:46:06 -0600185 def get_line_number(self, char_number: int) -> int:
186 """Given a char offset into a file, returns its line number."""
187 assert 0 <= char_number < sys.maxsize, char_number
188 return bisect.bisect_right(self._line_starts, char_number)
George Burgess IV853d65b2020-02-25 13:13:15 -0800189
Alex Klein1699fab2022-09-08 08:46:06 -0600190 def get_line_offset(self, char_number: int) -> int:
191 """Given a char offset into a file, returns its column number."""
192 assert 0 <= char_number < sys.maxsize, char_number
193 line_start_index = (
194 bisect.bisect_right(self._line_starts, char_number) - 1
195 )
196 return char_number - self._line_starts[line_start_index]
George Burgess IV853d65b2020-02-25 13:13:15 -0800197
Alex Klein1699fab2022-09-08 08:46:06 -0600198 @staticmethod
199 def for_text(data: str) -> "LineOffsetMap":
200 """Creates a LineOffsetMap for the given string."""
201 return LineOffsetMap(m.start() for m in re.finditer(r"\n", data))
George Burgess IV853d65b2020-02-25 13:13:15 -0800202
203
Ryan Beltran4706f342023-08-29 01:16:22 +0000204def transform_filepaths(
205 file_path: str, tidy_invocation_dir: Path
206) -> Optional[Path]:
207 """Try to transform a weird path into the true path via educated guessing.
208
209 Args:
210 file_path: The file path as reported by clang tidy.
211 tidy_invocation_dir: The working directory when tidy was invoked.
212
213 Returns:
214 Path which corresponds to input and exists or None.
215 """
216
217 if not file_path:
218 return None
219 path = Path(file_path)
220
221 def replace_path(pattern: str, replacement: str) -> Optional[Path]:
222 if pattern in file_path:
223 new_path = Path(re.sub(f"(^|.*/){pattern}", replacement, file_path))
224 if new_path.exists():
225 return new_path
226 return None
227
228 possible_replacements = (
229 # .../platform2 almost always refers to platform2 regardless of prefix.
230 ("platform2", PLATFORM2_PATH),
231 # .../usr/include/ sometimes refers to things in platform or platform2.
232 ("usr/include", PLATFORM2_PATH),
233 ("usr/include", PLATFORM_PATH),
234 # .../gen/include/ sometimes refers to things in platform or platform2.
235 ("gen/include", PLATFORM2_PATH),
236 ("gen/include", PLATFORM_PATH),
237 )
238
239 for pattern, replacement in possible_replacements:
240 path_guess = replace_path(pattern, str(replacement))
241 if path_guess:
242 return path_guess.resolve()
243
244 # Rarely (e.g., in the case of missing |#include|s, clang will emit relative
245 # file paths for diagnostics.
246 if path.is_absolute():
247 if path.exists():
248 return path.resolve()
249 else:
250 from_invocation_dir = tidy_invocation_dir / path
251 if from_invocation_dir.exists():
252 return from_invocation_dir.resolve()
253
254 logging.warning(
255 "Tidy referenced a file that cannot be located: %r",
256 file_path,
257 )
258 return path
259
260
Alex Klein1699fab2022-09-08 08:46:06 -0600261def parse_tidy_fixes_file(
262 tidy_invocation_dir: Path, yaml_data: Any
263) -> Iterable[TidyDiagnostic]:
264 """Parses a clang-tidy YAML file.
George Burgess IV853d65b2020-02-25 13:13:15 -0800265
Alex Klein1699fab2022-09-08 08:46:06 -0600266 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000267 yaml_data: The parsed YAML data from clang-tidy's fixits file.
268 tidy_invocation_dir: The directory clang-tidy was run in.
George Burgess IV853d65b2020-02-25 13:13:15 -0800269
Alex Klein1699fab2022-09-08 08:46:06 -0600270 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000271 A generator of |TidyDiagnostic|s.
Alex Klein1699fab2022-09-08 08:46:06 -0600272 """
273 assert tidy_invocation_dir.is_absolute(), tidy_invocation_dir
George Burgess IV853d65b2020-02-25 13:13:15 -0800274
Alex Klein1699fab2022-09-08 08:46:06 -0600275 if yaml_data is None:
276 return
George Burgess IV853d65b2020-02-25 13:13:15 -0800277
Alex Klein1699fab2022-09-08 08:46:06 -0600278 # A cache of file_path => LineOffsetMap so we only need to load offsets once
279 # per file per |parse_tidy_fixes_file| invocation.
280 cached_line_offsets = {}
George Burgess IV853d65b2020-02-25 13:13:15 -0800281
Alex Klein1699fab2022-09-08 08:46:06 -0600282 def get_line_offsets(file_path: Optional[Path]) -> LineOffsetMap:
283 """Gets a LineOffsetMap for the given |file_path|."""
284 assert not file_path or file_path.is_absolute(), file_path
George Burgess IV853d65b2020-02-25 13:13:15 -0800285
Alex Klein1699fab2022-09-08 08:46:06 -0600286 if file_path in cached_line_offsets:
287 return cached_line_offsets[file_path]
George Burgess IV853d65b2020-02-25 13:13:15 -0800288
Trent Apted4a0812b2023-05-15 15:33:55 +1000289 # Sometimes tidy will give us empty file names; they don't map to any
290 # file, and are generally issues it has with CFLAGS, etc. File offsets
291 # don't matter in those, so use an empty map.
Ryan Beltranc9063352023-04-26 20:48:43 +0000292 offsets = LineOffsetMap(())
Alex Klein1699fab2022-09-08 08:46:06 -0600293 if file_path:
Ryan Beltranc9063352023-04-26 20:48:43 +0000294 try:
295 offsets = LineOffsetMap.for_text(
296 file_path.read_text(encoding="utf-8")
297 )
298 except FileNotFoundError:
299 logging.warning(
300 "Cannot get offsets for %r since file does not exist.",
301 file_path,
302 )
Alex Klein1699fab2022-09-08 08:46:06 -0600303 cached_line_offsets[file_path] = offsets
304 return offsets
George Burgess IV853d65b2020-02-25 13:13:15 -0800305
Alex Klein1699fab2022-09-08 08:46:06 -0600306 try:
307 for diag in yaml_data["Diagnostics"]:
308 message = diag["DiagnosticMessage"]
309 file_path = message["FilePath"]
George Burgess IV853d65b2020-02-25 13:13:15 -0800310
Ryan Beltran4706f342023-08-29 01:16:22 +0000311 absolute_file_path = transform_filepaths(
312 file_path, tidy_invocation_dir
313 )
Alex Klein1699fab2022-09-08 08:46:06 -0600314 line_offsets = get_line_offsets(absolute_file_path)
George Burgess IV853d65b2020-02-25 13:13:15 -0800315
Alex Klein1699fab2022-09-08 08:46:06 -0600316 replacements = []
317 for replacement in message.get("Replacements", ()):
Ryan Beltran4706f342023-08-29 01:16:22 +0000318 replacement_file_path = transform_filepaths(
319 replacement["FilePath"], tidy_invocation_dir
320 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800321
Alex Klein1699fab2022-09-08 08:46:06 -0600322 # FIXME(gbiv): This happens in practice with things like
Trent Apted4a0812b2023-05-15 15:33:55 +1000323 # hicpp-member-init. Supporting it should be simple, but I'd
324 # like to get the basics running first.
Alex Klein1699fab2022-09-08 08:46:06 -0600325 if replacement_file_path != absolute_file_path:
326 logging.warning(
327 "Replacement %r wasn't in original file %r (diag: %r)",
328 replacement_file_path,
329 file_path,
330 diag,
331 )
332 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800333
Alex Klein1699fab2022-09-08 08:46:06 -0600334 start_offset = replacement["Offset"]
335 end_offset = start_offset + replacement["Length"]
336 replacements.append(
337 TidyReplacement(
338 new_text=replacement["ReplacementText"],
339 start_line=line_offsets.get_line_number(start_offset),
340 end_line=line_offsets.get_line_number(end_offset),
341 start_char=line_offsets.get_line_offset(start_offset),
342 end_char=line_offsets.get_line_offset(end_offset),
Ryan Beltran179d6bb2023-09-14 23:37:33 +0000343 start_offset=start_offset,
344 end_offset=end_offset,
Alex Klein1699fab2022-09-08 08:46:06 -0600345 )
346 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800347
Alex Klein1699fab2022-09-08 08:46:06 -0600348 expansion_locs = []
349 for note in diag.get("Notes", ()):
350 if not note["Message"].startswith("expanded from macro "):
351 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800352
Ryan Beltran4706f342023-08-29 01:16:22 +0000353 absolute_note_path = transform_filepaths(
354 note["FilePath"], tidy_invocation_dir
355 )
Alex Klein1699fab2022-09-08 08:46:06 -0600356 note_offsets = get_line_offsets(absolute_note_path)
357 expansion_locs.append(
358 TidyExpandedFrom(
359 file_path=absolute_note_path,
360 line_number=note_offsets.get_line_number(
361 note["FileOffset"]
362 ),
363 )
364 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800365
Alex Klein1699fab2022-09-08 08:46:06 -0600366 yield TidyDiagnostic(
367 diag_name=diag["DiagnosticName"],
368 message=message["Message"],
369 file_path=absolute_file_path,
370 line_number=line_offsets.get_line_number(message["FileOffset"]),
371 replacements=tuple(replacements),
372 expansion_locs=tuple(expansion_locs),
373 )
374 except KeyError as k:
375 key_name = k.args[0]
376 raise ClangTidySchemaError(f"Broken yaml: missing key {key_name!r}")
George Burgess IV853d65b2020-02-25 13:13:15 -0800377
378
379# Represents metadata about a clang-tidy invocation.
380class InvocationMetadata(NamedTuple):
Alex Klein1699fab2022-09-08 08:46:06 -0600381 """Metadata describing a singular invocation of clang-tidy."""
382
383 exit_code: int
384 invocation: List[str]
385 lint_target: str
386 stdstreams: str
387 wd: str
George Burgess IV853d65b2020-02-25 13:13:15 -0800388
389
390class ExceptionData:
Alex Klein1699fab2022-09-08 08:46:06 -0600391 """Info about an exception that can be sent across processes."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800392
Alex Klein1699fab2022-09-08 08:46:06 -0600393 def __init__(self):
Trent Apted3abd3d22023-05-17 11:40:51 +1000394 """Builds instance; only intended to be called from `except` blocks."""
Alex Klein1699fab2022-09-08 08:46:06 -0600395 self._str = traceback.format_exc()
George Burgess IV853d65b2020-02-25 13:13:15 -0800396
Alex Klein1699fab2022-09-08 08:46:06 -0600397 def __str__(self):
398 return self._str
George Burgess IV853d65b2020-02-25 13:13:15 -0800399
400
401def parse_tidy_invocation(
402 json_file: Path,
403) -> Union[ExceptionData, Tuple[InvocationMetadata, List[TidyDiagnostic]]]:
Alex Klein1699fab2022-09-08 08:46:06 -0600404 """Parses a clang-tidy invocation result based on a JSON file.
George Burgess IV853d65b2020-02-25 13:13:15 -0800405
Alex Klein1699fab2022-09-08 08:46:06 -0600406 This is intended to be run in a separate process, which Exceptions and
407 locking and such work notoriously poorly over, so it's never intended to
408 |raise| (except under a KeyboardInterrupt or similar).
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 json_file: The JSON invocation metadata file to parse.
George Burgess IV853d65b2020-02-25 13:13:15 -0800412
Alex Klein1699fab2022-09-08 08:46:06 -0600413 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000414 An |ExceptionData| instance on failure. On success, it returns a
415 (InvocationMetadata, [TidyLint]).
Alex Klein1699fab2022-09-08 08:46:06 -0600416 """
417 try:
418 assert json_file.suffix == ".json", json_file
George Burgess IV853d65b2020-02-25 13:13:15 -0800419
Alex Klein1699fab2022-09-08 08:46:06 -0600420 with json_file.open(encoding="utf-8") as f:
421 raw_meta = json.load(f)
George Burgess IV853d65b2020-02-25 13:13:15 -0800422
Alex Klein1699fab2022-09-08 08:46:06 -0600423 meta = InvocationMetadata(
424 exit_code=raw_meta["exit_code"],
425 invocation=[raw_meta["executable"]] + raw_meta["args"],
426 lint_target=raw_meta["lint_target"],
427 stdstreams=raw_meta["stdstreams"],
428 wd=raw_meta["wd"],
429 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800430
Alex Klein1699fab2022-09-08 08:46:06 -0600431 raw_crash_output = raw_meta.get("crash_output")
432 if raw_crash_output:
433 crash_reproducer_path = raw_crash_output["crash_reproducer_path"]
434 output = raw_crash_output["stdstreams"]
435 raise RuntimeError(
436 f"""\
George Burgess IV853d65b2020-02-25 13:13:15 -0800437Clang-tidy apparently crashed; dumping lots of invocation info:
438## Tidy JSON file target: {json_file}
439## Invocation: {meta.invocation}
440## Target: {meta.lint_target}
441## Crash reproducer is at: {crash_reproducer_path}
442## Output producing reproducer:
443{output}
444## Output from the crashing invocation:
445{meta.stdstreams}
Alex Klein1699fab2022-09-08 08:46:06 -0600446"""
447 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800448
Alex Klein1699fab2022-09-08 08:46:06 -0600449 yaml_file = json_file.with_suffix(".yaml")
Trent Apted4a0812b2023-05-15 15:33:55 +1000450 # If there is no yaml file, clang-tidy was either killed or found no
451 # lints.
Alex Klein1699fab2022-09-08 08:46:06 -0600452 if not yaml_file.exists():
453 if meta.exit_code:
454 raise RuntimeError(
455 "clang-tidy didn't produce an output file for "
456 f"{json_file}. Output:\n{meta.stdstreams}"
457 )
458 else:
459 return meta, []
George Burgess IV853d65b2020-02-25 13:13:15 -0800460
Alex Klein1699fab2022-09-08 08:46:06 -0600461 with yaml_file.open("rb") as f:
462 yaml_data = yaml.safe_load(f)
463 return meta, list(parse_tidy_fixes_file(Path(meta.wd), yaml_data))
464 except Exception:
465 return ExceptionData()
George Burgess IV853d65b2020-02-25 13:13:15 -0800466
467
468def generate_lints(board: str, ebuild_path: str) -> Path:
Alex Klein1699fab2022-09-08 08:46:06 -0600469 """Collects the lints for a given package on a given board.
George Burgess IV853d65b2020-02-25 13:13:15 -0800470
Alex Klein1699fab2022-09-08 08:46:06 -0600471 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000472 board: the board to collect lints for.
473 ebuild_path: the path to the ebuild to collect lints for.
George Burgess IV853d65b2020-02-25 13:13:15 -0800474
Alex Klein1699fab2022-09-08 08:46:06 -0600475 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000476 The path to a tmpdir that all of the lint YAML files (if any) will be
477 in. This will also be populated by JSON files containing
478 InvocationMetadata. The generation of this is handled by our compiler
479 wrapper.
Alex Klein1699fab2022-09-08 08:46:06 -0600480 """
481 logging.info("Running lints for %r on board %r", ebuild_path, board)
George Burgess IV853d65b2020-02-25 13:13:15 -0800482
Alex Klein1699fab2022-09-08 08:46:06 -0600483 osutils.RmDir(LINT_BASE, ignore_missing=True, sudo=True)
484 osutils.SafeMakedirs(LINT_BASE, 0o777, sudo=True)
George Burgess IV853d65b2020-02-25 13:13:15 -0800485
Alex Klein1699fab2022-09-08 08:46:06 -0600486 # FIXME(gbiv): |test| might be better here?
487 result = cros_build_lib.run(
488 [f"ebuild-{board}", ebuild_path, "clean", "compile"],
489 check=False,
490 print_cmd=True,
491 extra_env={"WITH_TIDY": "tricium"},
492 capture_output=True,
493 encoding="utf-8",
494 errors="replace",
495 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800496
Alex Klein1699fab2022-09-08 08:46:06 -0600497 if result.returncode:
498 status = (
499 f"failed with code {result.returncode}; output:\n{result.stdout}"
500 )
501 log_fn = logging.warning
502 else:
503 status = "succeeded"
504 log_fn = logging.info
George Burgess IV853d65b2020-02-25 13:13:15 -0800505
Alex Klein1699fab2022-09-08 08:46:06 -0600506 log_fn("Running |ebuild| on %s %s", ebuild_path, status)
507 lint_tmpdir = tempfile.mkdtemp(prefix="tricium_tidy")
508 osutils.CopyDirContents(LINT_BASE, lint_tmpdir)
509 return Path(lint_tmpdir)
George Burgess IV853d65b2020-02-25 13:13:15 -0800510
511
Alex Klein1699fab2022-09-08 08:46:06 -0600512def collect_lints(
513 lint_tmpdir: Path, yaml_pool: multiprocessing.Pool
514) -> Set[TidyDiagnostic]:
Trent Apted3abd3d22023-05-17 11:40:51 +1000515 """Collects lints for a given directory filled with linting artifacts."""
Alex Klein1699fab2022-09-08 08:46:06 -0600516 json_files = list(lint_tmpdir.glob("*.json"))
517 pending_parses = yaml_pool.imap(parse_tidy_invocation, json_files)
George Burgess IV853d65b2020-02-25 13:13:15 -0800518
Alex Klein1699fab2022-09-08 08:46:06 -0600519 parses_failed = 0
520 all_complaints = set()
521 for path, parse in zip(json_files, pending_parses):
522 if isinstance(parse, ExceptionData):
523 parses_failed += 1
524 logging.error(
525 "Parsing %r failed with an exception\n%s", path, parse
526 )
527 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800528
Alex Klein1699fab2022-09-08 08:46:06 -0600529 meta, complaints = parse
530 if meta.exit_code:
531 logging.warning(
532 "Invoking clang-tidy on %r with flags %r exited with code %d; "
533 "output:\n%s",
534 meta.lint_target,
535 meta.invocation,
536 meta.exit_code,
537 meta.stdstreams,
538 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800539
Alex Klein1699fab2022-09-08 08:46:06 -0600540 all_complaints.update(complaints)
George Burgess IV853d65b2020-02-25 13:13:15 -0800541
Alex Klein1699fab2022-09-08 08:46:06 -0600542 if parses_failed:
543 raise ClangTidyParseError(parses_failed, len(json_files))
George Burgess IV853d65b2020-02-25 13:13:15 -0800544
Alex Klein1699fab2022-09-08 08:46:06 -0600545 return all_complaints
George Burgess IV853d65b2020-02-25 13:13:15 -0800546
547
548def setup_tidy(board: str, ebuild_list: List[portage_util.EBuild]):
Alex Klein1699fab2022-09-08 08:46:06 -0600549 """Sets up to run clang-tidy on the given ebuilds for the given board."""
550 packages = [x.package for x in ebuild_list]
551 logging.info("Setting up to lint %r", packages)
George Burgess IV853d65b2020-02-25 13:13:15 -0800552
Alex Klein1699fab2022-09-08 08:46:06 -0600553 workon = workon_helper.WorkonHelper(
554 build_target_lib.get_default_sysroot_path(board)
555 )
556 workon.StopWorkingOnPackages(packages=[], use_all=True)
557 workon.StartWorkingOnPackages(packages)
George Burgess IV853d65b2020-02-25 13:13:15 -0800558
Alex Klein1699fab2022-09-08 08:46:06 -0600559 # We're going to be hacking with |ebuild| later on, so having all
560 # dependencies in place is necessary so one |ebuild| won't stomp on another.
561 cmd = [
562 f"emerge-{board}",
563 "--onlydeps",
564 # Since each `emerge` may eat up to `ncpu` cores, limit the maximum
565 # concurrency we can get here to (arbitrarily) 8 jobs. Having
566 # `configure`s and such run in parallel is nice.
567 f"-j{min(8, multiprocessing.cpu_count())}",
568 ]
569 cmd += packages
570 result = cros_build_lib.run(cmd, print_cmd=True, check=False)
571 if result.returncode:
572 logging.error(
573 "Setup failed with exit code %d; some lints may fail.",
574 result.returncode,
575 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800576
577
Alex Klein1699fab2022-09-08 08:46:06 -0600578def run_tidy(
579 board: str,
580 ebuild_list: List[portage_util.EBuild],
581 keep_dirs: bool,
582 parse_errors_are_nonfatal: bool,
583) -> Set[TidyDiagnostic]:
584 """Runs clang-tidy on the given ebuilds for the given board.
George Burgess IV853d65b2020-02-25 13:13:15 -0800585
Alex Klein1699fab2022-09-08 08:46:06 -0600586 Returns the set of |TidyDiagnostic|s produced by doing so.
587 """
588 # Since we rely on build actions _actually_ running, we can't live with a
589 # cache.
590 osutils.RmDir(
591 Path(build_target_lib.get_default_sysroot_path(board))
592 / "var"
593 / "cache"
594 / "portage",
595 ignore_missing=True,
596 sudo=True,
597 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800598
Alex Klein1699fab2022-09-08 08:46:06 -0600599 results = set()
600 # If clang-tidy dumps a lot of diags, it can take 1-10secs of CPU while
601 # holding the GIL to |yaml.safe_load| on my otherwise-idle dev box.
602 # |yaml_pool| lets us do this in parallel.
603 with multiprocessing.pool.Pool() as yaml_pool:
604 for ebuild in ebuild_list:
605 lint_tmpdir = generate_lints(board, ebuild.ebuild_path)
606 try:
607 results |= collect_lints(lint_tmpdir, yaml_pool)
608 except ClangTidyParseError:
609 if not parse_errors_are_nonfatal:
610 raise
611 logging.exception("Working on %r", ebuild)
612 finally:
613 if keep_dirs:
614 logging.info(
615 "Lints for %r are in %r",
616 ebuild.ebuild_path,
617 lint_tmpdir,
618 )
619 else:
620 osutils.RmDir(lint_tmpdir, ignore_missing=True, sudo=True)
621 return results
George Burgess IV853d65b2020-02-25 13:13:15 -0800622
623
Alex Klein1699fab2022-09-08 08:46:06 -0600624def resolve_package_ebuilds(
625 board: str, package_names: Iterable[str]
626) -> List[str]:
627 """Figures out ebuild paths for the given package names."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800628
Alex Klein1699fab2022-09-08 08:46:06 -0600629 def resolve_package(package_name_or_ebuild):
630 """Resolves a single package name an ebuild path."""
631 if package_name_or_ebuild.endswith(".ebuild"):
632 return package_name_or_ebuild
633 return cros_build_lib.run(
634 [f"equery-{board}", "w", package_name_or_ebuild],
635 check=True,
636 stdout=subprocess.PIPE,
637 encoding="utf-8",
638 ).stdout.strip()
George Burgess IV853d65b2020-02-25 13:13:15 -0800639
Trent Apted4a0812b2023-05-15 15:33:55 +1000640 # Resolving ebuilds takes time. If we get more than one (like when I'm
641 # tesing on 50 of them), parallelism speeds things up quite a bit.
Alex Klein1699fab2022-09-08 08:46:06 -0600642 with multiprocessing.pool.ThreadPool() as pool:
643 return pool.map(resolve_package, package_names)
George Burgess IV853d65b2020-02-25 13:13:15 -0800644
645
Alex Klein1699fab2022-09-08 08:46:06 -0600646def filter_tidy_lints(
647 only_files: Optional[Set[Path]],
648 git_repo_base: Optional[Path],
649 diags: Iterable[TidyDiagnostic],
650) -> List[TidyDiagnostic]:
651 """Transforms and filters the given TidyDiagnostics.
George Burgess IV853d65b2020-02-25 13:13:15 -0800652
Alex Klein1699fab2022-09-08 08:46:06 -0600653 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000654 only_files: a set of file paths, or None; if this is not None, only
655 |TidyDiagnostic|s in these files will be kept.
656 git_repo_base: if not None, only files in the given directory will be
657 kept. All paths of the returned diagnostics will be made relative to
658 |git_repo_base|.
659 diags: diagnostics to transform/filter.
George Burgess IV853d65b2020-02-25 13:13:15 -0800660
Alex Klein1699fab2022-09-08 08:46:06 -0600661 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000662 A sorted list of |TidyDiagnostic|s.
Alex Klein1699fab2022-09-08 08:46:06 -0600663 """
664 result_diags = []
665 total_diags = 0
George Burgess IV853d65b2020-02-25 13:13:15 -0800666
Alex Klein1699fab2022-09-08 08:46:06 -0600667 for diag in diags:
668 total_diags += 1
George Burgess IV853d65b2020-02-25 13:13:15 -0800669
Alex Klein1699fab2022-09-08 08:46:06 -0600670 if not diag.file_path:
Trent Apted4a0812b2023-05-15 15:33:55 +1000671 # Things like |-DFOO=1 -DFOO=2| can trigger diagnostics ("oh no
672 # you're redefining |FOO| with a different value") in 'virtual'
673 # files; these receive no name in clang.
Alex Klein1699fab2022-09-08 08:46:06 -0600674 logging.info(
675 "Dropping diagnostic %r, since it has no associated file", diag
676 )
677 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800678
Alex Klein1699fab2022-09-08 08:46:06 -0600679 file_path = Path(diag.file_path)
680 if only_files and file_path not in only_files:
681 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800682
Alex Klein1699fab2022-09-08 08:46:06 -0600683 if git_repo_base:
684 if git_repo_base not in file_path.parents:
685 continue
686 diag = diag.normalize_paths_to(git_repo_base)
George Burgess IV853d65b2020-02-25 13:13:15 -0800687
Alex Klein1699fab2022-09-08 08:46:06 -0600688 result_diags.append(diag)
George Burgess IV853d65b2020-02-25 13:13:15 -0800689
Alex Klein1699fab2022-09-08 08:46:06 -0600690 logging.info(
691 "Dropped %d/%d diags", total_diags - len(result_diags), total_diags
692 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800693
Alex Klein1699fab2022-09-08 08:46:06 -0600694 result_diags.sort()
695 return result_diags
George Burgess IV853d65b2020-02-25 13:13:15 -0800696
697
698def get_parser() -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -0600699 """Creates an argument parser for this script."""
700 parser = commandline.ArgumentParser(description=__doc__)
701 parser.add_argument(
702 "--output", required=True, type="path", help="File to write results to."
703 )
704 parser.add_argument(
705 "--git-repo-base",
706 type="path",
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000707 help=(
708 "Base directory of the git repo we're looking at. If specified, "
709 "only diagnostics in files in this directory will be emitted. All "
710 "diagnostic file paths will be made relative to this directory."
711 ),
Alex Klein1699fab2022-09-08 08:46:06 -0600712 )
713 parser.add_argument("--board", required=True, help="Board to run under.")
714 parser.add_argument(
715 "--package",
716 action="append",
717 required=True,
718 help="Package(s) to build and lint. Required.",
719 )
720 parser.add_argument(
721 "--keep-lint-dirs",
722 action="store_true",
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000723 help=(
724 "Keep directories with tidy lints around; meant primarily for "
725 "debugging."
726 ),
Alex Klein1699fab2022-09-08 08:46:06 -0600727 )
728 parser.add_argument(
729 "--nonfatal-parse-errors",
730 action="store_true",
731 help="Keep going even if clang-tidy's output is impossible to parse.",
732 )
733 parser.add_argument(
734 "file",
735 nargs="*",
736 type="path",
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000737 help=(
738 "File(s) to output lints for. If none are specified, this tool "
739 "outputs all lints that clang-tidy emits after applying filtering "
740 "from |--git-repo-base|, if applicable."
741 ),
Alex Klein1699fab2022-09-08 08:46:06 -0600742 )
743 return parser
George Burgess IV853d65b2020-02-25 13:13:15 -0800744
745
746def main(argv: List[str]) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600747 cros_build_lib.AssertInsideChroot()
748 parser = get_parser()
749 opts = parser.parse_args(argv)
750 opts.Freeze()
George Burgess IV853d65b2020-02-25 13:13:15 -0800751
Alex Klein1699fab2022-09-08 08:46:06 -0600752 only_files = {Path(f).resolve() for f in opts.file}
George Burgess IV853d65b2020-02-25 13:13:15 -0800753
Alex Klein1699fab2022-09-08 08:46:06 -0600754 git_repo_base = opts.git_repo_base
755 if git_repo_base:
756 git_repo_base = Path(opts.git_repo_base)
757 if not (git_repo_base / ".git").exists():
Trent Apted4a0812b2023-05-15 15:33:55 +1000758 # This script doesn't strictly care if there's a .git dir there;
759 # more of a smoke check.
Alex Klein1699fab2022-09-08 08:46:06 -0600760 parser.error(
761 f"Given git repo base ({git_repo_base}) has no .git dir"
762 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800763
Alex Klein1699fab2022-09-08 08:46:06 -0600764 package_ebuilds = [
765 portage_util.EBuild(x)
766 for x in resolve_package_ebuilds(opts.board, opts.package)
767 ]
George Burgess IV853d65b2020-02-25 13:13:15 -0800768
Alex Klein1699fab2022-09-08 08:46:06 -0600769 setup_tidy(opts.board, package_ebuilds)
770 lints = filter_tidy_lints(
771 only_files,
772 git_repo_base,
773 diags=run_tidy(
774 opts.board,
775 package_ebuilds,
776 opts.keep_lint_dirs,
777 opts.nonfatal_parse_errors,
778 ),
779 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800780
Alex Klein1699fab2022-09-08 08:46:06 -0600781 osutils.WriteFile(
782 opts.output,
783 json.dumps({"tidy_diagnostics": [x.to_dict() for x in lints]}),
784 atomic=True,
785 )