Source code for statick_tool.plugins.tool.cppcheck

"""Apply cppcheck tool and gather results."""

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

from packaging.version import Version

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


[docs] class CppcheckToolPlugin(ToolPlugin): """Apply cppcheck tool and gather results.""" # pylint: disable=super-init-not-called def __init__(self) -> None: """Initialize cppcheck extensions.""" self.valid_extensions = [".h", ".hpp", ".c", ".cc", ".cpp", ".cxx"] # pylint: enable=super-init-not-called
[docs] def get_name(self) -> str: """Get name of tool. Returns: Name of the tool. """ return "cppcheck"
[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( "--cppcheck-bin", dest="cppcheck_bin", type=str, help="cppcheck binary path" )
[docs] def get_binary( # pylint: disable=unused-argument self, level: Optional[str] = None, package: Optional[Package] = None ) -> str: """Get tool binary name. Args: level: The level of the scan. package: The package to scan. Returns: The name of the tool binary. """ binary = self.get_name() if ( self.plugin_context is not None and self.plugin_context.args.cppcheck_bin is not None ): binary = self.plugin_context.args.cppcheck_bin return binary
[docs] def parse_version(self, version_str: str) -> str: """Parse version of tool. If no version is found the function returns "0.0". Args: version_str: The version string to parse. Returns: The parsed version string. """ version = "0.0" ver_re = r"(.+) ([0-9]*\.?[0-9]+)" parse: Pattern[str] = re.compile(ver_re) match: Optional[Match[str]] = parse.match(version_str) if match: version = match.group(2) return version
# pylint: disable=too-many-locals, too-many-branches, too-many-return-statements
[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 and "headers" not in package ) or self.plugin_context is None: return [] flags: list[str] = [ "--report-progress", "--verbose", "--inline-suppr", "--language=c++", "--template=[{file}:{line}]: ({severity} {id}) {message}", ] flags += self.get_user_flags(level) user_version = self.plugin_context.config.get_tool_config( self.get_name(), level, "version" ) cppcheck_bin = self.get_binary() try: version = self.parse_version(self.get_version()) # If specific version is not specified just use the installed version. if user_version is not None and Version(version) != Version(user_version): logging.warning( "You need version %s of cppcheck, but you have %s. " "See README.md for instructions on how to install the " "proper version", user_version, version, ) return None except OSError as ex: logging.warning("Cppcheck not found! (%s)", ex) return None files: list[str] = [] include_dirs: list[str] = [] if "make_targets" in package: for target in package["make_targets"]: files += target["src"] if "include_dirs" in target: for include_dir in target["include_dirs"]: if include_dir not in include_dirs: include_dirs.append(include_dir) if "headers" in package: files += package["headers"] if not files: return [] include_args = [] for include_dir in include_dirs: if package.path in include_dir: include_args.append("-I") include_args.append(include_dir) try: output = subprocess.check_output( [cppcheck_bin] + flags + include_args + files, stderr=subprocess.STDOUT, universal_newlines=True, ) except subprocess.CalledProcessError as ex: output = ex.output logging.warning("cppcheck failed! Returncode = %d", ex.returncode) logging.warning("%s exception: %s", self.get_name(), ex.output) 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
# pylint: enable=too-many-locals, too-many-branches, too-many-return-statements
[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. """ # Sometimes you can't fix variableScope in old c code if match.group(1).endswith(".c") and match.group(4) == "variableScope": 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. """ cppcheck_re = r"\[(.+):(\d+)\]:\s\((.+?)\s(.+?)\)\s(.+)" parse: Pattern[str] = re.compile(cppcheck_re) issues: list[Issue] = [] warnings_mapping = self.load_mapping() for line in output.splitlines(): match: Optional[Match[str]] = parse.match(line) if ( match and line[1] != "*" and match.group(3) != "information" and not self.check_for_exceptions(match) ): dummy, extension = os.path.splitext(match.group(1)) if extension in self.valid_extensions: cert_reference = None if match.group(4) in warnings_mapping: cert_reference = warnings_mapping[match.group(4)] issues.append( Issue( match.group(1), int(match.group(2)), self.get_name(), match.group(3) + "/" + match.group(4), 5, match.group(5), cert_reference, ) ) return issues