all repos

clerk @ 3d92c0eea76d13390148586f42b31ea04cfdc1cd

missing tooling for ledger/hledger

clerk/journal/loader.go (view raw)

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