-
-
Notifications
You must be signed in to change notification settings - Fork 5
/
clang_tidy.py
221 lines (196 loc) · 8.23 KB
/
clang_tidy.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
"""Parse output from clang-tidy's stdout"""
import json
import os
from pathlib import Path, PurePath
import re
import subprocess
from typing import Tuple, Union, List, cast, Optional, Dict
from ..loggers import logger
from ..common_fs import FileObj
NOTE_HEADER = re.compile(r"^(.+):(\d+):(\d+):\s(\w+):(.*)\[([a-zA-Z\d\-\.]+)\]$")
class TidyNotification:
"""Create a object that decodes info from the clang-tidy output's initial line that
details a specific notification.
:param notification_line: The first line in the notification parsed into a
`tuple` of `str` that represent the different components of the
notification's details.
:param database: The compilation database deserialized from JSON, only if
:std:option:`--database` argument points to a valid path containing a
``compile_commands.json file``.
"""
def __init__(
self,
notification_line: Tuple[str, Union[int, str], Union[int, str], str, str, str],
database: Optional[List[Dict[str, str]]] = None,
):
# logger.debug("Creating tidy note from line %s", notification_line)
(
self.filename,
self.line,
#: The columns of the line that triggered the notification.
self.cols,
self.severity,
self.rationale,
#: The clang-tidy check that enabled the notification.
self.diagnostic,
) = notification_line
#: The rationale of the notification.
self.rationale = self.rationale.strip()
#: The priority level of notification (warning/error).
self.severity = self.severity.strip()
#: The line number of the source file.
self.line = int(self.line)
self.cols = int(self.cols)
rel_path = (
Path(self.filename)
.resolve()
.as_posix()
.replace(Path.cwd().as_posix() + "/", "")
)
if not PurePath(self.filename).is_absolute() and database is not None:
# get absolute path from compilation database:
# This is need for meson builds as they use paths relative to
# the build env (or wherever the database is usually located).
for unit in database:
if (
"file" in unit
and "directory" in unit
and unit["file"] == self.filename
):
rel_path = (
Path(unit["directory"], unit["file"])
.resolve()
.as_posix()
.replace(Path.cwd().as_posix() + "/", "")
)
break
#: The source filename concerning the notification.
self.filename = rel_path
#: A `list` of lines for the code-block in the notification.
self.fixit_lines: List[str] = []
@property
def diagnostic_link(self) -> str:
"""Creates a markdown link to the diagnostic documentation."""
link = f"[{self.diagnostic}](https://clang.llvm.org/extra/clang-tidy/checks/"
return link + "{}/{}.html)".format(*self.diagnostic.split("-", maxsplit=1))
def __repr__(self) -> str:
return (
f"<TidyNotification {self.filename}:{self.line}:{self.cols} "
+ f"{self.diagnostic}>"
)
class TidyAdvice:
def __init__(self, notes: List[TidyNotification]) -> None:
#: A buffer of the applied fixes from clang-tidy
self.patched: Optional[bytes] = None
self.notes = notes
def diagnostics_in_range(self, start: int, end: int) -> str:
"""Get a markdown formatted list of diagnostics found between a ``start``
and ``end`` range of lines."""
diagnostics = ""
for note in self.notes:
if note.line in range(start, end + 1): # range is inclusive
diagnostics += f"- {note.rationale} [{note.diagnostic_link}]\n"
return diagnostics
def run_clang_tidy(
command: str,
file_obj: FileObj,
checks: str,
lines_changed_only: int,
database: str,
extra_args: List[str],
db_json: Optional[List[Dict[str, str]]],
tidy_review: bool,
) -> TidyAdvice:
"""Run clang-tidy on a certain file.
:param command: The clang-tidy command to use (usually a resolved path).
:param file_obj: Information about the `FileObj`.
:param checks: The `str` of comma-separated regulate expressions that describe
the desired clang-tidy checks to be enabled/configured.
:param lines_changed_only: A flag that forces focus on only changes in the event's
diff info.
:param database: The path to the compilation database.
:param extra_args: A list of extra arguments used by clang-tidy as compiler
arguments.
.. note::
If the list is only 1 item long and there is a space in the first item,
then the list is reformed from splitting the first item by whitespace
characters.
.. code-block:: shell
cpp-linter --extra-arg="-std=c++14 -Wall"
is equivalent to
.. code-block:: shell
cpp-linter --extra-arg=-std=c++14 --extra-arg=-Wall
:param db_json: The compilation database deserialized from JSON, only if
``database`` parameter points to a valid path containing a
``compile_commands.json file``.
:param tidy_review: A flag to enable/disable creating a diff suggestion for
PR review comments.
"""
filename = file_obj.name.replace("/", os.sep)
cmds = [command]
if checks:
cmds.append(f"-checks={checks}")
if database:
cmds.append("-p")
cmds.append(database)
line_ranges = {
"name": filename,
"lines": file_obj.range_of_changed_lines(lines_changed_only, get_ranges=True),
}
if line_ranges["lines"]:
# logger.info("line_filter = %s", json.dumps([line_ranges]))
cmds.append(f"--line-filter={json.dumps([line_ranges])}")
if len(extra_args) == 1 and " " in extra_args[0]:
extra_args = extra_args[0].split()
for extra_arg in extra_args:
arg = extra_arg.strip('"')
cmds.append(f'--extra-arg={arg}')
cmds.append(filename)
logger.info('Running "%s"', " ".join(cmds))
results = subprocess.run(cmds, capture_output=True)
logger.debug("Output from clang-tidy:\n%s", results.stdout.decode())
if results.stderr:
logger.debug(
"clang-tidy made the following summary:\n%s", results.stderr.decode()
)
advice = parse_tidy_output(results.stdout.decode(), database=db_json)
if tidy_review:
# clang-tidy overwrites the file contents when applying fixes.
# create a cache of original contents
original_buf = Path(file_obj.name).read_bytes()
cmds.insert(1, "--fix-errors") # include compiler-suggested fixes
# run clang-tidy again to apply any fixes
logger.info('Getting fixes with "%s"', " ".join(cmds))
subprocess.run(cmds, check=True)
# store the modified output from clang-tidy
advice.patched = Path(file_obj.name).read_bytes()
# re-write original file contents
Path(file_obj.name).write_bytes(original_buf)
return advice
def parse_tidy_output(
tidy_out: str, database: Optional[List[Dict[str, str]]]
) -> TidyAdvice:
"""Parse clang-tidy stdout.
:param tidy_out: The stdout from clang-tidy.
:param database: The compilation database deserialized from JSON, only if
:std:option:`--database` argument points to a valid path containing a
``compile_commands.json file``.
"""
notification = None
tidy_notes = []
for line in tidy_out.splitlines():
match = re.match(NOTE_HEADER, line)
if match is not None:
notification = TidyNotification(
cast(
Tuple[str, Union[int, str], Union[int, str], str, str, str],
match.groups(),
),
database,
)
tidy_notes.append(notification)
elif notification is not None:
# append lines of code that are part of
# the previous line's notification
notification.fixit_lines.append(line)
return TidyAdvice(notes=tidy_notes)