Skip to content

Commit 59a8264

Browse files
refactoring
1 parent cba18cc commit 59a8264

6 files changed

Lines changed: 455 additions & 6 deletions

File tree

Makefile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,15 @@ git-clean: ## Ensure git working tree is clean
140140
@git diff --cached --quiet || (echo "Index has staged but uncommitted changes. Commit before publishing."; exit 1)
141141

142142
check-bumpver: ## Ensure bumpver is installed
143-
@$(PYTHON) -c "import bumpver" >/dev/null 2>&1 || (echo "Missing bumpver. Install dev deps: pip install -e '.[dev]'"; exit 1)
143+
@$(PYTHON) -c "import bumpver" >/dev/null 2>&1 || ( \
144+
echo "Missing bumpver. Installing bumpver..."; \
145+
$(PYTHON) -m pip install "bumpver>=2023.1129" >/dev/null; \
146+
$(PYTHON) -c "import bumpver" >/dev/null 2>&1 || ( \
147+
echo "bumpver still missing. Installing project dev dependencies..."; \
148+
$(PIP) install -e \".[dev]\"; \
149+
$(PYTHON) -c "import bumpver" >/dev/null 2>&1 || (echo "Failed to install bumpver."; exit 1); \
150+
); \
151+
)
144152

145153

146154
bump-patch: check-bumpver ## Bump patch version (updates pyproject.toml and code2logic/__init__.py)

