Files
pyscn/domain/analyze.go
DaisukeYoda b951910740 feat: improve HTML reports and health score calculation
## HTML Report Quality Improvements
- Fix dead code table displaying actual data instead of empty rows
- Correct file paths showing real paths instead of "unknown"
- Fix clone type display showing "Type-1", "Type-2" etc. instead of blank cells
- Improve template loop logic for proper item limiting

## Report Format Standardization
- Create shared FormatUtils for consistent formatting across all analysis types
- Standardize header widths, label alignment, and section structures
- Unify color schemes and risk level representations
- Add comprehensive summary statistics and warning sections

## Health Score Algorithm Enhancement
- Add project size normalization using logarithmic scaling for large projects
- Implement penalty caps: max 25 points per category (Complexity, Dead Code, Clones, CBO)
- Set minimum score threshold of 10 points to avoid complete failure
- Adjust grade thresholds: A(85+), B(70+), C(55+), D(40+), F(<40)
- Fix scoring issue where large projects always got 0/100 (Grade: F)

## Test Coverage Expansion
- Add comprehensive dead code test cases in testdata/python/
- Create simple, edge cases, and complex dead code pattern examples
- Improve test coverage for various unreachable code scenarios

## Results
- Large projects: 0/100 (F) → 50/100 (D)
- Small projects: appropriate scores (60-70 range)
- HTML reports now show detailed, accurate information
- Consistent professional formatting across all analysis types

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-10 20:10:37 +09:00

156 lines
4.9 KiB
Go

package domain
import (
"math"
"time"
)
// AnalyzeResponse represents the combined results of all analyses
type AnalyzeResponse struct {
// Analysis results
Complexity *ComplexityResponse `json:"complexity,omitempty" yaml:"complexity,omitempty"`
DeadCode *DeadCodeResponse `json:"dead_code,omitempty" yaml:"dead_code,omitempty"`
Clone *CloneResponse `json:"clone,omitempty" yaml:"clone,omitempty"`
CBO *CBOResponse `json:"cbo,omitempty" yaml:"cbo,omitempty"`
// Overall summary
Summary AnalyzeSummary `json:"summary" yaml:"summary"`
// Metadata
GeneratedAt time.Time `json:"generated_at" yaml:"generated_at"`
Duration int64 `json:"duration_ms" yaml:"duration_ms"`
Version string `json:"version" yaml:"version"`
}
// AnalyzeSummary provides an overall summary of all analyses
type AnalyzeSummary struct {
// File statistics
TotalFiles int `json:"total_files" yaml:"total_files"`
AnalyzedFiles int `json:"analyzed_files" yaml:"analyzed_files"`
SkippedFiles int `json:"skipped_files" yaml:"skipped_files"`
// Analysis status
ComplexityEnabled bool `json:"complexity_enabled" yaml:"complexity_enabled"`
DeadCodeEnabled bool `json:"dead_code_enabled" yaml:"dead_code_enabled"`
CloneEnabled bool `json:"clone_enabled" yaml:"clone_enabled"`
CBOEnabled bool `json:"cbo_enabled" yaml:"cbo_enabled"`
// Key metrics
TotalFunctions int `json:"total_functions" yaml:"total_functions"`
AverageComplexity float64 `json:"average_complexity" yaml:"average_complexity"`
HighComplexityCount int `json:"high_complexity_count" yaml:"high_complexity_count"`
DeadCodeCount int `json:"dead_code_count" yaml:"dead_code_count"`
CriticalDeadCode int `json:"critical_dead_code" yaml:"critical_dead_code"`
ClonePairs int `json:"clone_pairs" yaml:"clone_pairs"`
CloneGroups int `json:"clone_groups" yaml:"clone_groups"`
CodeDuplication float64 `json:"code_duplication_percentage" yaml:"code_duplication_percentage"`
CBOClasses int `json:"cbo_classes" yaml:"cbo_classes"`
HighCouplingClasses int `json:"high_coupling_classes" yaml:"high_coupling_classes"`
AverageCoupling float64 `json:"average_coupling" yaml:"average_coupling"`
// Overall health score (0-100)
HealthScore int `json:"health_score" yaml:"health_score"`
Grade string `json:"grade" yaml:"grade"` // A, B, C, D, F
}
// CalculateHealthScore calculates an overall health score based on analysis results
func (s *AnalyzeSummary) CalculateHealthScore() {
score := 100
// Calculate normalization factor for large projects
normalizationFactor := 1.0
if s.TotalFiles > 10 {
// Use logarithmic scaling to reduce penalty impact for large projects
normalizationFactor = math.Log10(float64(s.TotalFiles)) / 2.0
}
// Complexity penalty (max 25 points)
complexityPenalty := 0
if s.AverageComplexity > 20 {
complexityPenalty = 25
} else if s.AverageComplexity > 10 {
complexityPenalty = 15
} else if s.AverageComplexity > 5 {
complexityPenalty = 8
}
score -= complexityPenalty
// Dead code penalty (max 25 points, normalized for project size)
deadCodePenalty := 0
if s.DeadCodeCount > 0 {
rawPenalty := float64(s.DeadCodeCount) / normalizationFactor
deadCodePenalty = int(math.Min(25, rawPenalty))
}
// Additional penalty for critical dead code (max 15 points, normalized)
criticalPenalty := 0
if s.CriticalDeadCode > 0 {
rawCriticalPenalty := float64(s.CriticalDeadCode*3) / normalizationFactor
criticalPenalty = int(math.Min(15, rawCriticalPenalty))
}
totalDeadCodePenalty := deadCodePenalty + criticalPenalty
if totalDeadCodePenalty > 25 {
totalDeadCodePenalty = 25
}
score -= totalDeadCodePenalty
// Clone penalty (max 25 points, based on percentage)
clonePenalty := 0
if s.CodeDuplication > 40 {
clonePenalty = 25
} else if s.CodeDuplication > 25 {
clonePenalty = 15
} else if s.CodeDuplication > 10 {
clonePenalty = 8
}
score -= clonePenalty
// CBO penalty (max 25 points)
cboPenalty := 0
if s.CBOClasses > 0 {
couplingRatio := float64(s.HighCouplingClasses) / float64(s.CBOClasses)
if couplingRatio > 0.5 {
cboPenalty = 20
} else if couplingRatio > 0.3 {
cboPenalty = 12
} else if couplingRatio > 0.1 {
cboPenalty = 6
}
}
score -= cboPenalty
// Set minimum score to 10 (never completely fail)
if score < 10 {
score = 10
}
s.HealthScore = score
// Assign grade based on score (adjusted thresholds)
switch {
case score >= 85:
s.Grade = "A"
case score >= 70:
s.Grade = "B"
case score >= 55:
s.Grade = "C"
case score >= 40:
s.Grade = "D"
default:
s.Grade = "F"
}
}
// IsHealthy returns true if the codebase is considered healthy
func (s *AnalyzeSummary) IsHealthy() bool {
return s.HealthScore >= 70
}
// HasIssues returns true if any issues were found
func (s *AnalyzeSummary) HasIssues() bool {
return s.HighComplexityCount > 0 || s.DeadCodeCount > 0 || s.ClonePairs > 0 || s.HighCouplingClasses > 0
}