Skip to content

Commit 575bc03

Browse files
authored
Merge pull request #128 from SimplyLiz/feature/v8.1-refactoring-batch2
feat: v8.1 refactoring tools batch 2
2 parents b720cb1 + 73fe602 commit 575bc03

37 files changed

Lines changed: 5806 additions & 65 deletions

CHANGELOG.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,53 @@ The `--include-tests` flag now works end-to-end in `ckb impact diff`:
6161
- Properly sets `IsTest` flag on references based on file path
6262
- Filters test files from changed symbols when `--include-tests=false`
6363

64+
#### Dependency Cycle Detection (`findCycles`)
65+
Detect circular dependencies in module, directory, or file dependency graphs using Tarjan's SCC algorithm:
66+
67+
```bash
68+
# Via MCP
69+
findCycles { "granularity": "directory", "targetPath": "internal/" }
70+
```
71+
72+
- Uses Tarjan's strongly connected components to find real cycles
73+
- Recommends which edge to break (lowest coupling cost)
74+
- Severity classification: size ≥5 = high, ≥3 = medium, 2 = low
75+
- Available in `refactor` preset
76+
77+
#### Move/Relocate Change Type
78+
`prepareChange` and `planRefactor` now support `changeType: "move"` with a `targetPath` parameter:
79+
80+
```bash
81+
prepareChange { "target": "internal/old/handler.go", "changeType": "move", "targetPath": "pkg/handler.go" }
82+
```
83+
84+
- Scans all source files for import path references that need updating
85+
- Detects target directory conflicts (existing files with same name)
86+
- Generates move-specific refactoring steps in `planRefactor`
87+
88+
#### Extract Variable Flow Analysis
89+
`prepareChange` with `changeType: "extract"` now provides tree-sitter-based variable flow analysis when CGO is available:
90+
91+
- Identifies parameters (variables defined outside selection, used inside)
92+
- Identifies return values (variables defined inside, used after selection)
93+
- Classifies local variables (defined and consumed within selection)
94+
- Generates language-appropriate function signatures (Go, Python, JS/TS)
95+
- Graceful degradation: falls back to line-count heuristics without CGO
96+
97+
#### Suggested Refactoring Detection (`suggestRefactorings`)
98+
Proactive detection of refactoring opportunities by combining existing analyzers in parallel:
99+
100+
```bash
101+
suggestRefactorings { "scope": "internal/query", "minSeverity": "medium" }
102+
```
103+
104+
- **Complexity**: High cyclomatic/cognitive functions → `extract_function`, `simplify_function`
105+
- **Coupling**: Highly correlated file pairs → `reduce_coupling`, `split_file`
106+
- **Dead code**: Unused symbols → `remove_dead_code`
107+
- **Test gaps**: High-risk untested code → `add_tests`
108+
- Each suggestion includes severity, effort estimate, and priority score
109+
- Available in `refactor` preset
110+
64111
## [8.0.2] - 2026-01-22
65112
66113
### Added

internal/audit/analyzer.go

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,29 @@ import (
1111
"strings"
1212
"time"
1313

14+
"github.com/SimplyLiz/CodeMCP/internal/complexity"
1415
"github.com/SimplyLiz/CodeMCP/internal/coupling"
1516
)
1617

1718
// Analyzer performs risk analysis on codebases
1819
type Analyzer struct {
19-
repoRoot string
20-
logger *slog.Logger
21-
couplingAnalyzer *coupling.Analyzer
20+
repoRoot string
21+
logger *slog.Logger
22+
couplingAnalyzer *coupling.Analyzer
23+
complexityAnalyzer *complexity.Analyzer
2224
}
2325

2426
// NewAnalyzer creates a new risk analyzer
2527
func NewAnalyzer(repoRoot string, logger *slog.Logger) *Analyzer {
28+
var ca *complexity.Analyzer
29+
if complexity.IsAvailable() {
30+
ca = complexity.NewAnalyzer()
31+
}
2632
return &Analyzer{
27-
repoRoot: repoRoot,
28-
logger: logger,
29-
couplingAnalyzer: coupling.NewAnalyzer(repoRoot, logger),
33+
repoRoot: repoRoot,
34+
logger: logger,
35+
couplingAnalyzer: coupling.NewAnalyzer(repoRoot, logger),
36+
complexityAnalyzer: ca,
3037
}
3138
}
3239

