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

add caching #4

Closed
wants to merge 1 commit into from
Closed
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
35 changes: 29 additions & 6 deletions internal/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"go/token"
"os"
"os/exec"
"path/filepath"
"strings"
)

Expand Down Expand Up @@ -35,24 +36,46 @@ type Issue struct {

// Engine manages the linting process.
type Engine struct {
SymbolTable *SymbolTable
SymbolTable *symbolTable
RootDir string
}

// 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)
cacheFile := filepath.Join(rootDir, ".symbol_cache")
st := newSymbolTable(cacheFile)

if err := st.loadCache(); err != nil {
return nil, fmt.Errorf("error loading symbol table cache: %w", err)
}

engine := &Engine{
SymbolTable: st,
RootDir: rootDir,
}

if err := engine.UpdateSymbolTable(); err != nil {
return nil, fmt.Errorf("error updating symbol table: %w", err)
}
return &Engine{SymbolTable: st}, nil

return engine, nil
}

func (e *Engine) UpdateSymbolTable() error {
return e.SymbolTable.updateSymbols(e.RootDir)
}

// Run applies golangci-lint to the given file and returns a slice of issues.
func (e *Engine) Run(filename string) ([]Issue, error) {
if err := e.UpdateSymbolTable(); err != nil {
return nil, fmt.Errorf("error updating symbol table: %w", err)
}

issues, err := runGolangciLint(filename)
if err != nil {
return nil, fmt.Errorf("error running golangci-lint: %w", err)
}

filtered := e.filterUndefinedIssues(issues)
return filtered, nil
}
Expand All @@ -62,7 +85,7 @@ func (e *Engine) filterUndefinedIssues(issues []Issue) []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) {
if e.SymbolTable.isDefined(symbol) {
// ignore issues if the symbol is defined in the symbol table
continue
}
Expand Down
87 changes: 78 additions & 9 deletions internal/symbol_table.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
package internal

import (
"encoding/gob"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
"sync"
"time"
)

type SymbolTable struct {
symbols map[string]string // symbol name -> file path
}

func BuildSymbolTable(rootDir string) (*SymbolTable, error) {
st := &SymbolTable{symbols: make(map[string]string)}
func buildSymbolTable(rootDir string) (*symbolTable, error) {
st := &symbolTable{symbols: make(map[string]string)}
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
Expand All @@ -29,7 +28,77 @@ func BuildSymbolTable(rootDir string) (*SymbolTable, error) {
return st, err
}

func (st *SymbolTable) parseFile(filepath string) error {
type symbolTable struct {
symbols map[string]string // symbol name -> file path
lastUpdated map[string]time.Time
mutex sync.RWMutex
cacheFile string
}

func newSymbolTable(cacheFile string) *symbolTable {
return &symbolTable{
symbols: make(map[string]string),
lastUpdated: make(map[string]time.Time),
cacheFile: cacheFile,
}
}

func (st *symbolTable) loadCache() error {
st.mutex.Lock()
defer st.mutex.Unlock()

file, err := os.Open(st.cacheFile)
if err != nil {
if os.IsNotExist(err) {
return nil // ignore if no cache file exists
}
return err
}
defer file.Close()

decoder := gob.NewDecoder(file)
return decoder.Decode(&st.symbols)
}

func (st *symbolTable) saveCache() error {
st.mutex.RLock()
defer st.mutex.RUnlock()

file, err := os.Create(st.cacheFile)
if err != nil {
return err
}
defer file.Close()

encoder := gob.NewEncoder(file)
return encoder.Encode(st.symbols)
}

func (st *symbolTable) updateSymbols(rootDir string) error {
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && (filepath.Ext(path) == ".go" || filepath.Ext(path) == ".gno") {
lastMod := info.ModTime()
if lastUpdated, ok := st.lastUpdated[path]; !ok || lastMod.After(lastUpdated) {
if err := st.parseFile(path); err != nil {
return err
}
st.lastUpdated[path] = lastMod
}
}
return nil
})

if err == nil {
err = st.saveCache()
}

return err
}

func (st *symbolTable) parseFile(filepath string) error {
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filepath, nil, parser.AllErrors)
if err != nil {
Expand All @@ -53,12 +122,12 @@ func (st *SymbolTable) parseFile(filepath string) error {
return nil
}

func (st *SymbolTable) IsDefined(symbol string) bool {
func (st *symbolTable) isDefined(symbol string) bool {
_, exists := st.symbols[symbol]
return exists
}

func (st *SymbolTable) GetSymbolPath(symbol string) (string, bool) {
func (st *symbolTable) getSymbolPath(symbol string) (string, bool) {
path, exists := st.symbols[symbol]
return path, exists
}
103 changes: 63 additions & 40 deletions internal/symbol_table_test.go
Original file line number Diff line number Diff line change
@@ -1,55 +1,78 @@
package internal

import (
"os"
"path/filepath"
"testing"
"os"
"path/filepath"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestSymbolTable(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "symboltable-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
func TestSymbolTableCache(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "symboltable-cache-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)

// generate test files
file1Content := `package test
cacheFile := filepath.Join(tmpDir, ".symbol_cache")

file1Content := `package test
type TestStruct struct {}
func TestFunc() {}
var TestVar int
`
file1Path := filepath.Join(tmpDir, "file1.go")
err = os.WriteFile(file1Path, []byte(file1Content), 0644)
require.NoError(t, err)

st := newSymbolTable(cacheFile)
err = st.updateSymbols(tmpDir)
require.NoError(t, err)

assert.True(t, st.isDefined("TestStruct"))
assert.True(t, st.isDefined("TestFunc"))
assert.True(t, st.isDefined("TestVar"))

err = st.saveCache()
require.NoError(t, err)

st2 := newSymbolTable(cacheFile)
err = st2.loadCache()
require.NoError(t, err)

assert.True(t, st2.isDefined("TestStruct"))
assert.True(t, st2.isDefined("TestFunc"))
assert.True(t, st2.isDefined("TestVar"))

// update file
time.Sleep(time.Second)
file1UpdatedContent := `package test
type TestStruct struct {}
func TestFunc() {}
var TestVar int
func NewFunc() {}
`
err = os.WriteFile(filepath.Join(tmpDir, "file1.go"), []byte(file1Content), 0o644)
require.NoError(t, err)
err = os.WriteFile(file1Path, []byte(file1UpdatedContent), 0644)
require.NoError(t, err)

err = st2.updateSymbols(tmpDir)
require.NoError(t, err)

file2Content := `package test
assert.True(t, st2.isDefined("NewFunc"))

file2Content := `package test
type AnotherStruct struct {}
func AnotherFunc() {}
`
err = os.WriteFile(filepath.Join(tmpDir, "file2.go"), []byte(file2Content), 0o644)
require.NoError(t, err)

// create symbol table
st, err := BuildSymbolTable(tmpDir)
require.NoError(t, err)

assert.True(t, st.IsDefined("TestStruct"))
assert.True(t, st.IsDefined("TestFunc"))
assert.True(t, st.IsDefined("TestVar"))
assert.True(t, st.IsDefined("AnotherStruct"))
assert.True(t, st.IsDefined("AnotherFunc"))
assert.False(t, st.IsDefined("NonExistentSymbol"))

// validate symbol file paths
path, exists := st.GetSymbolPath("TestStruct")
assert.True(t, exists)
assert.Equal(t, filepath.Join(tmpDir, "file1.go"), path)

path, exists = st.GetSymbolPath("AnotherFunc")
assert.True(t, exists)
assert.Equal(t, filepath.Join(tmpDir, "file2.go"), path)

_, exists = st.GetSymbolPath("NonExistentSymbol")
assert.False(t, exists)
file2Path := filepath.Join(tmpDir, "file2.go")
err = os.WriteFile(file2Path, []byte(file2Content), 0644)
require.NoError(t, err)

err = st2.updateSymbols(tmpDir)
require.NoError(t, err)

assert.True(t, st2.isDefined("AnotherStruct"))

_, err = os.Stat(cacheFile)
assert.NoError(t, err)
}
Loading