blob: 11c7999f09a54d35a32ec968dbb89ae86e38ed17 [file] [log] [blame]
George Burgess IV853d65b2020-02-25 13:13:15 -08001# Copyright 2020 The Chromium OS Authors. All rights reserved.
2# 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.
229 if file_path:
230 offsets = LineOffsetMap.for_text(
231 file_path.read_text(encoding="utf-8")
232 )
233 else:
234 offsets = LineOffsetMap(())
235 cached_line_offsets[file_path] = offsets
236 return offsets
George Burgess IV853d65b2020-02-25 13:13:15 -0800237
Alex Klein1699fab2022-09-08 08:46:06 -0600238 # Rarely (e.g., in the case of missing |#include|s, clang will emit relative
239 # file paths for diagnostics. This fixes those.
240 def makeabs(file_path: str) -> Path:
241 """Resolves a |file_path| emitted by clang-tidy to an absolute path."""
242 if not file_path:
243 return None
244 path = Path(file_path)
245 if not path.is_absolute():
246 path = tidy_invocation_dir / path
247 return path.resolve()
George Burgess IV853d65b2020-02-25 13:13:15 -0800248
Alex Klein1699fab2022-09-08 08:46:06 -0600249 try:
250 for diag in yaml_data["Diagnostics"]:
251 message = diag["DiagnosticMessage"]
252 file_path = message["FilePath"]
George Burgess IV853d65b2020-02-25 13:13:15 -0800253
Alex Klein1699fab2022-09-08 08:46:06 -0600254 absolute_file_path = makeabs(file_path)
255 line_offsets = get_line_offsets(absolute_file_path)
George Burgess IV853d65b2020-02-25 13:13:15 -0800256
Alex Klein1699fab2022-09-08 08:46:06 -0600257 replacements = []
258 for replacement in message.get("Replacements", ()):
259 replacement_file_path = makeabs(replacement["FilePath"])
George Burgess IV853d65b2020-02-25 13:13:15 -0800260
Alex Klein1699fab2022-09-08 08:46:06 -0600261 # FIXME(gbiv): This happens in practice with things like
262 # hicpp-member-init. Supporting it should be simple, but I'd like to
263 # get the basics running first.
264 if replacement_file_path != absolute_file_path:
265 logging.warning(
266 "Replacement %r wasn't in original file %r (diag: %r)",
267 replacement_file_path,
268 file_path,
269 diag,
270 )
271 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800272
Alex Klein1699fab2022-09-08 08:46:06 -0600273 start_offset = replacement["Offset"]
274 end_offset = start_offset + replacement["Length"]
275 replacements.append(
276 TidyReplacement(
277 new_text=replacement["ReplacementText"],
278 start_line=line_offsets.get_line_number(start_offset),
279 end_line=line_offsets.get_line_number(end_offset),
280 start_char=line_offsets.get_line_offset(start_offset),
281 end_char=line_offsets.get_line_offset(end_offset),
282 )
283 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800284
Alex Klein1699fab2022-09-08 08:46:06 -0600285 expansion_locs = []
286 for note in diag.get("Notes", ()):
287 if not note["Message"].startswith("expanded from macro "):
288 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800289
Alex Klein1699fab2022-09-08 08:46:06 -0600290 absolute_note_path = makeabs(note["FilePath"])
291 note_offsets = get_line_offsets(absolute_note_path)
292 expansion_locs.append(
293 TidyExpandedFrom(
294 file_path=absolute_note_path,
295 line_number=note_offsets.get_line_number(
296 note["FileOffset"]
297 ),
298 )
299 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800300
Alex Klein1699fab2022-09-08 08:46:06 -0600301 yield TidyDiagnostic(
302 diag_name=diag["DiagnosticName"],
303 message=message["Message"],
304 file_path=absolute_file_path,
305 line_number=line_offsets.get_line_number(message["FileOffset"]),
306 replacements=tuple(replacements),
307 expansion_locs=tuple(expansion_locs),
308 )
309 except KeyError as k:
310 key_name = k.args[0]
311 raise ClangTidySchemaError(f"Broken yaml: missing key {key_name!r}")
George Burgess IV853d65b2020-02-25 13:13:15 -0800312
313
314# Represents metadata about a clang-tidy invocation.
315class InvocationMetadata(NamedTuple):
Alex Klein1699fab2022-09-08 08:46:06 -0600316 """Metadata describing a singular invocation of clang-tidy."""
317
318 exit_code: int
319 invocation: List[str]
320 lint_target: str
321 stdstreams: str
322 wd: str
George Burgess IV853d65b2020-02-25 13:13:15 -0800323
324
325class ExceptionData:
Alex Klein1699fab2022-09-08 08:46:06 -0600326 """Info about an exception that can be sent across processes."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800327
Alex Klein1699fab2022-09-08 08:46:06 -0600328 def __init__(self):
329 """Builds an instance; only intended to be called from `except` blocks."""
330 self._str = traceback.format_exc()
George Burgess IV853d65b2020-02-25 13:13:15 -0800331
Alex Klein1699fab2022-09-08 08:46:06 -0600332 def __str__(self):
333 return self._str
George Burgess IV853d65b2020-02-25 13:13:15 -0800334
335
336def parse_tidy_invocation(
337 json_file: Path,
338) -> Union[ExceptionData, Tuple[InvocationMetadata, List[TidyDiagnostic]]]:
Alex Klein1699fab2022-09-08 08:46:06 -0600339 """Parses a clang-tidy invocation result based on a JSON file.
George Burgess IV853d65b2020-02-25 13:13:15 -0800340
Alex Klein1699fab2022-09-08 08:46:06 -0600341 This is intended to be run in a separate process, which Exceptions and
342 locking and such work notoriously poorly over, so it's never intended to
343 |raise| (except under a KeyboardInterrupt or similar).
George Burgess IV853d65b2020-02-25 13:13:15 -0800344
Alex Klein1699fab2022-09-08 08:46:06 -0600345 Args:
346 json_file: The JSON invocation metadata file to parse.
George Burgess IV853d65b2020-02-25 13:13:15 -0800347
Alex Klein1699fab2022-09-08 08:46:06 -0600348 Returns:
349 An |ExceptionData| instance on failure. On success, it returns a
350 (InvocationMetadata, [TidyLint]).
351 """
352 try:
353 assert json_file.suffix == ".json", json_file
George Burgess IV853d65b2020-02-25 13:13:15 -0800354
Alex Klein1699fab2022-09-08 08:46:06 -0600355 with json_file.open(encoding="utf-8") as f:
356 raw_meta = json.load(f)
George Burgess IV853d65b2020-02-25 13:13:15 -0800357
Alex Klein1699fab2022-09-08 08:46:06 -0600358 meta = InvocationMetadata(
359 exit_code=raw_meta["exit_code"],
360 invocation=[raw_meta["executable"]] + raw_meta["args"],
361 lint_target=raw_meta["lint_target"],
362 stdstreams=raw_meta["stdstreams"],
363 wd=raw_meta["wd"],
364 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800365
Alex Klein1699fab2022-09-08 08:46:06 -0600366 raw_crash_output = raw_meta.get("crash_output")
367 if raw_crash_output:
368 crash_reproducer_path = raw_crash_output["crash_reproducer_path"]
369 output = raw_crash_output["stdstreams"]
370 raise RuntimeError(
371 f"""\
George Burgess IV853d65b2020-02-25 13:13:15 -0800372Clang-tidy apparently crashed; dumping lots of invocation info:
373## Tidy JSON file target: {json_file}
374## Invocation: {meta.invocation}
375## Target: {meta.lint_target}
376## Crash reproducer is at: {crash_reproducer_path}
377## Output producing reproducer:
378{output}
379## Output from the crashing invocation:
380{meta.stdstreams}
Alex Klein1699fab2022-09-08 08:46:06 -0600381"""
382 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800383
Alex Klein1699fab2022-09-08 08:46:06 -0600384 yaml_file = json_file.with_suffix(".yaml")
385 # If there is no yaml file, clang-tidy was either killed or found no lints.
386 if not yaml_file.exists():
387 if meta.exit_code:
388 raise RuntimeError(
389 "clang-tidy didn't produce an output file for "
390 f"{json_file}. Output:\n{meta.stdstreams}"
391 )
392 else:
393 return meta, []
George Burgess IV853d65b2020-02-25 13:13:15 -0800394
Alex Klein1699fab2022-09-08 08:46:06 -0600395 with yaml_file.open("rb") as f:
396 yaml_data = yaml.safe_load(f)
397 return meta, list(parse_tidy_fixes_file(Path(meta.wd), yaml_data))
398 except Exception:
399 return ExceptionData()
George Burgess IV853d65b2020-02-25 13:13:15 -0800400
401
402def generate_lints(board: str, ebuild_path: str) -> Path:
Alex Klein1699fab2022-09-08 08:46:06 -0600403 """Collects the lints for a given package on a given board.
George Burgess IV853d65b2020-02-25 13:13:15 -0800404
Alex Klein1699fab2022-09-08 08:46:06 -0600405 Args:
406 board: the board to collect lints for.
407 ebuild_path: the path to the ebuild to collect lints for.
George Burgess IV853d65b2020-02-25 13:13:15 -0800408
Alex Klein1699fab2022-09-08 08:46:06 -0600409 Returns:
410 The path to a tmpdir that all of the lint YAML files (if any) will be in.
411 This will also be populated by JSON files containing InvocationMetadata.
412 The generation of this is handled by our compiler wrapper.
413 """
414 logging.info("Running lints for %r on board %r", ebuild_path, board)
George Burgess IV853d65b2020-02-25 13:13:15 -0800415
Alex Klein1699fab2022-09-08 08:46:06 -0600416 osutils.RmDir(LINT_BASE, ignore_missing=True, sudo=True)
417 osutils.SafeMakedirs(LINT_BASE, 0o777, sudo=True)
George Burgess IV853d65b2020-02-25 13:13:15 -0800418
Alex Klein1699fab2022-09-08 08:46:06 -0600419 # FIXME(gbiv): |test| might be better here?
420 result = cros_build_lib.run(
421 [f"ebuild-{board}", ebuild_path, "clean", "compile"],
422 check=False,
423 print_cmd=True,
424 extra_env={"WITH_TIDY": "tricium"},
425 capture_output=True,
426 encoding="utf-8",
427 errors="replace",
428 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800429
Alex Klein1699fab2022-09-08 08:46:06 -0600430 if result.returncode:
431 status = (
432 f"failed with code {result.returncode}; output:\n{result.stdout}"
433 )
434 log_fn = logging.warning
435 else:
436 status = "succeeded"
437 log_fn = logging.info
George Burgess IV853d65b2020-02-25 13:13:15 -0800438
Alex Klein1699fab2022-09-08 08:46:06 -0600439 log_fn("Running |ebuild| on %s %s", ebuild_path, status)
440 lint_tmpdir = tempfile.mkdtemp(prefix="tricium_tidy")
441 osutils.CopyDirContents(LINT_BASE, lint_tmpdir)
442 return Path(lint_tmpdir)
George Burgess IV853d65b2020-02-25 13:13:15 -0800443
444
Alex Klein1699fab2022-09-08 08:46:06 -0600445def collect_lints(
446 lint_tmpdir: Path, yaml_pool: multiprocessing.Pool
447) -> Set[TidyDiagnostic]:
448 """Collects the lints for a given directory filled with linting artifacts."""
449 json_files = list(lint_tmpdir.glob("*.json"))
450 pending_parses = yaml_pool.imap(parse_tidy_invocation, json_files)
George Burgess IV853d65b2020-02-25 13:13:15 -0800451
Alex Klein1699fab2022-09-08 08:46:06 -0600452 parses_failed = 0
453 all_complaints = set()
454 for path, parse in zip(json_files, pending_parses):
455 if isinstance(parse, ExceptionData):
456 parses_failed += 1
457 logging.error(
458 "Parsing %r failed with an exception\n%s", path, parse
459 )
460 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800461
Alex Klein1699fab2022-09-08 08:46:06 -0600462 meta, complaints = parse
463 if meta.exit_code:
464 logging.warning(
465 "Invoking clang-tidy on %r with flags %r exited with code %d; "
466 "output:\n%s",
467 meta.lint_target,
468 meta.invocation,
469 meta.exit_code,
470 meta.stdstreams,
471 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800472
Alex Klein1699fab2022-09-08 08:46:06 -0600473 all_complaints.update(complaints)
George Burgess IV853d65b2020-02-25 13:13:15 -0800474
Alex Klein1699fab2022-09-08 08:46:06 -0600475 if parses_failed:
476 raise ClangTidyParseError(parses_failed, len(json_files))
George Burgess IV853d65b2020-02-25 13:13:15 -0800477
Alex Klein1699fab2022-09-08 08:46:06 -0600478 return all_complaints
George Burgess IV853d65b2020-02-25 13:13:15 -0800479
480
481def setup_tidy(board: str, ebuild_list: List[portage_util.EBuild]):
Alex Klein1699fab2022-09-08 08:46:06 -0600482 """Sets up to run clang-tidy on the given ebuilds for the given board."""
483 packages = [x.package for x in ebuild_list]
484 logging.info("Setting up to lint %r", packages)
George Burgess IV853d65b2020-02-25 13:13:15 -0800485
Alex Klein1699fab2022-09-08 08:46:06 -0600486 workon = workon_helper.WorkonHelper(
487 build_target_lib.get_default_sysroot_path(board)
488 )
489 workon.StopWorkingOnPackages(packages=[], use_all=True)
490 workon.StartWorkingOnPackages(packages)
George Burgess IV853d65b2020-02-25 13:13:15 -0800491
Alex Klein1699fab2022-09-08 08:46:06 -0600492 # We're going to be hacking with |ebuild| later on, so having all
493 # dependencies in place is necessary so one |ebuild| won't stomp on another.
494 cmd = [
495 f"emerge-{board}",
496 "--onlydeps",
497 # Since each `emerge` may eat up to `ncpu` cores, limit the maximum
498 # concurrency we can get here to (arbitrarily) 8 jobs. Having
499 # `configure`s and such run in parallel is nice.
500 f"-j{min(8, multiprocessing.cpu_count())}",
501 ]
502 cmd += packages
503 result = cros_build_lib.run(cmd, print_cmd=True, check=False)
504 if result.returncode:
505 logging.error(
506 "Setup failed with exit code %d; some lints may fail.",
507 result.returncode,
508 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800509
510
Alex Klein1699fab2022-09-08 08:46:06 -0600511def run_tidy(
512 board: str,
513 ebuild_list: List[portage_util.EBuild],
514 keep_dirs: bool,
515 parse_errors_are_nonfatal: bool,
516) -> Set[TidyDiagnostic]:
517 """Runs clang-tidy on the given ebuilds for the given board.
George Burgess IV853d65b2020-02-25 13:13:15 -0800518
Alex Klein1699fab2022-09-08 08:46:06 -0600519 Returns the set of |TidyDiagnostic|s produced by doing so.
520 """
521 # Since we rely on build actions _actually_ running, we can't live with a
522 # cache.
523 osutils.RmDir(
524 Path(build_target_lib.get_default_sysroot_path(board))
525 / "var"
526 / "cache"
527 / "portage",
528 ignore_missing=True,
529 sudo=True,
530 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800531
Alex Klein1699fab2022-09-08 08:46:06 -0600532 results = set()
533 # If clang-tidy dumps a lot of diags, it can take 1-10secs of CPU while
534 # holding the GIL to |yaml.safe_load| on my otherwise-idle dev box.
535 # |yaml_pool| lets us do this in parallel.
536 with multiprocessing.pool.Pool() as yaml_pool:
537 for ebuild in ebuild_list:
538 lint_tmpdir = generate_lints(board, ebuild.ebuild_path)
539 try:
540 results |= collect_lints(lint_tmpdir, yaml_pool)
541 except ClangTidyParseError:
542 if not parse_errors_are_nonfatal:
543 raise
544 logging.exception("Working on %r", ebuild)
545 finally:
546 if keep_dirs:
547 logging.info(
548 "Lints for %r are in %r",
549 ebuild.ebuild_path,
550 lint_tmpdir,
551 )
552 else:
553 osutils.RmDir(lint_tmpdir, ignore_missing=True, sudo=True)
554 return results
George Burgess IV853d65b2020-02-25 13:13:15 -0800555
556
Alex Klein1699fab2022-09-08 08:46:06 -0600557def resolve_package_ebuilds(
558 board: str, package_names: Iterable[str]
559) -> List[str]:
560 """Figures out ebuild paths for the given package names."""
George Burgess IV853d65b2020-02-25 13:13:15 -0800561
Alex Klein1699fab2022-09-08 08:46:06 -0600562 def resolve_package(package_name_or_ebuild):
563 """Resolves a single package name an ebuild path."""
564 if package_name_or_ebuild.endswith(".ebuild"):
565 return package_name_or_ebuild
566 return cros_build_lib.run(
567 [f"equery-{board}", "w", package_name_or_ebuild],
568 check=True,
569 stdout=subprocess.PIPE,
570 encoding="utf-8",
571 ).stdout.strip()
George Burgess IV853d65b2020-02-25 13:13:15 -0800572
Alex Klein1699fab2022-09-08 08:46:06 -0600573 # Resolving ebuilds takes time. If we get more than one (like when I'm tesing
574 # on 50 of them), parallelism speeds things up quite a bit.
575 with multiprocessing.pool.ThreadPool() as pool:
576 return pool.map(resolve_package, package_names)
George Burgess IV853d65b2020-02-25 13:13:15 -0800577
578
Alex Klein1699fab2022-09-08 08:46:06 -0600579def filter_tidy_lints(
580 only_files: Optional[Set[Path]],
581 git_repo_base: Optional[Path],
582 diags: Iterable[TidyDiagnostic],
583) -> List[TidyDiagnostic]:
584 """Transforms and filters the given TidyDiagnostics.
George Burgess IV853d65b2020-02-25 13:13:15 -0800585
Alex Klein1699fab2022-09-08 08:46:06 -0600586 Args:
587 only_files: a set of file paths, or None; if this is not None, only
588 |TidyDiagnostic|s in these files will be kept.
589 git_repo_base: if not None, only files in the given directory will be kept.
590 All paths of the returned diagnostics will be made relative to
591 |git_repo_base|.
592 diags: diagnostics to transform/filter.
George Burgess IV853d65b2020-02-25 13:13:15 -0800593
Alex Klein1699fab2022-09-08 08:46:06 -0600594 Returns:
595 A sorted list of |TidyDiagnostic|s.
596 """
597 result_diags = []
598 total_diags = 0
George Burgess IV853d65b2020-02-25 13:13:15 -0800599
Alex Klein1699fab2022-09-08 08:46:06 -0600600 for diag in diags:
601 total_diags += 1
George Burgess IV853d65b2020-02-25 13:13:15 -0800602
Alex Klein1699fab2022-09-08 08:46:06 -0600603 if not diag.file_path:
604 # Things like |-DFOO=1 -DFOO=2| can trigger diagnostics ("oh no you're
605 # redefining |FOO| with a different value") in 'virtual' files; these
606 # receive no name in clang.
607 logging.info(
608 "Dropping diagnostic %r, since it has no associated file", diag
609 )
610 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800611
Alex Klein1699fab2022-09-08 08:46:06 -0600612 file_path = Path(diag.file_path)
613 if only_files and file_path not in only_files:
614 continue
George Burgess IV853d65b2020-02-25 13:13:15 -0800615
Alex Klein1699fab2022-09-08 08:46:06 -0600616 if git_repo_base:
617 if git_repo_base not in file_path.parents:
618 continue
619 diag = diag.normalize_paths_to(git_repo_base)
George Burgess IV853d65b2020-02-25 13:13:15 -0800620
Alex Klein1699fab2022-09-08 08:46:06 -0600621 result_diags.append(diag)
George Burgess IV853d65b2020-02-25 13:13:15 -0800622
Alex Klein1699fab2022-09-08 08:46:06 -0600623 logging.info(
624 "Dropped %d/%d diags", total_diags - len(result_diags), total_diags
625 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800626
Alex Klein1699fab2022-09-08 08:46:06 -0600627 result_diags.sort()
628 return result_diags
George Burgess IV853d65b2020-02-25 13:13:15 -0800629
630
631def get_parser() -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -0600632 """Creates an argument parser for this script."""
633 parser = commandline.ArgumentParser(description=__doc__)
634 parser.add_argument(
635 "--output", required=True, type="path", help="File to write results to."
636 )
637 parser.add_argument(
638 "--git-repo-base",
639 type="path",
640 help="Base directory of the git repo we're looking at. If specified, "
641 "only diagnostics in files in this directory will be emitted. All "
642 "diagnostic file paths will be made relative to this directory.",
643 )
644 parser.add_argument("--board", required=True, help="Board to run under.")
645 parser.add_argument(
646 "--package",
647 action="append",
648 required=True,
649 help="Package(s) to build and lint. Required.",
650 )
651 parser.add_argument(
652 "--keep-lint-dirs",
653 action="store_true",
654 help="Keep directories with tidy lints around; meant primarily for "
655 "debugging.",
656 )
657 parser.add_argument(
658 "--nonfatal-parse-errors",
659 action="store_true",
660 help="Keep going even if clang-tidy's output is impossible to parse.",
661 )
662 parser.add_argument(
663 "file",
664 nargs="*",
665 type="path",
666 help="File(s) to output lints for. If none are specified, this tool "
667 "outputs all lints that clang-tidy emits after applying filtering "
668 "from |--git-repo-base|, if applicable.",
669 )
670 return parser
George Burgess IV853d65b2020-02-25 13:13:15 -0800671
672
673def main(argv: List[str]) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600674 cros_build_lib.AssertInsideChroot()
675 parser = get_parser()
676 opts = parser.parse_args(argv)
677 opts.Freeze()
George Burgess IV853d65b2020-02-25 13:13:15 -0800678
Alex Klein1699fab2022-09-08 08:46:06 -0600679 only_files = {Path(f).resolve() for f in opts.file}
George Burgess IV853d65b2020-02-25 13:13:15 -0800680
Alex Klein1699fab2022-09-08 08:46:06 -0600681 git_repo_base = opts.git_repo_base
682 if git_repo_base:
683 git_repo_base = Path(opts.git_repo_base)
684 if not (git_repo_base / ".git").exists():
685 # This script doesn't strictly care if there's a .git dir there; more of
686 # a smoke check.
687 parser.error(
688 f"Given git repo base ({git_repo_base}) has no .git dir"
689 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800690
Alex Klein1699fab2022-09-08 08:46:06 -0600691 package_ebuilds = [
692 portage_util.EBuild(x)
693 for x in resolve_package_ebuilds(opts.board, opts.package)
694 ]
George Burgess IV853d65b2020-02-25 13:13:15 -0800695
Alex Klein1699fab2022-09-08 08:46:06 -0600696 setup_tidy(opts.board, package_ebuilds)
697 lints = filter_tidy_lints(
698 only_files,
699 git_repo_base,
700 diags=run_tidy(
701 opts.board,
702 package_ebuilds,
703 opts.keep_lint_dirs,
704 opts.nonfatal_parse_errors,
705 ),
706 )
George Burgess IV853d65b2020-02-25 13:13:15 -0800707
Alex Klein1699fab2022-09-08 08:46:06 -0600708 osutils.WriteFile(
709 opts.output,
710 json.dumps({"tidy_diagnostics": [x.to_dict() for x in lints]}),
711 atomic=True,
712 )