blob: fd9f1820ac822eb48c1b05c07f520b1719a15840 [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
George Burgess IV853d65b2020-02-25 13:13:15 -080083
84
85class TidyExpandedFrom(NamedTuple):
Alex Klein1699fab2022-09-08 08:46:06 -060086 """Represents a macro expansion.
George Burgess IV853d65b2020-02-25 13:13:15 -080087
Alex Klein1699fab2022-09-08 08:46:06 -060088 When a diagnostic is inside of a macro expansion, clang-tidy emits
89 information about where said macro was expanded from. |TidyDiagnostic|s will
90 have one |TidyExpandedFrom| for each level of this expansion.
91 """
George Burgess IV853d65b2020-02-25 13:13:15 -080092
Alex Klein1699fab2022-09-08 08:46:06 -060093 file_path: Path
94 line_number: int
95
96 def to_dict(self) -> Dict[str, Any]:
97 """Converts this |TidyExpandedFrom| to a dict serializeable as JSON."""
98 return {
99 "file_path": self.file_path.as_posix(),
100 "line_number": self.line_number,
101 }
George Burgess IV853d65b2020-02-25 13:13:15 -0800102
103
104class Error(Exception):
Alex Klein1699fab2022-09-08 08:46:06 -0600105 """Base error class for tricium-clang-tidy."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800106
107
108class ClangTidyParseError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -0600109 """Raised when clang-tidy parsing jobs fail."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800110
Alex Klein1699fab2022-09-08 08:46:06 -0600111 def __init__(self, failed_jobs: int, total_jobs: int):
112 super().__init__(f"{failed_jobs}/{total_jobs} parse jobs failed")
113 self.failed_jobs = failed_jobs
114 self.total_jobs = total_jobs
George Burgess IV853d65b2020-02-25 13:13:15 -0800115
116
117class TidyDiagnostic(NamedTuple):
Alex Klein1699fab2022-09-08 08:46:06 -0600118 """A diagnostic emitted by clang-tidy.
George Burgess IV853d65b2020-02-25 13:13:15 -0800119
Alex Klein1699fab2022-09-08 08:46:06 -0600120 Note that we shove these in a set for cheap deduplication, and we sort based
121 on the natural element order here. Sorting is mostly just for
122 deterministic/pretty output.
123 """
George Burgess IV853d65b2020-02-25 13:13:15 -0800124
Alex Klein1699fab2022-09-08 08:46:06 -0600125 file_path: Path
126 line_number: int
127 diag_name: str
128 message: str
129 replacements: Tuple[TidyReplacement]
130 expansion_locs: Tuple[TidyExpandedFrom]
George Burgess IV853d65b2020-02-25 13:13:15 -0800131
Alex Klein1699fab2022-09-08 08:46:06 -0600132 def normalize_paths_to(self, where: str) -> "TidyDiagnostic":
133 """Creates a new TidyDiagnostic with all paths relative to |where|."""
134 return self._replace(
Trent Apted4a0812b2023-05-15 15:33:55 +1000135 # Use relpath because Path.relative_to requires that `self` is
136 # rooted at `where`.
Alex Klein1699fab2022-09-08 08:46:06 -0600137 file_path=Path(os.path.relpath(self.file_path, where)),
138 expansion_locs=tuple(
139 x._replace(file_path=Path(os.path.relpath(x.file_path, where)))
140 for x in self.expansion_locs
141 ),
142 )
143
144 def to_dict(self) -> Dict[str, Any]:
145 """Converts this |TidyDiagnostic| to a dict serializeable as JSON."""
146 return {
147 "file_path": self.file_path.as_posix(),
148 "line_number": self.line_number,
149 "diag_name": self.diag_name,
150 "message": self.message,
151 "replacements": [x._asdict() for x in self.replacements],
152 "expansion_locs": [x.to_dict() for x in self.expansion_locs],
153 }
George Burgess IV853d65b2020-02-25 13:13:15 -0800154
155
156class ClangTidySchemaError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -0600157 """Raised when we encounter malformed YAML."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800158
Alex Klein1699fab2022-09-08 08:46:06 -0600159 def __init__(self, err_msg: str):
160 super().__init__(err_msg)
161 self.err_msg = err_msg
George Burgess IV853d65b2020-02-25 13:13:15 -0800162
163
164class LineOffsetMap:
Alex Klein1699fab2022-09-08 08:46:06 -0600165 """Convenient API to turn offsets in a file into line numbers."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800166
Alex Klein1699fab2022-09-08 08:46:06 -0600167 def __init__(self, newline_locations: Iterable[int]):
168 line_starts = [x + 1 for x in newline_locations]
169 # The |bisect| logic in |get_line_number|/|get_line_offset| gets a bit
Trent Apted4a0812b2023-05-15 15:33:55 +1000170 # complicated around the first and last lines of a file. Adding
171 # boundaries here removes some complexity from those implementations.
Alex Klein1699fab2022-09-08 08:46:06 -0600172 line_starts.append(0)
173 line_starts.append(sys.maxsize)
174 line_starts.sort()
George Burgess IV853d65b2020-02-25 13:13:15 -0800175
Alex Klein1699fab2022-09-08 08:46:06 -0600176 assert line_starts[0] == 0, line_starts[0]
177 assert line_starts[1] != 0, line_starts[1]
178 assert line_starts[-2] < sys.maxsize, line_starts[-2]
179 assert line_starts[-1] == sys.maxsize, line_starts[-1]
George Burgess IV853d65b2020-02-25 13:13:15 -0800180
Alex Klein1699fab2022-09-08 08:46:06 -0600181 self._line_starts = line_starts
George Burgess IV853d65b2020-02-25 13:13:15 -0800182
Alex Klein1699fab2022-09-08 08:46:06 -0600183 def get_line_number(self, char_number: int) -> int:
184 """Given a char offset into a file, returns its line number."""
185 assert 0 <= char_number < sys.maxsize, char_number
186 return bisect.bisect_right(self._line_starts, char_number)
George Burgess IV853d65b2020-02-25 13:13:15 -0800187
Alex Klein1699fab2022-09-08 08:46:06 -0600188 def get_line_offset(self, char_number: int) -> int:
189 """Given a char offset into a file, returns its column number."""
190 assert 0 <= char_number < sys.maxsize, char_number
191 line_start_index = (
192 bisect.bisect_right(self._line_starts, char_number) - 1
193 )
194 return char_number - self._line_starts[line_start_index]
George Burgess IV853d65b2020-02-25 13:13:15 -0800195
Alex Klein1699fab2022-09-08 08:46:06 -0600196 @staticmethod
197 def for_text(data: str) -> "LineOffsetMap":
198 """Creates a LineOffsetMap for the given string."""
199 return LineOffsetMap(m.start() for m in re.finditer(r"\n", data))
George Burgess IV853d65b2020-02-25 13:13:15 -0800200
201
Ryan Beltran4706f342023-08-29 01:16:22 +0000202def transform_filepaths(
203 file_path: str, tidy_invocation_dir: Path
204) -> Optional[Path]:
205 """Try to transform a weird path into the true path via educated guessing.
206
207 Args:
208 file_path: The file path as reported by clang tidy.
209 tidy_invocation_dir: The working directory when tidy was invoked.
210
211 Returns:
212 Path which corresponds to input and exists or None.
213 """
214
215 if not file_path:
216 return None
217 path = Path(file_path)
218
219 def replace_path(pattern: str, replacement: str) -> Optional[Path]:
220 if pattern in file_path:
221 new_path = Path(re.sub(f"(^|.*/){pattern}", replacement, file_path))
222 if new_path.exists():
223 return new_path
224 return None
225
226 possible_replacements = (
227 # .../platform2 almost always refers to platform2 regardless of prefix.
228 ("platform2", PLATFORM2_PATH),
229 # .../usr/include/ sometimes refers to things in platform or platform2.
230 ("usr/include", PLATFORM2_PATH),
231 ("usr/include", PLATFORM_PATH),
232 # .../gen/include/ sometimes refers to things in platform or platform2.
233 ("gen/include", PLATFORM2_PATH),
234 ("gen/include", PLATFORM_PATH),
235 )
236
237 for pattern, replacement in possible_replacements:
238 path_guess = replace_path(pattern, str(replacement))
239 if path_guess:
240 return path_guess.resolve()
241
242 # Rarely (e.g., in the case of missing |#include|s, clang will emit relative
243 # file paths for diagnostics.
244 if path.is_absolute():
245 if path.exists():
246 return path.resolve()
247 else:
248 from_invocation_dir = tidy_invocation_dir / path
249 if from_invocation_dir.exists():
250 return from_invocation_dir.resolve()
251
252 logging.warning(
253 "Tidy referenced a file that cannot be located: %r",
254 file_path,
255 )
256 return path
257
258
Alex Klein1699fab2022-09-08 08:46:06 -0600259def parse_tidy_fixes_file(
260 tidy_invocation_dir: Path, yaml_data: Any
261) -> Iterable[TidyDiagnostic]:
262 """Parses a clang-tidy YAML file.
George Burgess IV853d65b2020-02-25 13:13:15 -0800263
Alex Klein1699fab2022-09-08 08:46:06 -0600264 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000265 yaml_data: The parsed YAML data from clang-tidy's fixits file.
266 tidy_invocation_dir: The directory clang-tidy was run in.
George Burgess IV853d65b2020-02-25 13:13:15 -0800267
Alex Klein1699fab2022-09-08 08:46:06 -0600268 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000269 A generator of |TidyDiagnostic|s.
Alex Klein1699fab2022-09-08 08:46:06 -0600270 """
271 assert tidy_invocation_dir.is_absolute(), tidy_invocation_dir
George Burgess IV853d65b2020-02-25 13:13:15 -0800272
Alex Klein1699fab2022-09-08 08:46:06 -0600273 if yaml_data is None:
274 return
George Burgess IV853d65b2020-02-25 13:13:15 -0800275
Alex Klein1699fab2022-09-08 08:46:06 -0600276 # A cache of file_path => LineOffsetMap so we only need to load offsets once
277 # per file per |parse_tidy_fixes_file| invocation.
278 cached_line_offsets = {}
George Burgess IV853d65b2020-02-25 13:13:15 -0800279
Alex Klein1699fab2022-09-08 08:46:06 -0600280 def get_line_offsets(file_path: Optional[Path]) -> LineOffsetMap:
281 """Gets a LineOffsetMap for the given |file_path|."""
282 assert not file_path or file_path.is_absolute(), file_path
George Burgess IV853d65b2020-02-25 13:13:15 -0800283
Alex Klein1699fab2022-09-08 08:46:06 -0600284 if file_path in cached_line_offsets:
285 return cached_line_offsets[file_path]
George Burgess IV853d65b2020-02-25 13:13:15 -0800286
Trent Apted4a0812b2023-05-15 15:33:55 +1000287 # Sometimes tidy will give us empty file names; they don't map to any
288 # file, and are generally issues it has with CFLAGS, etc. File offsets
289 # don't matter in those, so use an empty map.
Ryan Beltranc9063352023-04-26 20:48:43 +0000290 offsets = LineOffsetMap(())
Alex Klein1699fab2022-09-08 08:46:06 -0600291 if file_path:
Ryan Beltranc9063352023-04-26 20:48:43 +0000292 try:
293 offsets = LineOffsetMap.for_text(
294 file_path.read_text(encoding="utf-8")
295 )
296 except FileNotFoundError:
297 logging.warning(
298 "Cannot get offsets for %r since file does not exist.",
299 file_path,
300 )
Alex Klein1699fab2022-09-08 08:46:06 -0600301 cached_line_offsets[file_path] = offsets
302 return offsets
George Burgess IV853d65b2020-02-25 13:13:15 -0800303
Alex Klein1699fab2022-09-08 08:46:06 -0600304 try:
305 for diag in yaml_data["Diagnostics"]:
306 message = diag["DiagnosticMessage"]
307 file_path = message["FilePath"]
George Burgess IV853d65b2020-02-25 13:13:15 -0800308
Ryan Beltran4706f342023-08-29 01:16:22 +0000309 absolute_file_path = transform_filepaths(
310 file_path, tidy_invocation_dir
311 )
Alex Klein1699fab2022-09-08 08:46:06 -0600312 line_offsets = get_line_offsets(absolute_file_path)
George Burgess IV853d65b2020-02-25 13:13:15 -0800313
Alex Klein1699fab2022-09-08 08:46:06 -0600314 replacements = []
315 for replacement in message.get("Replacements", ()):
Ryan Beltran4706f342023-08-29 01:16:22 +0000316 replacement_file_path = transform_filepaths(
317 replacement["FilePath"], tidy_invocation_dir
318 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800319
Alex Klein1699fab2022-09-08 08:46:06 -0600320 # FIXME(gbiv): This happens in practice with things like
Trent Apted4a0812b2023-05-15 15:33:55 +1000321 # hicpp-member-init. Supporting it should be simple, but I'd
322 # like to get the basics running first.
Alex Klein1699fab2022-09-08 08:46:06 -0600323 if replacement_file_path != absolute_file_path:
324 logging.warning(
325 "Replacement %r wasn't in original file %r (diag: %r)",
326 replacement_file_path,
327 file_path,
328 diag,
329 )
330 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800331
Alex Klein1699fab2022-09-08 08:46:06 -0600332 start_offset = replacement["Offset"]
333 end_offset = start_offset + replacement["Length"]
334 replacements.append(
335 TidyReplacement(
336 new_text=replacement["ReplacementText"],
337 start_line=line_offsets.get_line_number(start_offset),
338 end_line=line_offsets.get_line_number(end_offset),
339 start_char=line_offsets.get_line_offset(start_offset),
340 end_char=line_offsets.get_line_offset(end_offset),
341 )
342 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800343
Alex Klein1699fab2022-09-08 08:46:06 -0600344 expansion_locs = []
345 for note in diag.get("Notes", ()):
346 if not note["Message"].startswith("expanded from macro "):
347 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800348
Ryan Beltran4706f342023-08-29 01:16:22 +0000349 absolute_note_path = transform_filepaths(
350 note["FilePath"], tidy_invocation_dir
351 )
Alex Klein1699fab2022-09-08 08:46:06 -0600352 note_offsets = get_line_offsets(absolute_note_path)
353 expansion_locs.append(
354 TidyExpandedFrom(
355 file_path=absolute_note_path,
356 line_number=note_offsets.get_line_number(
357 note["FileOffset"]
358 ),
359 )
360 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800361
Alex Klein1699fab2022-09-08 08:46:06 -0600362 yield TidyDiagnostic(
363 diag_name=diag["DiagnosticName"],
364 message=message["Message"],
365 file_path=absolute_file_path,
366 line_number=line_offsets.get_line_number(message["FileOffset"]),
367 replacements=tuple(replacements),
368 expansion_locs=tuple(expansion_locs),
369 )
370 except KeyError as k:
371 key_name = k.args[0]
372 raise ClangTidySchemaError(f"Broken yaml: missing key {key_name!r}")
George Burgess IV853d65b2020-02-25 13:13:15 -0800373
374
375# Represents metadata about a clang-tidy invocation.
376class InvocationMetadata(NamedTuple):
Alex Klein1699fab2022-09-08 08:46:06 -0600377 """Metadata describing a singular invocation of clang-tidy."""
378
379 exit_code: int
380 invocation: List[str]
381 lint_target: str
382 stdstreams: str
383 wd: str
George Burgess IV853d65b2020-02-25 13:13:15 -0800384
385
386class ExceptionData:
Alex Klein1699fab2022-09-08 08:46:06 -0600387 """Info about an exception that can be sent across processes."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800388
Alex Klein1699fab2022-09-08 08:46:06 -0600389 def __init__(self):
Trent Apted3abd3d22023-05-17 11:40:51 +1000390 """Builds instance; only intended to be called from `except` blocks."""
Alex Klein1699fab2022-09-08 08:46:06 -0600391 self._str = traceback.format_exc()
George Burgess IV853d65b2020-02-25 13:13:15 -0800392
Alex Klein1699fab2022-09-08 08:46:06 -0600393 def __str__(self):
394 return self._str
George Burgess IV853d65b2020-02-25 13:13:15 -0800395
396
397def parse_tidy_invocation(
398 json_file: Path,
399) -> Union[ExceptionData, Tuple[InvocationMetadata, List[TidyDiagnostic]]]:
Alex Klein1699fab2022-09-08 08:46:06 -0600400 """Parses a clang-tidy invocation result based on a JSON file.
George Burgess IV853d65b2020-02-25 13:13:15 -0800401
Alex Klein1699fab2022-09-08 08:46:06 -0600402 This is intended to be run in a separate process, which Exceptions and
403 locking and such work notoriously poorly over, so it's never intended to
404 |raise| (except under a KeyboardInterrupt or similar).
George Burgess IV853d65b2020-02-25 13:13:15 -0800405
Alex Klein1699fab2022-09-08 08:46:06 -0600406 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000407 json_file: The JSON invocation metadata file to parse.
George Burgess IV853d65b2020-02-25 13:13:15 -0800408
Alex Klein1699fab2022-09-08 08:46:06 -0600409 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000410 An |ExceptionData| instance on failure. On success, it returns a
411 (InvocationMetadata, [TidyLint]).
Alex Klein1699fab2022-09-08 08:46:06 -0600412 """
413 try:
414 assert json_file.suffix == ".json", json_file
George Burgess IV853d65b2020-02-25 13:13:15 -0800415
Alex Klein1699fab2022-09-08 08:46:06 -0600416 with json_file.open(encoding="utf-8") as f:
417 raw_meta = json.load(f)
George Burgess IV853d65b2020-02-25 13:13:15 -0800418
Alex Klein1699fab2022-09-08 08:46:06 -0600419 meta = InvocationMetadata(
420 exit_code=raw_meta["exit_code"],
421 invocation=[raw_meta["executable"]] + raw_meta["args"],
422 lint_target=raw_meta["lint_target"],
423 stdstreams=raw_meta["stdstreams"],
424 wd=raw_meta["wd"],
425 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800426
Alex Klein1699fab2022-09-08 08:46:06 -0600427 raw_crash_output = raw_meta.get("crash_output")
428 if raw_crash_output:
429 crash_reproducer_path = raw_crash_output["crash_reproducer_path"]
430 output = raw_crash_output["stdstreams"]
431 raise RuntimeError(
432 f"""\
George Burgess IV853d65b2020-02-25 13:13:15 -0800433Clang-tidy apparently crashed; dumping lots of invocation info:
434## Tidy JSON file target: {json_file}
435## Invocation: {meta.invocation}
436## Target: {meta.lint_target}
437## Crash reproducer is at: {crash_reproducer_path}
438## Output producing reproducer:
439{output}
440## Output from the crashing invocation:
441{meta.stdstreams}
Alex Klein1699fab2022-09-08 08:46:06 -0600442"""
443 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800444
Alex Klein1699fab2022-09-08 08:46:06 -0600445 yaml_file = json_file.with_suffix(".yaml")
Trent Apted4a0812b2023-05-15 15:33:55 +1000446 # If there is no yaml file, clang-tidy was either killed or found no
447 # lints.
Alex Klein1699fab2022-09-08 08:46:06 -0600448 if not yaml_file.exists():
449 if meta.exit_code:
450 raise RuntimeError(
451 "clang-tidy didn't produce an output file for "
452 f"{json_file}. Output:\n{meta.stdstreams}"
453 )
454 else:
455 return meta, []
George Burgess IV853d65b2020-02-25 13:13:15 -0800456
Alex Klein1699fab2022-09-08 08:46:06 -0600457 with yaml_file.open("rb") as f:
458 yaml_data = yaml.safe_load(f)
459 return meta, list(parse_tidy_fixes_file(Path(meta.wd), yaml_data))
460 except Exception:
461 return ExceptionData()
George Burgess IV853d65b2020-02-25 13:13:15 -0800462
463
464def generate_lints(board: str, ebuild_path: str) -> Path:
Alex Klein1699fab2022-09-08 08:46:06 -0600465 """Collects the lints for a given package on a given board.
George Burgess IV853d65b2020-02-25 13:13:15 -0800466
Alex Klein1699fab2022-09-08 08:46:06 -0600467 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000468 board: the board to collect lints for.
469 ebuild_path: the path to the ebuild to collect lints for.
George Burgess IV853d65b2020-02-25 13:13:15 -0800470
Alex Klein1699fab2022-09-08 08:46:06 -0600471 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000472 The path to a tmpdir that all of the lint YAML files (if any) will be
473 in. This will also be populated by JSON files containing
474 InvocationMetadata. The generation of this is handled by our compiler
475 wrapper.
Alex Klein1699fab2022-09-08 08:46:06 -0600476 """
477 logging.info("Running lints for %r on board %r", ebuild_path, board)
George Burgess IV853d65b2020-02-25 13:13:15 -0800478
Alex Klein1699fab2022-09-08 08:46:06 -0600479 osutils.RmDir(LINT_BASE, ignore_missing=True, sudo=True)
480 osutils.SafeMakedirs(LINT_BASE, 0o777, sudo=True)
George Burgess IV853d65b2020-02-25 13:13:15 -0800481
Alex Klein1699fab2022-09-08 08:46:06 -0600482 # FIXME(gbiv): |test| might be better here?
483 result = cros_build_lib.run(
484 [f"ebuild-{board}", ebuild_path, "clean", "compile"],
485 check=False,
486 print_cmd=True,
487 extra_env={"WITH_TIDY": "tricium"},
488 capture_output=True,
489 encoding="utf-8",
490 errors="replace",
491 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800492
Alex Klein1699fab2022-09-08 08:46:06 -0600493 if result.returncode:
494 status = (
495 f"failed with code {result.returncode}; output:\n{result.stdout}"
496 )
497 log_fn = logging.warning
498 else:
499 status = "succeeded"
500 log_fn = logging.info
George Burgess IV853d65b2020-02-25 13:13:15 -0800501
Alex Klein1699fab2022-09-08 08:46:06 -0600502 log_fn("Running |ebuild| on %s %s", ebuild_path, status)
503 lint_tmpdir = tempfile.mkdtemp(prefix="tricium_tidy")
504 osutils.CopyDirContents(LINT_BASE, lint_tmpdir)
505 return Path(lint_tmpdir)
George Burgess IV853d65b2020-02-25 13:13:15 -0800506
507
Alex Klein1699fab2022-09-08 08:46:06 -0600508def collect_lints(
509 lint_tmpdir: Path, yaml_pool: multiprocessing.Pool
510) -> Set[TidyDiagnostic]:
Trent Apted3abd3d22023-05-17 11:40:51 +1000511 """Collects lints for a given directory filled with linting artifacts."""
Alex Klein1699fab2022-09-08 08:46:06 -0600512 json_files = list(lint_tmpdir.glob("*.json"))
513 pending_parses = yaml_pool.imap(parse_tidy_invocation, json_files)
George Burgess IV853d65b2020-02-25 13:13:15 -0800514
Alex Klein1699fab2022-09-08 08:46:06 -0600515 parses_failed = 0
516 all_complaints = set()
517 for path, parse in zip(json_files, pending_parses):
518 if isinstance(parse, ExceptionData):
519 parses_failed += 1
520 logging.error(
521 "Parsing %r failed with an exception\n%s", path, parse
522 )
523 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800524
Alex Klein1699fab2022-09-08 08:46:06 -0600525 meta, complaints = parse
526 if meta.exit_code:
527 logging.warning(
528 "Invoking clang-tidy on %r with flags %r exited with code %d; "
529 "output:\n%s",
530 meta.lint_target,
531 meta.invocation,
532 meta.exit_code,
533 meta.stdstreams,
534 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800535
Alex Klein1699fab2022-09-08 08:46:06 -0600536 all_complaints.update(complaints)
George Burgess IV853d65b2020-02-25 13:13:15 -0800537
Alex Klein1699fab2022-09-08 08:46:06 -0600538 if parses_failed:
539 raise ClangTidyParseError(parses_failed, len(json_files))
George Burgess IV853d65b2020-02-25 13:13:15 -0800540
Alex Klein1699fab2022-09-08 08:46:06 -0600541 return all_complaints
George Burgess IV853d65b2020-02-25 13:13:15 -0800542
543
544def setup_tidy(board: str, ebuild_list: List[portage_util.EBuild]):
Alex Klein1699fab2022-09-08 08:46:06 -0600545 """Sets up to run clang-tidy on the given ebuilds for the given board."""
546 packages = [x.package for x in ebuild_list]
547 logging.info("Setting up to lint %r", packages)
George Burgess IV853d65b2020-02-25 13:13:15 -0800548
Alex Klein1699fab2022-09-08 08:46:06 -0600549 workon = workon_helper.WorkonHelper(
550 build_target_lib.get_default_sysroot_path(board)
551 )
552 workon.StopWorkingOnPackages(packages=[], use_all=True)
553 workon.StartWorkingOnPackages(packages)
George Burgess IV853d65b2020-02-25 13:13:15 -0800554
Alex Klein1699fab2022-09-08 08:46:06 -0600555 # We're going to be hacking with |ebuild| later on, so having all
556 # dependencies in place is necessary so one |ebuild| won't stomp on another.
557 cmd = [
558 f"emerge-{board}",
559 "--onlydeps",
560 # Since each `emerge` may eat up to `ncpu` cores, limit the maximum
561 # concurrency we can get here to (arbitrarily) 8 jobs. Having
562 # `configure`s and such run in parallel is nice.
563 f"-j{min(8, multiprocessing.cpu_count())}",
564 ]
565 cmd += packages
566 result = cros_build_lib.run(cmd, print_cmd=True, check=False)
567 if result.returncode:
568 logging.error(
569 "Setup failed with exit code %d; some lints may fail.",
570 result.returncode,
571 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800572
573
Alex Klein1699fab2022-09-08 08:46:06 -0600574def run_tidy(
575 board: str,
576 ebuild_list: List[portage_util.EBuild],
577 keep_dirs: bool,
578 parse_errors_are_nonfatal: bool,
579) -> Set[TidyDiagnostic]:
580 """Runs clang-tidy on the given ebuilds for the given board.
George Burgess IV853d65b2020-02-25 13:13:15 -0800581
Alex Klein1699fab2022-09-08 08:46:06 -0600582 Returns the set of |TidyDiagnostic|s produced by doing so.
583 """
584 # Since we rely on build actions _actually_ running, we can't live with a
585 # cache.
586 osutils.RmDir(
587 Path(build_target_lib.get_default_sysroot_path(board))
588 / "var"
589 / "cache"
590 / "portage",
591 ignore_missing=True,
592 sudo=True,
593 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800594
Alex Klein1699fab2022-09-08 08:46:06 -0600595 results = set()
596 # If clang-tidy dumps a lot of diags, it can take 1-10secs of CPU while
597 # holding the GIL to |yaml.safe_load| on my otherwise-idle dev box.
598 # |yaml_pool| lets us do this in parallel.
599 with multiprocessing.pool.Pool() as yaml_pool:
600 for ebuild in ebuild_list:
601 lint_tmpdir = generate_lints(board, ebuild.ebuild_path)
602 try:
603 results |= collect_lints(lint_tmpdir, yaml_pool)
604 except ClangTidyParseError:
605 if not parse_errors_are_nonfatal:
606 raise
607 logging.exception("Working on %r", ebuild)
608 finally:
609 if keep_dirs:
610 logging.info(
611 "Lints for %r are in %r",
612 ebuild.ebuild_path,
613 lint_tmpdir,
614 )
615 else:
616 osutils.RmDir(lint_tmpdir, ignore_missing=True, sudo=True)
617 return results
George Burgess IV853d65b2020-02-25 13:13:15 -0800618
619
Alex Klein1699fab2022-09-08 08:46:06 -0600620def resolve_package_ebuilds(
621 board: str, package_names: Iterable[str]
622) -> List[str]:
623 """Figures out ebuild paths for the given package names."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800624
Alex Klein1699fab2022-09-08 08:46:06 -0600625 def resolve_package(package_name_or_ebuild):
626 """Resolves a single package name an ebuild path."""
627 if package_name_or_ebuild.endswith(".ebuild"):
628 return package_name_or_ebuild
629 return cros_build_lib.run(
630 [f"equery-{board}", "w", package_name_or_ebuild],
631 check=True,
632 stdout=subprocess.PIPE,
633 encoding="utf-8",
634 ).stdout.strip()
George Burgess IV853d65b2020-02-25 13:13:15 -0800635
Trent Apted4a0812b2023-05-15 15:33:55 +1000636 # Resolving ebuilds takes time. If we get more than one (like when I'm
637 # tesing on 50 of them), parallelism speeds things up quite a bit.
Alex Klein1699fab2022-09-08 08:46:06 -0600638 with multiprocessing.pool.ThreadPool() as pool:
639 return pool.map(resolve_package, package_names)
George Burgess IV853d65b2020-02-25 13:13:15 -0800640
641
Alex Klein1699fab2022-09-08 08:46:06 -0600642def filter_tidy_lints(
643 only_files: Optional[Set[Path]],
644 git_repo_base: Optional[Path],
645 diags: Iterable[TidyDiagnostic],
646) -> List[TidyDiagnostic]:
647 """Transforms and filters the given TidyDiagnostics.
George Burgess IV853d65b2020-02-25 13:13:15 -0800648
Alex Klein1699fab2022-09-08 08:46:06 -0600649 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000650 only_files: a set of file paths, or None; if this is not None, only
651 |TidyDiagnostic|s in these files will be kept.
652 git_repo_base: if not None, only files in the given directory will be
653 kept. All paths of the returned diagnostics will be made relative to
654 |git_repo_base|.
655 diags: diagnostics to transform/filter.
George Burgess IV853d65b2020-02-25 13:13:15 -0800656
Alex Klein1699fab2022-09-08 08:46:06 -0600657 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000658 A sorted list of |TidyDiagnostic|s.
Alex Klein1699fab2022-09-08 08:46:06 -0600659 """
660 result_diags = []
661 total_diags = 0
George Burgess IV853d65b2020-02-25 13:13:15 -0800662
Alex Klein1699fab2022-09-08 08:46:06 -0600663 for diag in diags:
664 total_diags += 1
George Burgess IV853d65b2020-02-25 13:13:15 -0800665
Alex Klein1699fab2022-09-08 08:46:06 -0600666 if not diag.file_path:
Trent Apted4a0812b2023-05-15 15:33:55 +1000667 # Things like |-DFOO=1 -DFOO=2| can trigger diagnostics ("oh no
668 # you're redefining |FOO| with a different value") in 'virtual'
669 # files; these receive no name in clang.
Alex Klein1699fab2022-09-08 08:46:06 -0600670 logging.info(
671 "Dropping diagnostic %r, since it has no associated file", diag
672 )
673 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800674
Alex Klein1699fab2022-09-08 08:46:06 -0600675 file_path = Path(diag.file_path)
676 if only_files and file_path not in only_files:
677 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800678
Alex Klein1699fab2022-09-08 08:46:06 -0600679 if git_repo_base:
680 if git_repo_base not in file_path.parents:
681 continue
682 diag = diag.normalize_paths_to(git_repo_base)
George Burgess IV853d65b2020-02-25 13:13:15 -0800683
Alex Klein1699fab2022-09-08 08:46:06 -0600684 result_diags.append(diag)
George Burgess IV853d65b2020-02-25 13:13:15 -0800685
Alex Klein1699fab2022-09-08 08:46:06 -0600686 logging.info(
687 "Dropped %d/%d diags", total_diags - len(result_diags), total_diags
688 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800689
Alex Klein1699fab2022-09-08 08:46:06 -0600690 result_diags.sort()
691 return result_diags
George Burgess IV853d65b2020-02-25 13:13:15 -0800692
693
694def get_parser() -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -0600695 """Creates an argument parser for this script."""
696 parser = commandline.ArgumentParser(description=__doc__)
697 parser.add_argument(
698 "--output", required=True, type="path", help="File to write results to."
699 )
700 parser.add_argument(
701 "--git-repo-base",
702 type="path",
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000703 help=(
704 "Base directory of the git repo we're looking at. If specified, "
705 "only diagnostics in files in this directory will be emitted. All "
706 "diagnostic file paths will be made relative to this directory."
707 ),
Alex Klein1699fab2022-09-08 08:46:06 -0600708 )
709 parser.add_argument("--board", required=True, help="Board to run under.")
710 parser.add_argument(
711 "--package",
712 action="append",
713 required=True,
714 help="Package(s) to build and lint. Required.",
715 )
716 parser.add_argument(
717 "--keep-lint-dirs",
718 action="store_true",
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000719 help=(
720 "Keep directories with tidy lints around; meant primarily for "
721 "debugging."
722 ),
Alex Klein1699fab2022-09-08 08:46:06 -0600723 )
724 parser.add_argument(
725 "--nonfatal-parse-errors",
726 action="store_true",
727 help="Keep going even if clang-tidy's output is impossible to parse.",
728 )
729 parser.add_argument(
730 "file",
731 nargs="*",
732 type="path",
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000733 help=(
734 "File(s) to output lints for. If none are specified, this tool "
735 "outputs all lints that clang-tidy emits after applying filtering "
736 "from |--git-repo-base|, if applicable."
737 ),
Alex Klein1699fab2022-09-08 08:46:06 -0600738 )
739 return parser
George Burgess IV853d65b2020-02-25 13:13:15 -0800740
741
742def main(argv: List[str]) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600743 cros_build_lib.AssertInsideChroot()
744 parser = get_parser()
745 opts = parser.parse_args(argv)
746 opts.Freeze()
George Burgess IV853d65b2020-02-25 13:13:15 -0800747
Alex Klein1699fab2022-09-08 08:46:06 -0600748 only_files = {Path(f).resolve() for f in opts.file}
George Burgess IV853d65b2020-02-25 13:13:15 -0800749
Alex Klein1699fab2022-09-08 08:46:06 -0600750 git_repo_base = opts.git_repo_base
751 if git_repo_base:
752 git_repo_base = Path(opts.git_repo_base)
753 if not (git_repo_base / ".git").exists():
Trent Apted4a0812b2023-05-15 15:33:55 +1000754 # This script doesn't strictly care if there's a .git dir there;
755 # more of a smoke check.
Alex Klein1699fab2022-09-08 08:46:06 -0600756 parser.error(
757 f"Given git repo base ({git_repo_base}) has no .git dir"
758 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800759
Alex Klein1699fab2022-09-08 08:46:06 -0600760 package_ebuilds = [
761 portage_util.EBuild(x)
762 for x in resolve_package_ebuilds(opts.board, opts.package)
763 ]
George Burgess IV853d65b2020-02-25 13:13:15 -0800764
Alex Klein1699fab2022-09-08 08:46:06 -0600765 setup_tidy(opts.board, package_ebuilds)
766 lints = filter_tidy_lints(
767 only_files,
768 git_repo_base,
769 diags=run_tidy(
770 opts.board,
771 package_ebuilds,
772 opts.keep_lint_dirs,
773 opts.nonfatal_parse_errors,
774 ),
775 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800776
Alex Klein1699fab2022-09-08 08:46:06 -0600777 osutils.WriteFile(
778 opts.output,
779 json.dumps({"tidy_diagnostics": [x.to_dict() for x in lints]}),
780 atomic=True,
781 )