mirror of
https://github.com/ludo-technologies/pyscn.git
synced 2025-10-06 00:59:45 +03:00
## 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>
224 lines
7.0 KiB
Go
224 lines
7.0 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/ludo-technologies/pyscn/domain"
|
|
)
|
|
|
|
// DeadCodeFormatterImpl implements the DeadCodeFormatter interface
|
|
type DeadCodeFormatterImpl struct{}
|
|
|
|
// NewDeadCodeFormatter creates a new dead code formatter service
|
|
func NewDeadCodeFormatter() *DeadCodeFormatterImpl {
|
|
return &DeadCodeFormatterImpl{}
|
|
}
|
|
|
|
// Format formats the dead code analysis response according to the specified format
|
|
func (f *DeadCodeFormatterImpl) Format(response *domain.DeadCodeResponse, format domain.OutputFormat) (string, error) {
|
|
switch format {
|
|
case domain.OutputFormatText:
|
|
return f.formatText(response)
|
|
case domain.OutputFormatJSON:
|
|
return f.formatJSON(response)
|
|
case domain.OutputFormatYAML:
|
|
return f.formatYAML(response)
|
|
case domain.OutputFormatCSV:
|
|
return f.formatCSV(response)
|
|
case domain.OutputFormatHTML:
|
|
return f.formatHTML(response)
|
|
default:
|
|
return "", domain.NewUnsupportedFormatError(string(format))
|
|
}
|
|
}
|
|
|
|
// Write writes the formatted dead code output to the writer
|
|
func (f *DeadCodeFormatterImpl) Write(response *domain.DeadCodeResponse, format domain.OutputFormat, writer io.Writer) error {
|
|
output, err := f.Format(response, format)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = writer.Write([]byte(output))
|
|
if err != nil {
|
|
return domain.NewOutputError("failed to write output", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// FormatFinding formats a single dead code finding
|
|
func (f *DeadCodeFormatterImpl) FormatFinding(finding domain.DeadCodeFinding, format domain.OutputFormat) (string, error) {
|
|
switch format {
|
|
case domain.OutputFormatText:
|
|
return f.formatFindingTextLegacy(finding), nil
|
|
case domain.OutputFormatJSON:
|
|
return EncodeJSON(finding)
|
|
case domain.OutputFormatYAML:
|
|
return EncodeYAML(finding)
|
|
case domain.OutputFormatHTML:
|
|
// HTML formatting for individual findings is not typically needed
|
|
// Fall back to text format for individual findings
|
|
return f.formatFindingTextLegacy(finding), nil
|
|
default:
|
|
return "", domain.NewUnsupportedFormatError(string(format))
|
|
}
|
|
}
|
|
|
|
// formatText formats the response as human-readable text
|
|
func (f *DeadCodeFormatterImpl) formatText(response *domain.DeadCodeResponse) (string, error) {
|
|
var output strings.Builder
|
|
utils := NewFormatUtils()
|
|
|
|
// Header
|
|
output.WriteString(utils.FormatMainHeader("Dead Code Analysis Report"))
|
|
|
|
// Summary
|
|
stats := map[string]interface{}{
|
|
"Total Files": response.Summary.TotalFiles,
|
|
"Files with Dead Code": response.Summary.FilesWithDeadCode,
|
|
"Total Findings": response.Summary.TotalFindings,
|
|
"Functions Analyzed": response.Summary.TotalFunctions,
|
|
"Functions with Issues": response.Summary.FunctionsWithDeadCode,
|
|
}
|
|
output.WriteString(utils.FormatSummaryStats(stats))
|
|
|
|
// Severity distribution (using standard risk levels)
|
|
output.WriteString(utils.FormatRiskDistribution(
|
|
response.Summary.CriticalFindings, // Map Critical to High
|
|
response.Summary.WarningFindings, // Map Warning to Medium
|
|
response.Summary.InfoFindings)) // Map Info to Low
|
|
|
|
// File Details
|
|
if len(response.Files) > 0 && response.Summary.TotalFindings > 0 {
|
|
output.WriteString(utils.FormatSectionHeader("DETAILED FINDINGS"))
|
|
|
|
for _, file := range response.Files {
|
|
if len(file.Functions) > 0 {
|
|
output.WriteString(utils.FormatLabelWithIndent(0, "File", file.FilePath))
|
|
output.WriteString(strings.Repeat("-", HeaderWidth) + "\n")
|
|
|
|
for _, function := range file.Functions {
|
|
if len(function.Findings) > 0 {
|
|
output.WriteString(utils.FormatLabelWithIndent(SectionPadding, "Function", function.Name))
|
|
for _, finding := range function.Findings {
|
|
output.WriteString(f.formatFindingText(finding, utils) + "\n")
|
|
}
|
|
output.WriteString("\n")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
output.WriteString(utils.FormatSectionSeparator())
|
|
}
|
|
|
|
// Warnings
|
|
if len(response.Warnings) > 0 {
|
|
output.WriteString(utils.FormatWarningsSection(response.Warnings))
|
|
}
|
|
|
|
// Errors
|
|
if len(response.Errors) > 0 {
|
|
output.WriteString(utils.FormatSectionHeader("ERRORS"))
|
|
for _, error := range response.Errors {
|
|
output.WriteString(utils.FormatLabelWithIndent(SectionPadding, "❌", error))
|
|
}
|
|
output.WriteString(utils.FormatSectionSeparator())
|
|
}
|
|
|
|
return output.String(), nil
|
|
}
|
|
|
|
// formatFindingText formats a single finding as text
|
|
func (f *DeadCodeFormatterImpl) formatFindingText(finding domain.DeadCodeFinding, utils *FormatUtils) string {
|
|
// Convert severity to standard risk level
|
|
var standardRisk RiskLevel
|
|
switch finding.Severity {
|
|
case "critical":
|
|
standardRisk = RiskHigh
|
|
case "warning":
|
|
standardRisk = RiskMedium
|
|
case "info":
|
|
standardRisk = RiskLow
|
|
default:
|
|
standardRisk = RiskLow
|
|
}
|
|
|
|
coloredSeverity := utils.FormatRiskWithColor(standardRisk)
|
|
return fmt.Sprintf(" [%s] Line %d-%d: %s (%s)",
|
|
coloredSeverity,
|
|
finding.Location.StartLine,
|
|
finding.Location.EndLine,
|
|
finding.Description,
|
|
finding.Reason)
|
|
}
|
|
|
|
// Keep the old method for backward compatibility with FormatFinding
|
|
func (f *DeadCodeFormatterImpl) formatFindingTextLegacy(finding domain.DeadCodeFinding) string {
|
|
return fmt.Sprintf(" [%s] Line %d-%d: %s (%s)",
|
|
strings.ToUpper(string(finding.Severity)),
|
|
finding.Location.StartLine,
|
|
finding.Location.EndLine,
|
|
finding.Description,
|
|
finding.Reason)
|
|
}
|
|
|
|
// formatJSON formats the response as JSON
|
|
func (f *DeadCodeFormatterImpl) formatJSON(response *domain.DeadCodeResponse) (string, error) {
|
|
return EncodeJSON(response)
|
|
}
|
|
|
|
// formatYAML formats the response as YAML
|
|
func (f *DeadCodeFormatterImpl) formatYAML(response *domain.DeadCodeResponse) (string, error) {
|
|
return EncodeYAML(response)
|
|
}
|
|
|
|
// formatCSV formats the response as CSV
|
|
func (f *DeadCodeFormatterImpl) formatCSV(response *domain.DeadCodeResponse) (string, error) {
|
|
var output strings.Builder
|
|
writer := csv.NewWriter(&output)
|
|
|
|
// Write header
|
|
header := []string{"File", "Function", "Severity", "StartLine", "EndLine", "Reason", "Description"}
|
|
if err := writer.Write(header); err != nil {
|
|
return "", domain.NewOutputError("failed to write CSV header", err)
|
|
}
|
|
|
|
// Write findings
|
|
for _, file := range response.Files {
|
|
for _, function := range file.Functions {
|
|
for _, finding := range function.Findings {
|
|
record := []string{
|
|
finding.Location.FilePath,
|
|
finding.FunctionName,
|
|
string(finding.Severity),
|
|
fmt.Sprintf("%d", finding.Location.StartLine),
|
|
fmt.Sprintf("%d", finding.Location.EndLine),
|
|
finding.Reason,
|
|
finding.Description,
|
|
}
|
|
if err := writer.Write(record); err != nil {
|
|
return "", domain.NewOutputError("failed to write CSV record", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
writer.Flush()
|
|
if err := writer.Error(); err != nil {
|
|
return "", domain.NewOutputError("CSV writer error", err)
|
|
}
|
|
|
|
return output.String(), nil
|
|
}
|
|
|
|
// formatHTML formats the response as Lighthouse-style HTML
|
|
func (f *DeadCodeFormatterImpl) formatHTML(response *domain.DeadCodeResponse) (string, error) {
|
|
htmlFormatter := NewHTMLFormatter()
|
|
projectName := "Python Project" // Default project name, could be configurable
|
|
return htmlFormatter.FormatDeadCodeAsHTML(response, projectName)
|
|
}
|