all repos

clerk @ a1448b50802379b39dabce5ce0603df42b09c45d

missing tooling for ledger/hledger

clerk/journal/loader.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
tests: switch to txtar, 2 days ago
1
package journal
2
3
import (
4
	"fmt"
5
	"io/fs"
6
	"os"
7
	"path"
8
	"path/filepath"
9
	"slices"
10
	"strings"
11
12
	"olexsmir.xyz/clerk/journal/ast"
13
	"olexsmir.xyz/clerk/journal/lexer"
14
	"olexsmir.xyz/clerk/journal/parser"
15
)
16
17
type ParsedFile struct {
18
	Path       string
19
	Src        []byte
20
	Ast        *ast.Journal
21
	Includes   []*ParsedFile
22
	Errors     []*ast.ParseError
23
	FileErrors []*ast.FileError
24
}
25
26
// CollectFiles returns root and all its transitive includes.
27
func CollectFiles(root *ParsedFile) []*ParsedFile {
28
	var files []*ParsedFile
29
	visited := make(map[*ParsedFile]bool)
30
	var walk func(*ParsedFile)
31
	walk = func(pf *ParsedFile) {
32
		if visited[pf] {
33
			return
34
		}
35
		visited[pf] = true
36
		files = append(files, pf)
37
		for _, inc := range pf.Includes {
38
			walk(inc)
39
		}
40
	}
41
	walk(root)
42
	return files
43
}
44
45
type Loader struct {
46
	files map[string]*ParsedFile // key is absolute path (OS) or FS-relative path
47
	fsys  fs.FS                  // set during LoadFS
48
}
49
50
func NewLoader() *Loader {
51
	return &Loader{files: make(map[string]*ParsedFile)}
52
}
53
54
func (l *Loader) Load(fpath string) (*ParsedFile, error) {
55
	return l.loadFile(fpath, nil)
56
}
57
58
// LoadFS loads a journal from an [fs.FS], resolving includes through the same filesystem.
59
func (l *Loader) LoadFS(fsys fs.FS, fpath string) (*ParsedFile, error) {
60
	l.fsys = fsys
61
	defer func() { l.fsys = nil }()
62
	return l.loadFile(fpath, nil)
63
}
64
65
func (l *Loader) LoadBytes(path string, src []byte) (*ParsedFile, error) {
66
	return l.loadBytes(path, src, nil)
67
}
68
69
func (l *Loader) Reload(path string, src []byte) (*ParsedFile, error) {
70
	abs, err := filepath.Abs(path)
71
	if err != nil {
72
		return nil, err
73
	}
74
	delete(l.files, abs)
75
	return l.loadBytes(abs, src, nil)
76
}
77
78
// Roots returns files not transitively included by any loaded file.
79
func (l *Loader) Roots() []*ParsedFile {
80
	included := make(map[string]bool)
81
	for _, pf := range l.files {
82
		for _, inc := range pf.Includes {
83
			included[inc.Path] = true
84
		}
85
	}
86
	var roots []*ParsedFile
87
	for _, pf := range l.files {
88
		if !included[pf.Path] {
89
			roots = append(roots, pf)
90
		}
91
	}
92
	return roots
93
}
94
95
// Ordered returns all files in dependency order (included before includer)
96
func (l *Loader) Ordered() []*ParsedFile {
97
	visited := make(map[string]bool)
98
	var res []*ParsedFile
99
	var visit func(*ParsedFile)
100
	visit = func(pf *ParsedFile) {
101
		if visited[pf.Path] {
102
			return
103
		}
104
		visited[pf.Path] = true
105
		for _, inc := range pf.Includes {
106
			visit(inc)
107
		}
108
		res = append(res, pf)
109
	}
110
	for _, pf := range l.files {
111
		visit(pf)
112
	}
113
	return res
114
}
115
116
func (l *Loader) loadFile(fpath string, stack []string) (*ParsedFile, error) {
117
	if l.fsys != nil {
118
		fpath = path.Clean(fpath)
119
		if pf, ok := l.files[fpath]; ok {
120
			return pf, nil
121
		}
122
		src, err := fs.ReadFile(l.fsys, fpath)
123
		if err != nil {
124
			return nil, err
125
		}
126
		return l.loadBytes(fpath, src, stack)
127
	}
128
129
	abs, err := filepath.Abs(fpath)
130
	if err != nil {
131
		return nil, err
132
	}
133
	if pf, ok := l.files[abs]; ok {
134
		return pf, nil
135
	}
136
	src, err := os.ReadFile(abs)
137
	if err != nil {
138
		return nil, err
139
	}
140
	return l.loadBytes(abs, src, stack)
141
}
142
143
func (l *Loader) loadBytes(pathStr string, src []byte, stack []string) (*ParsedFile, error) {
144
	var fpath string
145
	if l.fsys != nil {
146
		fpath = path.Clean(pathStr)
147
	} else {
148
		abs, err := filepath.Abs(pathStr)
149
		if err != nil {
150
			return nil, err
151
		}
152
		fpath = abs
153
	}
154
155
	if slices.Contains(stack, fpath) {
156
		return nil, fmt.Errorf("include cycle: %s", strings.Join(append(stack, fpath), " → "))
157
	}
158
	if pf, ok := l.files[fpath]; ok {
159
		return pf, nil
160
	}
161
162
	lex := lexer.New(fpath, src)
163
	par := parser.New(lex)
164
	j := par.ParseJournal()
165
166
	pf := &ParsedFile{
167
		Path:     fpath,
168
		Src:      src,
169
		Ast:      j,
170
		Includes: []*ParsedFile{},
171
		Errors:   j.Errors,
172
	}
173
	l.files[fpath] = pf
174
175
	for _, entry := range j.Entries {
176
		inc, ok := entry.(*ast.IncludeDirective)
177
		if !ok {
178
			continue
179
		}
180
181
		var incPath string
182
		var matches []string
183
		var err error
184
		if l.fsys != nil {
185
			incPath = path.Join(path.Dir(fpath), inc.Path)
186
			matches, err = fs.Glob(l.fsys, incPath)
187
		} else {
188
			incPath = filepath.Join(filepath.Dir(fpath), inc.Path)
189
			matches, err = filepath.Glob(incPath)
190
		}
191
192
		if err != nil || len(matches) == 0 {
193
			pf.FileErrors = append(pf.FileErrors, &ast.FileError{
194
				Path:    incPath,
195
				Span:    inc.Span,
196
				Message: fmt.Sprintf("include not found: %s", inc.Path),
197
			})
198
			continue
199
		}
200
201
		for _, match := range matches {
202
			child, err := l.loadFile(match, append(stack, fpath))
203
			if err != nil {
204
				pf.FileErrors = append(pf.FileErrors, &ast.FileError{
205
					Path:    match,
206
					Span:    inc.Span,
207
					Message: err.Error(),
208
				})
209
				continue
210
			}
211
			pf.Includes = append(pf.Includes, child)
212
		}
213
	}
214
215
	return pf, nil
216
}