1"""Quality metrics extraction from crackerjack output."""

2

3 import re

4 import typing as t

5 from dataclasses import dataclass

6 from typing import Any

7

8

9@dataclass

10 class QualityMetrics:

11 """Structured quality metrics from crackerjack execution."""

12

13 coverage_percent: float | None = None

14 max_complexity: int | None = None

15 complexity_violations: int = 0

16 security_issues: int = 0

17 tests_passed: int = 0

18 tests_failed: int = 0

19 type_errors: int = 0

20 formatting_issues: int = 0

21

22 def to_dict(self) -> dict[str, Any]:

23 """Convert to dictionary, excluding None and zero values."""

24 return {

25 k: v

26 for k, v in self.__dict__.items()

27 if v is not None and (not isinstance(v, int) or v > 0)

28 }

29

30 def _format_coverage(self) -> str:

31 """Format coverage metric."""

32 if self.coverage_percent is None:

33 return ""

34 emoji = "✅" if self.coverage_percent >= 42 else "⚠️"

35 result = f"- {emoji} Coverage: {self.coverage_percent:.1f}%"

36 if self.coverage_percent < 42:

37 result += " (below 42% baseline)"

38 return result + "\n"

39

40 def _format_complexity(self) -> str:

41 """Format complexity metric."""

42 if not self.max_complexity:

43 return ""

44 emoji = "✅" if self.max_complexity <= 15 else "❌"

45 result = f"- {emoji} Max Complexity: {self.max_complexity}"

46 if self.max_complexity > 15:

47 result += " (exceeds limit of 15)"

48 return result + "\n"

49

50 def _format_violations(self) -> str:

51 """Format violation metrics."""

52 lines = []

53 if self.complexity_violations:

54 plural = "s" if self.complexity_violations != 1 else ""

55 lines.append(

56 f"- ⚠️ Complexity Violations: {self.complexity_violations} function{plural}",

57 )

58 if self.security_issues:

59 plural = "s" if self.security_issues != 1 else ""

60 lines.append(

61 f"- 🔒 Security Issues: {self.security_issues} (Bandit finding{plural})",

62 )

63 return "\n".join(lines) + ("\n" if lines else "")

64

65 def _format_tests(self) -> str:

66 """Format test results."""

67 if self.tests_failed:

68 return f"- ❌ Tests Failed: {self.tests_failed}\n"

69 if self.tests_passed:

70 return f"- ✅ Tests Passed: {self.tests_passed}\n"

71 return ""

72

73 def _format_errors(self) -> str:

74 """Format error metrics."""

75 lines = []

76 if self.type_errors:

77 lines.append(f"- 📝 Type Errors: {self.type_errors}")

78 if self.formatting_issues:

79 lines.append(f"- ✨ Formatting Issues: {self.formatting_issues}")

80 return "\n".join(lines) + ("\n" if lines else "")

81

82 def format_for_display(self) -> str:

83 """Format metrics for user-friendly display."""

84 if not self.to_dict():

85 return ""

86

87 return (

88 "\n📈 **Quality Metrics**:\n"

89 + self._format_coverage()

90 + self._format_complexity()

91 + self._format_violations()

92 + self._format_tests()

93 + self._format_errors()

94 )

95

96

97 class QualityMetricsExtractor:

98 """Extract structured quality metrics from crackerjack output."""

99

100 # Regex patterns for metric extraction

101 PATTERNS: t.Final[dict[str, str]] = {

102 "coverage": r"coverage:?\s*(\d+(?:\.\d+)?)%",

103 "complexity": r"Complexity of (\d+) is too high",

104 "security": r"B\d{3}:", # Bandit security codes

105 "tests": r"(\d+) passed(?:.*?(\d+) failed)?",

106 "type_errors": r"error:|Found (\d+) error",

107 "formatting": r"would reformat|line too long",

108 }

109

110 @classmethod

111 def extract(cls, stdout: str, stderr: str) -> QualityMetrics:

112 """Extract metrics from crackerjack output.

113

114 Args:

115 stdout: Standard output from crackerjack execution

116 stderr: Standard error from crackerjack execution

117

118 Returns:

119 QualityMetrics object with extracted values

120

121 """

122 metrics = QualityMetrics()

123 combined = stdout + stderr

124

125 # Coverage

126 if match := re.search( # REGEX OK: coverage pattern from PATTERNS dict

127 cls.PATTERNS["coverage"],

128 combined,

129 ):

130 metrics.coverage_percent = float(match.group(1))

131

132 # Complexity

133 complexity_matches = (

134 re.findall( # REGEX OK: complexity pattern from PATTERNS dict

135 cls.PATTERNS["complexity"],

136 stderr,

137 )

138 )

139 if complexity_matches:

140 complexities = [int(c) for c in complexity_matches]

141 metrics.max_complexity = max(complexities)

142 metrics.complexity_violations = len(complexities)

143

144 # Security (Bandit codes like B108, B603, etc.)

145 metrics.security_issues = len(

146 re.findall( # REGEX OK: security pattern from PATTERNS dict

147 cls.PATTERNS["security"],

148 stderr,

149 ),

150 )

151

152 # Tests

153 if match := re.search( # REGEX OK: test results pattern from PATTERNS dict

154 cls.PATTERNS["tests"],

155 stdout,

156 ):

157 metrics.tests_passed = int(match.group(1))

158 if match.group(2):

159 metrics.tests_failed = int(match.group(2))

160

161 # Type errors

162 type_error_match = re.search( # REGEX OK: type error count extraction

163 r"Found (\d+) error",

164 stderr,

165 )

166 if type_error_match:

167 metrics.type_errors = int(type_error_match.group(1))

168 else:

169 # Count error lines

170 metrics.type_errors = len(

171 [line for line in stderr.split("\n") if "error:" in line.lower()],

172 )

173

174 # Formatting

175 metrics.formatting_issues = len(

176 re.findall( # REGEX OK: formatting pattern from PATTERNS dict

177 cls.PATTERNS["formatting"],

178 combined,

179 ),

180 )

181

182 return metrics