blob: b3061847eb5f58cd37577f2bdadae0b3a1d0d637 [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
11import json
12import sys
Ryan Beltrance85d0f2022-08-09 21:36:39 +000013from typing import List, Text
Ryan Beltran1f2dd082022-04-25 18:42:32 +000014
15from chromite.lib import build_target_lib
16from chromite.lib import commandline
17from chromite.lib import cros_build_lib
Ryan Beltranb2175862022-04-28 19:55:57 +000018from chromite.lib import portage_util
Ryan Beltrance85d0f2022-08-09 21:36:39 +000019from chromite.lib import terminal
Ryan Beltran5514eab2022-04-28 21:40:24 +000020from chromite.lib import workon_helper
Ryan Beltran1f2dd082022-04-25 18:42:32 +000021from chromite.lib.parser import package_info
22from chromite.service import toolchain
23from chromite.utils import file_util
24
25
Alex Klein1699fab2022-09-08 08:46:06 -060026def parse_packages(
27 build_target: build_target_lib.BuildTarget, packages: List[str]
28) -> List[package_info.PackageInfo]:
29 """Parse packages and insert the category if none is given.
Ryan Beltranb2175862022-04-28 19:55:57 +000030
Alex Klein1699fab2022-09-08 08:46:06 -060031 Args:
32 build_target: build_target to find ebuild for
33 packages: user input package names to parse
Ryan Beltranb2175862022-04-28 19:55:57 +000034
Alex Klein1699fab2022-09-08 08:46:06 -060035 Returns:
36 A list of parsed PackageInfo objects
37 """
38 package_infos: List[package_info.PackageInfo] = []
39 for package in packages:
40 parsed = package_info.parse(package)
41 if not parsed.category:
42 # If a category is not specified, we can get it from the ebuild path.
43 if build_target.is_host():
44 ebuild_path = portage_util.FindEbuildForPackage(
45 package, build_target.root
46 )
47 else:
48 ebuild_path = portage_util.FindEbuildForBoardPackage(
49 package, build_target.name, build_target.root
50 )
51 ebuild_data = portage_util.EBuild(ebuild_path)
52 parsed = package_info.parse(ebuild_data.package)
53 package_infos.append(parsed)
54 return package_infos
Ryan Beltranb2175862022-04-28 19:55:57 +000055
56
Ryan Beltrance85d0f2022-08-09 21:36:39 +000057def format_lint(lint: toolchain.LinterFinding) -> Text:
Alex Klein1699fab2022-09-08 08:46:06 -060058 """Formats a lint for human readable printing.
Ryan Beltrance85d0f2022-08-09 21:36:39 +000059
Alex Klein1699fab2022-09-08 08:46:06 -060060 Example output:
61 [ClangTidy] In 'path/to/file.c' on line 36:
62 Also in 'path/to/file.c' on line 40:
63 Also in 'path/to/file.c' on lines 50-53:
64 You did something bad, don't do it.
Ryan Beltrance85d0f2022-08-09 21:36:39 +000065
Alex Klein1699fab2022-09-08 08:46:06 -060066 Args:
67 lint: A linter finding from the toolchain service.
Ryan Beltrance85d0f2022-08-09 21:36:39 +000068
Alex Klein1699fab2022-09-08 08:46:06 -060069 Returns:
70 A correctly formatted string ready to be displayed to the user.
71 """
Ryan Beltrance85d0f2022-08-09 21:36:39 +000072
Alex Klein1699fab2022-09-08 08:46:06 -060073 color = terminal.Color(True)
74 lines = []
75 linter_prefix = color.Color(
76 terminal.Color.YELLOW,
77 f"[{lint.linter}]",
78 background_color=terminal.Color.BLACK,
79 )
80 for loc in lint.locations:
81 if not lines:
82 location_prefix = f"\n{linter_prefix} In"
83 else:
84 location_prefix = " and in"
85 if loc.line_start != loc.line_end:
86 lines.append(
87 f"{location_prefix} '{loc.filepath}' "
88 f"lines {loc.line_start}-{loc.line_end}:"
89 )
90 else:
91 lines.append(
92 f"{location_prefix} '{loc.filepath}' line {loc.line_start}:"
93 )
94 message_lines = lint.message.split("\n")
95 for line in message_lines:
96 lines.append(f" {line}")
97 lines.append("")
98 return "\n".join(lines)
Ryan Beltrance85d0f2022-08-09 21:36:39 +000099
100
Ryan Beltrana32a1a12022-09-28 06:03:45 +0000101def json_format_lint(lint: toolchain.LinterFinding) -> Text:
102 """Formats a lint in json for machine parsing.
103
104 Args:
105 lint: A linter finding from the toolchain service.
106
107 Returns:
108 A correctly formatted json string ready to be displayed to the user.
109 """
110
111 def _dictify(original):
112 """Turns namedtuple's to dictionaries recursively."""
113 # Handle namedtuples
114 if isinstance(original, tuple) and hasattr(original, "_asdict"):
115 return _dictify(original._asdict())
116 # Handle collection types
117 elif hasattr(original, "__iter__"):
118 # Handle strings
119 if isinstance(original, (str, bytes)):
120 return original
121 # Handle dictionaries
122 elif isinstance(original, dict):
123 return {k: _dictify(v) for k, v in original.items()}
124 # Handle lists, sets, etc.
125 else:
126 return [_dictify(x) for x in original]
127 # Handle everything else
128 return original
129
130 return json.dumps(_dictify(lint))
131
132
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000133def get_arg_parser() -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -0600134 """Creates an argument parser for this script."""
135 default_board = cros_build_lib.GetDefaultBoard()
136 parser = commandline.ArgumentParser(description=__doc__)
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000137
Alex Klein1699fab2022-09-08 08:46:06 -0600138 board_group = parser.add_mutually_exclusive_group(
139 required=not default_board
140 )
141 board_group.add_argument(
142 "-b",
143 "--board",
144 "--build-target",
145 dest="board",
146 default=default_board,
147 help="The board to emerge packages for",
148 )
149 board_group.add_argument(
150 "--host", action="store_true", help="emerge for host instead of board."
151 )
152 board_group.add_argument(
153 "--fetch-only",
154 action="store_true",
155 help="Fetch lints from previous run without reseting or calling emerge.",
156 )
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000157
Alex Klein1699fab2022-09-08 08:46:06 -0600158 parser.add_argument(
159 "--differential",
160 action="store_true",
161 help="only lint lines touched by the last commit",
162 )
163 parser.add_argument(
164 "-o",
165 "--output",
166 default=sys.stdout,
167 help="File to use instead of stdout.",
168 )
169 parser.add_argument(
170 "--json", action="store_true", help="Output lints in JSON format."
171 )
172 parser.add_argument(
173 "--no-clippy",
174 dest="clippy",
175 action="store_false",
176 help="Disable cargo clippy linter.",
177 )
178 parser.add_argument(
179 "--no-tidy",
180 dest="tidy",
181 action="store_false",
182 help="Disable clang tidy linter.",
183 )
184 parser.add_argument(
185 "--no-golint",
186 dest="golint",
187 action="store_false",
188 help="Disable golint linter.",
189 )
190 parser.add_argument(
191 "packages",
192 nargs="*",
193 help="package(s) to emerge and retrieve lints for",
194 )
195 return parser
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000196
197
198def parse_args(argv: List[str]):
Alex Klein1699fab2022-09-08 08:46:06 -0600199 """Parses arguments in argv and returns the options."""
200 parser = get_arg_parser()
201 opts = parser.parse_args(argv)
202 opts.Freeze()
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000203
Alex Klein1699fab2022-09-08 08:46:06 -0600204 # A package must be specified unless we are in fetch-only mode
205 if not (opts.fetch_only or opts.packages):
206 parser.error("Emerge requires specified package(s).")
207 if opts.fetch_only and opts.packages:
208 parser.error("Cannot specify packages for fetch-only mode.")
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000209
Alex Klein1699fab2022-09-08 08:46:06 -0600210 return opts
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000211
212
213def main(argv: List[str]) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600214 cros_build_lib.AssertInsideChroot()
215 opts = parse_args(argv)
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000216
Alex Klein1699fab2022-09-08 08:46:06 -0600217 if opts.host:
218 # BuildTarget interprets None as host target
219 build_target = build_target_lib.BuildTarget(None)
Ryan Beltrandbd7b812022-06-08 23:36:16 +0000220 else:
Alex Klein1699fab2022-09-08 08:46:06 -0600221 build_target = build_target_lib.BuildTarget(opts.board)
222 packages = parse_packages(build_target, opts.packages)
223 package_atoms = [x.atom for x in packages]
Ryan Beltran1f2dd082022-04-25 18:42:32 +0000224
Alex Klein1699fab2022-09-08 08:46:06 -0600225 with workon_helper.WorkonScope(build_target, package_atoms):
226 build_linter = toolchain.BuildLinter(
227 packages, build_target.root, opts.differential
228 )
229 if opts.fetch_only:
230 lints = build_linter.fetch_findings(
231 use_clippy=opts.clippy,
232 use_tidy=opts.tidy,
233 use_golint=opts.golint,
234 )
235 else:
236 lints = build_linter.emerge_with_linting(
237 use_clippy=opts.clippy,
238 use_tidy=opts.tidy,
239 use_golint=opts.golint,
240 )
Ryan Beltrance85d0f2022-08-09 21:36:39 +0000241
Alex Klein1699fab2022-09-08 08:46:06 -0600242 if opts.json:
Ryan Beltrana32a1a12022-09-28 06:03:45 +0000243 formatted_output_inner = ",\n".join(json_format_lint(l) for l in lints)
244 formatted_output = f"[{formatted_output_inner}]"
Alex Klein1699fab2022-09-08 08:46:06 -0600245 else:
Ryan Beltrana32a1a12022-09-28 06:03:45 +0000246 formatted_output = "\n".join(format_lint(l) for l in lints)
Alex Klein1699fab2022-09-08 08:46:06 -0600247
248 with file_util.Open(opts.output, "w") as output_file:
249 output_file.write(formatted_output)
250 if not opts.json:
251 output_file.write(f"\nFound {len(lints)} lints.")
252 output_file.write("\n")