@@ -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
1819type 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
2527func 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(" )
0 commit comments