Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Consistant Formatting #49

Merged
merged 5 commits into from
Aug 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/tlin/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
288 changes: 288 additions & 0 deletions formatter/builder.go
Original file line number Diff line number Diff line change
@@ -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
}
48 changes: 15 additions & 33 deletions formatter/cyclomatic_complexity.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 2 additions & 0 deletions formatter/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Loading
Loading