Skip to content

Commit

Permalink
Refactor engine (#9)
Browse files Browse the repository at this point in the history
* refactor engine

* update README
notJoon authored Jul 18, 2024
1 parent 96b6b5d commit 9e8de0b
Showing 8 changed files with 341 additions and 252 deletions.
58 changes: 27 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -47,67 +47,63 @@ tlin .

tlin allows addition of custom lint rules beyond the default golangci-lint rules. To add a new lint rule, follow these steps:

1. Add a function defining the new rule in the `internal/rule_set.go` file.
> ⚠️ Must update relevant tests if you have added a new rule or formatter.
Example:
1. Implement the `LintRule` interface for your new rule:

```go
func (e *Engine) detectNewRule(filename string) ([]Issue, error) {
// rule implementation
type NewRule struct{}

func (r *NewRule) Check(filename string) ([]Issue, error) {
// Implement your lint rule logic here
// return a slice of Issues and any error encountered
}
```

2. Add the new rule to the `Run` method in the `internal/lint.go` file.
2. Register your new rule in the `registerDefaultRules` method of the `Engine` struct in `internal/engine.go`:

```go
newRuleIssues, err := e.detectNewRule(tempFile)
if err != nil {
return nil, fmt.Errorf("error detecting new rule: %w", err)
func (e *Engine) registerDefaultRules() {
e.rules = append(e.rules,
&GolangciLintRule{},
// ...
&NewRule{}, // Add your new rule here
)
}
filtered = append(filtered, newRuleIssues...)
```

3. (Optional) Create a new formatter for the new rule in the `formatter` pacakge.
a. Create a new file named after your lint rule (e.g., `new_rule.go`) in the `formatter` package.
3. (Optional) if your rule requires special formatting, create a new formatter in the `formatter` package:

b. Implement the `IssueFormatter` interface for your new rule:
a. Create a new file (e.g., `formatter/new_rule.go`).
b. Implement the `IssueFormatter` interface for your new rule:

```go
type NewRuleFormatter struct{}
```go
type NewRuleFormatter struct{}

func (f *NewRuleFormatter) Format(
issue internal.Issue,
snippet *internal.SourceCode,
func (f *NewRuleFormatter) Format(
issue internal.Issue,
snippet *internal.SourceCode,
) string {
// Implementation of the formatting logic for the new rule
// Implement formatting logic for new rule here.
}
```
```

c. Add the new formatter to the `GetFormatter` function in `formatter/fmt.go`:

```go
// rule set
const (
// ...
NewRule = "new_rule" // <- define the new rule as constant
)
c. Add the new formatter to the `GetFormatter` function in `formatter/fmt.go`.

```go
func GetFormatter(rule string) IssueFormatter {
switch rule {
// ...
case NewRule:
case "new_rule": // Add your new rule here
return &NewRuleFormatter{}
default:
return &DefaultFormatter{}
}
}
```

4. If necessary, update the `FormatIssueWithArrow` function in `formatter/fmt.go` to handle any special formatting requirements for your new rule.

By following these steps, you can add new lint rules and ensure they are properly formatted when displayed in the CLI.


## Contributing

We welcome all forms of contributions, including bug reports, feature requests, and pull requests. Please feel free to open an issue or submit a pull request.
26 changes: 13 additions & 13 deletions formatter/fmt.go
Original file line number Diff line number Diff line change
@@ -17,30 +17,30 @@ 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)
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{}
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 internal.Issue) string {
48 changes: 48 additions & 0 deletions internal/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Package internal provides the core functionality for a Go-compatible linting tool.
//
// This package implements a flexible and extensible linting engine that can be used
// to analyze Go and Gno code for potential issues, style violations, and areas of improvement.
// It is designed to be easily extendable with custom lint rules while providing a set of
// default rules out of the box.
//
// Key components:
//
// Engine: The main linting engine that coordinates the linting process.
// It manages a collection of lint rules and applies them to the given source files.
//
// LintRule: An interface that defines the contract for all lint rules.
// Each lint rule must implement the Check method to analyze the code and return issues.
//
// Issue: Represents a single lint issue found in the code, including its location and description.
//
// SymbolTable: A data structure that keeps track of defined symbols across the codebase,
// helping to reduce false positives in certain lint rules.
//
// SourceCode: A simple structure to represent the content of a source file as a collection of lines.
//
// The package also includes several helper functions for file operations, running external tools,
// and managing temporary files during the linting process.
//
// Usage:
//
// engine, err := internal.NewEngine("path/to/root/dir")
// if err != nil {
// // handle error
// }
//
// // Optionally add custom rules
// engine.AddRule(myCustomRule)
//
// issues, err := engine.Run("path/to/file.go")
// if err != nil {
// // handle error
// }
//
// // Process the found issues
// for _, issue := range issues {
// fmt.Printf("Found issue: %s at %s\n", issue.Message, issue.Start)
// }
//
// This package is intended for internal use within the linting tool and should not be
// imported by external packages.
package internal
135 changes: 135 additions & 0 deletions internal/engine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package internal

import (
"fmt"
"os"
"path/filepath"
"strings"
)

// Engine manages the linting process.
type Engine struct {
SymbolTable *SymbolTable
rules []LintRule
}

// NewEngine creates a new lint engine.
func NewEngine(rootDir string) (*Engine, error) {
st, err := BuildSymbolTable(rootDir)
if err != nil {
return nil, fmt.Errorf("error building symbol table: %w", err)
}

engine := &Engine{SymbolTable: st}
engine.registerDefaultRules()

return engine, nil
}

// registerDefaultRules adds the default set of lint rules to the engine.
func (e *Engine) registerDefaultRules() {
e.rules = append(e.rules,
&GolangciLintRule{},
&UnnecessaryElseRule{},
&UnusedFunctionRule{},
)
}

// AddRule allows adding custom lint rules to the engine.
func (e *Engine) AddRule(rule LintRule) {
e.rules = append(e.rules, rule)
}

// Run applies all lint rules to the given file and returns a slice of Issues.
func (e *Engine) Run(filename string) ([]Issue, error) {
tempFile, err := e.prepareFile(filename)
if err != nil {
return nil, err
}
defer e.cleanupTemp(tempFile)

var allIssues []Issue
for _, rule := range e.rules {
issues, err := rule.Check(tempFile)
if err != nil {
return nil, fmt.Errorf("error running lint rule: %w", err)
}
allIssues = append(allIssues, issues...)
}

filtered := e.filterUndefinedIssues(allIssues)

// map issues back to .gno file if necessary
if strings.HasSuffix(filename, ".gno") {
for i := range filtered {
filtered[i].Filename = filename
}
}

return filtered, nil
}

func (e *Engine) prepareFile(filename string) (string, error) {
if strings.HasSuffix(filename, "gno") {
return createTempGoFile(filename)
}
return filename, nil
}

func (e *Engine) cleanupTemp(temp string) {
if temp != "" && strings.HasPrefix(filepath.Base(temp), "temp_") {
_ = os.Remove(temp)
}
}

func (e *Engine) filterUndefinedIssues(issues []Issue) []Issue {
var filtered []Issue
for _, issue := range issues {
if issue.Rule == "typecheck" && strings.Contains(issue.Message, "undefined:") {
symbol := strings.TrimSpace(strings.TrimPrefix(issue.Message, "undefined:"))
if e.SymbolTable.IsDefined(symbol) {
// ignore issues if the symbol is defined in the symbol table
continue
}
}
filtered = append(filtered, issue)
}
return filtered
}

func createTempGoFile(gnoFile string) (string, error) {
content, err := os.ReadFile(gnoFile)
if err != nil {
return "", fmt.Errorf("error reading .gno file: %w", err)
}

dir := filepath.Dir(gnoFile)
tempFile, err := os.CreateTemp(dir, "temp_*.go")
if err != nil {
return "", fmt.Errorf("error creating temp file: %w", err)
}

_, err = tempFile.Write(content)
if err != nil {
os.Remove(tempFile.Name())
return "", fmt.Errorf("error writing to temp file: %w", err)
}

err = tempFile.Close()
if err != nil {
os.Remove(tempFile.Name())
return "", fmt.Errorf("error closing temp file: %w", err)
}

return tempFile.Name(), nil
}

// ReadSourceFile reads the content of a file and returns it as a `SourceCode` struct.
func ReadSourceCode(filename string) (*SourceCode, error) {
content, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
lines := strings.Split(string(content), "\n")
return &SourceCode{Lines: lines}, nil
}
Loading

0 comments on commit 9e8de0b

Please sign in to comment.