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"] ]