python-code-quality 0.1.15__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py_cq/__init__.py +3 -4
- py_cq/api.py +248 -0
- py_cq/cli.py +218 -129
- py_cq/config/config.toml +95 -0
- py_cq/context_hash.py +18 -8
- py_cq/execution_engine.py +182 -26
- py_cq/language_detector.py +4 -1
- py_cq/llm_formatter.py +200 -18
- py_cq/localtypes.py +53 -7
- py_cq/main.py +1 -1
- py_cq/parsers/__init__.py +1 -1
- py_cq/parsers/banditparser.py +43 -14
- py_cq/parsers/common.py +187 -25
- py_cq/parsers/compileparser.py +21 -9
- py_cq/parsers/complexityparser.py +40 -4
- py_cq/parsers/coverageparser.py +184 -70
- py_cq/parsers/exitcodeparser.py +11 -2
- py_cq/parsers/halsteadparser.py +42 -14
- py_cq/parsers/interrogateparser.py +261 -25
- py_cq/parsers/linecountparser.py +10 -2
- py_cq/parsers/maintainabilityparser.py +34 -4
- py_cq/parsers/pytestparser.py +77 -20
- py_cq/parsers/regexcountparser.py +13 -3
- py_cq/parsers/ruffparser.py +160 -12
- py_cq/parsers/typarser.py +175 -39
- py_cq/parsers/vultureparser.py +22 -12
- py_cq/table_formatter.py +43 -0
- py_cq/tool_registry.py +7 -6
- {python_code_quality-0.1.15.dist-info → python_code_quality-0.2.1.dist-info}/METADATA +88 -3
- python_code_quality-0.2.1.dist-info/RECORD +35 -0
- {python_code_quality-0.1.15.dist-info → python_code_quality-0.2.1.dist-info}/WHEEL +1 -1
- py_cq/config/config.yaml +0 -94
- python_code_quality-0.1.15.dist-info/RECORD +0 -33
- {python_code_quality-0.1.15.dist-info → python_code_quality-0.2.1.dist-info}/entry_points.txt +0 -0
py_cq/parsers/coverageparser.py
CHANGED
|
@@ -1,88 +1,202 @@
|
|
|
1
|
-
"""Parses raw coverage tool output into
|
|
2
|
-
The module defines `CoverageParser`, a concrete implementation of `AbstractParser`, which extracts overall and per-file coverage metrics from a `RawResult` object and normalises the data format for downstream processing."""
|
|
1
|
+
"""Parses raw coverage tool output into structured ToolResult instances with per-function granularity."""
|
|
3
2
|
|
|
3
|
+
import ast
|
|
4
4
|
import logging
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
from py_cq.localtypes import AbstractParser, RawResult, ToolResult
|
|
8
|
+
from py_cq.parsers.common import find_function_source, resolve_path
|
|
7
9
|
|
|
8
10
|
log = logging.getLogger("cq")
|
|
9
11
|
|
|
10
12
|
|
|
13
|
+
def _parse_line_ranges(s: str) -> set[int]:
|
|
14
|
+
"""Parse a comma-separated string of line ranges and individual lines.
|
|
15
|
+
|
|
16
|
+
Example: "1, 3-5, 10" -> {1, 3, 4, 5, 10}
|
|
17
|
+
"""
|
|
18
|
+
result: set[int] = set()
|
|
19
|
+
for part in s.split(","):
|
|
20
|
+
part = part.strip()
|
|
21
|
+
if "-" in part:
|
|
22
|
+
lo, hi = part.split("-", 1)
|
|
23
|
+
try:
|
|
24
|
+
result.update(range(int(lo), int(hi) + 1))
|
|
25
|
+
except ValueError:
|
|
26
|
+
pass
|
|
27
|
+
elif part.isdigit():
|
|
28
|
+
result.add(int(part))
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_signature(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
|
|
33
|
+
"""Return the signature of the function definition as a string."""
|
|
34
|
+
prefix = "async def" if isinstance(node, ast.AsyncFunctionDef) else "def"
|
|
35
|
+
args = ast.unparse(node.args)
|
|
36
|
+
returns = f" -> {ast.unparse(node.returns)}" if node.returns else ""
|
|
37
|
+
return f"{prefix} {node.name}({args}){returns}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _extract_functions(file: str, missing_lines_str: str) -> list[tuple[str, int, str]]:
|
|
41
|
+
"""Return (name, lineno, signature) for functions whose bodies overlap with the missing line ranges."""
|
|
42
|
+
try:
|
|
43
|
+
source = Path(file).read_text(encoding="utf-8", errors="replace")
|
|
44
|
+
tree = ast.parse(source)
|
|
45
|
+
except (OSError, SyntaxError, ValueError):
|
|
46
|
+
return []
|
|
47
|
+
missing = _parse_line_ranges(missing_lines_str)
|
|
48
|
+
seen: set[str] = set()
|
|
49
|
+
result: list[tuple[str, int, str]] = []
|
|
50
|
+
for node in sorted(ast.walk(tree), key=lambda n: getattr(n, "lineno", 0)):
|
|
51
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
52
|
+
end = getattr(node, "end_lineno", node.lineno)
|
|
53
|
+
if missing & set(range(node.lineno, end + 1)) and node.name not in seen:
|
|
54
|
+
seen.add(node.name)
|
|
55
|
+
result.append((node.name, node.lineno, _get_signature(node)))
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _find_test_file(source_file: str) -> str | None:
|
|
60
|
+
"""Return the test file path for source_file if a tests/ directory exists nearby."""
|
|
61
|
+
candidate_name = f"test_{Path(source_file).stem}.py"
|
|
62
|
+
try:
|
|
63
|
+
for ancestor in Path(source_file).parents:
|
|
64
|
+
try:
|
|
65
|
+
tests_dir = ancestor / "tests"
|
|
66
|
+
if tests_dir.is_dir():
|
|
67
|
+
return str(tests_dir / candidate_name).replace("\\", "/")
|
|
68
|
+
except (OSError, ValueError):
|
|
69
|
+
pass
|
|
70
|
+
except (OSError, ValueError):
|
|
71
|
+
pass
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
11
75
|
class CoverageParser(AbstractParser):
|
|
12
|
-
"""
|
|
13
|
-
Extends AbstractParser, extracting overall coverage percentages, per-file coverage values, normalising file paths, and preserving the tool's return code."""
|
|
76
|
+
"""Parser for coverage results."""
|
|
14
77
|
|
|
15
78
|
def parse(self, raw_result: RawResult) -> ToolResult:
|
|
16
|
-
"""Parse
|
|
17
|
-
|
|
18
|
-
Given a :class:`RawResult` containing the stdout of a coverage tool, the
|
|
19
|
-
method extracts every line that ends with a percent sign. Each such line
|
|
20
|
-
is expected to follow the format::
|
|
21
|
-
|
|
22
|
-
<file> <total_lines> <covered_lines> <coverage>%
|
|
23
|
-
|
|
24
|
-
The coverage percentage is converted to a fraction (e.g. 90\u202f% → 0.9) and
|
|
25
|
-
stored in ``metrics['coverage']`` for the overall ``TOTAL`` line, while
|
|
26
|
-
the per-file values are placed in ``details`` with the file path
|
|
27
|
-
normalised to use forward slashes. The tool's return code is added to
|
|
28
|
-
``details`` under the key ``'return_code'``.
|
|
29
|
-
|
|
30
|
-
Args:
|
|
31
|
-
raw_result (RawResult): The raw output from a coverage tool.
|
|
32
|
-
|
|
33
|
-
Returns:
|
|
34
|
-
ToolResult: A structured result containing the overall coverage
|
|
35
|
-
metric, per-file coverage percentages, and the tool's return code.
|
|
36
|
-
|
|
37
|
-
Example:
|
|
38
|
-
>>> parser = CoverageParser()
|
|
39
|
-
>>> raw = RawResult(
|
|
40
|
-
... stdout='src/main.py 100 90 90%\\\\nTOTAL 200 180 90%',
|
|
41
|
-
... return_code=0)
|
|
42
|
-
>>> result = parser.parse(raw)
|
|
43
|
-
>>> result.metrics['coverage']
|
|
44
|
-
0.9
|
|
45
|
-
>>> result.details['src/main.py']
|
|
46
|
-
0.9"""
|
|
47
|
-
tr = ToolResult(raw=raw_result)
|
|
79
|
+
"""Parse the coverage result."""
|
|
80
|
+
tr = ToolResult(raw=raw_result, project_path=raw_result.project_path)
|
|
48
81
|
lines = raw_result.stdout.splitlines()
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
82
|
+
base_dir = raw_result.project_path
|
|
83
|
+
|
|
84
|
+
file_data: dict[str, dict] = {}
|
|
85
|
+
for line in lines:
|
|
86
|
+
if "[" in line:
|
|
87
|
+
continue
|
|
52
88
|
parts = line.split()
|
|
53
|
-
if len(parts)
|
|
54
|
-
|
|
89
|
+
if len(parts) < 4 or not parts[3].endswith("%"):
|
|
90
|
+
continue
|
|
91
|
+
file_name = parts[0]
|
|
92
|
+
try:
|
|
93
|
+
coverage_pct = float(parts[3].rstrip("%")) / 100.0
|
|
94
|
+
except ValueError:
|
|
95
|
+
log.warning("Error parsing coverage percentage from line: %s", line)
|
|
96
|
+
continue
|
|
97
|
+
if file_name == "TOTAL":
|
|
98
|
+
tr.metrics["coverage"] = coverage_pct
|
|
99
|
+
else:
|
|
55
100
|
try:
|
|
56
|
-
|
|
57
|
-
except ValueError:
|
|
58
|
-
|
|
101
|
+
missing = int(parts[2])
|
|
102
|
+
except (ValueError, IndexError):
|
|
103
|
+
missing = None
|
|
104
|
+
missing_lines = " ".join(parts[4:]) if len(parts) > 4 else None
|
|
105
|
+
file_data[file_name.replace("\\", "/")] = {
|
|
106
|
+
"coverage": coverage_pct,
|
|
107
|
+
"missing": missing,
|
|
108
|
+
"missing_lines": missing_lines,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Build list-based details sorted worst-coverage-first so _single_issue_slices
|
|
112
|
+
# picks the most urgent file and function first.
|
|
113
|
+
details: dict[str, list] = {}
|
|
114
|
+
for file_name, data in sorted(
|
|
115
|
+
file_data.items(), key=lambda x: x[1].get("coverage", 1.0)
|
|
116
|
+
):
|
|
117
|
+
if data.get("missing") == 0:
|
|
118
|
+
continue
|
|
119
|
+
missing_lines_str = data.get("missing_lines")
|
|
120
|
+
coverage_pct = data["coverage"]
|
|
121
|
+
missing_count = data["missing"]
|
|
122
|
+
resolved = resolve_path(base_dir, file_name)
|
|
123
|
+
if missing_lines_str:
|
|
124
|
+
funcs = _extract_functions(resolved, missing_lines_str)
|
|
125
|
+
if funcs:
|
|
126
|
+
details[file_name] = [
|
|
127
|
+
{
|
|
128
|
+
"code": name,
|
|
129
|
+
"line": lineno,
|
|
130
|
+
"signature": sig,
|
|
131
|
+
"file_coverage": coverage_pct,
|
|
132
|
+
"missing": missing_count,
|
|
133
|
+
}
|
|
134
|
+
for name, lineno, sig in funcs
|
|
135
|
+
]
|
|
59
136
|
continue
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
137
|
+
# Fallback when --show-missing wasn't used or AST parsing failed
|
|
138
|
+
details[file_name] = [
|
|
139
|
+
{
|
|
140
|
+
"code": None,
|
|
141
|
+
"line": None,
|
|
142
|
+
"missing": missing_count,
|
|
143
|
+
"missing_lines": missing_lines_str,
|
|
144
|
+
"file_coverage": coverage_pct,
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
|
|
71
148
|
tr.details = details
|
|
72
149
|
return tr
|
|
73
150
|
|
|
74
|
-
def format_llm_message(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
151
|
+
def format_llm_message(
|
|
152
|
+
self, tr: ToolResult, *, context_lines: int = 15, limit: int = 1
|
|
153
|
+
) -> str:
|
|
154
|
+
for file, issues in tr.details.items():
|
|
155
|
+
if not isinstance(issues, list) or not issues:
|
|
156
|
+
continue
|
|
157
|
+
issue = issues[0]
|
|
158
|
+
if not isinstance(issue, dict):
|
|
159
|
+
continue
|
|
160
|
+
code = issue.get("code")
|
|
161
|
+
line = issue.get("line")
|
|
162
|
+
missing = issue.get("missing")
|
|
163
|
+
file_coverage = issue.get("file_coverage", 0.0)
|
|
164
|
+
missing_lines = issue.get("missing_lines")
|
|
165
|
+
try:
|
|
166
|
+
resolved_file = resolve_path(tr.project_path, file)
|
|
167
|
+
except (OSError, ValueError):
|
|
168
|
+
resolved_file = file
|
|
169
|
+
|
|
170
|
+
parts: list[str] = []
|
|
171
|
+
if code and line:
|
|
172
|
+
parts.append(f"{file}:{line} - {code} is missing tests")
|
|
173
|
+
fn_src = find_function_source(resolved_file, code)
|
|
174
|
+
if fn_src:
|
|
175
|
+
parts.append(fn_src)
|
|
176
|
+
else:
|
|
177
|
+
pct = (
|
|
178
|
+
f"{file_coverage:.0%} "
|
|
179
|
+
if isinstance(file_coverage, float) and file_coverage
|
|
180
|
+
else ""
|
|
181
|
+
)
|
|
182
|
+
miss_info = f"{missing} uncovered lines" if missing else "uncovered"
|
|
183
|
+
parts.append(f"{file} - {pct}coverage ({miss_info})")
|
|
184
|
+
if missing_lines:
|
|
185
|
+
parts.append(f" missing lines: {missing_lines}")
|
|
186
|
+
|
|
187
|
+
test_file = _find_test_file(file)
|
|
188
|
+
if test_file:
|
|
189
|
+
try:
|
|
190
|
+
resolved_test = resolve_path(tr.project_path, test_file)
|
|
191
|
+
last_line = len(
|
|
192
|
+
Path(resolved_test).read_text(encoding="utf-8").splitlines()
|
|
193
|
+
)
|
|
194
|
+
except (OSError, ValueError):
|
|
195
|
+
last_line = None
|
|
196
|
+
location = (
|
|
197
|
+
f"{test_file} after line {last_line}" if last_line else test_file
|
|
198
|
+
)
|
|
199
|
+
parts.append(f"\nAdd tests to: {location}")
|
|
200
|
+
|
|
201
|
+
return "\n".join(parts)
|
|
202
|
+
return ""
|
py_cq/parsers/exitcodeparser.py
CHANGED
|
@@ -7,10 +7,19 @@ class ExitCodeParser(AbstractParser):
|
|
|
7
7
|
"""Score 1.0 if the tool exited with code 0, else 0.0."""
|
|
8
8
|
|
|
9
9
|
def parse(self, raw_result: RawResult) -> ToolResult:
|
|
10
|
+
"""Parse the tool result and return a score based on the exit code."""
|
|
10
11
|
score = 1.0 if raw_result.return_code == 0 else 0.0
|
|
11
12
|
return ToolResult(raw=raw_result, metrics={"exit_code": score})
|
|
12
13
|
|
|
13
|
-
def format_llm_message(
|
|
14
|
+
def format_llm_message(
|
|
15
|
+
self, tr: ToolResult, *, context_lines: int = 15, limit: int = 1
|
|
16
|
+
) -> str:
|
|
17
|
+
"""Format the tool result as a string message for the LLM."""
|
|
18
|
+
|
|
14
19
|
output = tr.raw.stdout.strip() or tr.raw.stderr.strip()
|
|
15
20
|
lines = output.splitlines()[:context_lines]
|
|
16
|
-
return
|
|
21
|
+
return (
|
|
22
|
+
"\n".join(lines)
|
|
23
|
+
if lines
|
|
24
|
+
else "Tool exited with non-zero status (no output)"
|
|
25
|
+
)
|
py_cq/parsers/halsteadparser.py
CHANGED
|
@@ -5,10 +5,8 @@ converts the JSON output from Halstead metric tools into a
|
|
|
5
5
|
``ToolResult``. It extracts bug estimates and program volume, applies
|
|
6
6
|
maximum thresholds, and aggregates file- and function-level metrics."""
|
|
7
7
|
|
|
8
|
-
import json
|
|
9
|
-
|
|
10
8
|
from py_cq.localtypes import AbstractParser, RawResult, ToolResult
|
|
11
|
-
from py_cq.parsers.common import score_logistic_variant
|
|
9
|
+
from py_cq.parsers.common import _relative_path, parse_json_dict, score_logistic_variant
|
|
12
10
|
|
|
13
11
|
|
|
14
12
|
class HalsteadParser(AbstractParser):
|
|
@@ -27,7 +25,7 @@ class HalsteadParser(AbstractParser):
|
|
|
27
25
|
def parse(self, raw_result: RawResult) -> ToolResult:
|
|
28
26
|
"""Parses Halstead tool JSON output and returns a ToolResult.
|
|
29
27
|
|
|
30
|
-
The method reads ``raw_result.stdout
|
|
28
|
+
The method reads ``raw_result.stdout``-a JSON string containing
|
|
31
29
|
per-file and per-function Halstead metrics. For each file it
|
|
32
30
|
populates a ``ToolResult`` detail entry with bug-free and
|
|
33
31
|
smallness scores. If a file contains an ``error`` key, the
|
|
@@ -59,15 +57,23 @@ class HalsteadParser(AbstractParser):
|
|
|
59
57
|
# {"total": {"h1": 6, "h2": 18, "N1": 13, "N2": 22, "vocabulary": 24, "length": 35, "calculated_length": 90.56842503028855, "volume": 160.4736875252405, "difficulty": 3.6666666666666665, "effort": 588.4035209258818, "time": 32.68908449588233, "bugs": 0.05349122917508017},
|
|
60
58
|
# "functions": {"calc_dist": {"h1": 3, "h2": 9, "N1": 5, "N2": 10, "vocabulary": 12, "length": 15, "calculated_length": 33.28421251514428, "volume": 53.77443751081735, "difficulty": 1.6666666666666667, "effort": 89.62406251802892, "time": 4.9791145843349405, "bugs": 0.017924812503605784}, "find_nearest_city": {"h1": 1, "h2": 2, "N1": 1, "N2": 2, "vocabulary": 3, "length": 3, "calculated_length": 2.0, "volume": 4.754887502163469, "difficulty": 0.5, "effort": 2.3774437510817346, "time": 0.1320802083934297, "bugs": 0.0015849625007211565}, "generate_tour": {"h1": 2, "h2": 5, "N1": 6, "N2": 8, "vocabulary": 7, "length": 14, "calculated_length": 13.60964047443681, "volume": 39.302968908806456, "difficulty": 1.6, "effort": 62.884750254090335, "time": 3.493597236338352, "bugs": 0.01310098963626882}, "main": {"h1": 0, "h2": 0, "N1": 0, "N2": 0, "vocabulary": 0, "length": 0, "calculated_length": 0, "volume": 0, "difficulty": 0, "effort": 0, "time": 0.0, "bugs": 0.0}}}
|
|
61
59
|
tr = ToolResult(raw=raw_result)
|
|
60
|
+
data = parse_json_dict(raw_result.stdout)
|
|
61
|
+
if data is None:
|
|
62
|
+
tr.metrics = {
|
|
63
|
+
"file_bug_free": 1.0,
|
|
64
|
+
"file_smallness": 1.0,
|
|
65
|
+
"functions_bug_free": 1.0,
|
|
66
|
+
"functions_smallness": 1.0,
|
|
67
|
+
}
|
|
68
|
+
return tr
|
|
62
69
|
MAX_FILE_BUGS = 1
|
|
63
|
-
MAX_FILE_VOLUME =
|
|
70
|
+
MAX_FILE_VOLUME = 3000
|
|
64
71
|
MAX_FUNCTION_BUGS = 0.2
|
|
65
72
|
MAX_FUNCTION_VOLUME = 600
|
|
66
73
|
min_file_nb = 1.0
|
|
67
74
|
min_file_sm = 1.0
|
|
68
75
|
min_function_nb = 1.0
|
|
69
76
|
min_function_sm = 1.0
|
|
70
|
-
data = json.loads(raw_result.stdout)
|
|
71
77
|
for file, values in data.items():
|
|
72
78
|
file_name = file.replace("\\", "/")
|
|
73
79
|
if file_name not in tr.details:
|
|
@@ -104,6 +110,7 @@ class HalsteadParser(AbstractParser):
|
|
|
104
110
|
"smallness": sm,
|
|
105
111
|
"bugs": function_values.get("bugs", 0),
|
|
106
112
|
"volume": function_values.get("volume", 0),
|
|
113
|
+
"difficulty": function_values.get("difficulty", 0),
|
|
107
114
|
}
|
|
108
115
|
tr.metrics = {
|
|
109
116
|
"file_bug_free": min_file_nb,
|
|
@@ -113,7 +120,9 @@ class HalsteadParser(AbstractParser):
|
|
|
113
120
|
}
|
|
114
121
|
return tr
|
|
115
122
|
|
|
116
|
-
def format_llm_message(
|
|
123
|
+
def format_llm_message(
|
|
124
|
+
self, tr: ToolResult, *, context_lines: int = 15, limit: int = 1
|
|
125
|
+
) -> str:
|
|
117
126
|
"""Return the worst Halstead offender as an actionable defect description."""
|
|
118
127
|
if not tr.metrics:
|
|
119
128
|
return "No Halstead details available"
|
|
@@ -127,6 +136,7 @@ class HalsteadParser(AbstractParser):
|
|
|
127
136
|
worst_score = 1.0
|
|
128
137
|
worst_bugs = None
|
|
129
138
|
worst_volume = None
|
|
139
|
+
worst_difficulty = None
|
|
130
140
|
|
|
131
141
|
for file_name, file_data in tr.details.items():
|
|
132
142
|
if is_function_metric:
|
|
@@ -138,6 +148,7 @@ class HalsteadParser(AbstractParser):
|
|
|
138
148
|
worst_function = func_name
|
|
139
149
|
worst_bugs = func_data.get("bugs")
|
|
140
150
|
worst_volume = func_data.get("volume")
|
|
151
|
+
worst_difficulty = func_data.get("difficulty")
|
|
141
152
|
else:
|
|
142
153
|
s = file_data.get("bug_free" if is_bug_metric else "smallness", 1.0)
|
|
143
154
|
if s < worst_score:
|
|
@@ -149,15 +160,32 @@ class HalsteadParser(AbstractParser):
|
|
|
149
160
|
if worst_file is None:
|
|
150
161
|
return f"**{metric_name}** score: {score:.3f}"
|
|
151
162
|
|
|
152
|
-
|
|
163
|
+
path = _relative_path(worst_file)
|
|
164
|
+
location = f"{path} - function `{worst_function}`" if worst_function else path
|
|
153
165
|
if is_bug_metric:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
f"
|
|
157
|
-
|
|
158
|
-
|
|
166
|
+
parts = []
|
|
167
|
+
if worst_bugs is not None:
|
|
168
|
+
parts.append(f"bugs: {worst_bugs:.3f}")
|
|
169
|
+
if worst_volume is not None:
|
|
170
|
+
parts.append(f"volume: {worst_volume:.0f}")
|
|
171
|
+
if worst_difficulty is not None:
|
|
172
|
+
parts.append(f"difficulty: {worst_difficulty:.1f}")
|
|
173
|
+
detail = f" ({', '.join(parts)})" if parts else ""
|
|
174
|
+
if (
|
|
175
|
+
worst_difficulty is not None
|
|
176
|
+
and worst_volume is not None
|
|
177
|
+
and worst_difficulty > worst_volume / 50
|
|
178
|
+
):
|
|
179
|
+
advice = "Simplify branching logic, reduce nesting, or consolidate repeated operator patterns."
|
|
180
|
+
else:
|
|
181
|
+
advice = (
|
|
182
|
+
"Extract helper functions to reduce the function's size and scope."
|
|
183
|
+
)
|
|
184
|
+
return f"{location} has high estimated bug density{detail}\n\n{advice}"
|
|
159
185
|
else:
|
|
160
|
-
detail =
|
|
186
|
+
detail = (
|
|
187
|
+
f" (volume: {worst_volume:.0f})" if worst_volume is not None else ""
|
|
188
|
+
)
|
|
161
189
|
return (
|
|
162
190
|
f"{location} is too large{detail}\n\n"
|
|
163
191
|
f"Split into smaller functions or modules."
|