mirror of
https://github.com/ludo-technologies/pyscn.git
synced 2025-10-06 00:59:45 +03:00
* chore: standardize project name to pyscn throughout codebase - Fix project name inconsistency from pyqol to pyscn in all files - Update HTML report titles and branding to pyscn - Rename config file: pyqol.yaml.example → .pyscn.yaml.example - Update test function names and documentation references - Clean copyright attribution in LICENSE file - Remove temporary files (.DS_Store, coverage files, venv) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: clean up CHANGELOG and remove legacy beta versions - Update CHANGELOG.md to reflect current state with v0.1.0-beta.13 - Remove references to deleted beta versions (beta.1-12, b7) - Add explanation note about removed versions with distribution issues - Update installation instructions to use latest beta version 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: DaisukeYoda <daisukeyoda@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
501 lines
20 KiB
Go
501 lines
20 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/ludo-technologies/pyscn/domain"
|
|
)
|
|
|
|
// AnalyzeFormatter handles formatting of unified analysis reports
|
|
type AnalyzeFormatter struct {
|
|
complexityFormatter *OutputFormatterImpl
|
|
deadCodeFormatter *DeadCodeFormatterImpl
|
|
cloneFormatter *CloneOutputFormatter
|
|
}
|
|
|
|
// NewAnalyzeFormatter creates a new analyze formatter
|
|
func NewAnalyzeFormatter() *AnalyzeFormatter {
|
|
return &AnalyzeFormatter{
|
|
complexityFormatter: NewOutputFormatter(),
|
|
deadCodeFormatter: NewDeadCodeFormatter(),
|
|
cloneFormatter: NewCloneOutputFormatter(),
|
|
}
|
|
}
|
|
|
|
// Write formats and writes the unified analysis response
|
|
func (f *AnalyzeFormatter) Write(response *domain.AnalyzeResponse, format domain.OutputFormat, writer io.Writer) error {
|
|
switch format {
|
|
case domain.OutputFormatText:
|
|
return f.writeText(response, writer)
|
|
case domain.OutputFormatJSON:
|
|
return WriteJSON(writer, response)
|
|
case domain.OutputFormatYAML:
|
|
return WriteYAML(writer, response)
|
|
case domain.OutputFormatCSV:
|
|
return f.writeCSV(response, writer)
|
|
case domain.OutputFormatHTML:
|
|
return f.writeHTML(response, writer)
|
|
default:
|
|
return domain.NewUnsupportedFormatError(string(format))
|
|
}
|
|
}
|
|
|
|
// writeText formats the response as plain text
|
|
func (f *AnalyzeFormatter) writeText(response *domain.AnalyzeResponse, writer io.Writer) error {
|
|
fmt.Fprintf(writer, "pyscn Comprehensive Analysis Report\n")
|
|
fmt.Fprintf(writer, "====================================\n\n")
|
|
fmt.Fprintf(writer, "Generated: %s\n\n", response.GeneratedAt.Format(time.RFC3339))
|
|
|
|
// Summary section
|
|
fmt.Fprintf(writer, "Overall Health Score: %d/100 (Grade: %s)\n",
|
|
response.Summary.HealthScore, response.Summary.Grade)
|
|
fmt.Fprintf(writer, "Analysis Duration: %.2fs\n\n", float64(response.Duration)/1000.0)
|
|
|
|
// File statistics
|
|
fmt.Fprintf(writer, "File Statistics:\n")
|
|
fmt.Fprintf(writer, " Total Files: %d\n", response.Summary.TotalFiles)
|
|
fmt.Fprintf(writer, " Analyzed: %d\n", response.Summary.AnalyzedFiles)
|
|
fmt.Fprintf(writer, " Skipped: %d\n\n", response.Summary.SkippedFiles)
|
|
|
|
// Complexity analysis results
|
|
if response.Complexity != nil && response.Summary.ComplexityEnabled {
|
|
fmt.Fprintf(writer, "Complexity Analysis:\n")
|
|
fmt.Fprintf(writer, "--------------------\n")
|
|
fmt.Fprintf(writer, " Total Functions: %d\n", response.Summary.TotalFunctions)
|
|
fmt.Fprintf(writer, " Average Complexity: %.2f\n", response.Summary.AverageComplexity)
|
|
fmt.Fprintf(writer, " High Complexity Count: %d\n\n", response.Summary.HighComplexityCount)
|
|
}
|
|
|
|
// Dead code analysis results
|
|
if response.DeadCode != nil && response.Summary.DeadCodeEnabled {
|
|
fmt.Fprintf(writer, "Dead Code Detection:\n")
|
|
fmt.Fprintf(writer, "-------------------\n")
|
|
fmt.Fprintf(writer, " Total Issues: %d\n", response.Summary.DeadCodeCount)
|
|
fmt.Fprintf(writer, " Critical Issues: %d\n\n", response.Summary.CriticalDeadCode)
|
|
}
|
|
|
|
// Clone detection results
|
|
if response.Clone != nil && response.Summary.CloneEnabled {
|
|
fmt.Fprintf(writer, "Clone Detection:\n")
|
|
fmt.Fprintf(writer, "---------------\n")
|
|
fmt.Fprintf(writer, " Clone Pairs: %d\n", response.Summary.ClonePairs)
|
|
fmt.Fprintf(writer, " Clone Groups: %d\n", response.Summary.CloneGroups)
|
|
fmt.Fprintf(writer, " Code Duplication: %.2f%%\n\n", response.Summary.CodeDuplication)
|
|
}
|
|
|
|
// Recommendations
|
|
fmt.Fprintf(writer, "Recommendations:\n")
|
|
fmt.Fprintf(writer, "---------------\n")
|
|
if response.Summary.HighComplexityCount > 0 {
|
|
fmt.Fprintf(writer, " • Refactor %d high-complexity functions\n", response.Summary.HighComplexityCount)
|
|
}
|
|
if response.Summary.DeadCodeCount > 0 {
|
|
fmt.Fprintf(writer, " • Remove %d dead code segments\n", response.Summary.DeadCodeCount)
|
|
}
|
|
if response.Summary.CodeDuplication > 10 {
|
|
fmt.Fprintf(writer, " • Reduce code duplication (currently %.1f%%)\n", response.Summary.CodeDuplication)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// writeJSON formats the response as JSON
|
|
// writeCSV formats the response as CSV (summary only)
|
|
func (f *AnalyzeFormatter) writeCSV(response *domain.AnalyzeResponse, writer io.Writer) error {
|
|
// Write header
|
|
fmt.Fprintf(writer, "Metric,Value\n")
|
|
|
|
// Write summary metrics
|
|
fmt.Fprintf(writer, "Health Score,%d\n", response.Summary.HealthScore)
|
|
fmt.Fprintf(writer, "Grade,%s\n", response.Summary.Grade)
|
|
fmt.Fprintf(writer, "Total Files,%d\n", response.Summary.TotalFiles)
|
|
fmt.Fprintf(writer, "Analyzed Files,%d\n", response.Summary.AnalyzedFiles)
|
|
fmt.Fprintf(writer, "Average Complexity,%.2f\n", response.Summary.AverageComplexity)
|
|
fmt.Fprintf(writer, "High Complexity Count,%d\n", response.Summary.HighComplexityCount)
|
|
fmt.Fprintf(writer, "Dead Code Count,%d\n", response.Summary.DeadCodeCount)
|
|
fmt.Fprintf(writer, "Critical Dead Code,%d\n", response.Summary.CriticalDeadCode)
|
|
fmt.Fprintf(writer, "Clone Pairs,%d\n", response.Summary.ClonePairs)
|
|
fmt.Fprintf(writer, "Clone Groups,%d\n", response.Summary.CloneGroups)
|
|
fmt.Fprintf(writer, "Code Duplication,%.2f\n", response.Summary.CodeDuplication)
|
|
fmt.Fprintf(writer, "Total Classes Analyzed,%d\n", response.Summary.CBOClasses)
|
|
fmt.Fprintf(writer, "High Dependency Classes,%d\n", response.Summary.HighCouplingClasses)
|
|
fmt.Fprintf(writer, "Average Dependencies,%.2f\n", response.Summary.AverageCoupling)
|
|
|
|
return nil
|
|
}
|
|
|
|
// writeHTML formats the response as HTML
|
|
func (f *AnalyzeFormatter) writeHTML(response *domain.AnalyzeResponse, writer io.Writer) error {
|
|
funcMap := template.FuncMap{
|
|
"join": func(elems []string, sep string) string {
|
|
return strings.Join(elems, sep)
|
|
},
|
|
}
|
|
tmpl := template.Must(template.New("analyze").Funcs(funcMap).Parse(analyzeHTMLTemplate))
|
|
return tmpl.Execute(writer, response)
|
|
}
|
|
|
|
// HTML template for unified report
|
|
const analyzeHTMLTemplate = `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>pyscn Analysis Report</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
line-height: 1.6;
|
|
color: #333;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
}
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
.header {
|
|
background: white;
|
|
border-radius: 10px;
|
|
padding: 30px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
|
}
|
|
.header h1 {
|
|
color: #667eea;
|
|
margin-bottom: 10px;
|
|
}
|
|
.score-badge {
|
|
display: inline-block;
|
|
padding: 10px 20px;
|
|
border-radius: 50px;
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
margin: 10px 0;
|
|
}
|
|
.grade-a { background: #4caf50; color: white; }
|
|
.grade-b { background: #8bc34a; color: white; }
|
|
.grade-c { background: #ff9800; color: white; }
|
|
.grade-d { background: #ff5722; color: white; }
|
|
.grade-f { background: #f44336; color: white; }
|
|
|
|
.tabs {
|
|
background: white;
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
|
}
|
|
.tab-buttons {
|
|
display: flex;
|
|
background: #f5f5f5;
|
|
}
|
|
.tab-button {
|
|
flex: 1;
|
|
padding: 15px;
|
|
border: none;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
transition: all 0.3s;
|
|
}
|
|
.tab-button.active {
|
|
background: white;
|
|
color: #667eea;
|
|
font-weight: bold;
|
|
}
|
|
.tab-content {
|
|
display: none;
|
|
padding: 30px;
|
|
}
|
|
.tab-content.active {
|
|
display: block;
|
|
}
|
|
|
|
.metric-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
margin: 20px 0;
|
|
}
|
|
.metric-card {
|
|
background: #f8f9fa;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
text-align: center;
|
|
}
|
|
.metric-value {
|
|
font-size: 32px;
|
|
font-weight: bold;
|
|
color: #667eea;
|
|
}
|
|
.metric-label {
|
|
color: #666;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 20px 0;
|
|
}
|
|
.table th, .table td {
|
|
padding: 12px;
|
|
text-align: left;
|
|
border-bottom: 1px solid #ddd;
|
|
}
|
|
.table th {
|
|
background: #f8f9fa;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.risk-low { color: #4caf50; }
|
|
.risk-medium { color: #ff9800; }
|
|
.risk-high { color: #f44336; }
|
|
|
|
.severity-critical { color: #f44336; }
|
|
.severity-warning { color: #ff9800; }
|
|
.severity-info { color: #2196f3; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>pyscn Analysis Report</h1>
|
|
<p>Generated: {{.GeneratedAt.Format "2006-01-02 15:04:05"}}</p>
|
|
<div class="score-badge grade-{{if eq .Summary.Grade "A"}}a{{else if eq .Summary.Grade "B"}}b{{else if eq .Summary.Grade "C"}}c{{else if eq .Summary.Grade "D"}}d{{else}}f{{end}}">
|
|
Health Score: {{.Summary.HealthScore}}/100 (Grade: {{.Summary.Grade}})
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tabs">
|
|
<div class="tab-buttons">
|
|
<button class="tab-button active" onclick="showTab('summary')">Summary</button>
|
|
{{if .Summary.ComplexityEnabled}}
|
|
<button class="tab-button" onclick="showTab('complexity')">Complexity</button>
|
|
{{end}}
|
|
{{if .Summary.DeadCodeEnabled}}
|
|
<button class="tab-button" onclick="showTab('deadcode')">Dead Code</button>
|
|
{{end}}
|
|
{{if .Summary.CloneEnabled}}
|
|
<button class="tab-button" onclick="showTab('clone')">Clone Detection</button>
|
|
{{end}}
|
|
{{if .Summary.CBOEnabled}}
|
|
<button class="tab-button" onclick="showTab('cbo')">Dependency Analysis</button>
|
|
{{end}}
|
|
</div>
|
|
|
|
<div id="summary" class="tab-content active">
|
|
<h2>Analysis Summary</h2>
|
|
<div class="metric-grid">
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{.Summary.TotalFiles}}</div>
|
|
<div class="metric-label">Total Files</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{.Summary.AnalyzedFiles}}</div>
|
|
<div class="metric-label">Analyzed Files</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{printf "%.2f" .Summary.AverageComplexity}}</div>
|
|
<div class="metric-label">Avg Complexity</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{.Summary.DeadCodeCount}}</div>
|
|
<div class="metric-label">Dead Code Issues</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{.Summary.ClonePairs}}</div>
|
|
<div class="metric-label">Clone Pairs</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{printf "%.1f%%" .Summary.CodeDuplication}}</div>
|
|
<div class="metric-label">Code Duplication</div>
|
|
</div>
|
|
{{if .Summary.CBOEnabled}}
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{.Summary.CBOClasses}}</div>
|
|
<div class="metric-label">Total Classes</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{.Summary.HighCouplingClasses}}</div>
|
|
<div class="metric-label">High Dependencies</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{printf "%.2f" .Summary.AverageCoupling}}</div>
|
|
<div class="metric-label">Avg Dependencies</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
|
|
{{if .Summary.ComplexityEnabled}}
|
|
<div id="complexity" class="tab-content">
|
|
<h2>Complexity Analysis</h2>
|
|
{{if .Complexity}}
|
|
<div class="metric-grid">
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{len .Complexity.Functions}}</div>
|
|
<div class="metric-label">Total Functions</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{printf "%.2f" .Complexity.Summary.AverageComplexity}}</div>
|
|
<div class="metric-label">Average</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{.Complexity.Summary.MaxComplexity}}</div>
|
|
<div class="metric-label">Maximum</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Top Complex Functions</h3>
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Function</th>
|
|
<th>File</th>
|
|
<th>Complexity</th>
|
|
<th>Risk</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range $i, $f := .Complexity.Functions}}
|
|
{{if lt $i 10}}
|
|
<tr>
|
|
<td>{{$f.Name}}</td>
|
|
<td>{{$f.FilePath}}</td>
|
|
<td>{{$f.Metrics.Complexity}}</td>
|
|
<td class="risk-{{$f.RiskLevel}}">{{$f.RiskLevel}}</td>
|
|
</tr>
|
|
{{end}}
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .Summary.DeadCodeEnabled}}
|
|
<div id="deadcode" class="tab-content">
|
|
<h2>Dead Code Detection</h2>
|
|
{{if .DeadCode}}
|
|
<div class="metric-grid">
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{.DeadCode.Summary.TotalFindings}}</div>
|
|
<div class="metric-label">Total Issues</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{.DeadCode.Summary.CriticalFindings}}</div>
|
|
<div class="metric-label">Critical</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{.DeadCode.Summary.WarningFindings}}</div>
|
|
<div class="metric-label">Warnings</div>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .Summary.CloneEnabled}}
|
|
<div id="clone" class="tab-content">
|
|
<h2>Clone Detection</h2>
|
|
{{if .Clone}}
|
|
<div class="metric-grid">
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{.Clone.Statistics.TotalClonePairs}}</div>
|
|
<div class="metric-label">Clone Pairs</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{.Clone.Statistics.TotalCloneGroups}}</div>
|
|
<div class="metric-label">Clone Groups</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{printf "%.2f" .Clone.Statistics.AverageSimilarity}}</div>
|
|
<div class="metric-label">Avg Similarity</div>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .Summary.CBOEnabled}}
|
|
<div id="cbo" class="tab-content">
|
|
<h2>Dependency Analysis</h2>
|
|
<p style="margin-bottom: 20px; color: #666;">Class coupling metrics (CBO - Coupling Between Objects)</p>
|
|
{{if .CBO}}
|
|
<div class="metric-grid">
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{.CBO.Summary.TotalClasses}}</div>
|
|
<div class="metric-label">Total Classes</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{.CBO.Summary.HighRiskClasses}}</div>
|
|
<div class="metric-label">High Risk Classes</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{printf "%.2f" .CBO.Summary.AverageCBO}}</div>
|
|
<div class="metric-label">Average Dependencies</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value">{{.CBO.Summary.MaxCBO}}</div>
|
|
<div class="metric-label">Max Dependencies</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Most Dependent Classes</h3>
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Class</th>
|
|
<th>File</th>
|
|
<th>Dependencies</th>
|
|
<th>Risk Level</th>
|
|
<th>Dependent Classes</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range $i, $c := .CBO.Classes}}
|
|
{{if lt $i 10}}
|
|
<tr>
|
|
<td>{{$c.Name}}</td>
|
|
<td>{{$c.FilePath}}</td>
|
|
<td>{{$c.Metrics.CouplingCount}}</td>
|
|
<td class="risk-{{$c.RiskLevel}}">{{$c.RiskLevel}}</td>
|
|
<td>{{join $c.Metrics.DependentClasses ", "}}</td>
|
|
</tr>
|
|
{{end}}
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function showTab(tabName) {
|
|
// Hide all tabs
|
|
const tabs = document.querySelectorAll('.tab-content');
|
|
tabs.forEach(tab => tab.classList.remove('active'));
|
|
|
|
// Remove active from all buttons
|
|
const buttons = document.querySelectorAll('.tab-button');
|
|
buttons.forEach(btn => btn.classList.remove('active'));
|
|
|
|
// Show selected tab
|
|
document.getElementById(tabName).classList.add('active');
|
|
|
|
// Mark button as active
|
|
event.target.classList.add('active');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>`
|