blob: c550caa104301eaf48474ce13060d6d3a3ced9bd [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2021 The ChromiumOS Authors
Ryan Beltrancfc5c362021-03-02 18:36:18 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Runs cargo clippy across the given files, dumping diagnostics to a JSON file.
6
7This script is intended specifically for use with Tricium (go/tricium).
8"""
9
10import json
Chris McDonald59650c32021-07-20 15:29:28 -060011import logging
Ryan Beltrancfc5c362021-03-02 18:36:18 +000012import os
13from pathlib import Path
Ryan Beltrana4b45a32021-08-11 08:26:38 +000014import re
Chris McDonald59650c32021-07-20 15:29:28 -060015from typing import Any, Dict, Iterable, List, NamedTuple, Text
Ryan Beltrancfc5c362021-03-02 18:36:18 +000016
17from chromite.lib import commandline
18from chromite.lib import cros_build_lib
Ryan Beltrancfc5c362021-03-02 18:36:18 +000019
Ryan Beltrancfc5c362021-03-02 18:36:18 +000020
21class Error(Exception):
Alex Klein1699fab2022-09-08 08:46:06 -060022 """Base error class for tricium-cargo-clippy."""
Ryan Beltrancfc5c362021-03-02 18:36:18 +000023
24
Ryan Beltranc0fa16a2021-08-05 20:45:14 +000025class CargoClippyPackagePathError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -060026 """Raised when no Package Path is provided."""
Ryan Beltranc0fa16a2021-08-05 20:45:14 +000027
Alex Klein1699fab2022-09-08 08:46:06 -060028 def __init__(self, source: Text):
29 super().__init__(f"{source} does not start with a package path")
30 self.source = source
31
Ryan Beltranc0fa16a2021-08-05 20:45:14 +000032
Ryan Beltrancfc5c362021-03-02 18:36:18 +000033class CargoClippyJSONError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -060034 """Raised when cargo-clippy parsing jobs are not proper JSON."""
Ryan Beltrancfc5c362021-03-02 18:36:18 +000035
Alex Klein1699fab2022-09-08 08:46:06 -060036 def __init__(self, source: Text, line_num: int):
37 super().__init__(f"{source}:{line_num}: is not valid JSON")
38 self.source = source
39 self.line_num = line_num
Ryan Beltrancfc5c362021-03-02 18:36:18 +000040
41
42class CargoClippyReasonError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -060043 """Raised when cargo-clippy parsing jobs don't provide a "reason" field."""
Ryan Beltrancfc5c362021-03-02 18:36:18 +000044
Alex Klein1699fab2022-09-08 08:46:06 -060045 def __init__(self, source: Text, line_num: int):
46 super().__init__(f"{source}:{line_num}: is missing its reason")
47 self.source = source
48 self.line_num = line_num
Ryan Beltrancfc5c362021-03-02 18:36:18 +000049
50
51class CargoClippyFieldError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -060052 """Raised when cargo-clippy parsing jobs fail to determine a field."""
Ryan Beltrancfc5c362021-03-02 18:36:18 +000053
Alex Klein1699fab2022-09-08 08:46:06 -060054 def __init__(self, source: Text, line_num: int, field: Text):
55 super().__init__(
Trent Aptedc20bb6d2023-05-10 15:00:03 +100056 f"{source}:{line_num}: {field} could not be parsed from original"
57 " json"
Alex Klein1699fab2022-09-08 08:46:06 -060058 )
59 self.source = source
60 self.line_num = line_num
61 self.field = field
Ryan Beltrancfc5c362021-03-02 18:36:18 +000062
63
64def resolve_path(file_path: Text) -> Text:
Alex Klein1699fab2022-09-08 08:46:06 -060065 return str(Path(file_path).resolve())
Ryan Beltrancfc5c362021-03-02 18:36:18 +000066
67
68class CodeLocation(NamedTuple):
Alex Klein1699fab2022-09-08 08:46:06 -060069 """Holds the location a ClippyDiagnostic Finding."""
Ryan Beltrancfc5c362021-03-02 18:36:18 +000070
Alex Klein1699fab2022-09-08 08:46:06 -060071 file_path: Text
72 line_start: int
73 line_end: int
74 column_start: int
75 column_end: int
76
77 def to_dict(self):
78 return {**self._asdict(), "file_path": self.file_path}
Ryan Beltrancfc5c362021-03-02 18:36:18 +000079
80
81class ClippyDiagnostic(NamedTuple):
Alex Klein1699fab2022-09-08 08:46:06 -060082 """Holds information about a compiler message from Clippy."""
Ryan Beltrancfc5c362021-03-02 18:36:18 +000083
Alex Klein1699fab2022-09-08 08:46:06 -060084 locations: Iterable["CodeLocation"]
85 level: Text
86 message: Text
87
88 def as_json(self):
89 return json.dumps(
90 {
91 **self._asdict(),
92 "locations": [loc.to_dict() for loc in self.locations],
93 }
94 )
Ryan Beltrancfc5c362021-03-02 18:36:18 +000095
96
Ryan Beltrancfc5c362021-03-02 18:36:18 +000097def parse_locations(
Alex Klein1699fab2022-09-08 08:46:06 -060098 orig_json: Dict[Text, Any], package_path: Text, git_repo: Text
99) -> Iterable["CodeLocation"]:
100 """The code locations associated with this diagnostic as an iter.
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000101
Alex Klein1699fab2022-09-08 08:46:06 -0600102 The relevant code location can appear in either the messages[spans] field,
103 which will be used if present, or else child messages each have their own
104 locations specified.
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000105
Alex Klein1699fab2022-09-08 08:46:06 -0600106 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000107 orig_json: An iterable of clippy entries in original json.
108 package_path: A resolved path to the rust package.
109 git_repo: Base directory for git repo to strip out in diagnostics.
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000110
Alex Klein1699fab2022-09-08 08:46:06 -0600111 Yields:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000112 A CodeLocation object associated with a relevant span.
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000113
Alex Klein1699fab2022-09-08 08:46:06 -0600114 Raises:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000115 CargoClippyFieldError: Parsing failed to determine any code locations.
Alex Klein1699fab2022-09-08 08:46:06 -0600116 """
117 spans = orig_json.get("message", {}).get("spans", [])
118 children = orig_json.get("message", {}).get("children", [])
119 for child in children:
120 spans = spans + child.get("spans", [])
121 locations = set()
122 for span in spans:
123 file_path = os.path.join(package_path, span.get("file_name"))
124 if git_repo and file_path.startswith(f"{git_repo}/"):
125 file_path = file_path[len(git_repo) + 1 :]
126 else:
127 # Remove ebuild work directories from prefix
128 # Such as: "**/<package>-9999/work/<package>-9999/"
129 # or: "**/<package>-0.24.52-r9/work/<package>-0.24.52/"
130 file_path = re.sub(
131 r"(.*/)?([^/]+)-[^/]+/work/[^/]+/+", "", file_path
132 )
133 location = CodeLocation(
134 file_path=file_path,
135 line_start=span.get("line_start"),
136 line_end=span.get("line_end"),
137 column_start=span.get("column_start"),
138 column_end=span.get("column_end"),
139 )
140 if location not in locations:
141 locations.add(location)
142 yield location
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000143
144
145def parse_level(src: Text, src_line: int, orig_json: Dict[Text, Any]) -> Text:
Alex Klein1699fab2022-09-08 08:46:06 -0600146 """The level (error or warning) associated with this diagnostic.
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000147
Alex Klein1699fab2022-09-08 08:46:06 -0600148 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000149 src: Name of the file orig_json was found in.
150 src_line: Line number where orig_json was found.
151 orig_json: An iterable of clippy entries in original json.
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000152
Alex Klein1699fab2022-09-08 08:46:06 -0600153 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000154 The level of the diagnostic as a string (either error or warning).
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000155
Alex Klein1699fab2022-09-08 08:46:06 -0600156 Raises:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000157 CargoClippyFieldError: Parsing failed to determine the level.
Alex Klein1699fab2022-09-08 08:46:06 -0600158 """
159 level = orig_json.get("level")
160 if not level:
161 level = orig_json.get("message", {}).get("level")
162 if not level:
163 raise CargoClippyFieldError(src, src_line, "level")
164 return level
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000165
166
Alex Klein1699fab2022-09-08 08:46:06 -0600167def parse_message(src: Text, src_line: int, orig_json: Dict[Text, Any]) -> Text:
168 """The formatted linter message for this diagnostic.
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000169
Alex Klein1699fab2022-09-08 08:46:06 -0600170 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000171 src: Name of the file orig_json was found in.
172 src_line: Line number where orig_json was found.
173 orig_json: An iterable of clippy entries in original json.
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000174
Alex Klein1699fab2022-09-08 08:46:06 -0600175 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000176 The rendered message of the diagnostic.
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000177
Alex Klein1699fab2022-09-08 08:46:06 -0600178 Raises:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000179 CargoClippyFieldError: Parsing failed to determine the message.
Alex Klein1699fab2022-09-08 08:46:06 -0600180 """
181 message = orig_json.get("message", {}).get("rendered")
182 if message is None:
183 raise CargoClippyFieldError(src, src_line, "message")
184 return message
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000185
186
187def parse_diagnostics(
Alex Klein1699fab2022-09-08 08:46:06 -0600188 src: Text, orig_jsons: Iterable[Text], git_repo: Text
189) -> ClippyDiagnostic:
190 """Parses original JSON to find the fields of a Clippy Diagnostic.
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000191
Alex Klein1699fab2022-09-08 08:46:06 -0600192 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000193 src: Name of the file orig_json was found in.
194 orig_jsons: An iterable of clippy entries in original json.
195 git_repo: Base directory for git repo to strip out in diagnostics.
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000196
Alex Klein1699fab2022-09-08 08:46:06 -0600197 Yields:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000198 A ClippyDiagnostic for orig_json.
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000199
Alex Klein1699fab2022-09-08 08:46:06 -0600200 Raises:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000201 CargoClippyJSONError: if a diagnostic is not valid JSON.
202 CargoClippyReasonError: if a diagnostic is missing a "reason" field.
203 CargoClippyFieldError: if a field cannot be determined while parsing.
Alex Klein1699fab2022-09-08 08:46:06 -0600204 """
205 for src_line, orig_json in enumerate(orig_jsons):
206 try:
207 line_json = json.loads(orig_json)
208 except json.decoder.JSONDecodeError:
209 json_error = CargoClippyJSONError(src, src_line)
210 logging.error(json_error)
211 raise json_error
Ryan Beltranc0fa16a2021-08-05 20:45:14 +0000212
Alex Klein1699fab2022-09-08 08:46:06 -0600213 # We pass the path to the package in a special JSON on the first line
214 if src_line == 0:
215 package_path = line_json.get("package_path")
216 if not package_path:
217 raise CargoClippyPackagePathError(src)
218 package_path = resolve_path(package_path)
219 continue
Ryan Beltranc0fa16a2021-08-05 20:45:14 +0000220
Alex Klein1699fab2022-09-08 08:46:06 -0600221 # Clippy outputs several types of logs, as distinguished by the "reason"
222 # field, but we only want to process "compiler-message" logs.
223 reason = line_json.get("reason")
224 if reason is None:
225 reason_error = CargoClippyReasonError(src, src_line)
226 logging.error(reason_error)
227 raise reason_error
228 if reason != "compiler-message":
229 continue
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000230
Alex Klein1699fab2022-09-08 08:46:06 -0600231 locations = parse_locations(line_json, package_path, git_repo)
232 level = parse_level(src, src_line, line_json)
233 message = parse_message(src, src_line, line_json)
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000234
Alex Klein1699fab2022-09-08 08:46:06 -0600235 # TODO(ryanbeltran): Export suggested replacements
236 yield ClippyDiagnostic(locations, level, message)
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000237
238
Ryan Beltran923a1312021-07-30 00:28:13 +0000239def parse_files(input_dir: Text, git_repo: Text) -> Iterable[ClippyDiagnostic]:
Alex Klein1699fab2022-09-08 08:46:06 -0600240 """Gets all compiler-message lints from all the input files in input_dir.
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000241
Alex Klein1699fab2022-09-08 08:46:06 -0600242 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000243 input_dir: path to directory to scan for files
244 git_repo: Base directory for git repo to strip out in diagnostics.
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000245
Alex Klein1699fab2022-09-08 08:46:06 -0600246 Yields:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000247 Clippy Diagnostics objects found in files in the input directory
Alex Klein1699fab2022-09-08 08:46:06 -0600248 """
249 for root_path, _, file_names in os.walk(input_dir):
250 for file_name in file_names:
251 file_path = os.path.join(root_path, file_name)
252 with open(file_path, encoding="utf-8") as clippy_file:
253 yield from parse_diagnostics(file_path, clippy_file, git_repo)
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000254
255
256def filter_diagnostics(
Alex Klein1699fab2022-09-08 08:46:06 -0600257 diags: Iterable[ClippyDiagnostic],
258) -> Iterable[ClippyDiagnostic]:
259 """Filters diagnostics and validates schemas."""
260 for diag in diags:
261 # ignore redundant messages: "aborting due to previous error..."
262 if "aborting due to previous error" in diag.message:
263 continue
264 # findings with no location are never useful
265 if not diag.locations:
266 continue
267 yield diag
Ryan Beltran43a00662021-05-17 16:55:24 +0000268
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000269
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000270def get_arg_parser() -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -0600271 """Creates an argument parser for this script."""
272 parser = commandline.ArgumentParser(description=__doc__)
273 parser.add_argument(
274 "--output", required=True, type="path", help="File to write results to."
275 )
276 parser.add_argument(
277 "--clippy-json-dir",
278 type="path",
279 help="Directory where clippy outputs were previously written to.",
280 )
281 parser.add_argument(
282 "--git-repo-path",
283 type="path",
284 default="",
285 help="Base directory for git repo to strip out in diagnostics.",
286 )
287 return parser
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000288
289
290def main(argv: List[str]) -> None:
Alex Klein1699fab2022-09-08 08:46:06 -0600291 cros_build_lib.AssertInsideChroot()
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000292
Alex Klein1699fab2022-09-08 08:46:06 -0600293 logging.basicConfig()
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000294
Alex Klein1699fab2022-09-08 08:46:06 -0600295 parser = get_arg_parser()
296 opts = parser.parse_args(argv)
297 opts.Freeze()
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000298
Alex Klein1699fab2022-09-08 08:46:06 -0600299 input_dir = resolve_path(opts.clippy_json_dir)
300 output_path = resolve_path(opts.output)
301 git_repo = opts.git_repo_path
Ryan Beltrancfc5c362021-03-02 18:36:18 +0000302
Alex Klein1699fab2022-09-08 08:46:06 -0600303 diagnostics = filter_diagnostics(parse_files(input_dir, git_repo))
304 with open(output_path, "w", encoding="utf-8") as output_file:
305 output_file.writelines(f"{diag}\n" for diag in diagnostics)