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: generate issues from source and refactor to make it as a library #73

Merged
merged 5 commits into from
Sep 27, 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
86 changes: 9 additions & 77 deletions cmd/tlin/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"go/parser"
"go/token"
"os"
"path/filepath"
"sort"
"strings"
"time"
Expand All @@ -18,8 +17,8 @@ import (
"github.com/gnoswap-labs/tlin/internal"
"github.com/gnoswap-labs/tlin/internal/analysis/cfg"
"github.com/gnoswap-labs/tlin/internal/fixer"
"github.com/gnoswap-labs/tlin/internal/lints"
tt "github.com/gnoswap-labs/tlin/internal/types"
"github.com/gnoswap-labs/tlin/lint"
"go.uber.org/zap"
)

Expand All @@ -42,11 +41,6 @@ type Config struct {
ConfidenceThreshold float64
}

type LintEngine interface {
Run(filePath string) ([]tt.Issue, error)
IgnoreRule(rule string)
}

func main() {
logger, _ := zap.NewProduction()
defer logger.Sync()
Expand All @@ -56,7 +50,7 @@ func main() {
ctx, cancel := context.WithTimeout(context.Background(), config.Timeout)
defer cancel()

engine, err := internal.NewEngine(".")
engine, err := internal.NewEngine(".", nil)
if err != nil {
logger.Fatal("Failed to initialize lint engine", zap.Error(err))
}
Expand Down Expand Up @@ -133,8 +127,8 @@ func runWithTimeout(ctx context.Context, f func()) {
}
}

func runNormalLintProcess(ctx context.Context, logger *zap.Logger, engine LintEngine, paths []string, jsonOutput string) {
issues, err := processFiles(ctx, logger, engine, paths, processFile)
func runNormalLintProcess(ctx context.Context, logger *zap.Logger, engine lint.LintEngine, paths []string, jsonOutput string) {
issues, err := lint.ProcessFiles(ctx, logger, engine, paths, lint.ProcessFile)
if err != nil {
logger.Error("Error processing files", zap.Error(err))
os.Exit(1)
Expand All @@ -148,8 +142,8 @@ func runNormalLintProcess(ctx context.Context, logger *zap.Logger, engine LintEn
}

func runCyclomaticComplexityAnalysis(ctx context.Context, logger *zap.Logger, paths []string, threshold int, jsonOutput string) {
issues, err := processFiles(ctx, logger, nil, paths, func(_ LintEngine, path string) ([]tt.Issue, error) {
return processCyclomaticComplexity(path, threshold)
issues, err := lint.ProcessFiles(ctx, logger, nil, paths, func(_ lint.LintEngine, path string) ([]tt.Issue, error) {
return lint.ProcessCyclomaticComplexity(path, threshold)
})
if err != nil {
logger.Error("Error processing files for cyclomatic complexity", zap.Error(err))
Expand Down Expand Up @@ -192,11 +186,11 @@ func runCFGAnalysis(_ context.Context, logger *zap.Logger, paths []string, funcN
}
}

func runAutoFix(ctx context.Context, logger *zap.Logger, engine LintEngine, paths []string, dryRun bool, confidenceThreshold float64) {
func runAutoFix(ctx context.Context, logger *zap.Logger, engine lint.LintEngine, paths []string, dryRun bool, confidenceThreshold float64) {
fix := fixer.New(dryRun, confidenceThreshold)

for _, path := range paths {
issues, err := processPath(ctx, logger, engine, path, processFile)
issues, err := lint.ProcessPath(ctx, logger, engine, path, lint.ProcessFile)
if err != nil {
logger.Error("error processing path", zap.String("path", path), zap.Error(err))
continue
Expand All @@ -209,64 +203,6 @@ func runAutoFix(ctx context.Context, logger *zap.Logger, engine LintEngine, path
}
}

func processFiles(ctx context.Context, logger *zap.Logger, engine LintEngine, paths []string, processor func(LintEngine, string) ([]tt.Issue, error)) ([]tt.Issue, error) {
var allIssues []tt.Issue
for _, path := range paths {
issues, err := processPath(ctx, logger, engine, path, processor)
if err != nil {
logger.Error("Error processing path", zap.String("path", path), zap.Error(err))
return nil, err
}
allIssues = append(allIssues, issues...)
}

return allIssues, nil
}

func processPath(_ context.Context, logger *zap.Logger, engine LintEngine, path string, processor func(LintEngine, string) ([]tt.Issue, error)) ([]tt.Issue, error) {
info, err := os.Stat(path)
if err != nil {
return nil, fmt.Errorf("error accessing %s: %w", path, err)
}

var issues []tt.Issue
if info.IsDir() {
err = filepath.Walk(path, func(filePath string, fileInfo os.FileInfo, err error) error {
if err != nil {
return err
}
if !fileInfo.IsDir() && hasDesiredExtension(filePath) {
fileIssues, err := processor(engine, filePath)
if err != nil {
logger.Error("Error processing file", zap.String("file", filePath), zap.Error(err))
} else {
issues = append(issues, fileIssues...)
}
}
return nil
})
if err != nil {
return nil, fmt.Errorf("error walking directory %s: %w", path, err)
}
} else if hasDesiredExtension(path) {
fileIssues, err := processor(engine, path)
if err != nil {
return nil, err
}
issues = append(issues, fileIssues...)
}

return issues, nil
}

func processFile(engine LintEngine, filePath string) ([]tt.Issue, error) {
return engine.Run(filePath)
}

func processCyclomaticComplexity(path string, threshold int) ([]tt.Issue, error) {
return lints.DetectHighCyclomaticComplexity(path, threshold)
}

func printIssues(logger *zap.Logger, issues []tt.Issue, jsonOutput string) {
issuesByFile := make(map[string][]tt.Issue)
for _, issue := range issues {
Expand All @@ -287,7 +223,7 @@ func printIssues(logger *zap.Logger, issues []tt.Issue, jsonOutput string) {
logger.Error("Error reading source file", zap.String("file", filename), zap.Error(err))
continue
}
output := formatter.GenetateFormattedIssue(fileIssues, sourceCode)
output := formatter.GenerateFormattedIssue(fileIssues, sourceCode)
fmt.Println(output)
}
} else {
Expand All @@ -309,7 +245,3 @@ func printIssues(logger *zap.Logger, issues []tt.Issue, jsonOutput string) {
}
}
}

func hasDesiredExtension(path string) bool {
return filepath.Ext(path) == ".go" || filepath.Ext(path) == ".gno"
}
115 changes: 5 additions & 110 deletions cmd/tlin/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ func (m *mockLintEngine) Run(filePath string) ([]types.Issue, error) {
return args.Get(0).([]types.Issue), args.Error(1)
}

func (m *mockLintEngine) RunSource(source []byte) ([]types.Issue, error) {
args := m.Called(source)
return args.Get(0).([]types.Issue), args.Error(1)
}

func (m *mockLintEngine) IgnoreRule(rule string) {
m.Called(rule)
}
Expand Down Expand Up @@ -100,116 +105,6 @@ func TestParseFlags(t *testing.T) {
}
}

func TestProcessFile(t *testing.T) {
t.Parallel()
expectedIssues := []types.Issue{
{
Rule: "test-rule",
Filename: "test.go",
Start: token.Position{Filename: "test.go", Offset: 0, Line: 1, Column: 1},
End: token.Position{Filename: "test.go", Offset: 10, Line: 1, Column: 11},
Message: "Test issue",
},
}
mockEngine := setupMockEngine(expectedIssues, "test.go")

issues, err := processFile(mockEngine, "test.go")

assert.NoError(t, err)
assert.Equal(t, expectedIssues, issues)
mockEngine.AssertExpectations(t)
}

func TestProcessPath(t *testing.T) {
t.Parallel()
logger, _ := zap.NewProduction()
ctx := context.Background()

tempDir, err := os.MkdirTemp("", "test")
assert.NoError(t, err)
defer os.RemoveAll(tempDir)

paths := createTempFiles(t, tempDir, "test1.go", "test2.go")

expectedIssues := []types.Issue{
{
Rule: "rule1",
Filename: paths[0],
Start: token.Position{Filename: paths[0], Offset: 0, Line: 1, Column: 1},
End: token.Position{Filename: paths[0], Offset: 10, Line: 1, Column: 11},
Message: "Test issue 1",
},
{
Rule: "rule2",
Filename: paths[1],
Start: token.Position{Filename: paths[1], Offset: 0, Line: 1, Column: 1},
End: token.Position{Filename: paths[1], Offset: 10, Line: 1, Column: 11},
Message: "Test issue 2",
},
}

mockEngine := new(mockLintEngine)
mockEngine.On("Run", paths[0]).Return([]types.Issue{expectedIssues[0]}, nil)
mockEngine.On("Run", paths[1]).Return([]types.Issue{expectedIssues[1]}, nil)

issues, err := processPath(ctx, logger, mockEngine, tempDir, processFile)

assert.NoError(t, err)
assert.Len(t, issues, 2)
assert.Contains(t, issues, expectedIssues[0])
assert.Contains(t, issues, expectedIssues[1])
mockEngine.AssertExpectations(t)
}

func TestProcessFiles(t *testing.T) {
t.Parallel()
logger, _ := zap.NewProduction()
ctx := context.Background()

tempDir, err := os.MkdirTemp("", "test")
assert.NoError(t, err)
defer os.RemoveAll(tempDir)

paths := createTempFiles(t, tempDir, "test1.go", "test2.go")

expectedIssues := []types.Issue{
{
Rule: "rule1",
Filename: paths[0],
Start: token.Position{Filename: paths[0], Offset: 0, Line: 1, Column: 1},
End: token.Position{Filename: paths[0], Offset: 10, Line: 1, Column: 11},
Message: "Test issue 1",
},
{
Rule: "rule2",
Filename: paths[1],
Start: token.Position{Filename: paths[1], Offset: 0, Line: 1, Column: 1},
End: token.Position{Filename: paths[1], Offset: 10, Line: 1, Column: 11},
Message: "Test issue 2",
},
}

mockEngine := new(mockLintEngine)
mockEngine.On("Run", paths[0]).Return([]types.Issue{expectedIssues[0]}, nil)
mockEngine.On("Run", paths[1]).Return([]types.Issue{expectedIssues[1]}, nil)

issues, err := processFiles(ctx, logger, mockEngine, paths, processFile)

assert.NoError(t, err)
assert.Len(t, issues, 2)
assert.Contains(t, issues, expectedIssues[0])
assert.Contains(t, issues, expectedIssues[1])
mockEngine.AssertExpectations(t)
}

func TestHasDesiredExtension(t *testing.T) {
t.Parallel()
assert.True(t, hasDesiredExtension("test.go"))
assert.True(t, hasDesiredExtension("test.gno"))
assert.False(t, hasDesiredExtension("test.txt"))
assert.False(t, hasDesiredExtension("test"))
}

func TestRunWithTimeout(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
Expand Down
4 changes: 2 additions & 2 deletions formatter/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ type IssueFormatter interface {
Format(issue tt.Issue, snippet *internal.SourceCode) string
}

// GenetateFormattedIssue formats a slice of issues into a human-readable 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 GenetateFormattedIssue(issues []tt.Issue, snippet *internal.SourceCode) string {
func GenerateFormattedIssue(issues []tt.Issue, snippet *internal.SourceCode) string {
var builder strings.Builder
for _, issue := range issues {
// builder.WriteString(formatIssueHeader(issue))
Expand Down
8 changes: 4 additions & 4 deletions formatter/formatter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ error: empty-if

`

result := GenetateFormattedIssue(issues, code)
result := GenerateFormattedIssue(issues, code)

assert.Equal(t, expected, result, "Formatted output does not match expected")

Expand Down Expand Up @@ -87,7 +87,7 @@ error: empty-if

`

resultWithTabs := GenetateFormattedIssue(issues, sourceCodeWithTabs)
resultWithTabs := GenerateFormattedIssue(issues, sourceCodeWithTabs)

assert.Equal(t, expectedWithTabs, resultWithTabs, "Formatted output with tabs does not match expected")
}
Expand Down Expand Up @@ -156,7 +156,7 @@ error: example

`

result := GenetateFormattedIssue(issues, code)
result := GenerateFormattedIssue(issues, code)

assert.Equal(t, expected, result, "Formatted output with multiple digit line numbers does not match expected")
}
Expand Down Expand Up @@ -211,7 +211,7 @@ The code inside the 'else' block has been moved outside, as it will only be exec

`

result := GenetateFormattedIssue(issues, code)
result := GenerateFormattedIssue(issues, code)
t.Logf("result: %s", result)
assert.Equal(t, expected, result, "Formatted output does not match expected for unnecessary else")
}
Expand Down
41 changes: 38 additions & 3 deletions internal/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ type Engine struct {
}

// NewEngine creates a new lint engine.
func NewEngine(rootDir string) (*Engine, error) {
st, err := BuildSymbolTable(rootDir)
func NewEngine(rootDir string, source []byte) (*Engine, error) {
st, err := BuildSymbolTable(rootDir, source)
if err != nil {
return nil, fmt.Errorf("error building symbol table: %w", err)
}
Expand Down Expand Up @@ -67,7 +67,7 @@ func (e *Engine) Run(filename string) ([]tt.Issue, error) {
}
defer e.cleanupTemp(tempFile)

node, fset, err := lints.ParseFile(tempFile)
node, fset, err := lints.ParseFile(tempFile, nil)
if err != nil {
return nil, fmt.Errorf("error parsing file: %w", err)
}
Expand Down Expand Up @@ -107,6 +107,41 @@ func (e *Engine) Run(filename string) ([]tt.Issue, error) {
return filtered, nil
}

// Run applies all lint rules to the given source and returns a slice of Issues.
func (e *Engine) RunSource(source []byte) ([]tt.Issue, error) {
node, fset, err := lints.ParseFile("", source)
if err != nil {
return nil, fmt.Errorf("error parsing content: %w", err)
}

var wg sync.WaitGroup
var mu sync.Mutex

var allIssues []tt.Issue
for _, rule := range e.rules {
wg.Add(1)
go func(r LintRule) {
defer wg.Done()
if e.ignoredRules[rule.Name()] {
return
}
issues, err := rule.Check("", node, fset)
if err != nil {
return
}

mu.Lock()
allIssues = append(allIssues, issues...)
mu.Unlock()
}(rule)
}
wg.Wait()

filtered := e.filterUndefinedIssues(allIssues)

return filtered, nil
}

func (e *Engine) IgnoreRule(rule string) {
if e.ignoredRules == nil {
e.ignoredRules = make(map[string]bool)
Expand Down
Loading
Loading