@@ -116,12 +123,12 @@ func (a *Analyzer) analyzeFile(ctx context.Context, repoRoot, file string) (*Ris
116123
factors := make([]RiskFactor, 0, 8)
117124
fullPath := filepath.Join(repoRoot, file)
118125

119-
// 1. Complexity (0-20 contribution)
120-
complexity := a.getComplexity(fullPath)
121-
complexityContrib := min(float64(complexity)/100, 1.0) * 20
126+
// 1. Complexity (0-20 contribution) with per-function breakdown
127+
totalComplexity, functionRisks := a.getComplexityDetailed(ctx, fullPath)
128+
complexityContrib := min(float64(totalComplexity)/100, 1.0) * 20
122129
factors = append(factors, RiskFactor{
123130
Factor: FactorComplexity,
124-
Value: fmt.Sprintf("%d", complexity),
131+
Value: fmt.Sprintf("%d", totalComplexity),
125132
Weight: RiskWeights[FactorComplexity],
126133
Contribution: complexityContrib,
127134
})
@@ -230,11 +237,12 @@ func (a *Analyzer) analyzeFile(ctx context.Context, repoRoot, file string) (*Ris
230237
recommendation := a.generateRecommendation(factors)
231238

232239
return &RiskItem{
233-
File: file,
234-
RiskScore: totalScore,
235-
RiskLevel: GetRiskLevel(totalScore),
236-
Factors: factors,
237-
Recommendation: recommendation,
240+
File: file,
241+
RiskScore: totalScore,
242+
RiskLevel: GetRiskLevel(totalScore),
243+
Factors: factors,
244+
Recommendation: recommendation,
245+
FunctionComplexity: functionRisks,
238246
}, nil
239247
}
240248

@@ -269,18 +277,51 @@ func (a *Analyzer) findSourceFiles(repoRoot string) ([]string, error) {
269277
return files, err
270278
}
271279

272-
// getComplexity estimates complexity based on file size and structure
273-
func (a *Analyzer) getComplexity(filePath string) int {
280+
// getComplexityDetailed returns total complexity and per-function breakdown.
281+
// When the tree-sitter complexity analyzer is available, delegates to it for
282+
// accurate per-function cyclomatic+cognitive scores. Falls back to string-counting heuristic.
283+
func (a *Analyzer) getComplexityDetailed(ctx context.Context, filePath string) (int, []FunctionRisk) {
284+
// Try tree-sitter analyzer first
285+
if a.complexityAnalyzer != nil {
286+
fc, err := a.complexityAnalyzer.AnalyzeFile(ctx, filePath)
287+
if err == nil && fc != nil && fc.Error == "" && len(fc.Functions) > 0 {
288+
// Convert to FunctionRisk and sort by cyclomatic descending
289+
risks := make([]FunctionRisk, 0, len(fc.Functions))
290+
for _, f := range fc.Functions {
291+
risks = append(risks, FunctionRisk{
292+
Name: f.Name,
293+
StartLine: f.StartLine,
294+
EndLine: f.EndLine,
295+
Cyclomatic: f.Cyclomatic,
296+
Cognitive: f.Cognitive,
297+
Lines: f.Lines,
298+
})
299+
}
300+
sort.Slice(risks, func(i, j int) bool {
301+
return risks[i].Cyclomatic > risks[j].Cyclomatic
302+
})
303+
// Cap at top 10 per file
304+
if len(risks) > 10 {
305+
risks = risks[:10]
306+
}
307+
return fc.TotalCyclomatic, risks
308+
}
309+
}
310+
311+
// Fallback: simple heuristic, no per-function breakdown
312+
return a.getComplexityHeuristic(filePath), nil
313+
}
314+
315+
// getComplexityHeuristic estimates complexity based on string counting.
316+
func (a *Analyzer) getComplexityHeuristic(filePath string) int {
274317
content, err := os.ReadFile(filePath)
275318
if err != nil {
276319
return 0
277320
}
278321

279-
// Simple heuristic: count decision points
280322
text := string(content)
281323
complexity := 1 // Base complexity
282324

283-
// Count various complexity indicators
284325
complexity += strings.Count(text, "if ") + strings.Count(text, "if(")
285326
complexity += strings.Count(text, "else ")
286327
complexity += strings.Count(text, "for ") + strings.Count(text, "for(")

internal/audit/audit_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -258,21 +258,21 @@ func main() {
258258
t.Fatal(err)
259259
}
260260

261-
complexity := analyzer.getComplexity(testFile)
261+
complexity := analyzer.getComplexityHeuristic(testFile)
262262
// Should detect: 2 if, 1 for, 1 switch, 2 case, 1 &&
263263
// Base complexity 1 + 2 + 1 + 1 + 2 + 1 = 8
264264
if complexity < 5 {
265-
t.Errorf("getComplexity() = %d, want >= 5", complexity)
265+
t.Errorf("getComplexityHeuristic() = %d, want >= 5", complexity)
266266
}
267267
}
268268

269269
func TestGetComplexityNonexistent(t *testing.T) {
270270
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
271271
analyzer := NewAnalyzer("/tmp", logger)
272272

273-
complexity := analyzer.getComplexity("/nonexistent/file.go")
273+
complexity := analyzer.getComplexityHeuristic("/nonexistent/file.go")
274274
if complexity != 0 {
275-
t.Errorf("getComplexity() for nonexistent file = %d, want 0", complexity)
275+
t.Errorf("getComplexityHeuristic() for nonexistent file = %d, want 0", complexity)
276276
}
277277
}
278278

internal/audit/types.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,25 @@ type RiskAnalysis struct {
1414
QuickWins []QuickWin `json:"quickWins"`
1515
}
1616

17+
// FunctionRisk contains per-function complexity metrics within a risky file.
18+
type FunctionRisk struct {
19+
Name string `json:"name"`
20+
StartLine int `json:"startLine"`
21+
EndLine int `json:"endLine"`
22+
Cyclomatic int `json:"cyclomatic"`
23+
Cognitive int `json:"cognitive"`
24+
Lines int `json:"lines"`
25+
}
26+
1727
// RiskItem represents a single file/module with risk assessment
1828
type RiskItem struct {
19-
File string `json:"file"`
20-
Module string `json:"module,omitempty"`
21-
RiskScore float64 `json:"riskScore"` // 0-100
22-
RiskLevel string `json:"riskLevel"` // "critical" | "high" | "medium" | "low"
23-
Factors []RiskFactor `json:"factors"`
24-
Recommendation string `json:"recommendation,omitempty"`
29+
File string `json:"file"`
30+
Module string `json:"module,omitempty"`
31+
RiskScore float64 `json:"riskScore"` // 0-100
32+
RiskLevel string `json:"riskLevel"` // "critical" | "high" | "medium" | "low"
33+
Factors []RiskFactor `json:"factors"`
34+
Recommendation string `json:"recommendation,omitempty"`
35+
FunctionComplexity []FunctionRisk `json:"functionComplexity,omitempty"`
2536
}
2637

2738
// RiskFactor represents a contributing factor to the risk score

0 commit comments

Comments
 (0)