Skip to content

Commit

Permalink
refactor: use text templates for formatting issues (#98)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
0xtekgrinder authored Oct 30, 2024
1 parent 0f21a4d commit dba079d
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 202 deletions.
264 changes: 143 additions & 121 deletions formatter/builder.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package formatter

import (
"bytes"
"fmt"
"strings"
"text/template"
"unicode"

"github.com/fatih/color"
Expand Down Expand Up @@ -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{}
Expand All @@ -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)
Expand All @@ -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 {
Expand Down
Loading

0 comments on commit dba079d

Please sign in to comment.