import os
import re
from string import Template
from .code_quality import Finding
from .exceptions import WarningsConfigError
from .warnings_checker import WarningsChecker
DOXYGEN_WARNING_REGEX = r"(?:(?P<path1>(?:[/.]|[A-Za-z]).+?):(?P<line1>-?\d+):\s*(?P<severity1>[Ww]arning|[Ee]rror)|<.+>:(?P<line2>-?\d+)(?::\s*(?P<severity2>[Ww]arning|[Ee]rror))?): (?P<description1>.+(?:(?!\s*([Nn]otice|[Ww]arning|[Ee]rror): )[^/<\n][^:\n][^/\n].+)*)|\s*\b(?P<severity3>[Nn]otice|[Ww]arning|[Ee]rror): (?!notes)(?P<description2>.+)\n?"
doxy_pattern = re.compile(DOXYGEN_WARNING_REGEX)
SPHINX_WARNING_REGEX = r"(?m)^(?:((?P<path1>.+?):(?P<line1>\d+|None)?):?\s*)?(?P<severity1>DEBUG|INFO|WARNING|ERROR|SEVERE|CRITICAL):\s*(?P<description1>.+)$"
sphinx_pattern = re.compile(SPHINX_WARNING_REGEX)
PYTHON_XMLRUNNER_REGEX = r"(\s*(?P<severity1>ERROR|FAILED) (\[\d+\.\d{3}s\]: \s*(?P<description1>.+)))\n?"
xmlrunner_pattern = re.compile(PYTHON_XMLRUNNER_REGEX)
COVERITY_WARNING_REGEX = r"(?P<path>[\w\.\\/\- ]+)(:(?P<line>\d+)(:(?P<column>\d+))?)?: ?CID (?P<cid>\d+) \(#(?P<curr>\d+) of (?P<max>\d+)\): (?P<checker>.+): (?P<classification>[\w ]+),.+"
coverity_pattern = re.compile(COVERITY_WARNING_REGEX)
class RegexChecker(WarningsChecker):
name = "regex"
pattern = None
SEVERITY_MAP = {
"debug": "info",
"info": "info",
"notice": "info",
"warning": "major",
"error": "critical",
"severe": "critical",
"critical": "critical",
"failed": "critical",
}
def check(self, content):
"""Function for counting the number of warnings in a specific text
Args:
content (str): The content to parse
"""
matches = re.finditer(self.pattern, content)
for match in matches:
match_string = match.group(0).strip()
if self._is_excluded(match_string):
continue
self.count += 1
self.logger.info(match_string)
self.logger.debug(match_string)
if self.cq_enabled:
self.add_code_quality_finding(match)
def add_code_quality_finding(self, match):
"""Add code quality finding
Args:
match (re.Match): The regex match
"""
groups = {name: result for name, result in match.groupdict().items() if result}
description = next((result for name, result in groups.items() if name.startswith("description")), None)
if not description:
return # No description was found, which is the bare minimum
finding = Finding(self.cq_description_template.substitute(description=description))
finding.severity = next((self.SEVERITY_MAP[result.lower()] for name, result in groups.items()
if name.startswith("severity")), "info")
finding.path = next((result for name, result in groups.items()
if name.startswith("path")), self.cq_default_path)
finding.line = next((result for name, result in groups.items() if name.startswith("line")), 1)
self.cq_findings.append(finding.to_dict())
[docs]
class CoverityChecker(RegexChecker):
name = "coverity"
pattern = coverity_pattern
def __init__(self, *logging_args):
super().__init__(*logging_args)
self._cq_description_template = Template("Coverity: CID $cid: $checker")
self.checkers = {
"unclassified": CoverityClassificationChecker("unclassified", *logging_args),
"pending": CoverityClassificationChecker("pending", *logging_args),
"bug": CoverityClassificationChecker("bug", *logging_args),
"intentional": CoverityClassificationChecker("intentional", *logging_args),
"false positive": CoverityClassificationChecker("false positive", *logging_args),
}
@property
def cq_findings(self):
"""List[dict]: list of code quality findings"""
for checker in self.checkers.values():
self._cq_findings.extend(checker.cq_findings)
return self._cq_findings
@property
def cq_description_template(self):
"""Template: string.Template instance based on the configured template string"""
return self._cq_description_template
@cq_description_template.setter
def cq_description_template(self, template_obj):
self._cq_description_template = template_obj
[docs]
def return_count(self):
"""Getter function for the amount of warnings found
Returns:
int: Number of warnings found
"""
self.count = 0
for checker in self.checkers.values():
self.count += checker.return_count()
return self.count
[docs]
def return_check_limits(self):
"""Function for checking whether the warning count is within the configured limits
Returns:
int: 0 if the amount of warnings is within limits, the count of warnings otherwise
(or 1 in case of a count of 0 warnings)
"""
count = 0
for checker in self.checkers.values():
count += checker.return_check_limits()
if count:
self.logger.warning(f"Returning error code {count}.")
return count
[docs]
def check(self, content):
"""
Function for counting the number of warnings, but adopted for Coverity
output
Args:
content (str): The content to parse
"""
matches = re.finditer(self.pattern, content)
for match in matches:
if (classification := match.group("classification").lower()) in self.checkers:
checker = self.checkers[classification]
checker.cq_enabled = self.cq_enabled
checker.exclude_patterns = self.exclude_patterns
checker.cq_description_template = self.cq_description_template
checker.cq_default_path = self.cq_default_path
checker.check(match)
else:
self.logger.warning(f"Unrecognized classification {match.group('classification')!r}")
[docs]
def parse_config(self, config):
"""Process configuration
Args:
config (dict): Content of configuration file
"""
config.pop("enabled")
if value := config.pop("cq_description_template", None):
self.cq_description_template = Template(value)
if value := config.pop("cq_default_path", None):
self.cq_default_path = value
if value := config.pop("exclude", None):
self.add_patterns(value, self.exclude_patterns)
for classification, checker_config in config.items():
classification_key = classification.lower().replace("_", " ")
if classification_key in self.checkers:
self.checkers[classification_key].parse_config(checker_config)
else:
self.logger.warning(f"Unrecognized classification {classification!r}")
class CoverityClassificationChecker(WarningsChecker):
name = "coverity_sub"
logging_fmt = "{checker.name_repr}: {checker.classification:<14} | {message}"
SEVERITY_MAP = {
"false positive": "info",
"intentional": "info",
"bug": "major",
"unclassified": "major",
"pending": "critical",
}
def __init__(self, classification, *args):
"""Initialize the CoverityClassificationChecker:
Args:
classification (str): The coverity classification
"""
super().__init__(*args)
self.classification = classification
@property
def cq_description_template(self):
"""Template: string.Template instance based on the configured template string"""
return self._cq_description_template
@cq_description_template.setter
def cq_description_template(self, template_obj):
self._cq_description_template = template_obj
def add_code_quality_finding(self, match):
"""Add code quality finding
Args:
match (re.Match): The regex match
"""
groups = {name: result for name, result in match.groupdict().items() if result}
try:
description = self.cq_description_template.substitute(os.environ, **groups)
except KeyError as err:
raise WarningsConfigError(f"Failed to find environment variable from configuration value "
f"'cq_description_template': {err}") from err
finding = Finding(description)
if classification_raw := groups.get("classification"):
finding.severity = self.SEVERITY_MAP[classification_raw.lower()]
if path := groups.get("path", self.cq_default_path):
finding.path = path
if line := groups.get("line", 1):
finding.line = line
if column := groups.get("column", 1):
finding.column = column
self.cq_findings.append(finding.to_dict())
def check(self, content):
"""
Function for counting the number of warnings, but adopted for Coverity output.
Multiple warnings for the same CID are counted as one.
Args:
content (re.Match): The regex match
"""
match_string = content.group(0).strip()
if not self._is_excluded(match_string) and (content.group("curr") == content.group("max")):
self.count += 1
self.logger.info(match_string)
self.logger.debug(match_string)
if self.cq_enabled:
self.add_code_quality_finding(content)
[docs]
class DoxyChecker(RegexChecker):
name = "doxygen"
pattern = doxy_pattern
[docs]
class SphinxChecker(RegexChecker):
name = "sphinx"
pattern = sphinx_pattern
sphinx_deprecation_regex = r"(?m)^(?:(.+?:(?:\d+|None)?):?\s*)?(DEBUG|INFO|WARNING|ERROR|SEVERE|(?:\w+Sphinx\d+Warning)):\s*(.+)$"
sphinx_deprecation_regex_in_match = "RemovedInSphinx\\d+Warning"
[docs]
def include_sphinx_deprecation(self):
"""
Adds the pattern for sphinx_deprecation_regex to the list patterns to include and alters the main pattern
"""
self.pattern = re.compile(self.sphinx_deprecation_regex)
self.add_patterns([self.sphinx_deprecation_regex_in_match], self.include_patterns)
[docs]
class XMLRunnerChecker(RegexChecker):
name = "xmlrunner"
pattern = xmlrunner_pattern