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