Skip to content

Commit

Permalink
formatter factory
Browse files Browse the repository at this point in the history
  • Loading branch information
notJoon committed Jul 18, 2024
1 parent f0ef3c1 commit f4f6747
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 225 deletions.
3 changes: 2 additions & 1 deletion cmd/tlin/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"path/filepath"
"sort"

"github.com/gnoswap-labs/lint/formatter"
lint "github.com/gnoswap-labs/lint/internal"
)

Expand Down Expand Up @@ -86,7 +87,7 @@ func main() {
fmt.Printf("error reading source file %s: %v\n", filename, err)
continue
}
output := lint.FormatIssuesWithArrows(issues, sourceCode)
output := formatter.FormatIssuesWithArrows(issues, sourceCode)
fmt.Println(output)
}

Expand Down
4 changes: 4 additions & 0 deletions formatter/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Package formatter provides functionality for formatting lint issues
// in a human-readable format. It includes various formatters for different
// types of issues and utility functions for text manipulation.
package formatter
49 changes: 49 additions & 0 deletions formatter/fmt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package formatter

import (
"strings"

"github.com/gnoswap-labs/lint/internal"
)

// rule set
const (
UnnecessaryElse = "unnecessary-else"
)

// 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 internal.Issue, snippet *internal.SourceCode) 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{}
default:
return &GeneralIssueFormatter{}
}
}

// 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 []internal.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()
}

// 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 internal.Issue) string {
return errorStyle.Sprint("error: ") + ruleStyle.Sprint(issue.Rule) + "\n" +
lineStyle.Sprint(" --> ") + fileStyle.Sprint(issue.Filename) + "\n"
}
113 changes: 104 additions & 9 deletions internal/print_test.go → formatter/fmt_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package internal
package formatter

