diff --git a/cmd/tlin/main.go b/cmd/tlin/main.go index c169b8b..be956d6 100644 --- a/cmd/tlin/main.go +++ b/cmd/tlin/main.go @@ -263,7 +263,7 @@ func printIssues(logger *zap.Logger, issues []tt.Issue) { logger.Error("Error reading source file", zap.String("file", filename), zap.Error(err)) continue } - output := formatter.FormatIssuesWithArrows(fileIssues, sourceCode) + output := formatter.GenetateFormattedIssue(fileIssues, sourceCode) fmt.Println(output) } } diff --git a/formatter/builder.go b/formatter/builder.go new file mode 100644 index 0000000..73d42fa --- /dev/null +++ b/formatter/builder.go @@ -0,0 +1,288 @@ +package formatter + +import ( + "fmt" + "strings" + + "github.com/fatih/color" + "github.com/gnoswap-labs/lint/internal" + tt "github.com/gnoswap-labs/lint/internal/types" +) + +// rule set +const ( + UnnecessaryElse = "unnecessary-else" + UnnecessaryTypeConv = "unnecessary-type-conversion" + SimplifySliceExpr = "simplify-slice-range" + CycloComplexity = "high-cyclomatic-complexity" + EmitFormat = "emit-format" + SliceBound = "slice-bounds-check" +) + +const tabWidth = 8 + +var ( + errorStyle = color.New(color.FgRed, color.Bold) + warningStyle = color.New(color.FgHiYellow, color.Bold) + ruleStyle = color.New(color.FgYellow, color.Bold) + fileStyle = color.New(color.FgCyan, color.Bold) + lineStyle = color.New(color.FgBlue, color.Bold) + messageStyle = color.New(color.FgRed, color.Bold) + suggestionStyle = color.New(color.FgGreen, color.Bold) +) + +// IssueFormatter is the interface that wraps the Format method. +// Implementations of this interface are responsible for formatting specific types of lint issues. +type IssueFormatter interface { + Format(issue tt.Issue, snippet *internal.SourceCode) string +} + +// GenetateFormattedIssue formats a slice of issues into a human-readable string. +// It uses the appropriate formatter for each issue based on its rule. +func GenetateFormattedIssue(issues []tt.Issue, snippet *internal.SourceCode) string { + var builder strings.Builder + for _, issue := range issues { + builder.WriteString(formatIssueHeader(issue)) + formatter := getFormatter(issue.Rule) + builder.WriteString(formatter.Format(issue, snippet)) + } + return builder.String() +} + +// getFormatter is a factory function that returns the appropriate IssueFormatter +// based on the given rule. +// If no specific formatter is found for the given rule, it returns a GeneralIssueFormatter. +func getFormatter(rule string) IssueFormatter { + switch rule { + case UnnecessaryElse: + return &UnnecessaryElseFormatter{} + case SimplifySliceExpr: + return &SimplifySliceExpressionFormatter{} + case UnnecessaryTypeConv: + return &UnnecessaryTypeConversionFormatter{} + case CycloComplexity: + return &CyclomaticComplexityFormatter{} + case EmitFormat: + return &EmitFormatFormatter{} + case SliceBound: + return &SliceBoundsCheckFormatter{} + default: + return &GeneralIssueFormatter{} + } +} + +// formatIssueHeader creates a formatted header string for a given issue. +// The header includes the rule and the filename. (e.g. "error: unused-variable\n --> test.go") +func formatIssueHeader(issue tt.Issue) string { + return errorStyle.Sprint("error: ") + ruleStyle.Sprint(issue.Rule) + "\n" + + lineStyle.Sprint(" --> ") + fileStyle.Sprint(issue.Filename) + "\n" +} + +func buildSuggestion(result *strings.Builder, issue tt.Issue, lineStyle, suggestionStyle *color.Color, startLine int) { + maxLineNumWidth := calculateMaxLineNumWidth(issue.End.Line) + padding := strings.Repeat(" ", maxLineNumWidth) + + result.WriteString(suggestionStyle.Sprintf("Suggestion:\n")) + for i, line := range strings.Split(issue.Suggestion, "\n") { + lineNum := fmt.Sprintf("%d", startLine+i) + + if maxLineNumWidth < len(lineNum) { + maxLineNumWidth = len(lineNum) + } + + result.WriteString(lineStyle.Sprintf("%s%s | ", padding[:maxLineNumWidth-len(lineNum)], lineNum)) + result.WriteString(fmt.Sprintf("%s\n", line)) + } + result.WriteString("\n") +} + +func buildNote(result *strings.Builder, issue tt.Issue, suggestionStyle *color.Color) { + result.WriteString(suggestionStyle.Sprint("Note: ")) + result.WriteString(fmt.Sprintf("%s\n", issue.Note)) + result.WriteString("\n") +} + +/***** Issue Formatter Builder *****/ + +type IssueFormatterBuilder struct { + result strings.Builder + issue tt.Issue + snippet *internal.SourceCode +} + +func NewIssueFormatterBuilder(issue tt.Issue, snippet *internal.SourceCode) *IssueFormatterBuilder { + return &IssueFormatterBuilder{ + issue: issue, + snippet: snippet, + } +} + +func (b *IssueFormatterBuilder) AddHeader() *IssueFormatterBuilder { + // add error type and rule name + b.result.WriteString(errorStyle.Sprint("error: ")) + b.result.WriteString(ruleStyle.Sprintln(b.issue.Rule)) + + // add file name + b.result.WriteString(lineStyle.Sprint(" --> ")) + b.result.WriteString(fileStyle.Sprintln(b.issue.Filename)) + + // add separator + maxLineNumWidth := calculateMaxLineNumWidth(b.issue.End.Line) + padding := strings.Repeat(" ", maxLineNumWidth+1) + b.result.WriteString(lineStyle.Sprintf("%s|\n", padding)) + + return b +} + +func (b *IssueFormatterBuilder) AddCodeSnippet() *IssueFormatterBuilder { + startLine := b.issue.Start.Line + endLine := b.issue.End.Line + maxLineNumWidth := calculateMaxLineNumWidth(endLine) + + // add separator + padding := strings.Repeat(" ", maxLineNumWidth+1) + b.result.WriteString(lineStyle.Sprintf("%s|\n", padding)) + + for i := startLine; i <= endLine; i++ { + // check that the line number does not go out of range of snippet.Lines + if i-1 < 0 || i-1 >= len(b.snippet.Lines) { + continue + } + + line := expandTabs(b.snippet.Lines[i-1]) + lineNum := fmt.Sprintf("%*d", maxLineNumWidth, i) + + b.result.WriteString(lineStyle.Sprintf("%s | ", lineNum)) + b.result.WriteString(line + "\n") + } + + return b +} + +func (b *IssueFormatterBuilder) AddUnderlineAndMessage() *IssueFormatterBuilder { + startLine := b.issue.Start.Line + endLine := b.issue.End.Line + maxLineNumWidth := calculateMaxLineNumWidth(endLine) + padding := strings.Repeat(" ", maxLineNumWidth+1) + + b.result.WriteString(lineStyle.Sprintf("%s| ", padding)) + + if startLine <= 0 || startLine > len(b.snippet.Lines) || endLine <= 0 || endLine > len(b.snippet.Lines) { + b.result.WriteString(messageStyle.Sprintf("Error: Invalid line numbers\n")) + return b + } + + // draw underline from start column to end column + underlineStart := calculateVisualColumn(b.snippet.Lines[startLine-1], b.issue.Start.Column) + underlineEnd := calculateVisualColumn(b.snippet.Lines[endLine-1], b.issue.End.Column) + underlineLength := underlineEnd - underlineStart + 1 + + b.result.WriteString(strings.Repeat(" ", underlineStart)) + b.result.WriteString(messageStyle.Sprintf("%s\n", strings.Repeat("~", underlineLength))) + + b.result.WriteString(lineStyle.Sprintf("%s| ", padding)) + b.result.WriteString(messageStyle.Sprintf("%s\n\n", b.issue.Message)) + + return b +} + +func (b *IssueFormatterBuilder) AddSuggestion() *IssueFormatterBuilder { + if b.issue.Suggestion == "" { + return b + } + + maxLineNumWidth := calculateMaxLineNumWidth(b.issue.End.Line) + padding := strings.Repeat(" ", maxLineNumWidth+1) + + b.result.WriteString(suggestionStyle.Sprint("Suggestion:\n")) + b.result.WriteString(lineStyle.Sprintf("%s|\n", padding)) + + suggestionLines := strings.Split(b.issue.Suggestion, "\n") + for i, line := range suggestionLines { + lineNum := fmt.Sprintf("%*d", maxLineNumWidth, b.issue.Start.Line+i) + b.result.WriteString(lineStyle.Sprintf("%s | %s\n", lineNum, line)) + } + + b.result.WriteString(lineStyle.Sprintf("%s|\n", padding)) + b.result.WriteString("\n") + + return b +} + +func (b *IssueFormatterBuilder) AddNote() *IssueFormatterBuilder { + if b.issue.Note == "" { + return b + } + + b.result.WriteString(suggestionStyle.Sprint("Note: ")) + b.result.WriteString(b.issue.Note) + b.result.WriteString("\n\n") + + return b +} + +type BaseFormatter struct{} + +func (f *BaseFormatter) Format(issue tt.Issue, snippet *internal.SourceCode) string { + builder := NewIssueFormatterBuilder(issue, snippet) + return builder. + AddHeader(). + AddCodeSnippet(). + AddUnderlineAndMessage(). + AddSuggestion(). + AddNote(). + Build() +} + +func (b *IssueFormatterBuilder) Build() string { + return b.result.String() +} + +func calculateMaxLineNumWidth(endLine int) int { + return len(fmt.Sprintf("%d", endLine)) +} + +// expandTabs replaces tab characters('\t') with spaces. +// Assuming a table width of 8. +func expandTabs(line string) string { + var expanded strings.Builder + for i, ch := range line { + if ch == '\t' { + spaceCount := tabWidth - (i % tabWidth) + expanded.WriteString(strings.Repeat(" ", spaceCount)) + } else { + expanded.WriteRune(ch) + } + } + return expanded.String() +} + +// calculateVisualColumn calculates the visual column position +// in a string. taking into account tab characters. +func calculateVisualColumn(line string, column int) int { + if column < 0 { + return 0 + } + visualColumn := 0 + for i, ch := range line { + if i+1 == column { + break + } + if ch == '\t' { + visualColumn += tabWidth - (visualColumn % tabWidth) + } else { + visualColumn++ + } + } + return visualColumn +} + +func calculateMaxLineLength(lines []string, start, end int) int { + maxLen := 0 + for i := start - 1; i < end; i++ { + if len(lines[i]) > maxLen { + maxLen = len(lines[i]) + } + } + return maxLen +} diff --git a/formatter/cyclomatic_complexity.go b/formatter/cyclomatic_complexity.go index 503c5eb..e17a177 100644 --- a/formatter/cyclomatic_complexity.go +++ b/formatter/cyclomatic_complexity.go @@ -11,40 +11,22 @@ import ( type CyclomaticComplexityFormatter struct{} func (f *CyclomaticComplexityFormatter) Format(issue tt.Issue, snippet *internal.SourceCode) string { - var result strings.Builder - - maxLineNumWidth := len(fmt.Sprintf("%d", len(snippet.Lines))) - - // add vertical line - vl := fmt.Sprintf("%s |\n", strings.Repeat(" ", maxLineNumWidth)) - result.WriteString(lineStyle.Sprintf(vl)) - - // function declaration - functionDeclaration := snippet.Lines[issue.Start.Line-1] - result.WriteString(formatLine(issue.Start.Line, maxLineNumWidth, functionDeclaration)) - - // print complexity - complexityInfo := fmt.Sprintf("Cyclomatic Complexity: %s", strings.TrimPrefix(issue.Message, "function ")) - result.WriteString(formatLine(0, maxLineNumWidth, complexityInfo)) - - // print suggestion - result.WriteString("\n") - result.WriteString(suggestionStyle.Sprint("Suggestion: ")) - result.WriteString(issue.Suggestion) - - // print note - result.WriteString("\n") - result.WriteString(suggestionStyle.Sprint("Note: ")) - result.WriteString(issue.Note) + builder := NewIssueFormatterBuilder(issue, snippet) + return builder. + AddCodeSnippet(). + AddComplexityInfo(). + AddSuggestion(). + AddNote(). + Build() +} - result.WriteString("\n") +func (b *IssueFormatterBuilder) AddComplexityInfo() *IssueFormatterBuilder { + maxLineNumWidth := calculateMaxLineNumWidth(b.issue.End.Line) + padding := strings.Repeat(" ", maxLineNumWidth+1) - return result.String() -} + complexityInfo := fmt.Sprintf("Cyclomatic Complexity: %s", strings.TrimPrefix(b.issue.Message, "function ")) + b.result.WriteString(lineStyle.Sprintf("%s| ", padding)) + b.result.WriteString(messageStyle.Sprintf("%s\n\n", complexityInfo)) -func formatLine(lineNum, maxWidth int, content string) string { - if lineNum > 0 { - return lineStyle.Sprintf(fmt.Sprintf("%%%dd | ", maxWidth), lineNum) + content + "\n" - } - return lineStyle.Sprintf(fmt.Sprintf("%%%ds | ", maxWidth), "") + messageStyle.Sprint(content) + "\n" + return b } diff --git a/formatter/doc.go b/formatter/doc.go index f451866..f41c657 100644 --- a/formatter/doc.go +++ b/formatter/doc.go @@ -2,3 +2,5 @@ // in a human-readable format. It includes various formatters for different // types of issues and utility functions for text manipulation. package formatter + +type TemplateFormatter struct{} diff --git a/formatter/fmt.go b/formatter/fmt.go deleted file mode 100644 index 39e563a..0000000 --- a/formatter/fmt.go +++ /dev/null @@ -1,105 +0,0 @@ -package formatter - -import ( - "fmt" - "strings" - - "github.com/fatih/color" - "github.com/gnoswap-labs/lint/internal" - tt "github.com/gnoswap-labs/lint/internal/types" -) - -// rule set -const ( - UnnecessaryElse = "unnecessary-else" - UnnecessaryTypeConv = "unnecessary-type-conversion" - SimplifySliceExpr = "simplify-slice-range" - CycloComplexity = "high-cyclomatic-complexity" - EmitFormat = "emit-format" - SliceBound = "slice-bounds-check" -) - -// IssueFormatter is the interface that wraps the Format method. -// Implementations of this interface are responsible for formatting specific types of lint issues. -type IssueFormatter interface { - Format(issue tt.Issue, snippet *internal.SourceCode) string -} - -// FormatIssuesWithArrows formats a slice of issues into a human-readable string. -// It uses the appropriate formatter for each issue based on its rule. -func FormatIssuesWithArrows(issues []tt.Issue, snippet *internal.SourceCode) string { - var builder strings.Builder - for _, issue := range issues { - builder.WriteString(formatIssueHeader(issue)) - formatter := getFormatter(issue.Rule) - builder.WriteString(formatter.Format(issue, snippet)) - } - return builder.String() -} - -// getFormatter is a factory function that returns the appropriate IssueFormatter -// based on the given rule. -// If no specific formatter is found for the given rule, it returns a GeneralIssueFormatter. -func getFormatter(rule string) IssueFormatter { - switch rule { - case UnnecessaryElse: - return &UnnecessaryElseFormatter{} - case SimplifySliceExpr: - return &SimplifySliceExpressionFormatter{} - case UnnecessaryTypeConv: - return &UnnecessaryTypeConversionFormatter{} - case CycloComplexity: - return &CyclomaticComplexityFormatter{} - case EmitFormat: - return &EmitFormatFormatter{} - case SliceBound: - return &SliceBoundsCheckFormatter{} - default: - return &GeneralIssueFormatter{} - } -} - -// formatIssueHeader creates a formatted header string for a given issue. -// The header includes the rule and the filename. (e.g. "error: unused-variable\n --> test.go") -func formatIssueHeader(issue tt.Issue) string { - return errorStyle.Sprint("error: ") + ruleStyle.Sprint(issue.Rule) + "\n" + - lineStyle.Sprint(" --> ") + fileStyle.Sprint(issue.Filename) + "\n" -} - -func buildSuggestion(result *strings.Builder, issue tt.Issue, lineStyle, suggestionStyle *color.Color, startLine int) { - maxLineNumWidth := calculateMaxLineNumWidth(issue.End.Line) - padding := strings.Repeat(" ", maxLineNumWidth) - - result.WriteString(suggestionStyle.Sprintf("Suggestion:\n")) - for i, line := range strings.Split(issue.Suggestion, "\n") { - lineNum := fmt.Sprintf("%d", startLine+i) - - if maxLineNumWidth < len(lineNum) { - maxLineNumWidth = len(lineNum) - } - - result.WriteString(lineStyle.Sprintf("%s%s | ", padding[:maxLineNumWidth-len(lineNum)], lineNum)) - result.WriteString(fmt.Sprintf("%s\n", line)) - } - result.WriteString("\n") -} - -func buildNote(result *strings.Builder, issue tt.Issue, suggestionStyle *color.Color) { - result.WriteString(suggestionStyle.Sprint("Note: ")) - result.WriteString(fmt.Sprintf("%s\n", issue.Note)) - result.WriteString("\n") -} - -func calculateMaxLineNumWidth(endLine int) int { - return len(fmt.Sprintf("%d", endLine)) -} - -func calculateMaxLineLength(lines []string, start, end int) int { - maxLen := 0 - for i := start - 1; i < end; i++ { - if len(lines[i]) > maxLen { - maxLen = len(lines[i]) - } - } - return maxLen -} diff --git a/formatter/format_emit.go b/formatter/format_emit.go index a005139..1699e65 100644 --- a/formatter/format_emit.go +++ b/formatter/format_emit.go @@ -10,33 +10,35 @@ import ( type EmitFormatFormatter struct{} -func (f *EmitFormatFormatter) Format( - issue tt.Issue, - snippet *internal.SourceCode, -) string { - var result strings.Builder +func (f *EmitFormatFormatter) Format(issue tt.Issue, snippet *internal.SourceCode) string { + builder := NewIssueFormatterBuilder(issue, snippet) + return builder. + // AddHeader(). + AddCodeSnippet(). + AddUnderlineAndMessage(). + AddEmitFormatSuggestion(). + AddNote(). + Build() +} + +func (b *IssueFormatterBuilder) AddEmitFormatSuggestion() *IssueFormatterBuilder { + if b.issue.Suggestion == "" { + return b + } - maxLineNumWidth := calculateMaxLineNumWidth(issue.End.Line) + maxLineNumWidth := calculateMaxLineNumWidth(b.issue.End.Line) padding := strings.Repeat(" ", maxLineNumWidth+1) - result.WriteString(lineStyle.Sprintf("%s|\n", padding)) + b.result.WriteString(suggestionStyle.Sprint("Suggestion:\n")) + b.result.WriteString(lineStyle.Sprintf("%s|\n", padding)) - startLine := issue.Start.Line - endLine := issue.End.Line - for i := startLine; i <= endLine; i++ { - line := expandTabs(snippet.Lines[i-1]) - result.WriteString(lineStyle.Sprintf("%s%d | ", padding[:maxLineNumWidth-len(fmt.Sprintf("%d", i))], i)) - result.WriteString(line + "\n") + suggestionLines := strings.Split(b.issue.Suggestion, "\n") + for i, line := range suggestionLines { + lineNum := fmt.Sprintf("%*d", maxLineNumWidth, b.issue.Start.Line+i) + b.result.WriteString(lineStyle.Sprintf("%s | %s\n", lineNum, line)) } - result.WriteString(lineStyle.Sprintf("%s| ", padding)) - result.WriteString(messageStyle.Sprintf("%s\n", strings.Repeat("~", calculateMaxLineLength(snippet.Lines, startLine, endLine)))) - result.WriteString(lineStyle.Sprintf("%s| ", padding)) - result.WriteString(messageStyle.Sprintf("%s\n\n", issue.Message)) - - buildSuggestion(&result, issue, lineStyle, suggestionStyle, startLine) - - // buildNote(&result, issue, suggestionStyle) + b.result.WriteString(lineStyle.Sprintf("%s|\n", padding)) - return result.String() + return b } diff --git a/formatter/fmt_test.go b/formatter/formatter_test.go similarity index 71% rename from formatter/fmt_test.go rename to formatter/formatter_test.go index 1748a72..d40c447 100644 --- a/formatter/fmt_test.go +++ b/formatter/formatter_test.go @@ -43,17 +43,19 @@ func TestFormatIssuesWithArrows(t *testing.T) { --> test.go | 4 | x := 1 - | ^ x declared but not used + | ~~ + | x declared but not used error: empty-if --> test.go | 5 | if true {} - | ^ empty branch + | ~~~~~~~~~ + | empty branch ` - result := FormatIssuesWithArrows(issues, code) + result := GenetateFormattedIssue(issues, code) assert.Equal(t, expected, result, "Formatted output does not match expected") @@ -73,17 +75,19 @@ error: empty-if --> test.go | 4 | x := 1 - | ^ x declared but not used + | ~~ + | x declared but not used error: empty-if --> test.go | 5 | if true {} - | ^ empty branch + | ~~~~~~~~~ + | empty branch ` - resultWithTabs := FormatIssuesWithArrows(issues, sourceCodeWithTabs) + resultWithTabs := GenetateFormattedIssue(issues, sourceCodeWithTabs) assert.Equal(t, expectedWithTabs, resultWithTabs, "Formatted output with tabs does not match expected") } @@ -133,23 +137,26 @@ func TestFormatIssuesWithArrows_MultipleDigitsLineNumbers(t *testing.T) { --> test.go | 4 | x := 1 // unused variable - | ^ x declared but not used + | ~~ + | x declared but not used error: empty-if --> test.go | 5 | if true {} // empty if statement - | ^ empty branch + | ~~~~~~~~~ + | empty branch error: example --> test.go | 10 | println("end") - | ^ example issue + | ~~~~~~~~ + | example issue ` - result := FormatIssuesWithArrows(issues, code) + result := GenetateFormattedIssue(issues, code) assert.Equal(t, expected, result, "Formatted output with multiple digit line numbers does not match expected") } @@ -192,17 +199,19 @@ func TestFormatIssuesWithArrows_UnnecessaryElse(t *testing.T) { | unnecessary else block Suggestion: + | 4 | if condition { 5 | return true 6 | } 7 | return false - + | Note: Unnecessary 'else' block removed. The code inside the 'else' block has been moved outside, as it will only be executed when the 'if' condition is false. ` - result := FormatIssuesWithArrows(issues, code) + result := GenetateFormattedIssue(issues, code) + t.Logf("result: %s", result) assert.Equal(t, expected, result, "Formatted output does not match expected for unnecessary else") } @@ -233,10 +242,13 @@ func TestUnnecessaryTypeConversionFormatter(t *testing.T) { expected := ` | 5 | result := int(myInt) - | ^ unnecessary type conversion + | ~~~~~~~~~~~ + | unnecessary type conversion Suggestion: + | 5 | Remove the type conversion. Change ` + "`int(myInt)`" + ` to just ` + "`myInt`" + `. + | Note: Unnecessary type conversions can make the code less readable and may slightly impact performance. They are safe to remove when the expression already has the desired type. @@ -246,66 +258,3 @@ Note: Unnecessary type conversions can make the code less readable and may sligh assert.Equal(t, expected, result, "Formatted output should match expected output") } - -func TestEmitFormatFormatter_Format(t *testing.T) { - t.Parallel() - formatter := &EmitFormatFormatter{} - - tests := []struct { - name string - issue tt.Issue - snippet *internal.SourceCode - expected string - }{ - { - name: "Simple Emit format issue", - issue: tt.Issue{ - Rule: "emit-format", - Filename: "test.go", - Start: token.Position{Line: 3, Column: 5}, - End: token.Position{Line: 5, Column: 6}, - Message: "Consider formatting std.Emit call for better readability", - Suggestion: `std.Emit( - "OwnershipChange", - "newOwner", newOwner.String(), - "oldOwner", oldOwner.String(), -)`, - // Note: "Formatting std.Emit calls with multiple key-value pairs improves readability.", - }, - snippet: &internal.SourceCode{ - Lines: []string{ - "package main", - "", - "func main() {", - " std.Emit(\"OwnershipChange\", \"newOwner\", newOwner.String(), \"oldOwner\", oldOwner.String())", - "}", - }, - }, - expected: ` | -3 | func main() { -4 | std.Emit("OwnershipChange", "newOwner", newOwner.String(), "oldOwner", oldOwner.String()) -5 | } - | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - | Consider formatting std.Emit call for better readability - -Suggestion: -3 | std.Emit( -4 | "OwnershipChange", -5 | "newOwner", newOwner.String(), -6 | "oldOwner", oldOwner.String(), -7 | ) - -`, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - // Format does not render lint rule and filename - result := formatter.Format(tt.issue, tt.snippet) - assert.Equal(t, tt.expected, result) - }) - } -} diff --git a/formatter/general.go b/formatter/general.go index 492a947..8d757ad 100644 --- a/formatter/general.go +++ b/formatter/general.go @@ -1,26 +1,10 @@ package formatter import ( - "fmt" - "strings" - - "github.com/fatih/color" "github.com/gnoswap-labs/lint/internal" tt "github.com/gnoswap-labs/lint/internal/types" ) -const tabWidth = 8 - -var ( - errorStyle = color.New(color.FgRed, color.Bold) - warningStyle = color.New(color.FgHiYellow, color.Bold) - ruleStyle = color.New(color.FgYellow, color.Bold) - fileStyle = color.New(color.FgCyan, color.Bold) - lineStyle = color.New(color.FgBlue, color.Bold) - messageStyle = color.New(color.FgRed, color.Bold) - suggestionStyle = color.New(color.FgGreen, color.Bold) -) - // GeneralIssueFormatter is a formatter for general lint issues. type GeneralIssueFormatter struct{} @@ -30,65 +14,10 @@ func (f *GeneralIssueFormatter) Format( issue tt.Issue, snippet *internal.SourceCode, ) string { - var result strings.Builder - - lineIndex := issue.Start.Line - 1 - if lineIndex < 0 { - lineIndex = 0 - } - if lineIndex >= len(snippet.Lines) { - lineIndex = len(snippet.Lines) - 1 - } - - lineNumberStr := fmt.Sprintf("%d", issue.Start.Line) - padding := strings.Repeat(" ", len(lineNumberStr)-1) - result.WriteString(lineStyle.Sprintf(" %s|\n", padding)) - - if len(snippet.Lines) > 0 { - line := expandTabs(snippet.Lines[lineIndex]) - result.WriteString(lineStyle.Sprintf("%d | ", issue.Start.Line)) - result.WriteString(line + "\n") - - visualColumn := calculateVisualColumn(line, issue.Start.Column) - result.WriteString(lineStyle.Sprintf(" %s| ", padding)) - result.WriteString(strings.Repeat(" ", visualColumn)) - result.WriteString(messageStyle.Sprintf("^ %s\n\n", issue.Message)) - } else { - result.WriteString(messageStyle.Sprintf("Unable to display line. File might be empty.\n")) - result.WriteString(messageStyle.Sprintf("Issue: %s\n\n", issue.Message)) - } - - return result.String() -} - -// expandTabs replaces tab characters('\t') with spaces. -// Assuming a table width of 8. -func expandTabs(line string) string { - var expanded strings.Builder - for i, ch := range line { - if ch == '\t' { - spaceCount := tabWidth - (i % tabWidth) - expanded.WriteString(strings.Repeat(" ", spaceCount)) - } else { - expanded.WriteRune(ch) - } - } - return expanded.String() -} - -// calculateVisualColumn calculates the visual column position -// in a string. taking into account tab characters. -func calculateVisualColumn(line string, column int) int { - visualColumn := 0 - for i, ch := range line { - if i+1 == column { - break - } - if ch == '\t' { - visualColumn += tabWidth - (visualColumn % tabWidth) - } else { - visualColumn++ - } - } - return visualColumn + builder := NewIssueFormatterBuilder(issue, snippet) + return builder. + // AddHeader(). + AddCodeSnippet(). + AddUnderlineAndMessage(). + Build() } diff --git a/formatter/simplify_slice_expr.go b/formatter/simplify_slice_expr.go index 509448c..955111a 100644 --- a/formatter/simplify_slice_expr.go +++ b/formatter/simplify_slice_expr.go @@ -1,36 +1,19 @@ package formatter import ( - "fmt" - "strings" - "github.com/gnoswap-labs/lint/internal" tt "github.com/gnoswap-labs/lint/internal/types" ) type SimplifySliceExpressionFormatter struct{} -func (f *SimplifySliceExpressionFormatter) Format( - issue tt.Issue, - snippet *internal.SourceCode, -) string { - var result strings.Builder - - lineNumberStr := fmt.Sprintf("%d", issue.Start.Line) - padding := strings.Repeat(" ", len(lineNumberStr)-1) - result.WriteString(lineStyle.Sprintf(" %s|\n", padding)) - - line := expandTabs(snippet.Lines[issue.Start.Line-1]) - result.WriteString(lineStyle.Sprintf("%d | ", issue.Start.Line)) - result.WriteString(line + "\n") - - visualColumn := calculateVisualColumn(line, issue.Start.Column) - result.WriteString(lineStyle.Sprintf(" %s| ", padding)) - result.WriteString(strings.Repeat(" ", visualColumn)) - result.WriteString(messageStyle.Sprintf("^ %s\n\n", issue.Message)) - - buildSuggestion(&result, issue, lineStyle, suggestionStyle, issue.Start.Line) - buildNote(&result, issue, suggestionStyle) - - return result.String() +func (f *SimplifySliceExpressionFormatter) Format(issue tt.Issue, snippet *internal.SourceCode) string { + builder := NewIssueFormatterBuilder(issue, snippet) + return builder. + // AddHeader(). + AddCodeSnippet(). + AddUnderlineAndMessage(). + AddSuggestion(). + AddNote(). + Build() } diff --git a/formatter/slice_bound.go b/formatter/slice_bound.go index 9996062..11493f7 100644 --- a/formatter/slice_bound.go +++ b/formatter/slice_bound.go @@ -1,9 +1,6 @@ package formatter import ( - "fmt" - "strings" - "github.com/gnoswap-labs/lint/internal" tt "github.com/gnoswap-labs/lint/internal/types" ) @@ -14,30 +11,22 @@ func (f *SliceBoundsCheckFormatter) Format( issue tt.Issue, snippet *internal.SourceCode, ) string { - var result strings.Builder - - maxLineNumWidth := calculateMaxLineNumWidth(issue.End.Line) - padding := strings.Repeat(" ", maxLineNumWidth+1) - - startLine := issue.Start.Line - endLine := issue.End.Line - for i := startLine; i <= endLine; i++ { - line := expandTabs(snippet.Lines[i-1]) - result.WriteString(lineStyle.Sprintf("%s%d | ", padding[:maxLineNumWidth-len(fmt.Sprintf("%d", i))], i)) - result.WriteString(line + "\n") - } - - result.WriteString(lineStyle.Sprintf("%s| ", padding)) - result.WriteString(messageStyle.Sprintf("%s\n", strings.Repeat("~", calculateMaxLineLength(snippet.Lines, startLine, endLine)))) - result.WriteString(lineStyle.Sprintf("%s| ", padding)) - result.WriteString(messageStyle.Sprintf("%s\n\n", issue.Message)) + builder := NewIssueFormatterBuilder(issue, snippet) + return builder. + // AddHeader(). + AddCodeSnippet(). + AddUnderlineAndMessage(). + AddWarning(). + Build() +} - result.WriteString(warningStyle.Sprint("warning: ")) - if issue.Category == "index-access" { - result.WriteString("Index access without bounds checking can lead to runtime panics.\n") - } else if issue.Category == "slice-expression" { - result.WriteString("Slice expressions without proper length checks may cause unexpected behavior.\n\n") +func (b *IssueFormatterBuilder) AddWarning() *IssueFormatterBuilder { + b.result.WriteString(warningStyle.Sprint("warning: ")) + if b.issue.Category == "index-access" { + b.result.WriteString("Index access without bounds checking can lead to runtime panics.\n") + } else if b.issue.Category == "slice-expression" { + b.result.WriteString("Slice expressions without proper length checks may cause unexpected behavior.\n\n") } - return result.String() + return b } diff --git a/formatter/unnecessary_else.go b/formatter/unnecessary_else.go index 5e7c52f..72b4db0 100644 --- a/formatter/unnecessary_else.go +++ b/formatter/unnecessary_else.go @@ -8,64 +8,56 @@ import ( tt "github.com/gnoswap-labs/lint/internal/types" ) -// UnnecessaryElseFormatter is a formatter specifically designed for the "unnecessary-else" rule. type UnnecessaryElseFormatter struct{} -func (f *UnnecessaryElseFormatter) Format( - issue tt.Issue, - snippet *internal.SourceCode, -) string { +func (f *UnnecessaryElseFormatter) Format(issue tt.Issue, snippet *internal.SourceCode) string { var result strings.Builder - ifStartLine, elseEndLine := issue.Start.Line-2, issue.End.Line - code := strings.Join(snippet.Lines, "\n") - problemSnippet := internal.ExtractSnippet(issue, code, ifStartLine-1, elseEndLine-1) - suggestion, err := internal.RemoveUnnecessaryElse(problemSnippet) - if err != nil { - suggestion = problemSnippet - } + // 1. Calculate dimensions + startLine := issue.Start.Line - 2 // Include the 'if' line + endLine := issue.End.Line + maxLineNumWidth := calculateMaxLineNumWidth(endLine) + maxLineLength := calculateMaxLineLength(snippet.Lines, startLine, endLine) - maxLineNumWidth := calculateMaxLineNumWidth(elseEndLine) - padding := strings.Repeat(" ", maxLineNumWidth-1) - result.WriteString(lineStyle.Sprintf(" %s|\n", padding)) + // 2. Write header + padding := strings.Repeat(" ", maxLineNumWidth+1) + result.WriteString(lineStyle.Sprintf("%s|\n", padding)) - maxLen := calculateMaxLineLength(snippet.Lines, ifStartLine, elseEndLine) - for i := ifStartLine; i <= elseEndLine; i++ { + // 3. Write code snippet + for i := startLine; i <= endLine; i++ { line := expandTabs(snippet.Lines[i-1]) - lineNumberStr := fmt.Sprintf("%*d", maxLineNumWidth, i) - result.WriteString(lineStyle.Sprintf("%s | ", lineNumberStr)) - result.WriteString(line + "\n") + lineNum := fmt.Sprintf("%*d", maxLineNumWidth, i) + result.WriteString(lineStyle.Sprintf("%s | %s\n", lineNum, line)) } - result.WriteString(lineStyle.Sprintf(" %s| ", padding)) - result.WriteString(messageStyle.Sprintf("%s\n", strings.Repeat("~", maxLen))) - result.WriteString(lineStyle.Sprintf(" %s| ", padding)) + // 4. Write underline and message + result.WriteString(lineStyle.Sprintf("%s| ", padding)) + result.WriteString(messageStyle.Sprintf("%s\n", strings.Repeat("~", maxLineLength))) + result.WriteString(lineStyle.Sprintf("%s| ", padding)) result.WriteString(messageStyle.Sprintf("%s\n\n", issue.Message)) - result.WriteString(formatSuggestion(issue, suggestion, ifStartLine)) - result.WriteString("\n") - - return result.String() -} - -func formatSuggestion(issue tt.Issue, improvedSnippet string, startLine int) string { - var result strings.Builder - lines := strings.Split(improvedSnippet, "\n") - maxLineNumWidth := calculateMaxLineNumWidth(issue.End.Line) + // 5. Write suggestion + code := strings.Join(snippet.Lines, "\n") + problemSnippet := internal.ExtractSnippet(issue, code, startLine-1, endLine-1) + suggestion, err := internal.RemoveUnnecessaryElse(problemSnippet) + if err != nil { + suggestion = problemSnippet + } result.WriteString(suggestionStyle.Sprint("Suggestion:\n")) - - for i, line := range lines { + result.WriteString(lineStyle.Sprintf("%s|\n", padding)) + suggestionLines := strings.Split(suggestion, "\n") + for i, line := range suggestionLines { lineNum := fmt.Sprintf("%*d", maxLineNumWidth, startLine+i) - result.WriteString(lineStyle.Sprintf("%s | ", lineNum)) - result.WriteString(fmt.Sprintln(line)) + result.WriteString(lineStyle.Sprintf("%s | %s\n", lineNum, line)) } - - // Add a note explaining the improvement + result.WriteString(lineStyle.Sprintf("%s|", padding)) result.WriteString("\n") + + // 6. Write note result.WriteString(suggestionStyle.Sprint("Note: ")) result.WriteString("Unnecessary 'else' block removed.\n") - result.WriteString("The code inside the 'else' block has been moved outside, as it will only be executed when the 'if' condition is false.\n") + result.WriteString("The code inside the 'else' block has been moved outside, as it will only be executed when the 'if' condition is false.\n\n") return result.String() } diff --git a/formatter/unnecessary_type_conv.go b/formatter/unnecessary_type_conv.go index 5234134..89073f7 100644 --- a/formatter/unnecessary_type_conv.go +++ b/formatter/unnecessary_type_conv.go @@ -1,36 +1,19 @@ package formatter import ( - "fmt" - "strings" - "github.com/gnoswap-labs/lint/internal" tt "github.com/gnoswap-labs/lint/internal/types" ) type UnnecessaryTypeConversionFormatter struct{} -func (f *UnnecessaryTypeConversionFormatter) Format( - issue tt.Issue, - snippet *internal.SourceCode, -) string { - var result strings.Builder - - lineNumberStr := fmt.Sprintf("%d", issue.Start.Line) - padding := strings.Repeat(" ", len(lineNumberStr)-1) - result.WriteString(lineStyle.Sprintf(" %s|\n", padding)) - - line := expandTabs(snippet.Lines[issue.Start.Line-1]) - result.WriteString(lineStyle.Sprintf("%d | ", issue.Start.Line)) - result.WriteString(line + "\n") - - visualColumn := calculateVisualColumn(line, issue.Start.Column) - result.WriteString(lineStyle.Sprintf(" %s| ", padding)) - result.WriteString(strings.Repeat(" ", visualColumn)) - result.WriteString(messageStyle.Sprintf("^ %s\n\n", issue.Message)) - - buildSuggestion(&result, issue, lineStyle, suggestionStyle, issue.Start.Line) - buildNote(&result, issue, suggestionStyle) - - return result.String() +func (f *UnnecessaryTypeConversionFormatter) Format(issue tt.Issue, snippet *internal.SourceCode) string { + builder := NewIssueFormatterBuilder(issue, snippet) + return builder. + // AddHeader(). + AddCodeSnippet(). + AddUnderlineAndMessage(). + AddSuggestion(). + AddNote(). + Build() } diff --git a/internal/engine.go b/internal/engine.go index 919b05e..a6a7123 100644 --- a/internal/engine.go +++ b/internal/engine.go @@ -38,7 +38,7 @@ func (e *Engine) registerDefaultRules() { &SimplifySliceExprRule{}, &UnnecessaryConversionRule{}, &LoopAllocationRule{}, - &SliceBoundCheckRule{}, + // &SliceBoundCheckRule{}, &EmitFormatRule{}, &DetectCycleRule{}, &GnoSpecificRule{}, diff --git a/internal/lints/lint_test.go b/internal/lints/lint_test.go index 6a28585..22d09d9 100644 --- a/internal/lints/lint_test.go +++ b/internal/lints/lint_test.go @@ -91,6 +91,10 @@ func example2() int { issues, err := DetectUnnecessaryElse(tmpfile, node, fset) require.NoError(t, err) + for i, issue := range issues { + t.Logf("Suggestion %d: %v", i, issue.Suggestion) + } + assert.Equal(t, tt.expected, len(issues), "Number of detected unnecessary else statements doesn't match expected") if len(issues) > 0 { diff --git a/internal/lints/slice_bound.go b/internal/lints/slice_bound.go index c5d0799..b301c48 100644 --- a/internal/lints/slice_bound.go +++ b/internal/lints/slice_bound.go @@ -8,6 +8,7 @@ import ( tt "github.com/gnoswap-labs/lint/internal/types" ) +// TODO: Make more precisely. func DetectSliceBoundCheck(filename string, node *ast.File, fset *token.FileSet) ([]tt.Issue, error) { var issues []tt.Issue ast.Inspect(node, func(n ast.Node) bool {