Skip to content

Commit eca3bc3

Browse files
authored
Introduce a caching FileParser (#126)
This migrates the file parsing functions to a stateful FileParser type. This lets us implement a file-level cache so that repeated attempts to parse the same file (via includes) return the cached result.
1 parent 377f1ba commit eca3bc3

File tree

6 files changed

+399
-17
lines changed

6 files changed

+399
-17
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,6 @@ jobs:
7474
run: go build -v ./...
7575
- name: Test
7676
run: go test -v ./...
77+
- name: Test (race detector)
78+
if: matrix.go == '1.25'
79+
run: go test -race ./...

ast.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func Doc(node ast.Node) string {
5353
// The target can either be in the current program's scope or it can refer to
5454
// an included file using dot notation. Included files must exist in one of the
5555
// given search directories.
56-
func Resolve(name string, program *ast.Program, dirs []string) (ast.Node, error) {
56+
func Resolve(name string, program *ast.Program, parser *FileParser) (ast.Node, error) {
5757
defs := program.Definitions
5858

5959
if strings.Contains(name, ".") {
@@ -73,7 +73,7 @@ func Resolve(name string, program *ast.Program, dirs []string) (ast.Node, error)
7373
return nil, fmt.Errorf("missing \"include\" for type reference %q", name)
7474
}
7575

76-
program, _, err := ParseFile(ipath, dirs)
76+
program, _, err := parser.ParseFile(ipath)
7777
if err != nil {
7878
return nil, err
7979
}
@@ -98,12 +98,12 @@ func Resolve(name string, program *ast.Program, dirs []string) (ast.Node, error)
9898
// - "Enum.Value" (ast.EnumItem)
9999
// - "include.Constant" (ast.Constant)
100100
// - "include.Enum.Value" (ast.EnumItem)
101-
func ResolveConstant(ref ast.ConstantReference, program *ast.Program, dirs []string) (ast.Node, error) {
101+
func ResolveConstant(ref ast.ConstantReference, program *ast.Program, parser *FileParser) (ast.Node, error) {
102102
parts := strings.SplitN(ref.Name, ".", 3)
103103

104-
n, err := Resolve(parts[0], program, dirs)
104+
n, err := Resolve(parts[0], program, parser)
105105
if err != nil && len(parts) > 1 {
106-
n, err = Resolve(parts[0]+"."+parts[1], program, dirs)
106+
n, err = Resolve(parts[0]+"."+parts[1], program, parser)
107107
}
108108
if err != nil {
109109
return n, fmt.Errorf("%q could not be resolved", ref.Name)
@@ -125,8 +125,8 @@ func ResolveConstant(ref ast.ConstantReference, program *ast.Program, dirs []str
125125
// resolve the target node's own type. This is useful when the reference
126126
// points to an [ast.Typedef] or [ast.Constant], for example, and the caller
127127
// is primarily intererested in the target's ast.Type.
128-
func ResolveType(ref ast.TypeReference, program *ast.Program, dirs []string) (ast.Node, error) {
129-
n, err := Resolve(ref.Name, program, dirs)
128+
func ResolveType(ref ast.TypeReference, program *ast.Program, parser *FileParser) (ast.Node, error) {
129+
n, err := Resolve(ref.Name, program, parser)
130130
if err != nil {
131131
return nil, err
132132
}

check.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ type C struct {
171171
Check string
172172
Messages Messages
173173
logger *log.Logger
174+
parser *FileParser
174175
parseInfo *idl.Info
175176
}
176177

@@ -203,23 +204,23 @@ func (c *C) Errorf(node ast.Node, message string, args ...any) {
203204

204205
// Resolve resolves a name.
205206
func (c *C) Resolve(name string) ast.Node {
206-
if n, err := Resolve(name, c.Program, c.Dirs); err == nil {
207+
if n, err := Resolve(name, c.Program, c.parser); err == nil {
207208
return n
208209
}
209210
return nil
210211
}
211212

212213
// ResolveConstant resolves a constant reference to its target.
213214
func (c *C) ResolveConstant(ref ast.ConstantReference) ast.Node {
214-
if n, err := ResolveConstant(ref, c.Program, c.Dirs); err == nil {
215+
if n, err := ResolveConstant(ref, c.Program, c.parser); err == nil {
215216
return n
216217
}
217218
return nil
218219
}
219220

220221
// ResolveType resolves a type reference to its target type.
221222
func (c *C) ResolveType(ref ast.TypeReference) ast.Node {
222-
if n, err := ResolveType(ref, c.Program, c.Dirs); err == nil {
223+
if n, err := ResolveType(ref, c.Program, c.parser); err == nil {
223224
return n
224225
}
225226
return nil

linter.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
// Linter is a configured Thrift linter.
3131
type Linter struct {
3232
checks Checks
33+
parser *FileParser
3334
logger *log.Logger
3435
includes []string
3536
}
@@ -60,14 +61,15 @@ func NewLinter(checks Checks, options ...Option) *Linter {
6061
for _, option := range options {
6162
option(l)
6263
}
64+
l.parser = NewFileParser(l.includes)
6365
l.logger.Printf("checks: %s\n", checks)
6466
l.logger.Printf("includes: %s\n", strings.Join(l.includes, " "))
6567
return l
6668
}
6769

6870
// Lint lints a single input file.
6971
func (l *Linter) Lint(r io.Reader, filename string) (Messages, error) {
70-
program, info, err := Parse(r)
72+
program, info, err := l.parser.Parse(r, filename)
7173
if err != nil {
7274
var parseError *idl.ParseError
7375
if errors.As(err, &parseError) {
@@ -119,6 +121,7 @@ func (l *Linter) lint(program *ast.Program, filename string, parseInfo *idl.Info
119121
Dirs: append([]string{filepath.Dir(filename)}, l.includes...),
120122
Program: program,
121123
logger: l.logger,
124+
parser: l.parser,
122125
parseInfo: parseInfo,
123126
}
124127
activeChecks := overridableChecks{root: &l.checks}

parse.go

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"io"
2020
"os"
2121
"path/filepath"
22+
"sync"
2223

2324
"go.uber.org/thriftrw/ast"
2425
"go.uber.org/thriftrw/idl"
@@ -36,23 +37,70 @@ func Parse(r io.Reader) (*ast.Program, *idl.Info, error) {
3637
return prog, cfg.Info, err
3738
}
3839

39-
// ParseFile parses a Thrift file. The filename must appear in one of the
40-
// given directories.
41-
func ParseFile(filename string, dirs []string) (*ast.Program, *idl.Info, error) {
40+
type parsedFile struct {
41+
prog *ast.Program
42+
info *idl.Info
43+
}
44+
45+
// FileParser caches parsed Thrift files to avoid re-parsing included files.
46+
// It is safe for concurrent use by multiple goroutines.
47+
type FileParser struct {
48+
mu sync.RWMutex
49+
cache map[string]parsedFile
50+
dirs []string
51+
}
52+
53+
// NewParser returns a new Parser with a list of directories that will be
54+
// searched for files.
55+
func NewFileParser(dirs []string) *FileParser {
56+
return &FileParser{
57+
cache: make(map[string]parsedFile),
58+
dirs: dirs,
59+
}
60+
}
61+
62+
// Parse parses Thrift document content with the given filename.
63+
func (p *FileParser) Parse(r io.Reader, filename string) (*ast.Program, *idl.Info, error) {
64+
filename, err := filepath.Abs(filename)
65+
if err != nil {
66+
return nil, nil, err
67+
}
68+
69+
p.mu.RLock()
70+
cached, ok := p.cache[filename]
71+
p.mu.RUnlock()
72+
73+
if ok {
74+
return cached.prog, cached.info, nil
75+
}
76+
77+
prog, info, err := Parse(r)
78+
if err == nil {
79+
p.mu.Lock()
80+
p.cache[filename] = parsedFile{prog: prog, info: info}
81+
p.mu.Unlock()
82+
}
83+
84+
return prog, info, err
85+
}
86+
87+
// ParseFile parses a Thrift file from its filename.
88+
func (p *FileParser) ParseFile(filename string) (*ast.Program, *idl.Info, error) {
4289
if filepath.IsAbs(filename) {
4390
if f, err := os.Open(filename); err == nil {
4491
defer f.Close()
45-
return Parse(f)
92+
return p.Parse(f, filename)
4693
}
4794
return nil, nil, fmt.Errorf("%s not found", filename)
4895
}
4996

97+
dirs := append([]string{filepath.Dir(filename)}, p.dirs...)
5098
for _, dir := range dirs {
5199
if f, err := os.Open(filepath.Join(dir, filename)); err == nil {
52100
defer f.Close()
53-
return Parse(f)
101+
return p.Parse(f, f.Name())
54102
}
55103
}
56104

57-
return nil, nil, fmt.Errorf("%s not found in %s", filename, dirs)
105+
return nil, nil, fmt.Errorf("%s not found in %s", filename, p.dirs)
58106
}

0 commit comments

Comments
 (0)