import (
"go/token"
"os"
"path/filepath"
"strings"
"testing"

"github.com/gnoswap-labs/lint/internal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestFormatIssuesWithArrows(t *testing.T) {
sourceCode := &SourceCode{
sourceCode := &internal.SourceCode{
Lines: []string{
"package main",
"",
Expand All @@ -19,7 +24,7 @@ func TestFormatIssuesWithArrows(t *testing.T) {
},
}

issues := []Issue{
issues := []internal.Issue{
{
Rule: "unused-variable",
Filename: "test.go",
Expand Down Expand Up @@ -55,7 +60,7 @@ error: empty-if
assert.Equal(t, expected, result, "Formatted output does not match expected")

// Test with tab characters
sourceCodeWithTabs := &SourceCode{
sourceCodeWithTabs := &internal.SourceCode{
Lines: []string{
"package main",
"",
Expand Down Expand Up @@ -86,7 +91,7 @@ error: empty-if
}

func TestFormatIssuesWithArrows_MultipleDigitsLineNumbers(t *testing.T) {
sourceCode := &SourceCode{
sourceCode := &internal.SourceCode{
Lines: []string{
"package main",
"",
Expand All @@ -101,7 +106,7 @@ func TestFormatIssuesWithArrows_MultipleDigitsLineNumbers(t *testing.T) {
},
}

issues := []Issue{
issues := []internal.Issue{
{
Rule: "unused-variable",
Filename: "test.go",
Expand Down Expand Up @@ -151,7 +156,7 @@ error: example
}

func TestFormatIssuesWithArrows_UnnecessaryElse(t *testing.T) {
sourceCode := &SourceCode{
sourceCode := &internal.SourceCode{
Lines: []string{
"package main",
"",
Expand All @@ -165,7 +170,7 @@ func TestFormatIssuesWithArrows_UnnecessaryElse(t *testing.T) {
},
}

issues := []Issue{
issues := []internal.Issue{
{
Rule: "unnecessary-else",
Filename: "test.go",
Expand All @@ -189,6 +194,96 @@ func TestFormatIssuesWithArrows_UnnecessaryElse(t *testing.T) {
`

result := FormatIssuesWithArrows(issues, sourceCode)

assert.Equal(t, expected, result, "Formatted output does not match expected for unnecessary else")
}

func TestIntegratedLintEngine(t *testing.T) {
t.Skip("skipping integrated lint engine test")
tests := []struct {
name string
code string
expected []string
}{
{
name: "Detect unused issues",
code: `
package main
import (
"fmt"
)
func main() {
x := 1
fmt.Println("Hello")
}
`,
expected: []string{
"x declared and not used",
},
},
{
name: "Detect multiple issues",
code: `
package main
import (
"fmt"
"strings"
)
func main() {
x := 1
y := "unused"
fmt.Println("Hello")
}
`,
expected: []string{
"x declared and not used",
"y declared and not used",
`"strings" imported and not used`,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "lint-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)

tmpfile := filepath.Join(tmpDir, "test.go")
err = os.WriteFile(tmpfile, []byte(tt.code), 0o644)
require.NoError(t, err)

rootDir := "."
engine, err := internal.NewEngine(rootDir)
if err != nil {
t.Fatalf("unexpected error initializing lint engine: %v", err)
}

issues, err := engine.Run(tmpfile)
require.NoError(t, err)

assert.Equal(t, len(tt.expected), len(issues), "Number of issues doesn't match")

for _, exp := range tt.expected {
found := false
for _, issue := range issues {
if strings.Contains(issue.Message, exp) {
found = true
break
}
}
assert.True(t, found, "Expected issue not found: "+exp)
}

if len(issues) > 0 {
sourceCode, err := internal.ReadSourceCode(tmpfile)
require.NoError(t, err)
formattedIssues := FormatIssuesWithArrows(issues, sourceCode)
t.Logf("Found issues with arrows:\n%s", formattedIssues)
}
})
}
}
78 changes: 78 additions & 0 deletions formatter/general.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package formatter

import (
"fmt"
"strings"

"github.com/fatih/color"
"github.com/gnoswap-labs/lint/internal"
)

const tabWidth = 8

var (
errorStyle = color.New(color.FgRed, 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)
)

// GeneralIssueFormatter is a formatter for general lint issues.
type GeneralIssueFormatter struct{}

// Format formats a general lint issue into a human-readable string.
// It takes an Issue and a SourceCode snippet as input and returns a formatted string.
func (f *GeneralIssueFormatter) Format(
issue internal.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))

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
}
42 changes: 42 additions & 0 deletions formatter/unnecessary_else.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package formatter

import (
"fmt"
"strings"

"github.com/gnoswap-labs/lint/internal"
)

// UnnecessaryElseFormatter is a formatter specifically designed for the "unnecessary-else" rule.
type UnnecessaryElseFormatter struct{}

func (f *UnnecessaryElseFormatter) Format(
issue internal.Issue,
snippet *internal.SourceCode,
) string {
var result strings.Builder
ifStartLine, elseEndLine := issue.Start.Line-2, issue.End.Line
maxLineNumberStr := fmt.Sprintf("%d", elseEndLine)
padding := strings.Repeat(" ", len(maxLineNumberStr)-1)

result.WriteString(lineStyle.Sprintf(" %s|\n", padding))

maxLen := 0
for i := ifStartLine; i <= elseEndLine; i++ {
if len(snippet.Lines[i-1]) > maxLen {
maxLen = len(snippet.Lines[i-1])
}
line := expandTabs(snippet.Lines[i-1])
lineNumberStr := fmt.Sprintf("%d", i)
linePadding := strings.Repeat(" ", len(maxLineNumberStr)-len(lineNumberStr))
result.WriteString(lineStyle.Sprintf("%s%s | ", linePadding, lineNumberStr))
result.WriteString(line + "\n")
}

result.WriteString(lineStyle.Sprintf(" %s| ", padding))
result.WriteString(messageStyle.Sprintf("%s\n", strings.Repeat("~", maxLen)))
result.WriteString(lineStyle.Sprintf(" %s| ", padding))
result.WriteString(messageStyle.Sprintf("%s\n\n", issue.Message))

return result.String()
}
Loading

0 comments on commit f4f6747

Please sign in to comment.