bot/code_review_bot/tasks/clang_tidy.py (120 lines of code) (raw):
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import re
import structlog
from code_review_bot import Issue, Level, Reliability
from code_review_bot.tasks.base import AnalysisTask
logger = structlog.get_logger(__name__)
ISSUE_MARKDOWN = """
## clang-tidy {level}
- **Message**: {message}
- **Location**: {location}
- **In patch**: {in_patch}
- **Clang check**: {check}
- **Publishable check**: {publishable_check}
- **Expanded Macro**: {expanded_macro}
- **Publishable **: {publishable}
- **Checker reliability **: {reliability} (false positive risk)
{notes}
"""
ISSUE_NOTE_MARKDOWN = """
- **Note**: {message}
- **Location**: {location}
```
{body}
```
"""
ERROR_MARKDOWN = """
**Message**: ```{message}```
**Location**: {location}
"""
CLANG_MACRO_DETECTION = re.compile(r"^expanded from macro")
class ClangTidyIssue(Issue):
"""
An issue reported by clang-tidy
"""
def __init__(
self,
analyzer,
revision,
path,
line,
column,
check,
message,
level=Level.Warning,
reliability=Reliability.Unknown,
reason=None,
publish=True,
):
assert isinstance(reliability, Reliability)
super().__init__(
analyzer,
revision,
path,
line=int(line),
nb_lines=1, # Only 1 line affected on clang-tidy
check=check,
column=int(column),
level=level,
message=message,
)
self.notes = []
self.reliability = reliability
self.publishable_check = publish
self.reason = reason
def is_build_error(self):
return True if self.level == Level.Error else False
def as_error(self):
assert self.is_build_error(), "ClangTidyIssue is not a build error."
return ERROR_MARKDOWN.format(
message=self.message, location=f"{self.path}:{self.line}"
)
@property
def display_name(self):
"""
Display name to identify clearly if it's static-analysis issue or a
build error
"""
return "Build Error" if self.is_build_error() else self.analyzer.display_name
def validates(self):
"""
Publish clang-tidy issues when:
* check is marked as publishable
* is not from an expanded macro
"""
return self.has_publishable_check() and not self.is_expanded_macro()
def is_expanded_macro(self):
"""
Is the issue only found in an expanded macro ?
"""
if not self.notes:
return False
# Only consider first note
note = self.notes[0]
return CLANG_MACRO_DETECTION.match(note.message) is not None
def has_publishable_check(self):
"""
Is this issue using a publishable check ?
"""
return self.publishable_check is True
def as_text(self):
"""
Build the text body published on reporters
"""
message = self.message
if len(message) > 0:
message = message[0].capitalize() + message[1:]
body = f"{self.level.name}: {message} [clang-tidy: {self.check}]"
# Always add body as it's been cleaned up
if self.reason:
body += f"\n{self.reason}"
# Also add the reliability of the checker
if self.reliability != Reliability.Unknown:
body += f"\nChecker reliability is {self.reliability.value}, meaning that the false positive ratio is {self.reliability.invert}."
return body
def as_markdown(self):
return ISSUE_MARKDOWN.format(
level=self.level.value,
message=self.message,
location=f"{self.path}:{self.line}:{self.column}",
reason=self.reason,
check=self.check,
in_patch="yes" if self.revision.contains(self) else "no",
publishable_check="yes" if self.has_publishable_check() else "no",
publishable="yes" if self.is_publishable() else "no",
expanded_macro="yes" if self.is_expanded_macro() else "no",
reliability=self.reliability.value,
notes="\n".join(
[
ISSUE_NOTE_MARKDOWN.format(
message=n.message,
location=f"{n.path}:{n.line}:{n.column}",
body=n.body,
)
for n in self.notes
]
),
)
class ClangTidyTask(AnalysisTask):
"""
Support issues from source-test clang-tidy tasks
"""
artifacts = ["public/code-review/clang-tidy.json"]
@property
def display_name(self):
return "clang-tidy"
def build_help_message(self, files):
return "`./mach static-analysis check --outgoing` (C/C++)"
def parse_issues(self, artifacts, revision):
return [
ClangTidyIssue(
analyzer=self,
revision=revision,
path=path,
line=warning["line"],
column=warning["column"],
check=warning["flag"],
level=Level(warning.get("type", "warning")),
message=warning["message"],
reliability=Reliability(warning["reliability"])
if "reliability" in warning
else Reliability.Unknown,
reason=warning.get("reason"),
publish=warning.get("publish"),
)
for artifact in artifacts.values()
for path, items in artifact["files"].items()
for warning in items["warnings"]
]