blob: 6c2abebcbe5ae3250027b62b41d2cc4708cd7075 [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.
80 """
81 if not lint.suggested_fixes:
82 return None
83
84 new_fixes_by_file: DefaultDict[
85 Path, List[toolchain.SuggestedFix]
86 ] = collections.defaultdict(list)
87 for fix in lint.suggested_fixes:
88 filepath = Path(fix.location.filepath)
89 # These are files that we locate, and are usually generated files.
90 if filepath.is_absolute():
91 logging.warning(
92 "Skipped applying fix due to invalid path: %s", filepath
93 )
94 return None
95 # Make sure this file exists in platform2
96 file_in_platform2 = PLATFORM2_PATH / filepath
97 if not file_in_platform2.exists():
98 logging.warning(
99 "Skipped applying fix due to invalid path: %s", filepath
100 )
101 return None
102 if file_in_platform2 not in file_lengths:
103 file_lengths[file_in_platform2] = len(
104 file_in_platform2.read_text(encoding="utf-8")
105 )
106 if fix.location.end_offset > file_lengths[file_in_platform2]:
107 logging.warning(
108 "Skipped applying fix due to out of bounds change to: %s",
109 filepath,
110 )
111 return None
112 new_fixes_by_file[file_in_platform2].append(fix)
113
114 return new_fixes_by_file
115
116
117def get_noconflict_fixes(
118 lints: List[toolchain.LinterFinding],
119) -> Tuple[
120 DefaultDict[Path, List[toolchain.SuggestedFix]],
121 List[toolchain.LinterFinding],
122]:
123 """Get a conflict free set of replacements to apply for each file.
124
125 Fixes will not be included in results if they:
126 A) include a replacement to a path which does not exist
127 B) include a replacement to a path outside of platform2
128 C) include a replacement to file location that exceeds the file size
129 D) overlap a previous replacement.
130
131 Args:
132 lints: List of lints to aggregate suggested fixes from.
133
134 Returns:
135 A tuple including:
136 0) the mapping of paths to a list of their suggested fixes
137 1) the list of lints which were fixed
138 """
139 fixes_by_file: DefaultDict[
140 Path, List[toolchain.SuggestedFix]
141 ] = collections.defaultdict(list)
142 lints_fixed = []
143 file_lengths = {}
144 for lint in lints:
145 new_fixes_by_file = process_fixes_by_file(lint, file_lengths)
146 if not new_fixes_by_file:
147 continue
148 files_with_overlap = set(
149 filepath
150 for filepath, new_fixes in new_fixes_by_file.items()
151 if has_overlap(fixes_by_file[filepath], new_fixes)
152 )
153 if files_with_overlap:
154 logging.warning(
155 "Skipped applying fix for %s due to conflicts in:\n\t%s.",
156 lint.name,
157 "\n\t".join(files_with_overlap),
158 )
159 else:
160 for filepath, new_fixes in new_fixes_by_file.items():
161 fixes_by_file[filepath].extend(new_fixes)
162 lints_fixed.append(lint)
163
164 return fixes_by_file, lints_fixed
165
166
167def has_overlap(
168 prior_fixes: List[toolchain.SuggestedFix],
169 new_fixes: List[toolchain.SuggestedFix],
170) -> bool:
171 """Check if new fixes have overlapping ranges with a prior replacement.
172
173 Note: this implementation is n^2, but the amount of lints in a single file
174 is experimentally pretty small, so optimizing this is probably not a large
175 concern.
176 """
177 for new in new_fixes:
178 for old in prior_fixes:
179 if (
180 (old.location.start_offset <= new.location.start_offset)
181 and (new.location.start_offset <= old.location.end_offset)
182 ) or (
183 (old.location.start_offset <= new.location.end_offset)
184 and (new.location.end_offset <= old.location.end_offset)
185 ):
186 return True
187 return False
188
189
190def apply_edits(content: Text, fixes: List[toolchain.SuggestedFix]) -> Text:
191 """Modify a file by applying a list of fixes."""
192
193 # We need to be able to apply fixes in reverse order within a file to
194 # preserve code locations.
195 def fix_sort_key(fix: toolchain.SuggestedFix) -> int:
196 return fix.location.start_offset
197
198 pieces = []
199 content_end = len(content)
200 for fix in sorted(fixes, key=fix_sort_key, reverse=True):
201 pieces += [
202 content[fix.location.end_offset : content_end],
203 fix.replacement,
204 ]
205 content_end = fix.location.start_offset
206 pieces.append(content[:content_end])
207
208 return "".join(reversed(pieces))
209
210
211def apply_fixes(
212 lints: List[toolchain.LinterFinding],
213) -> Tuple[List[toolchain.LinterFinding], Iterable[Path]]:
214 """Modify files in Platform2 to apply suggested fixes from linter findings.
215
216 Some fixes which cannot be applied cleanly will be discarded (see the
217 `get_noconflict_fixes_by_file` description for more details).
218
219 Args:
220 lints: LinterFindings to apply potential fixes from.
221
222 Returns:
223 A tuple including:
224 0) The list of lints which were fixed
225 1) The list of files which were modified
226 """
227
228 fixes_by_file, lints_fixed = get_noconflict_fixes(lints)
229
230 for filepath, fixes in fixes_by_file.items():
231 file_content = filepath.read_text(encoding="utf-8")
232 rewrite = apply_edits(file_content, fixes)
233 filepath.write_text(rewrite, encoding="utf-8")
234
235 return lints_fixed, fixes_by_file.keys()
236
237
Ryan Beltrance85d0f2022-08-09 21:36:39 +0000238def format_lint(lint: toolchain.LinterFinding) -> Text:
Alex Klein68b270c2023-04-14 14:42:50 -0600239 """Formats a lint for human-readable printing.
Ryan Beltrance85d0f2022-08-09 21:36:39 +0000240
Alex Klein1699fab2022-09-08 08:46:06 -0600241 Example output:
242 [ClangTidy] In 'path/to/file.c' on line 36:
243 Also in 'path/to/file.c' on line 40:
244 Also in 'path/to/file.c' on lines 50-53:
245 You did something bad, don't do it.
Ryan Beltrance85d0f2022-08-09 21:36:39 +0000246
Alex Klein1699fab2022-09-08 08:46:06 -0600247 Args:
Alex Klein68b270c2023-04-14 14:42:50 -0600248 lint: A linter finding from the toolchain service.
Ryan Beltrance85d0f2022-08-09 21:36:39 +0000249
Alex Klein1699fab2022-09-08 08:46:06 -0600250 Returns:
Alex Klein68b270c2023-04-14 14:42:50 -0600251 A correctly formatted string ready to be displayed to the user.
Alex Klein1699fab2022-09-08 08:46:06 -0600252 """
Ryan Beltrance85d0f2022-08-09 21:36:39 +0000253
Alex Klein1699fab2022-09-08 08:46:06 -0600254 color = terminal.Color(True)
255 lines = []
256 linter_prefix = color.Color(
257 terminal.Color.YELLOW,
258 f"[{lint.linter}]",
259 background_color=terminal.Color.BLACK,
260 )
261 for loc in lint.locations:
Ryan Beltran4706f342023-08-29 01:16:22 +0000262 filepath = make_relative_to_cros(loc.filepath)
Alex Klein1699fab2022-09-08 08:46:06 -0600263 if not lines:
264 location_prefix = f"\n{linter_prefix} In"
265 else:
266 location_prefix = " and in"
267 if loc.line_start != loc.line_end:
268 lines.append(
Ryan Beltran4706f342023-08-29 01:16:22 +0000269 f"{location_prefix} '{filepath}' "
Alex Klein1699fab2022-09-08 08:46:06 -0600270 f"lines {loc.line_start}-{loc.line_end}:"
271 )
272 else:
273 lines.append(
Ryan Beltran4706f342023-08-29 01:16:22 +0000274 f"{location_prefix} '{filepath}' line {loc.line_start}:"
Alex Klein1699fab2022-09-08 08:46:06 -0600275 )
276 message_lines = lint.message.split("\n")
277 for line in message_lines:
278 lines.append(f" {line}")
279 lines.append("")
280 return "\n".join(lines)
Ryan Beltrance85d0f2022-08-09 21:36:39 +0000281
282
Ryan Beltrana32a1a12022-09-28 06:03:45 +0000283def json_format_lint(lint: toolchain.LinterFinding) -> Text:
284 """Formats a lint in json for machine parsing.
285
286 Args:
287 lint: A linter finding from the toolchain service.
288
289 Returns:
290 A correctly formatted json string ready to be displayed to the user.
291 """
292
293 def _dictify(original):
294 """Turns namedtuple's to dictionaries recursively."""
295 # Handle namedtuples
296 if isinstance(original, tuple) and hasattr(original, "_asdict"):
297 return _dictify(original._asdict())
298 # Handle collection types
299 elif hasattr(original, "__iter__"):
300 # Handle strings
301 if isinstance(original, (str, bytes)):
302 return original
303 # Handle dictionaries
304 elif isinstance(original, dict):
305 return {k: _dictify(v) for k, v in original.items()}
306 # Handle lists, sets, etc.
307 else:
308 return [_dictify(x) for x in original]
Ryan Beltranc37fb392023-05-11 18:24:40 +0000309 # Handle PackageInfo objects
310 elif isinstance(original, package_info.PackageInfo):
311 return original.atom
Ryan Beltrana32a1a12022-09-28 06:03:45 +0000312 # Handle everything else
313 return original
314
315 return json.dumps(_dictify(lint))
316
317
Ryan Beltran378934c2022-11-23 00:44:26 +0000318def get_all_sysroots() -> List[Text]:
319 """Gets all available sysroots for both host and boards."""
320 host_root = Path(build_target_lib.BuildTarget(None).root)
321 roots = [str(host_root)]
322 build_dir = host_root / "build"
323 for board in os.listdir(build_dir):
324 if board != "bin":
325 board_root = build_dir / board
326 if board_root.is_dir():
327 roots.append(str(board_root))
328 return roots
329
330
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000331def get_arg_parser() -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -0600332 """Creates an argument parser for this script."""
333 default_board = cros_build_lib.GetDefaultBoard()
334 parser = commandline.ArgumentParser(description=__doc__)
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000335
Ryan Beltran378934c2022-11-23 00:44:26 +0000336 board_group = parser.add_mutually_exclusive_group()
Alex Klein1699fab2022-09-08 08:46:06 -0600337 board_group.add_argument(
338 "-b",
339 "--board",
340 "--build-target",
341 dest="board",
342 default=default_board,
343 help="The board to emerge packages for",
344 )
345 board_group.add_argument(
346 "--host", action="store_true", help="emerge for host instead of board."
347 )
Ryan Beltran378934c2022-11-23 00:44:26 +0000348 parser.add_argument(
Alex Klein1699fab2022-09-08 08:46:06 -0600349 "--fetch-only",
350 action="store_true",
Alex Klein68b270c2023-04-14 14:42:50 -0600351 help="Fetch lints from previous run without resetting or calling "
352 "emerge.",
Alex Klein1699fab2022-09-08 08:46:06 -0600353 )
Alex Klein1699fab2022-09-08 08:46:06 -0600354 parser.add_argument(
Ryan Beltran179d6bb2023-09-14 23:37:33 +0000355 "--apply-fixes",
356 action="store_true",
357 help="Apply suggested fixes from linters.",
358 )
359 parser.add_argument(
Alex Klein1699fab2022-09-08 08:46:06 -0600360 "--differential",
361 action="store_true",
362 help="only lint lines touched by the last commit",
363 )
364 parser.add_argument(
365 "-o",
366 "--output",
367 default=sys.stdout,
368 help="File to use instead of stdout.",
369 )
370 parser.add_argument(
371 "--json", action="store_true", help="Output lints in JSON format."
372 )
373 parser.add_argument(
374 "--no-clippy",
375 dest="clippy",
376 action="store_false",
377 help="Disable cargo clippy linter.",
378 )
379 parser.add_argument(
380 "--no-tidy",
381 dest="tidy",
382 action="store_false",
383 help="Disable clang tidy linter.",
384 )
385 parser.add_argument(
386 "--no-golint",
387 dest="golint",
388 action="store_false",
389 help="Disable golint linter.",
390 )
391 parser.add_argument(
Ryan Beltran378934c2022-11-23 00:44:26 +0000392 "--iwyu",
393 action="store_true",
394 help="Enable include-what-you-use linter.",
395 )
396 parser.add_argument(
Alex Klein1699fab2022-09-08 08:46:06 -0600397 "packages",
398 nargs="*",
399 help="package(s) to emerge and retrieve lints for",
400 )
401 return parser
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000402
403
404def parse_args(argv: List[str]):
Alex Klein1699fab2022-09-08 08:46:06 -0600405 """Parses arguments in argv and returns the options."""
406 parser = get_arg_parser()
407 opts = parser.parse_args(argv)
408 opts.Freeze()
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000409
Alex Klein1699fab2022-09-08 08:46:06 -0600410 # A package must be specified unless we are in fetch-only mode
411 if not (opts.fetch_only or opts.packages):
Ryan Beltran378934c2022-11-23 00:44:26 +0000412 parser.error("Emerge mode requires specified package(s).")
Alex Klein1699fab2022-09-08 08:46:06 -0600413 if opts.fetch_only and opts.packages:
414 parser.error("Cannot specify packages for fetch-only mode.")
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000415
Ryan Beltran378934c2022-11-23 00:44:26 +0000416 # A board must be specified unless we are in fetch-only mode
417 if not (opts.fetch_only or opts.board or opts.host):
418 parser.error("Emerge mode requires either --board or --host.")
419
Alex Klein1699fab2022-09-08 08:46:06 -0600420 return opts
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000421
422
423def main(argv: List[str]) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600424 cros_build_lib.AssertInsideChroot()
425 opts = parse_args(argv)
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000426
Alex Klein1699fab2022-09-08 08:46:06 -0600427 if opts.host:
428 # BuildTarget interprets None as host target
429 build_target = build_target_lib.BuildTarget(None)
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000430 else:
Alex Klein1699fab2022-09-08 08:46:06 -0600431 build_target = build_target_lib.BuildTarget(opts.board)
432 packages = parse_packages(build_target, opts.packages)
433 package_atoms = [x.atom for x in packages]
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000434
Alex Klein1699fab2022-09-08 08:46:06 -0600435 with workon_helper.WorkonScope(build_target, package_atoms):
436 build_linter = toolchain.BuildLinter(
437 packages, build_target.root, opts.differential
438 )
439 if opts.fetch_only:
Ryan Beltran179d6bb2023-09-14 23:37:33 +0000440 if opts.apply_fixes:
441 logging.warning(
442 "Apply fixes with fetch_only may lead to fixes being"
443 " applied incorrectly if source files have changed!"
444 )
Ryan Beltran378934c2022-11-23 00:44:26 +0000445 if opts.host or opts.board:
446 roots = [build_target.root]
447 else:
448 roots = get_all_sysroots()
449 lints = []
450 for root in roots:
451 build_linter.sysroot = root
452 lints.extend(
453 build_linter.fetch_findings(
454 use_clippy=opts.clippy,
455 use_tidy=opts.tidy,
456 use_golint=opts.golint,
457 use_iwyu=opts.iwyu,
458 )
459 )
Alex Klein1699fab2022-09-08 08:46:06 -0600460 else:
461 lints = build_linter.emerge_with_linting(
462 use_clippy=opts.clippy,
463 use_tidy=opts.tidy,
464 use_golint=opts.golint,
Ryan Beltran378934c2022-11-23 00:44:26 +0000465 use_iwyu=opts.iwyu,
Alex Klein1699fab2022-09-08 08:46:06 -0600466 )
Ryan Beltrance85d0f2022-08-09 21:36:39 +0000467
Alex Klein1699fab2022-09-08 08:46:06 -0600468 if opts.json:
Ryan Beltrana32a1a12022-09-28 06:03:45 +0000469 formatted_output_inner = ",\n".join(json_format_lint(l) for l in lints)
470 formatted_output = f"[{formatted_output_inner}]"
Alex Klein1699fab2022-09-08 08:46:06 -0600471 else:
Ryan Beltrana32a1a12022-09-28 06:03:45 +0000472 formatted_output = "\n".join(format_lint(l) for l in lints)
Alex Klein1699fab2022-09-08 08:46:06 -0600473
Ryan Beltran179d6bb2023-09-14 23:37:33 +0000474 if opts.apply_fixes:
475 fixed_lints, modified_files = apply_fixes(lints)
476 if opts.json:
477 formatted_fixes_inner = ",\n".join(
478 json_format_lint(l) for l in lints
479 )
480 formatted_fixes = f"[{formatted_fixes_inner}]"
481 else:
482 formatted_fixes = "\n".join(format_lint(l) for l in fixed_lints)
483
Alex Klein1699fab2022-09-08 08:46:06 -0600484 with file_util.Open(opts.output, "w") as output_file:
485 output_file.write(formatted_output)
486 if not opts.json:
487 output_file.write(f"\nFound {len(lints)} lints.")
Ryan Beltran179d6bb2023-09-14 23:37:33 +0000488 if opts.apply_fixes:
489 output_file.write("\n\n\n--------- Fixed Problems ---------\n\n")
490 output_file.write(formatted_fixes)
491 if not opts.json:
492 output_file.write(
493 f"\nFixed {len(fixed_lints)}/{len(lints)} lints."
494 )
495 output_file.write("\n\n\n--------- Modified Files ---------\n\n")
496 output_file.write("\n".join(str(f) for f in modified_files))
Alex Klein1699fab2022-09-08 08:46:06 -0600497 output_file.write("\n")