Source code for statick_tool.plugins.tool.spotbugs

"""Apply spotbugs tool and gather results."""

import logging
import os
import subprocess
import xml.etree.ElementTree as etree
from typing import Optional

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


[docs] class SpotbugsToolPlugin(ToolPlugin): """Apply spotbugs tool and gather results."""
[docs] def get_name(self) -> str: """Get name of tool. Returns: Name of the tool. """ return "spotbugs"
[docs] @classmethod def get_tool_dependencies(cls) -> list[str]: """Get a list of tools that must run before this one. Returns: List of tool dependencies. """ return ["make"]
[docs] def scan(self, package: Package, level: str) -> Optional[list[Issue]]: """Run tool and gather output. Args: package: The package to process. level: The level to process. Returns: List of issues or None. """ # Sanity check - make sure mvn exists if not self.command_exists("mvn"): logging.warning( "Couldn't find 'mvn' command, can't run Spotbugs Maven integration" ) return None if self.plugin_context is None: return None flags: list[str] = [ "-Dspotbugs.effort=Max", "-Dspotbugs.threshold=Low", "-Dspotbugs.xmlOutput=true", ] flags += self.get_user_flags(level) include_file: Optional[str] = self.plugin_context.config.get_tool_config( self.get_name(), level, "include" ) exclude_file: Optional[str] = self.plugin_context.config.get_tool_config( self.get_name(), level, "exclude" ) if include_file is not None: include_file_path = self.plugin_context.resources.get_file(include_file) flags += [f"-Dspotbugs.includeFilterFile={include_file_path}"] if exclude_file is not None: exclude_file_path = self.plugin_context.resources.get_file(exclude_file) flags += [f"-Dspotbugs.excludeFilterFile={exclude_file_path}"] issues: list[Issue] = [] total_output: str = "" for pom in package["top_poms"]: try: # The spotbugs:spotbugs-maven-plugin split is auto-concatenated output = subprocess.check_output( ["mvn", "com.github.spotbugs:spotbugs-maven-plugin:spotbugs"] + flags, cwd=os.path.dirname(pom), stderr=subprocess.STDOUT, universal_newlines=True, ) except subprocess.CalledProcessError as ex: output = ex.output logging.warning("spotbugs 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 maven! (%s)", ex) return None logging.debug("%s", output) total_output += output # The results will be output to (pom path)/target/spotbugs.xml for each pom for pom in package["all_poms"]: if os.path.exists( os.path.join(os.path.dirname(pom), "target", "spotbugs.xml") ): with open( os.path.join(os.path.dirname(pom), "target", "spotbugs.xml"), encoding="utf8", ) as outfile: issues += self.parse_file_output(outfile.read()) # type: ignore return issues
[docs] def parse_file_output( # pylint: disable=too-many-locals self, output: str ) -> Optional[list[Issue]]: """Parse tool output and report issues. Args: output: Output string. Returns: List of issues or None. """ issues: list[Issue] = [] # Load the plugin mapping if possible warnings_mapping = self.load_mapping() try: output_xml = etree.fromstring(output) except etree.ParseError as ex: logging.warning( "Couldn't parse Spotbugs output (%s)! Provided output was:\n%s", ex, output, ) return None # This might be better to return empty issues list here. for file_entry in output_xml.findall("file"): # Generate the filename file_base = file_entry.attrib["classname"].replace(".", os.sep) java_path_string = f"{file_base}.java" file_path = "" for source_dir in output_xml.findall("Project/SrcDir"): if source_dir.text is not None: norm_src_path = os.path.normpath(source_dir.text) joined_path = os.path.join(norm_src_path, java_path_string) if os.path.exists(joined_path): file_path = joined_path break if not file_path: logging.warning( "Couldn't find file for class %s", file_entry.attrib["classname"] ) file_path = java_path_string for issue in file_entry.findall("BugInstance"): severity = 1 if issue.attrib["priority"] == "Normal": severity = 3 elif issue.attrib["priority"] == "High": severity = 5 cert_reference = None if issue.attrib["type"] in warnings_mapping: cert_reference = warnings_mapping[issue.attrib["type"]] issues.append( Issue( file_path, int(issue.attrib["lineNumber"]), self.get_name(), issue.attrib["type"], severity, issue.attrib["message"], cert_reference, ) ) return issues