Skip to content

Commit a6f4e5d

Browse files
authored
feat: capture and output clang version used in feedback (#124)
also fix some errors: 1. about filter files per clang tool 2. merged suggestions' file name must match exactly 3. tool summary exclusion from PR review summary when tool was not used at all OR review was not requested from a particular tool
1 parent 89361f0 commit a6f4e5d

File tree

11 files changed

+127
-44
lines changed

11 files changed

+127
-44
lines changed

cpp_linter/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,10 @@ def main():
7575
)
7676
end_log_group()
7777

78-
capture_clang_tools_output(files=files, args=args)
78+
clang_versions = capture_clang_tools_output(files=files, args=args)
7979

8080
start_log_group("Posting comment(s)")
81-
rest_api_client.post_feedback(files=files, args=args)
81+
rest_api_client.post_feedback(files=files, args=args, clang_versions=clang_versions)
8282
end_log_group()
8383

8484

cpp_linter/clang_tools/__init__.py

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from concurrent.futures import ProcessPoolExecutor, as_completed
22
import json
33
from pathlib import Path
4+
import re
45
import subprocess
5-
from textwrap import indent
6-
from typing import Optional, List, Dict, Tuple
6+
from typing import Optional, List, Dict, Tuple, cast
77
import shutil
88

99
from ..common_fs import FileObj
@@ -77,38 +77,59 @@ def _run_on_single_file(
7777
return file.name, log_stream.getvalue(), tidy_note, format_advice
7878

7979

80-
def capture_clang_tools_output(files: List[FileObj], args: Args):
80+
VERSION_PATTERN = re.compile(r"version\s(\d+\.\d+\.\d+)")
81+
82+
83+
def _capture_tool_version(cmd: str) -> str:
84+
"""Get version number from output for executable used."""
85+
version_out = subprocess.run(
86+
[cmd, "--version"], capture_output=True, check=True, text=True
87+
)
88+
matched = VERSION_PATTERN.search(version_out.stdout)
89+
if matched is None: # pragma: no cover
90+
raise RuntimeError(
91+
f"Failed to get version numbers from `{cmd} --version` output"
92+
)
93+
ver = cast(str, matched.group(1))
94+
logger.info("`%s --version`: %s", cmd, ver)
95+
return ver
96+
97+
98+
class ClangVersions:
99+
def __init__(self) -> None:
100+
self.tidy: Optional[str] = None
101+
self.format: Optional[str] = None
102+
103+
104+
def capture_clang_tools_output(files: List[FileObj], args: Args) -> ClangVersions:
81105
"""Execute and capture all output from clang-tidy and clang-format. This aggregates
82106
results in the :attr:`~cpp_linter.Globals.OUTPUT`.
83107
84108
:param files: A list of files to analyze.
85109
:param args: A namespace of parsed args from the :doc:`CLI <../cli_args>`.
86110
"""
87111

88-
def show_tool_version_output(cmd: str): # show version output for executable used
89-
version_out = subprocess.run(
90-
[cmd, "--version"], capture_output=True, check=True
91-
)
92-
logger.info("%s --version\n%s", cmd, indent(version_out.stdout.decode(), "\t"))
93-
94112
tidy_cmd, format_cmd = (None, None)
95113
tidy_filter, format_filter = (None, None)
114+
clang_versions = ClangVersions()
96115
if args.style: # if style is an empty value, then clang-format is skipped
97116
format_cmd = assemble_version_exec("clang-format", args.version)
98-
assert format_cmd is not None, "clang-format executable was not found"
99-
show_tool_version_output(format_cmd)
100-
tidy_filter = TidyFileFilter(
117+
if format_cmd is None: # pragma: no cover
118+
raise FileNotFoundError("clang-format executable was not found")
119+
clang_versions.format = _capture_tool_version(format_cmd)
120+
format_filter = FormatFileFilter(
101121
extensions=args.extensions,
102-
ignore_value=args.ignore_tidy,
122+
ignore_value=args.ignore_format,
103123
)
104124
if args.tidy_checks != "-*":
105125
# if all checks are disabled, then clang-tidy is skipped
106126
tidy_cmd = assemble_version_exec("clang-tidy", args.version)
107-
assert tidy_cmd is not None, "clang-tidy executable was not found"
108-
show_tool_version_output(tidy_cmd)
109-
format_filter = FormatFileFilter(
127+
if tidy_cmd is None: # pragma: no cover
128+
raise FileNotFoundError("clang-tidy executable was not found")
129+
clang_versions.tidy = _capture_tool_version(tidy_cmd)
130+
tidy_filter = TidyFileFilter(
110131
extensions=args.extensions,
111-
ignore_value=args.ignore_format,
132+
ignore_value=args.ignore_tidy,
112133
)
113134

114135
db_json: Optional[List[Dict[str, str]]] = None
@@ -155,3 +176,4 @@ def show_tool_version_output(cmd: str): # show version output for executable us
155176
break
156177
else: # pragma: no cover
157178
raise ValueError(f"Failed to find {file_name} in list of files.")
179+
return clang_versions

cpp_linter/clang_tools/clang_tidy.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,16 @@ def get_suggestions_from_patch(
133133

134134
def _has_related_suggestion(suggestion: Suggestion) -> bool:
135135
for known in review_comments.suggestions:
136-
if known.line_end >= suggestion.line_end >= known.line_start:
136+
if (
137+
known.file_name == suggestion.file_name
138+
and known.line_end >= suggestion.line_end >= known.line_start
139+
):
137140
known.comment += f"\n{suggestion.comment}"
138141
return True
139142
return False
140143

141144
# now check for clang-tidy warnings with no fixes applied
145+
assert isinstance(review_comments.tool_total["clang-tidy"], int)
142146
for note in self.notes:
143147
if not note.applied_fixes: # if no fix was applied
144148
line_numb = int(note.line)

cpp_linter/clang_tools/patcher.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,17 @@ def __init__(self) -> None:
5050
#: The list of actual comments
5151
self.suggestions: List[Suggestion] = []
5252

53-
self.tool_total: Dict[str, int] = {"clang-tidy": 0, "clang-format": 0}
53+
self.tool_total: Dict[str, Optional[int]] = {
54+
"clang-tidy": None,
55+
"clang-format": None,
56+
}
5457
"""The total number of concerns about a specific clang tool.
5558
5659
This may not equate to the length of `suggestions` because
5760
1. There is no guarantee that all suggestions will fit within the PR's diff.
5861
2. Suggestions are a combined result of advice from both tools.
62+
63+
A `None` value means a review was not requested from the corresponding tool.
5964
"""
6065

6166
self.full_patch: Dict[str, str] = {"clang-tidy": "", "clang-format": ""}
@@ -69,17 +74,26 @@ def merge_similar_suggestion(self, suggestion: Suggestion) -> bool:
6974
"""
7075
for known in self.suggestions:
7176
if (
72-
known.line_end == suggestion.line_end
77+
known.file_name == suggestion.file_name
78+
and known.line_end == suggestion.line_end
7379
and known.line_start == suggestion.line_start
7480
):
7581
known.comment += f"\n{suggestion.comment}"
7682
return True
7783
return False
7884

79-
def serialize_to_github_payload(self) -> Tuple[str, List[Dict[str, Any]]]:
85+
def serialize_to_github_payload(
86+
# avoid circular imports by accepting primitive types (instead of ClangVersions)
87+
self,
88+
tidy_version: Optional[str],
89+
format_version: Optional[str],
90+
) -> Tuple[str, List[Dict[str, Any]]]:
8091
"""Serialize this object into a summary and list of comments compatible
8192
with Github's REST API.
8293
94+
:param tidy_version: The version numbers of the clang-tidy used.
95+
:param format_version: The version numbers of the clang-format used.
96+
8397
:returns: The returned tuple contains a brief summary (at index ``0``)
8498
that contains markdown text describing the summary of the review
8599
comments.
@@ -98,6 +112,12 @@ def serialize_to_github_payload(self) -> Tuple[str, List[Dict[str, Any]]]:
98112
posted_tool_advice["clang-tidy"] += 1
99113

100114
for tool_name in ("clang-tidy", "clang-format"):
115+
tool_version = tidy_version
116+
if tool_name == "clang-format":
117+
tool_version = format_version
118+
if tool_version is None or self.tool_total[tool_name] is None:
119+
continue # if tool wasn't used
120+
summary += f"### Used {tool_name} v{tool_version}\n\n"
101121
if (
102122
len(comments)
103123
and posted_tool_advice[tool_name] != self.tool_total[tool_name]
@@ -115,8 +135,7 @@ def serialize_to_github_payload(self) -> Tuple[str, List[Dict[str, Any]]]:
115135
)
116136
elif not self.tool_total[tool_name]:
117137
summary += f"No concerns from {tool_name}.\n"
118-
result = (summary, comments)
119-
return result
138+
return (summary, comments)
120139

121140

122141
class PatchMixin(ABC):
@@ -164,8 +183,9 @@ def get_suggestions_from_patch(
164183
assert tool_name in review_comments.full_patch
165184
review_comments.full_patch[tool_name] += f"{patch.text}"
166185
assert tool_name in review_comments.tool_total
186+
tool_total = review_comments.tool_total[tool_name] or 0
167187
for hunk in patch.hunks:
168-
review_comments.tool_total[tool_name] += 1
188+
tool_total += 1
169189
if summary_only:
170190
continue
171191
new_hunk_range = file_obj.is_hunk_contained(hunk)
@@ -193,3 +213,5 @@ def get_suggestions_from_patch(
193213
comment.comment = body
194214
if not review_comments.merge_similar_suggestion(comment):
195215
review_comments.suggestions.append(comment)
216+
217+
review_comments.tool_total[tool_name] = tool_total

cpp_linter/rest_api/__init__.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ..common_fs.file_filter import FileFilter
1313
from ..cli import Args
1414
from ..loggers import logger, log_response_msg
15+
from ..clang_tools import ClangVersions
1516

1617

1718
USER_OUTREACH = (
@@ -168,6 +169,7 @@ def make_comment(
168169
files: List[FileObj],
169170
format_checks_failed: int,
170171
tidy_checks_failed: int,
172+
clang_versions: ClangVersions,
171173
len_limit: Optional[int] = None,
172174
) -> str:
173175
"""Make an MarkDown comment from the given advice. Also returns a count of
@@ -176,6 +178,7 @@ def make_comment(
176178
:param files: A list of objects, each describing a file's information.
177179
:param format_checks_failed: The amount of clang-format checks that have failed.
178180
:param tidy_checks_failed: The amount of clang-tidy checks that have failed.
181+
:param clang_versions: The versions of the clang tools used.
179182
:param len_limit: The length limit of the comment generated.
180183
181184
:Returns: The markdown comment as a `str`
@@ -199,12 +202,14 @@ def adjust_limit(limit: Optional[int], text: str) -> Optional[int]:
199202
files=files,
200203
checks_failed=format_checks_failed,
201204
len_limit=len_limit,
205+
version=clang_versions.format,
202206
)
203207
if tidy_checks_failed:
204208
comment += RestApiClient._make_tidy_comment(
205209
files=files,
206210
checks_failed=tidy_checks_failed,
207211
len_limit=adjust_limit(limit=len_limit, text=comment),
212+
version=clang_versions.tidy,
208213
)
209214
else:
210215
prefix = ":heavy_check_mark:\nNo problems need attention."
@@ -215,9 +220,12 @@ def _make_format_comment(
215220
files: List[FileObj],
216221
checks_failed: int,
217222
len_limit: Optional[int] = None,
223+
version: Optional[str] = None,
218224
) -> str:
219225
"""make a comment describing clang-format errors"""
220-
comment = "\n<details><summary>clang-format reports: <strong>"
226+
comment = "\n<details><summary>clang-format{} reports: <strong>".format(
227+
"" if version is None else f" (v{version})"
228+
)
221229
comment += f"{checks_failed} file(s) not formatted</strong></summary>\n\n"
222230
closer = "\n</details>"
223231
checks_failed = 0
@@ -238,9 +246,12 @@ def _make_tidy_comment(
238246
files: List[FileObj],
239247
checks_failed: int,
240248
len_limit: Optional[int] = None,
249+
version: Optional[str] = None,
241250
) -> str:
242251
"""make a comment describing clang-tidy errors"""
243-
comment = "\n<details><summary>clang-tidy reports: <strong>"
252+
comment = "\n<details><summary>clang-tidy{} reports: <strong>".format(
253+
"" if version is None else f" (v{version})"
254+
)
244255
comment += f"{checks_failed} concern(s)</strong></summary>\n\n"
245256
closer = "\n</details>"
246257
for file_obj in files:
@@ -276,11 +287,13 @@ def post_feedback(
276287
self,
277288
files: List[FileObj],
278289
args: Args,
290+
clang_versions: ClangVersions,
279291
):
280292
"""Post action's results using REST API.
281293
282294
:param files: A list of objects, each describing a file's information.
283295
:param args: A namespace of arguments parsed from the :doc:`CLI <../cli_args>`.
296+
:param clang_versions: The version of the clang tools used.
284297
"""
285298
raise NotImplementedError("Must be defined in the derivative")
286299

cpp_linter/rest_api/github_api.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
)
2626
from ..clang_tools.clang_tidy import tally_tidy_advice
2727
from ..clang_tools.patcher import ReviewComments, PatchMixin
28+
from ..clang_tools import ClangVersions
2829
from ..cli import Args
2930
from ..loggers import logger, log_commander
3031
from ..git import parse_diff, get_diff
@@ -187,6 +188,7 @@ def post_feedback(
187188
self,
188189
files: List[FileObj],
189190
args: Args,
191+
clang_versions: ClangVersions,
190192
):
191193
format_checks_failed = tally_format_advice(files)
192194
tidy_checks_failed = tally_tidy_advice(files)
@@ -198,6 +200,7 @@ def post_feedback(
198200
files=files,
199201
format_checks_failed=format_checks_failed,
200202
tidy_checks_failed=tidy_checks_failed,
203+
clang_versions=clang_versions,
201204
len_limit=None,
202205
)
203206
with open(environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as summary:
@@ -225,6 +228,7 @@ def post_feedback(
225228
files=files,
226229
format_checks_failed=format_checks_failed,
227230
tidy_checks_failed=tidy_checks_failed,
231+
clang_versions=clang_versions,
228232
len_limit=65535,
229233
)
230234

@@ -253,6 +257,7 @@ def post_feedback(
253257
format_review=args.format_review,
254258
no_lgtm=args.no_lgtm,
255259
passive_reviews=args.passive_reviews,
260+
clang_versions=clang_versions,
256261
)
257262

258263
def make_annotations(
@@ -388,6 +393,7 @@ def post_review(
388393
format_review: bool,
389394
no_lgtm: bool,
390395
passive_reviews: bool,
396+
clang_versions: ClangVersions,
391397
):
392398
url = f"{self.api_url}/repos/{self.repo}/pulls/{self.pull_request}"
393399
response = self.api_request(url=url)
@@ -419,11 +425,15 @@ def post_review(
419425
summary_only=summary_only,
420426
review_comments=review_comments,
421427
)
422-
(summary, comments) = review_comments.serialize_to_github_payload()
428+
(summary, comments) = review_comments.serialize_to_github_payload(
429+
# avoid circular imports by passing primitive types
430+
tidy_version=clang_versions.tidy,
431+
format_version=clang_versions.format,
432+
)
423433
if not summary_only:
424434
payload_comments.extend(comments)
425435
body += summary
426-
if sum(review_comments.tool_total.values()):
436+
if sum([x for x in review_comments.tool_total.values() if isinstance(x, int)]):
427437
event = "REQUEST_CHANGES"
428438
else:
429439
if no_lgtm:
@@ -457,6 +467,8 @@ def create_review_comments(
457467
:param review_comments: An object (passed by reference) that is used to store
458468
the results.
459469
"""
470+
tool_name = "clang-tidy" if tidy_tool else "clang-format"
471+
review_comments.tool_total[tool_name] = 0
460472
for file_obj in files:
461473
tool_advice: Optional[PatchMixin]
462474
if tidy_tool:

tests/capture_tools_output/test_database_path.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from cpp_linter.loggers import logger
1212
from cpp_linter.common_fs import FileObj, CACHE_PATH
1313
from cpp_linter.rest_api.github_api import GithubApiClient
14-
from cpp_linter.clang_tools import capture_clang_tools_output
14+
from cpp_linter.clang_tools import ClangVersions, capture_clang_tools_output
1515
from cpp_linter.clang_tools.clang_format import tally_format_advice
1616
from cpp_linter.clang_tools.clang_tidy import tally_tidy_advice
1717
from cpp_linter.cli import Args
@@ -96,7 +96,7 @@ def test_ninja_database(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
9696
args.lines_changed_only = 0 # analyze complete file
9797

9898
# run clang-tidy and verify paths of project files were matched with database paths
99-
capture_clang_tools_output(files, args=args)
99+
clang_versions: ClangVersions = capture_clang_tools_output(files, args=args)
100100
found_project_file = False
101101
for concern in [a.tidy_advice for a in files if a.tidy_advice]:
102102
for note in concern.notes:
@@ -112,6 +112,7 @@ def test_ninja_database(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
112112
files=files,
113113
tidy_checks_failed=tidy_checks_failed,
114114
format_checks_failed=format_checks_failed,
115+
clang_versions=clang_versions,
115116
)
116117

117118
assert tidy_checks_failed

0 commit comments

Comments
 (0)