From dba079d2198f811468b4d3748ca03bb1c4be31b3 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <72015889+0xtekgrinder@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:23:44 +0100 Subject: [PATCH] refactor: use text templates for formatting issues (#98) fixes #75 This PR refactor how the formatter works to use text/template go package. This replicates so far how the old formatter is working --- formatter/builder.go | 264 ++++++++++++++++------------- formatter/cyclomatic_complexity.go | 40 ++--- formatter/formatter_test.go | 4 +- formatter/general.go | 33 ++-- formatter/missing_mod_pacakge.go | 16 -- formatter/missing_mod_package.go | 13 ++ formatter/slice_bound.go | 37 ++-- 7 files changed, 205 insertions(+), 202 deletions(-) delete mode 100644 formatter/missing_mod_pacakge.go create mode 100644 formatter/missing_mod_package.go diff --git a/formatter/builder.go b/formatter/builder.go index a4022ed..21c97c7 100644 --- a/formatter/builder.go +++ b/formatter/builder.go @@ -1,8 +1,10 @@ package formatter import ( + "bytes" "fmt" "strings" + "text/template" "unicode" "github.com/fatih/color" @@ -31,29 +33,16 @@ var ( noStyle = color.New(color.FgWhite) ) -// issueFormatter is the interface that wraps the Format method. +// issueFormatter is the interface that wraps the issueTemplate method. // Implementations of this interface are responsible for formatting specific types of lint issues. -// -// ! TODO: Use template to format issue type issueFormatter interface { - Format(issue tt.Issue, snippet *internal.SourceCode) string + IssueTemplate() string } -// GenerateFormattedIssue formats a slice of issues into a human-readable string. -// It uses the appropriate formatter for each issue based on its rule. -func GenerateFormattedIssue(issues []tt.Issue, snippet *internal.SourceCode) string { - var builder strings.Builder - for _, issue := range issues { - formatter := getFormatter(issue.Rule) - builder.WriteString(formatter.Format(issue, snippet)) - } - return builder.String() -} - -// getFormatter is a factory function that returns the appropriate IssueFormatter +// getIssueFormatter 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 { +func getIssueFormatter(rule string) issueFormatter { switch rule { case CycloComplexity: return &CyclomaticComplexityFormatter{} @@ -66,20 +55,39 @@ func getFormatter(rule string) issueFormatter { } } +// GenerateFormattedIssue formats a slice of issues into a human-readable string. +// It uses the appropriate formatter for each issue based on its rule. +func GenerateFormattedIssue(issues []tt.Issue, snippet *internal.SourceCode) string { + var builder strings.Builder + for _, issue := range issues { + formatter := getIssueFormatter(issue.Rule) + formattedIssue := buildIssue(issue, snippet, formatter) + builder.WriteString(formattedIssue) + } + return builder.String() +} + /***** Issue Formatter Builder *****/ -type issueFormatterBuilder struct { - snippet *internal.SourceCode - padding string - commonIndent string - result strings.Builder - issue tt.Issue - startLine int - endLine int - maxLineNumWidth int +type IssueData struct { + Category string + Severity string + Rule string + Filename string + Padding string + StartLine int + StartColumn int + EndLine int + EndColumn int + MaxLineNumWidth int + Message string + Suggestion string + Note string + SnippetLines []string + CommonIndent string } -func newIssueFormatterBuilder(issue tt.Issue, snippet *internal.SourceCode) *issueFormatterBuilder { +func buildIssue(issue tt.Issue, snippet *internal.SourceCode, formatter issueFormatter) string { startLine := issue.Start.Line endLine := issue.End.Line maxLineNumWidth := calculateMaxLineNumWidth(endLine) @@ -92,150 +100,164 @@ func newIssueFormatterBuilder(issue tt.Issue, snippet *internal.SourceCode) *iss commonIndent = findCommonIndent(snippet.Lines[startLine-1 : endLine]) } - return &issueFormatterBuilder{ - issue: issue, - snippet: snippet, - startLine: startLine, - endLine: endLine, - maxLineNumWidth: maxLineNumWidth, - padding: padding, - commonIndent: commonIndent, + data := IssueData{ + Severity: issue.Severity.String(), + Category: issue.Category, + Rule: issue.Rule, + Filename: issue.Filename, + StartLine: issue.Start.Line, + StartColumn: issue.Start.Column, + EndLine: issue.End.Line, + EndColumn: issue.End.Column, + Message: issue.Message, + Suggestion: issue.Suggestion, + Note: issue.Note, + MaxLineNumWidth: maxLineNumWidth, + Padding: padding, + CommonIndent: commonIndent, + SnippetLines: snippet.Lines, } + + funcMap := template.FuncMap{ + "header": header, + "suggestion": suggestion, + "note": note, + "snippet": codeSnippet, + "underlineAndMessage": underlineAndMessage, + "message": message, + "warning": warning, + "complexityInfo": complexityInfo, + } + + issueTemplate := formatter.IssueTemplate() + tmpl := template.Must(template.New("issue").Funcs(funcMap).Parse(issueTemplate)) + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + return fmt.Sprintf("Error formatting issue: %v", err) + } + return buf.String() } -func (b *issueFormatterBuilder) AddHeader() *issueFormatterBuilder { - // add header type and rule name - switch b.issue.Severity { - case tt.SeverityError: - b.writeStyledLine(errorStyle, "error: ") - case tt.SeverityWarning: - b.writeStyledLine(warningStyle, "warning: ") - case tt.SeverityInfo: - b.writeStyledLine(messageStyle, "info: ") +// utils functions used in the text templates + +func header(rule string, severity string, maxLineNumWidth int, filename string, startLine int, startColumn int) string { + var endString string + switch severity { + case "ERROR": + endString = errorStyle.Sprintf("error: ") + case "WARNING": + endString = warningStyle.Sprintf("warning: ") + case "INFO": + endString = messageStyle.Sprintf("info: ") } - b.writeStyledLine(ruleStyle, "%s\n", b.issue.Rule) + endString += ruleStyle.Sprintf("%s\n", rule) - // add file name - padding := strings.Repeat(" ", b.maxLineNumWidth) - b.writeStyledLine(lineStyle, "%s--> ", padding) - b.writeStyledLine(fileStyle, "%s:%d:%d\n", b.issue.Filename, b.issue.Start.Line, b.issue.Start.Column) + padding := strings.Repeat(" ", maxLineNumWidth) + endString += lineStyle.Sprintf("%s--> ", padding) + endString += fileStyle.Sprintf("%s:%d:%d\n", filename, startLine, startColumn) - return b + return endString } -func (b *issueFormatterBuilder) AddCodeSnippet() *issueFormatterBuilder { - // add separator - b.writeStyledLine(lineStyle, "%s|\n", b.padding) +func codeSnippet(snippetLines []string, startLine int, endLine int, maxLineNumWidth int, commonIndent string, padding string) string { + var endString string + endString = lineStyle.Sprintf("%s|\n", padding) - for i := b.startLine; i <= b.endLine; i++ { - if i-1 < 0 || i-1 >= len(b.snippet.Lines) { + for i := startLine; i <= endLine; i++ { + if i-1 < 0 || i-1 >= len(snippetLines) { continue } - line := b.snippet.Lines[i-1] - line = strings.TrimPrefix(line, b.commonIndent) - lineNum := fmt.Sprintf("%*d", b.maxLineNumWidth, i) + line := snippetLines[i-1] + line = strings.TrimPrefix(line, commonIndent) + lineNum := fmt.Sprintf("%*d", maxLineNumWidth, i) - b.writeStyledLine(lineStyle, "%s | ", lineNum) - b.writeStyledLine(noStyle, "%s\n", line) + endString += lineStyle.Sprintf("%s | ", lineNum) + endString += noStyle.Sprintf("%s\n", line) } - return b + return endString } -func (b *issueFormatterBuilder) AddUnderlineAndMessage() *issueFormatterBuilder { - b.writeStyledLine(lineStyle, "%s| ", b.padding) +func underlineAndMessage(message string, padding string, startLine int, endLine int, startColumn int, endColumn int, snippetLines []string, commonIndent string, note string) string { + var endString string + endString = lineStyle.Sprintf("%s| ", padding) - if !b.isValidLineRange() { - b.writeStyledLine(messageStyle, "%s\n\n", b.issue.Message) - return b + if !isValidLineRange(startLine, endLine, snippetLines) { + endString += messageStyle.Sprintf("%s\n", message) + return endString } - commonIndentWidth := calculateVisualColumn(b.commonIndent, len(b.commonIndent)+1) + commonIndentWidth := calculateVisualColumn(commonIndent, len(commonIndent)+1) // calculate underline start position - underlineStart := calculateVisualColumn(b.snippet.Lines[b.startLine-1], b.issue.Start.Column) - commonIndentWidth + underlineStart := calculateVisualColumn(snippetLines[startLine-1], startColumn) - commonIndentWidth if underlineStart < 0 { underlineStart = 0 } // calculate underline end position - underlineEnd := calculateVisualColumn(b.snippet.Lines[b.endLine-1], b.issue.End.Column) - commonIndentWidth + underlineEnd := calculateVisualColumn(snippetLines[endLine-1], endColumn) - commonIndentWidth underlineLength := underlineEnd - underlineStart + 1 - b.result.WriteString(strings.Repeat(" ", underlineStart)) - b.writeStyledLine(messageStyle, "%s\n", strings.Repeat("^", underlineLength)) - b.writeStyledLine(lineStyle, "%s|\n", b.padding) + endString += fmt.Sprint(strings.Repeat(" ", underlineStart)) + endString += messageStyle.Sprintf("%s\n", strings.Repeat("^", underlineLength)) + endString += lineStyle.Sprintf("%s|\n", padding) - b.writeStyledLine(lineStyle, "%s= ", b.padding) - b.writeStyledLine(messageStyle, "%s\n", b.issue.Message) + endString += lineStyle.Sprintf("%s= ", padding) + endString += messageStyle.Sprintf("%s", message) - if b.issue.Note == "" { - b.result.WriteString("\n") + if note == "" { + endString += "\n" } - return b + return endString } -func (b *issueFormatterBuilder) AddMessage() *issueFormatterBuilder { - b.writeStyledLine(messageStyle, "%s\n\n", b.issue.Message) - - return b -} - -func (b *issueFormatterBuilder) AddSuggestion() *issueFormatterBuilder { - if b.issue.Suggestion == "" { - return b +func suggestion(suggestion string, padding string, maxLineNumWidth int, startLine int) string { + if suggestion == "" { + return "" } - b.writeStyledLine(suggestionStyle, "suggestion:\n") - b.writeStyledLine(lineStyle, "%s|\n", b.padding) + var endString string + endString = suggestionStyle.Sprintf("suggestion:\n") + endString += lineStyle.Sprintf("%s|\n", padding) - suggestionLines := strings.Split(b.issue.Suggestion, "\n") + suggestionLines := strings.Split(suggestion, "\n") for i, line := range suggestionLines { - lineNum := fmt.Sprintf("%*d", b.maxLineNumWidth, b.issue.Start.Line+i) - b.writeStyledLine(lineStyle, "%s | ", lineNum) - b.writeStyledLine(noStyle, "%s\n", line) + lineNum := fmt.Sprintf("%*d", maxLineNumWidth, startLine+i) + endString += lineStyle.Sprintf("%s | ", lineNum) + endString += noStyle.Sprintf("%s\n", line) } - b.writeStyledLine(lineStyle, "%s|\n\n", b.padding) - - return b + endString += lineStyle.Sprintf("%s|\n\n", padding) + return endString } -func (b *issueFormatterBuilder) AddNote() *issueFormatterBuilder { - if b.issue.Note == "" { - return b +func note(note string, padding string, suggestion string) string { + if note == "" { + return "" } - b.writeStyledLine(lineStyle, "%s= ", b.padding) - b.result.WriteString(noStyle.Sprint("note: ")) + var endString string + endString += lineStyle.Sprintf("%s= ", padding) + endString += noStyle.Sprintf("note: ") - b.writeStyledLine(noStyle, "%s\n", b.issue.Note) - if b.issue.Suggestion == "" { - b.result.WriteString("\n") + endString += noStyle.Sprintf("%s", note) + if suggestion == "" { + endString += "\n" } - - return b -} - -func (b *issueFormatterBuilder) writeStyledLine(style *color.Color, format string, a ...interface{}) { - b.result.WriteString(style.Sprintf(format, a...)) -} - -type BaseFormatter struct{} - -func (b *issueFormatterBuilder) Build() string { - return b.result.String() + return endString } -func (b *issueFormatterBuilder) isValidLineRange() bool { - return b.startLine > 0 && - b.endLine > 0 && - b.startLine <= b.endLine && - b.startLine <= len(b.snippet.Lines) && - b.endLine <= len(b.snippet.Lines) +func isValidLineRange(startLine int, endLine int, snippetLines []string) bool { + return startLine > 0 && + endLine > 0 && + startLine <= endLine && + startLine <= len(snippetLines) && + endLine <= len(snippetLines) } func calculateMaxLineNumWidth(endLine int) int { diff --git a/formatter/cyclomatic_complexity.go b/formatter/cyclomatic_complexity.go index ba6de4b..65a1670 100644 --- a/formatter/cyclomatic_complexity.go +++ b/formatter/cyclomatic_complexity.go @@ -3,31 +3,31 @@ package formatter import ( "fmt" "strings" - - "github.com/gnolang/tlin/internal" - tt "github.com/gnolang/tlin/internal/types" ) type CyclomaticComplexityFormatter struct{} -func (f *CyclomaticComplexityFormatter) Format(issue tt.Issue, snippet *internal.SourceCode) string { - builder := newIssueFormatterBuilder(issue, snippet) - return builder. - AddHeader(). - AddCodeSnippet(). - AddComplexityInfo(). - AddNote(). - AddSuggestion(). - Build() -} +func (f *CyclomaticComplexityFormatter) IssueTemplate() string { + return `{{header .Rule .Severity .MaxLineNumWidth .Filename .StartLine .StartColumn -}} +{{snippet .SnippetLines .StartLine .EndLine .MaxLineNumWidth .CommonIndent .Padding -}} +{{underlineAndMessage .Message .Padding .StartLine .EndLine .StartColumn .EndColumn .SnippetLines .CommonIndent .Note -}} +{{complexityInfo .Padding .Message }} -func (b *issueFormatterBuilder) AddComplexityInfo() *issueFormatterBuilder { - maxLineNumWidth := calculateMaxLineNumWidth(b.issue.End.Line) - padding := strings.Repeat(" ", maxLineNumWidth+1) +{{- if .Note }} +{{note .Note .Padding .Suggestion}} +{{- end }} + +{{- if .Suggestion }} +{{suggestion .Suggestion .Padding .MaxLineNumWidth .StartLine}} +{{- end }} +` +} - 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 complexityInfo(padding string, message string) string { + var endString string + complexityInfo := fmt.Sprintf("Cyclomatic Complexity: %s", strings.TrimPrefix(message, "function ")) + endString = lineStyle.Sprintf("%s| ", padding) + endString += messageStyle.Sprintf("%s\n", complexityInfo) - return b + return endString } diff --git a/formatter/formatter_test.go b/formatter/formatter_test.go index 4677962..4a2a4a3 100644 --- a/formatter/formatter_test.go +++ b/formatter/formatter_test.go @@ -170,7 +170,6 @@ error: example func TestUnnecessaryTypeConversionFormatter(t *testing.T) { t.Parallel() - formatter := &GeneralIssueFormatter{} issue := tt.Issue{ Rule: "unnecessary-type-conversion", @@ -206,9 +205,10 @@ suggestion: 5 | Remove the type conversion. Change ` + "`int(myInt)`" + ` to just ` + "`myInt`" + `. | + ` - result := formatter.Format(issue, snippet) + result := GenerateFormattedIssue([]tt.Issue{issue}, snippet) assert.Equal(t, expected, result, "Formatted output should match expected output") } diff --git a/formatter/general.go b/formatter/general.go index 9f2e088..9c32c86 100644 --- a/formatter/general.go +++ b/formatter/general.go @@ -1,25 +1,18 @@ package formatter -import ( - "github.com/gnolang/tlin/internal" - tt "github.com/gnolang/tlin/internal/types" -) - -// 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 tt.Issue, - snippet *internal.SourceCode, -) string { - builder := newIssueFormatterBuilder(issue, snippet) - return builder. - AddHeader(). - AddCodeSnippet(). - AddUnderlineAndMessage(). - AddNote(). - AddSuggestion(). - Build() +func (f *GeneralIssueFormatter) IssueTemplate() string { + return `{{header .Rule .Severity .MaxLineNumWidth .Filename .StartLine .StartColumn -}} +{{snippet .SnippetLines .StartLine .EndLine .MaxLineNumWidth .CommonIndent .Padding -}} +{{underlineAndMessage .Message .Padding .StartLine .EndLine .StartColumn .EndColumn .SnippetLines .CommonIndent .Note}} + +{{- if .Note }} +{{note .Note .Padding .Suggestion}} +{{- end }} + +{{- if .Suggestion }} +{{suggestion .Suggestion .Padding .MaxLineNumWidth .StartLine}} +{{- end }} +` } diff --git a/formatter/missing_mod_pacakge.go b/formatter/missing_mod_pacakge.go deleted file mode 100644 index 77d8982..0000000 --- a/formatter/missing_mod_pacakge.go +++ /dev/null @@ -1,16 +0,0 @@ -package formatter - -import ( - "github.com/gnolang/tlin/internal" - tt "github.com/gnolang/tlin/internal/types" -) - -type MissingModPackageFormatter struct{} - -func (f *MissingModPackageFormatter) Format(issue tt.Issue, snippet *internal.SourceCode) string { - builder := newIssueFormatterBuilder(issue, snippet) - return builder. - AddHeader(). - AddMessage(). - Build() -} diff --git a/formatter/missing_mod_package.go b/formatter/missing_mod_package.go new file mode 100644 index 0000000..8221f35 --- /dev/null +++ b/formatter/missing_mod_package.go @@ -0,0 +1,13 @@ +package formatter + +type MissingModPackageFormatter struct{} + +func (f *MissingModPackageFormatter) IssueTemplate() string { + return `{{header .Rule .Severity .MaxLineNumWidth .Filename .StartLine .StartColumn -}} +{{message .Message}} +` +} + +func message(message string) string { + return messageStyle.Sprintf("%s\n", message) +} diff --git a/formatter/slice_bound.go b/formatter/slice_bound.go index 2ff1e76..7d6b583 100644 --- a/formatter/slice_bound.go +++ b/formatter/slice_bound.go @@ -1,32 +1,23 @@ package formatter -import ( - "github.com/gnolang/tlin/internal" - tt "github.com/gnolang/tlin/internal/types" -) - type SliceBoundsCheckFormatter struct{} -func (f *SliceBoundsCheckFormatter) Format( - issue tt.Issue, - snippet *internal.SourceCode, -) string { - builder := newIssueFormatterBuilder(issue, snippet) - return builder. - AddHeader(). - AddCodeSnippet(). - AddUnderlineAndMessage(). - AddWarning(). - Build() +func (f *SliceBoundsCheckFormatter) IssueTemplate() string { + return `{{header .Rule .Severity .MaxLineNumWidth .Filename .StartLine .StartColumn -}} +{{snippet .SnippetLines .StartLine .EndLine .MaxLineNumWidth .CommonIndent .Padding -}} +{{underlineAndMessage .Message .Padding .StartLine .EndLine .StartColumn .EndColumn .SnippetLines .CommonIndent .Note}} +{{warning .Category -}} +` } -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") +func warning(category string) string { + var endString string + endString = warningStyle.Sprint("warning: ") + if category == "index-access" { + endString += "Index access without bounds checking can lead to runtime panics.\n" + } else if category == "slice-expression" { + endString += "Slice expressions without proper length checks may cause unexpected behavior.\n" } - return b + return endString }