blob: a89010494a914cfc27f115fde77af98afda4e9d5 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2022 The ChromiumOS Authors
Ryan Beltran1f2dd082022-04-25 18:42:32 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""This script emerges packages and retrieves their lints.
6
7Currently support is provided for both general and differential linting of C++
8with Clang Tidy and Rust with Cargo Clippy for all packages within platform2.
9"""
10
Ryan Beltran179d6bb2023-09-14 23:37:33 +000011import collections
Ryan Beltran1f2dd082022-04-25 18:42:32 +000012import json
Ryan Beltran179d6bb2023-09-14 23:37:33 +000013import logging
Ryan Beltran378934c2022-11-23 00:44:26 +000014import os
15from pathlib import Path
Ryan Beltran1f2dd082022-04-25 18:42:32 +000016import sys
Ryan Beltran179d6bb2023-09-14 23:37:33 +000017from typing import DefaultDict, Dict, Iterable, List, Optional, Text, Tuple
Ryan Beltran1f2dd082022-04-25 18:42:32 +000018
19from chromite.lib import build_target_lib
20from chromite.lib import commandline
Ryan Beltran4706f342023-08-29 01:16:22 +000021from chromite.lib import constants
Ryan Beltran1f2dd082022-04-25 18:42:32 +000022from chromite.lib import cros_build_lib
Ryan Beltranb2175862022-04-28 19:55:57 +000023from chromite.lib import portage_util
Ryan Beltrance85d0f2022-08-09 21:36:39 +000024from chromite.lib import terminal
Ryan Beltran5514eab2022-04-28 21:40:24 +000025from chromite.lib import workon_helper
Ryan Beltran1f2dd082022-04-25 18:42:32 +000026from chromite.lib.parser import package_info
27from chromite.service import toolchain
28from chromite.utils import file_util
29
30
Ryan Beltran179d6bb2023-09-14 23:37:33 +000031PLATFORM2_PATH = constants.CHROOT_SOURCE_ROOT / "src/platform2"
32
33
Alex Klein1699fab2022-09-08 08:46:06 -060034def parse_packages(
35 build_target: build_target_lib.BuildTarget, packages: List[str]
36) -> List[package_info.PackageInfo]:
37 """Parse packages and insert the category if none is given.
Ryan Beltranb2175862022-04-28 19:55:57 +000038
Alex Klein1699fab2022-09-08 08:46:06 -060039 Args:
Alex Klein68b270c2023-04-14 14:42:50 -060040 build_target: build_target to find ebuild for
41 packages: user input package names to parse
Ryan Beltranb2175862022-04-28 19:55:57 +000042
Alex Klein1699fab2022-09-08 08:46:06 -060043 Returns:
Alex Klein68b270c2023-04-14 14:42:50 -060044 A list of parsed PackageInfo objects
Alex Klein1699fab2022-09-08 08:46:06 -060045 """
46 package_infos: List[package_info.PackageInfo] = []
47 for package in packages:
48 parsed = package_info.parse(package)
49 if not parsed.category:
Alex Klein68b270c2023-04-14 14:42:50 -060050 # If a category is not specified, get it from the ebuild path.
Alex Klein1699fab2022-09-08 08:46:06 -060051 if build_target.is_host():
52 ebuild_path = portage_util.FindEbuildForPackage(
53 package, build_target.root
54 )
55 else:
56 ebuild_path = portage_util.FindEbuildForBoardPackage(
57 package, build_target.name, build_target.root
58 )
59 ebuild_data = portage_util.EBuild(ebuild_path)
60 parsed = package_info.parse(ebuild_data.package)
61 package_infos.append(parsed)
62 return package_infos
Ryan Beltranb2175862022-04-28 19:55:57 +000063
64
Ryan Beltran4706f342023-08-29 01:16:22 +000065def make_relative_to_cros(file_path: str) -> Path:
66 """removes /mnt/host/source from file_paths if present."""
67 path = Path(file_path)
68 try:
69 return path.relative_to(constants.CHROOT_SOURCE_ROOT)
70 except ValueError:
71 return path
72
73
Ryan Beltran179d6bb2023-09-14 23:37:33 +000074def process_fixes_by_file(
75 lint: toolchain.LinterFinding, file_lengths: Dict[Path, int]
76) -> Optional[DefaultDict[Path, List[toolchain.SuggestedFix]]]:
77 """Get fixes grouped by file if all the fixes apply to valid files.
78
79 If any fixes modify invalid files this returns None.
Ryan Beltranf83a3972023-09-21 00:22:25 +000080
81 Args:
82 lint: LinterFinding to get fixes from
83 file_lengths: dictionary of previously determined file lengths which
84 may be modified with additional entries
Ryan Beltran179d6bb2023-09-14 23:37:33 +000085 """
86 if not lint.suggested_fixes:
87 return None
88
89 new_fixes_by_file: DefaultDict[
90 Path, List[toolchain.SuggestedFix]
91 ] = collections.defaultdict(list)
92 for fix in lint.suggested_fixes:
93 filepath = Path(fix.location.filepath)
94 # These are files that we locate, and are usually generated files.
95 if filepath.is_absolute():
96 logging.warning(
97 "Skipped applying fix due to invalid path: %s", filepath
98 )
99 return None
100 # Make sure this file exists in platform2
101 file_in_platform2 = PLATFORM2_PATH / filepath
102 if not file_in_platform2.exists():
103 logging.warning(
104 "Skipped applying fix due to invalid path: %s", filepath
105 )
106 return None
107 if file_in_platform2 not in file_lengths:
108 file_lengths[file_in_platform2] = len(
109 file_in_platform2.read_text(encoding="utf-8")
110 )
111 if fix.location.end_offset > file_lengths[file_in_platform2]:
112 logging.warning(
113 "Skipped applying fix due to out of bounds change to: %s",
114 filepath,
115 )
116 return None
117 new_fixes_by_file[file_in_platform2].append(fix)
118
119 return new_fixes_by_file
120
121
122def get_noconflict_fixes(
123 lints: List[toolchain.LinterFinding],
124) -> Tuple[
125 DefaultDict[Path, List[toolchain.SuggestedFix]],
126 List[toolchain.LinterFinding],
127]:
128 """Get a conflict free set of replacements to apply for each file.
129
130 Fixes will not be included in results if they:
131 A) include a replacement to a path which does not exist
132 B) include a replacement to a path outside of platform2
133 C) include a replacement to file location that exceeds the file size
134 D) overlap a previous replacement.
135
136 Args:
137 lints: List of lints to aggregate suggested fixes from.
138
139 Returns:
140 A tuple including:
141 0) the mapping of paths to a list of their suggested fixes
142 1) the list of lints which were fixed
143 """
144 fixes_by_file: DefaultDict[
145 Path, List[toolchain.SuggestedFix]
146 ] = collections.defaultdict(list)
147 lints_fixed = []
148 file_lengths = {}
149 for lint in lints:
150 new_fixes_by_file = process_fixes_by_file(lint, file_lengths)
151 if not new_fixes_by_file:
152 continue
153 files_with_overlap = set(
154 filepath
155 for filepath, new_fixes in new_fixes_by_file.items()
156 if has_overlap(fixes_by_file[filepath], new_fixes)
157 )
158 if files_with_overlap:
159 logging.warning(
160 "Skipped applying fix for %s due to conflicts in:\n\t%s.",
161 lint.name,
162 "\n\t".join(files_with_overlap),
163 )
164 else:
165 for filepath, new_fixes in new_fixes_by_file.items():
166 fixes_by_file[filepath].extend(new_fixes)
167 lints_fixed.append(lint)
168
169 return fixes_by_file, lints_fixed
170
171
172def has_overlap(
173 prior_fixes: List[toolchain.SuggestedFix],
174 new_fixes: List[toolchain.SuggestedFix],
175) -> bool:
176 """Check if new fixes have overlapping ranges with a prior replacement.
177
178 Note: this implementation is n^2, but the amount of lints in a single file
179 is experimentally pretty small, so optimizing this is probably not a large
180 concern.
181 """
182 for new in new_fixes:
183 for old in prior_fixes:
184 if (
185 (old.location.start_offset <= new.location.start_offset)
186 and (new.location.start_offset <= old.location.end_offset)
187 ) or (
188 (old.location.start_offset <= new.location.end_offset)
189 and (new.location.end_offset <= old.location.end_offset)
190 ):
191 return True
192 return False
193
194
195def apply_edits(content: Text, fixes: List[toolchain.SuggestedFix]) -> Text:
196 """Modify a file by applying a list of fixes."""
197
198 # We need to be able to apply fixes in reverse order within a file to
199 # preserve code locations.
200 def fix_sort_key(fix: toolchain.SuggestedFix) -> int:
201 return fix.location.start_offset
202
203 pieces = []
204 content_end = len(content)
205 for fix in sorted(fixes, key=fix_sort_key, reverse=True):
206 pieces += [
207 content[fix.location.end_offset : content_end],
208 fix.replacement,
209 ]
210 content_end = fix.location.start_offset
211 pieces.append(content[:content_end])
212
213 return "".join(reversed(pieces))
214
215
216def apply_fixes(
217 lints: List[toolchain.LinterFinding],
218) -> Tuple[List[toolchain.LinterFinding], Iterable[Path]]:
219 """Modify files in Platform2 to apply suggested fixes from linter findings.
220
221 Some fixes which cannot be applied cleanly will be discarded (see the
222 `get_noconflict_fixes_by_file` description for more details).
223
224 Args:
225 lints: LinterFindings to apply potential fixes from.
226
227 Returns:
228 A tuple including:
229 0) The list of lints which were fixed
230 1) The list of files which were modified
231 """
232
233 fixes_by_file, lints_fixed = get_noconflict_fixes(lints)
234
235 for filepath, fixes in fixes_by_file.items():
236 file_content = filepath.read_text(encoding="utf-8")
237 rewrite = apply_edits(file_content, fixes)
238 filepath.write_text(rewrite, encoding="utf-8")
239
240 return lints_fixed, fixes_by_file.keys()
241
242
Ryan Beltrance85d0f2022-08-09 21:36:39 +0000243def format_lint(lint: toolchain.LinterFinding) -> Text:
Alex Klein68b270c2023-04-14 14:42:50 -0600244 """Formats a lint for human-readable printing.
Ryan Beltrance85d0f2022-08-09 21:36:39 +0000245
Alex Klein1699fab2022-09-08 08:46:06 -0600246 Example output:
247 [ClangTidy] In 'path/to/file.c' on line 36:
248 Also in 'path/to/file.c' on line 40:
249 Also in 'path/to/file.c' on lines 50-53:
250 You did something bad, don't do it.
Ryan Beltrance85d0f2022-08-09 21:36:39 +0000251
Alex Klein1699fab2022-09-08 08:46:06 -0600252 Args:
Alex Klein68b270c2023-04-14 14:42:50 -0600253 lint: A linter finding from the toolchain service.
Ryan Beltrance85d0f2022-08-09 21:36:39 +0000254
Alex Klein1699fab2022-09-08 08:46:06 -0600255 Returns:
Alex Klein68b270c2023-04-14 14:42:50 -0600256 A correctly formatted string ready to be displayed to the user.
Alex Klein1699fab2022-09-08 08:46:06 -0600257 """
Ryan Beltrance85d0f2022-08-09 21:36:39 +0000258
Alex Klein1699fab2022-09-08 08:46:06 -0600259 color = terminal.Color(True)
260 lines = []
261 linter_prefix = color.Color(
262 terminal.Color.YELLOW,
263 f"[{lint.linter}]",
264 background_color=terminal.Color.BLACK,
265 )
266 for loc in lint.locations:
Ryan Beltran4706f342023-08-29 01:16:22 +0000267 filepath = make_relative_to_cros(loc.filepath)
Alex Klein1699fab2022-09-08 08:46:06 -0600268 if not lines:
269 location_prefix = f"\n{linter_prefix} In"
270 else:
271 location_prefix = " and in"
272 if loc.line_start != loc.line_end:
273 lines.append(
Ryan Beltran4706f342023-08-29 01:16:22 +0000274 f"{location_prefix} '{filepath}' "
Alex Klein1699fab2022-09-08 08:46:06 -0600275 f"lines {loc.line_start}-{loc.line_end}:"
276 )
277 else:
278 lines.append(
Ryan Beltran4706f342023-08-29 01:16:22 +0000279 f"{location_prefix} '{filepath}' line {loc.line_start}:"
Alex Klein1699fab2022-09-08 08:46:06 -0600280 )
281 message_lines = lint.message.split("\n")
282 for line in message_lines:
283 lines.append(f" {line}")
284 lines.append("")
285 return "\n".join(lines)
Ryan Beltrance85d0f2022-08-09 21:36:39 +0000286
287
Ryan Beltrana32a1a12022-09-28 06:03:45 +0000288def json_format_lint(lint: toolchain.LinterFinding) -> Text:
289 """Formats a lint in json for machine parsing.
290
291 Args:
292 lint: A linter finding from the toolchain service.
293
294 Returns:
295 A correctly formatted json string ready to be displayed to the user.
296 """
297
298 def _dictify(original):
299 """Turns namedtuple's to dictionaries recursively."""
300 # Handle namedtuples
301 if isinstance(original, tuple) and hasattr(original, "_asdict"):
302 return _dictify(original._asdict())
303 # Handle collection types
304 elif hasattr(original, "__iter__"):
305 # Handle strings
306 if isinstance(original, (str, bytes)):
307 return original
308 # Handle dictionaries
309 elif isinstance(original, dict):
310 return {k: _dictify(v) for k, v in original.items()}
311 # Handle lists, sets, etc.
312 else:
313 return [_dictify(x) for x in original]
Ryan Beltranc37fb392023-05-11 18:24:40 +0000314 # Handle PackageInfo objects
315 elif isinstance(original, package_info.PackageInfo):
316 return original.atom
Ryan Beltrana32a1a12022-09-28 06:03:45 +0000317 # Handle everything else
318 return original
319
320 return json.dumps(_dictify(lint))
321
322
Ryan Beltran378934c2022-11-23 00:44:26 +0000323def get_all_sysroots() -> List[Text]:
324 """Gets all available sysroots for both host and boards."""
325 host_root = Path(build_target_lib.BuildTarget(None).root)
326 roots = [str(host_root)]
327 build_dir = host_root / "build"
328 for board in os.listdir(build_dir):
329 if board != "bin":
330 board_root = build_dir / board
331 if board_root.is_dir():
332 roots.append(str(board_root))
333 return roots
334
335
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000336def get_arg_parser() -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -0600337 """Creates an argument parser for this script."""
338 default_board = cros_build_lib.GetDefaultBoard()
339 parser = commandline.ArgumentParser(description=__doc__)
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000340
Ryan Beltran378934c2022-11-23 00:44:26 +0000341 board_group = parser.add_mutually_exclusive_group()
Alex Klein1699fab2022-09-08 08:46:06 -0600342 board_group.add_argument(
343 "-b",
344 "--board",
345 "--build-target",
346 dest="board",
347 default=default_board,
348 help="The board to emerge packages for",
349 )
350 board_group.add_argument(
351 "--host", action="store_true", help="emerge for host instead of board."
352 )
Ryan Beltran378934c2022-11-23 00:44:26 +0000353 parser.add_argument(
Alex Klein1699fab2022-09-08 08:46:06 -0600354 "--fetch-only",
355 action="store_true",
Alex Klein68b270c2023-04-14 14:42:50 -0600356 help="Fetch lints from previous run without resetting or calling "
357 "emerge.",
Alex Klein1699fab2022-09-08 08:46:06 -0600358 )
Alex Klein1699fab2022-09-08 08:46:06 -0600359 parser.add_argument(
Ryan Beltran179d6bb2023-09-14 23:37:33 +0000360 "--apply-fixes",
361 action="store_true",
362 help="Apply suggested fixes from linters.",
363 )
364 parser.add_argument(
Ryan Beltranf83a3972023-09-21 00:22:25 +0000365 "--filter-names",
366 help="Only keep lints if the name contains one of the provided filters",
367 action="append",
368 )
369 parser.add_argument(
Alex Klein1699fab2022-09-08 08:46:06 -0600370 "--differential",
371 action="store_true",
372 help="only lint lines touched by the last commit",
373 )
374 parser.add_argument(
375 "-o",
376 "--output",
377 default=sys.stdout,
378 help="File to use instead of stdout.",
379 )
380 parser.add_argument(
381 "--json", action="store_true", help="Output lints in JSON format."
382 )
383 parser.add_argument(
384 "--no-clippy",
385 dest="clippy",
386 action="store_false",
387 help="Disable cargo clippy linter.",
388 )
389 parser.add_argument(
390 "--no-tidy",
391 dest="tidy",
392 action="store_false",
393 help="Disable clang tidy linter.",
394 )
395 parser.add_argument(
396 "--no-golint",
397 dest="golint",
398 action="store_false",
399 help="Disable golint linter.",
400 )
401 parser.add_argument(
Ryan Beltran378934c2022-11-23 00:44:26 +0000402 "--iwyu",
403 action="store_true",
404 help="Enable include-what-you-use linter.",
405 )
406 parser.add_argument(
Alex Klein1699fab2022-09-08 08:46:06 -0600407 "packages",
408 nargs="*",
409 help="package(s) to emerge and retrieve lints for",
410 )
411 return parser
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000412
413
414def parse_args(argv: List[str]):
Alex Klein1699fab2022-09-08 08:46:06 -0600415 """Parses arguments in argv and returns the options."""
416 parser = get_arg_parser()
417 opts = parser.parse_args(argv)
418 opts.Freeze()
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000419
Alex Klein1699fab2022-09-08 08:46:06 -0600420 # A package must be specified unless we are in fetch-only mode
421 if not (opts.fetch_only or opts.packages):
Ryan Beltran378934c2022-11-23 00:44:26 +0000422 parser.error("Emerge mode requires specified package(s).")
Alex Klein1699fab2022-09-08 08:46:06 -0600423 if opts.fetch_only and opts.packages:
424 parser.error("Cannot specify packages for fetch-only mode.")
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000425
Ryan Beltran378934c2022-11-23 00:44:26 +0000426 # A board must be specified unless we are in fetch-only mode
427 if not (opts.fetch_only or opts.board or opts.host):
428 parser.error("Emerge mode requires either --board or --host.")
429
Alex Klein1699fab2022-09-08 08:46:06 -0600430 return opts
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000431
432
Ryan Beltranf83a3972023-09-21 00:22:25 +0000433def filter_lints(
434 lints: List[toolchain.LinterFinding], names_filters: List[Text]
435) -> List[toolchain.LinterFinding]:
436 """Filter linter finding by name."""
437 return [l for l in lints if any(f in l.name for f in names_filters)]
438
439
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000440def main(argv: List[str]) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600441 cros_build_lib.AssertInsideChroot()
442 opts = parse_args(argv)
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000443
Alex Klein1699fab2022-09-08 08:46:06 -0600444 if opts.host:
445 # BuildTarget interprets None as host target
446 build_target = build_target_lib.BuildTarget(None)
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000447 else:
Alex Klein1699fab2022-09-08 08:46:06 -0600448 build_target = build_target_lib.BuildTarget(opts.board)
449 packages = parse_packages(build_target, opts.packages)
450 package_atoms = [x.atom for x in packages]
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000451
Alex Klein1699fab2022-09-08 08:46:06 -0600452 with workon_helper.WorkonScope(build_target, package_atoms):
453 build_linter = toolchain.BuildLinter(
454 packages, build_target.root, opts.differential
455 )
456 if opts.fetch_only:
Ryan Beltran179d6bb2023-09-14 23:37:33 +0000457 if opts.apply_fixes:
458 logging.warning(
459 "Apply fixes with fetch_only may lead to fixes being"
460 " applied incorrectly if source files have changed!"
461 )
Ryan Beltran378934c2022-11-23 00:44:26 +0000462 if opts.host or opts.board:
463 roots = [build_target.root]
464 else:
465 roots = get_all_sysroots()
466 lints = []
467 for root in roots:
468 build_linter.sysroot = root
469 lints.extend(
470 build_linter.fetch_findings(
471 use_clippy=opts.clippy,
472 use_tidy=opts.tidy,
473 use_golint=opts.golint,
474 use_iwyu=opts.iwyu,
475 )
476 )
Alex Klein1699fab2022-09-08 08:46:06 -0600477 else:
478 lints = build_linter.emerge_with_linting(
479 use_clippy=opts.clippy,
480 use_tidy=opts.tidy,
481 use_golint=opts.golint,
Ryan Beltran378934c2022-11-23 00:44:26 +0000482 use_iwyu=opts.iwyu,
Alex Klein1699fab2022-09-08 08:46:06 -0600483 )
Ryan Beltrance85d0f2022-08-09 21:36:39 +0000484
Ryan Beltranf83a3972023-09-21 00:22:25 +0000485 if opts.filter_names:
486 lints = filter_lints(lints, opts.filter_names)
487
Alex Klein1699fab2022-09-08 08:46:06 -0600488 if opts.json:
Ryan Beltrana32a1a12022-09-28 06:03:45 +0000489 formatted_output_inner = ",\n".join(json_format_lint(l) for l in lints)
490 formatted_output = f"[{formatted_output_inner}]"
Alex Klein1699fab2022-09-08 08:46:06 -0600491 else:
Ryan Beltrana32a1a12022-09-28 06:03:45 +0000492 formatted_output = "\n".join(format_lint(l) for l in lints)
Alex Klein1699fab2022-09-08 08:46:06 -0600493
Ryan Beltran179d6bb2023-09-14 23:37:33 +0000494 if opts.apply_fixes:
495 fixed_lints, modified_files = apply_fixes(lints)
496 if opts.json:
497 formatted_fixes_inner = ",\n".join(
498 json_format_lint(l) for l in lints
499 )
500 formatted_fixes = f"[{formatted_fixes_inner}]"
501 else:
502 formatted_fixes = "\n".join(format_lint(l) for l in fixed_lints)
503
Alex Klein1699fab2022-09-08 08:46:06 -0600504 with file_util.Open(opts.output, "w") as output_file:
505 output_file.write(formatted_output)
506 if not opts.json:
507 output_file.write(f"\nFound {len(lints)} lints.")
Ryan Beltran179d6bb2023-09-14 23:37:33 +0000508 if opts.apply_fixes:
509 output_file.write("\n\n\n--------- Fixed Problems ---------\n\n")
510 output_file.write(formatted_fixes)
511 if not opts.json:
512 output_file.write(
513 f"\nFixed {len(fixed_lints)}/{len(lints)} lints."
514 )
515 output_file.write("\n\n\n--------- Modified Files ---------\n\n")
Ryan Beltranf83a3972023-09-21 00:22:25 +0000516 output_file.write("\n".join(str(f) for f in sorted(modified_files)))
Alex Klein1699fab2022-09-08 08:46:06 -0600517 output_file.write("\n")