Source code for statick_tool.plugins.tool.clang_tidy

"""Apply clang-tidy tool and gather results."""

import argparse
import logging
import re
import subprocess
from typing import Match, Optional, Pattern

from statick_tool.issue import Issue
from statick_tool.package import Package
from statick_tool.tool_plugin import ToolPlugin


[docs] class ClangTidyToolPlugin(ToolPlugin): """Apply clang-tidy tool and gather results."""
[docs] def get_name(self) -> str: """Get name of tool. Returns: Name of the tool. """ return "clang-tidy"
[docs] @classmethod def get_tool_dependencies(cls) -> list[str]: """Get a list of tools that must run before this one. Returns: A list of tool dependencies. """ return ["make"]
[docs] def gather_args(self, args: argparse.Namespace) -> None: """Gather arguments. Args: args: Flags for this plugin will be added to these existing arguments. """ args.add_argument( "--clang-tidy-bin", dest="clang_tidy_bin", type=str, help="clang-tidy binary path", )
[docs] def scan(self, package: Package, level: str) -> Optional[list[Issue]]: """Run tool and gather output. Args: package: The package to scan. level: The level of the scan. Returns: A list of issues found by the tool. """ if ( "make_targets" not in package or "src_dir" not in package or "bin_dir" not in package ): return [] if self.plugin_context is None: return [] clang_tidy_bin = self.get_binary() user_version = self.plugin_context.config.get_tool_config( self.get_name(), level, "version" ) if user_version is not None: clang_tidy_bin = f"{clang_tidy_bin}-{user_version}" # If the user explicitly specifies a binary, let that override the user_version if self.plugin_context.args.clang_tidy_bin is not None: clang_tidy_bin = self.plugin_context.args.clang_tidy_bin flags: list[str] = [ "-header-filter=" + package["src_dir"] + "/.*", "-p", package["bin_dir"] + "/compile_commands.json", "-extra-arg=-fopenmp=libomp", ] flags += self.get_user_flags(level) files: list[str] = [] if "make_targets" in package: for target in package["make_targets"]: files += target["src"] try: output = subprocess.check_output( [clang_tidy_bin] + flags + files, stderr=subprocess.STDOUT, universal_newlines=True, ) if ( "clang-diagnostic-error" in output ): # pylint: disable=unsupported-membership-test raise subprocess.CalledProcessError(-1, clang_tidy_bin, output) except subprocess.CalledProcessError as ex: output = ex.output if ex.returncode != 1: logging.warning("clang-tidy failed! Returncode = %d", ex.returncode) logging.warning("%s exception: %s", self.get_name(), ex.output) return None except OSError as ex: logging.warning("Couldn't find %s! (%s)", clang_tidy_bin, ex) return None logging.debug("%s", output) if self.plugin_context and self.plugin_context.args.output_directory: with open(self.get_name() + ".log", "w", encoding="utf8") as fid: fid.write(output) issues: list[Issue] = self.parse_tool_output(output) return issues
[docs] @classmethod def check_for_exceptions(cls, match: Match[str]) -> bool: """Manual exceptions. Args: match: The regex match object. Returns: True if the match is an exception, False otherwise. """ # You are allowed to have 'using namespace' in source files if ( match.group(1).endswith(".cpp") or match.group(1).endswith(".cc") ) and match.group(6) == "google-build-using-namespace": return True return False
[docs] def parse_tool_output(self, output: str) -> list[Issue]: """Parse tool output and report issues. Args: output: The output from the tool. Returns: A list of issues found by the tool. """ clang_tidy_re = r"(.+):(\d+):(\d+):\s(.+):\s(.+)\s\[(.+)\]" parse: Pattern[str] = re.compile(clang_tidy_re) issues: list[Issue] = [] # Load the plugin mapping if possible warnings_mapping = self.load_mapping() for line in output.splitlines(): match: Optional[Match[str]] = parse.match(line) if match and not self.check_for_exceptions(match): if ( line[1] != "*" and match.group(3) != "information" and match.group(4) != "note" ): cert_reference = None if match.group(6) in warnings_mapping: cert_reference = warnings_mapping[match.group(6)] issues.append( Issue( match.group(1), int(match.group(2)), self.get_name(), match.group(4) + "/" + match.group(6), 3, match.group(5), cert_reference, ) ) return issues