code2logic/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,14 @@
156156
MarkdownSchema,
157157
JSONSchema,
158158
)
159+
from .quality import (
160+
QualityAnalyzer,
161+
QualityReport,
162+
QualityIssue,
163+
analyze_quality,
164+
get_quality_summary,
165+
)
166+
from .similarity import get_refactoring_suggestions
159167
from .chunked_reproduction import (
160168
ChunkedReproducer,
161169
ChunkedResult,

code2logic/generators.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -774,12 +774,17 @@ def _build_method_row(self, path: str, class_name: str, f: FunctionInfo,
774774

775775
def _function_to_dict(self, f: FunctionInfo, detail: str) -> dict:
776776
"""Convert function to dict for nested output."""
777+
# Clean function name (remove any newlines or special chars)
778+
name = f.name.replace('\n', '').strip() if f.name else ''
779+
777780
data = {
778-
'name': f.name,
781+
'name': name,
779782
'signature': self._build_signature(f),
780783
}
781784
if detail in ('standard', 'full'):
782-
data['intent'] = f.intent
785+
# Clean intent - remove newlines and limit length
786+
intent = f.intent.replace('\n', ' ').strip()[:100] if f.intent else ''
787+
data['intent'] = intent
783788
if detail == 'full':
784789
data['lines'] = f.lines
785790
data['is_async'] = f.is_async
@@ -791,9 +796,17 @@ def _method_to_dict(self, f: FunctionInfo, detail: str) -> dict:
791796

792797
def _build_signature(self, f: FunctionInfo) -> str:
793798
"""Build compact signature string."""
794-
params = ','.join(f.params[:4])
795-
if len(f.params) > 4:
796-
params += f'...+{len(f.params)-4}'
799+
# Clean params - remove newlines and extra spaces
800+
clean_params = []
801+
for p in f.params[:6]:
802+
p_clean = p.replace('\n', ' ').replace(' ', ' ').strip()
803+
if p_clean:
804+
clean_params.append(p_clean)
805+
806+
params = ','.join(clean_params)
807+
if len(f.params) > 6:
808+
params += f'...+{len(f.params)-6}'
809+
797810
ret = f"->{f.return_type}" if f.return_type else ""
798811
return f"({params}){ret}"
799812

code2logic/quality.py

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
"""
2+
Code quality analysis module.
3+
4+
Detects quality issues and provides refactoring recommendations.
5+
"""
6+
7+
from dataclasses import dataclass, field
8+
from typing import List, Dict, Any
9+
from .models import ModuleInfo, ProjectInfo
10+
11+
12+
@dataclass
13+
class QualityIssue:
14+
"""Represents a code quality issue."""
15+
type: str
16+
severity: str # 'high', 'medium', 'low'
17+
file: str
18+
name: str
19+
value: int
20+
threshold: int
21+
recommendation: str
22+
23+
24+
@dataclass
25+
class QualityReport:
26+
"""Complete quality analysis report."""
27+
issues: List[QualityIssue] = field(default_factory=list)
28+
metrics: Dict[str, Any] = field(default_factory=dict)
29+
score: float = 100.0
30+
31+
def to_dict(self) -> Dict[str, Any]:
32+
"""Convert to dictionary."""
33+
return {
34+
'score': self.score,
35+
'issues_count': len(self.issues),
36+
'issues': [
37+
{
38+
'type': i.type,
39+
'severity': i.severity,
40+
'file': i.file,
41+
'name': i.name,
42+
'value': i.value,
43+
'threshold': i.threshold,
44+
'recommendation': i.recommendation,
45+
}
46+
for i in self.issues
47+
],
48+
'metrics': self.metrics,
49+
}
50+
51+
52+
class QualityAnalyzer:
53+
"""
54+
Analyzes code quality and generates recommendations.
55+
56+
Thresholds:
57+
- file_lines: Max lines per file (default 500)
58+
- function_lines: Max lines per function (default 50)
59+
- class_methods: Max methods per class (default 20)
60+
- function_params: Max parameters (default 7)
61+
- cyclomatic_complexity: Max complexity (default 10)
62+
"""
63+
64+
DEFAULT_THRESHOLDS = {
65+
'file_lines': 500,
66+
'function_lines': 50,
67+
'class_methods': 20,
68+
'function_params': 7,
69+
}
70+
71+
def __init__(self, thresholds: Dict[str, int] = None):
72+
"""Initialize with custom thresholds."""
73+
self.thresholds = {**self.DEFAULT_THRESHOLDS}
74+
if thresholds:
75+
self.thresholds.update(thresholds)
76+
77+
def analyze(self, project: ProjectInfo) -> QualityReport:
78+
"""
79+
Analyze project quality.
80+
81+
Args:
82+
project: ProjectInfo to analyze
83+
84+
Returns:
85+
QualityReport with issues and recommendations
86+
"""
87+
report = QualityReport()
88+
report.metrics = {
89+
'total_files': project.total_files,
90+
'total_lines': project.total_lines,
91+
'total_classes': sum(len(m.classes) for m in project.modules),
92+
'total_functions': sum(len(m.functions) for m in project.modules),
93+
}
94+
95+
for module in project.modules:
96+
self._analyze_module(module, report)
97+
98+
# Calculate score (deduct points for issues)
99+
deductions = {
100+
'high': 10,
101+
'medium': 5,
102+
'low': 2,
103+
}
104+
for issue in report.issues:
105+
report.score -= deductions.get(issue.severity, 0)
106+
report.score = max(0, report.score)
107+
108+
return report
109+
110+
def analyze_modules(self, modules: List[ModuleInfo]) -> QualityReport:
111+
"""Analyze a list of modules."""
112+
report = QualityReport()
113+
report.metrics = {
114+
'total_files': len(modules),
115+
'total_lines': sum(m.lines_total for m in modules),
116+
'total_classes': sum(len(m.classes) for m in modules),
117+
'total_functions': sum(len(m.functions) for m in modules),
118+
}
119+
120+
for module in modules:
121+
self._analyze_module(module, report)
122+
123+
# Calculate score
124+
deductions = {'high': 10, 'medium': 5, 'low': 2}
125+
for issue in report.issues:
126+
report.score -= deductions.get(issue.severity, 0)
127+
report.score = max(0, report.score)
128+
129+
return report
130+
131+
def _analyze_module(self, module: ModuleInfo, report: QualityReport):
132+
"""Analyze a single module."""
133+
# Check file length
134+
if module.lines_total > self.thresholds['file_lines']:
135+
severity = 'high' if module.lines_total > self.thresholds['file_lines'] * 2 else 'medium'
136+
report.issues.append(QualityIssue(
137+
type='long_file',
138+
severity=severity,
139+
file=module.path,
140+
name=module.path,
141+
value=module.lines_total,
142+
threshold=self.thresholds['file_lines'],
143+
recommendation=self._get_file_recommendation(module),
144+
))
145+
146+
# Check functions
147+
for func in module.functions:
148+
self._check_function(func, module.path, report)
149+
150+
# Check classes
151+
for cls in module.classes:
152+
self._check_class(cls, module.path, report)
153+
154+
def _check_function(self, func, file_path: str, report: QualityReport):
155+
"""Check function quality."""
156+
# Long function
157+
if func.lines > self.thresholds['function_lines']:
158+
severity = 'high' if func.lines > self.thresholds['function_lines'] * 2 else 'medium'
159+
report.issues.append(QualityIssue(
160+
type='long_function',
161+
severity=severity,
162+
file=file_path,
163+
name=func.name,
164+
value=func.lines,
165+
threshold=self.thresholds['function_lines'],
166+
recommendation=f"Split '{func.name}' into smaller functions. Consider extracting logical blocks into separate helpers.",
167+
))
168+
169+
# Too many parameters
170+
if len(func.params) > self.thresholds['function_params']:
171+
report.issues.append(QualityIssue(
172+
type='many_parameters',
173+
severity='medium',
174+
file=file_path,
175+
name=func.name,
176+
value=len(func.params),
177+
threshold=self.thresholds['function_params'],
178+
recommendation=f"Reduce parameters in '{func.name}'. Consider using a config object or dataclass.",
179+
))
180+
181+
def _check_class(self, cls, file_path: str, report: QualityReport):
182+
"""Check class quality."""
183+
# Too many methods
184+
if len(cls.methods) > self.thresholds['class_methods']:
185+
report.issues.append(QualityIssue(
186+
type='large_class',
187+
severity='medium',
188+
file=file_path,
189+
name=cls.name,
190+
value=len(cls.methods),
191+
threshold=self.thresholds['class_methods'],
192+
recommendation=f"Class '{cls.name}' has too many methods. Consider splitting into smaller classes or using composition.",
193+
))
194+
195+
# Check each method
196+
for method in cls.methods:
197+
self._check_function(method, file_path, report)
198+
199+
def _get_file_recommendation(self, module: ModuleInfo) -> str:
200+
"""Generate recommendation for long file."""
201+
class_count = len(module.classes)
202+
func_count = len(module.functions)
203+
204+
if class_count > 3:
205+
return f"File has {class_count} classes. Split into separate modules, one class per file."
206+
elif func_count > 10:
207+
return f"File has {func_count} functions. Group related functions into separate modules."
208+
else:
209+
return "Consider breaking this file into smaller, focused modules."
210+
211+
212+
def analyze_quality(project: ProjectInfo, thresholds: Dict[str, int] = None) -> QualityReport:
213+
"""
214+
Convenience function to analyze project quality.
215+
216+
Args:
217+
project: ProjectInfo to analyze
218+
thresholds: Optional custom thresholds
219+
220+
Returns:
221+
QualityReport
222+
"""
223+
analyzer = QualityAnalyzer(thresholds)
224+
return analyzer.analyze(project)
225+
226+
227+
def get_quality_summary(report: QualityReport) -> str:
228+
"""
229+
Generate human-readable quality summary.
230+
231+
Args:
232+
report: QualityReport from analysis
233+
234+
Returns:
235+
Formatted summary string
236+
"""
237+
lines = [
238+
f"Quality Score: {report.score:.1f}/100",
239+
f"Issues Found: {len(report.issues)}",
240+
"",
241+
]
242+
243+
if report.issues:
244+
lines.append("Issues by Severity:")
245+
high = sum(1 for i in report.issues if i.severity == 'high')
246+
medium = sum(1 for i in report.issues if i.severity == 'medium')
247+
low = sum(1 for i in report.issues if i.severity == 'low')
248+
249+
if high:
250+
lines.append(f" 🔴 High: {high}")
251+
if medium:
252+
lines.append(f" 🟡 Medium: {medium}")
253+
if low:
254+
lines.append(f" 🟢 Low: {low}")
255+
256+
lines.append("")
257+
lines.append("Top Issues:")
258+
for issue in report.issues[:5]:
259+
lines.append(f" - [{issue.severity.upper()}] {issue.type}: {issue.name}")
260+
lines.append(f" {issue.recommendation}")
261+
else:
262+
lines.append("✅ No quality issues detected!")
263+
264+
return "\n".join(lines)

0 commit comments

Comments
 (0)