all repos

clerk @ 225a2d3

missing tooling for ledger/hledger

clerk/journal/loader.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: diagnostics, 20 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
// Ordered returns all files in dependency order (included before includer)
50
func (l *Loader) Ordered() []*ParsedFile {
51
	visited := make(map[string]bool)
52
	var res []*ParsedFile
53
	var visit func(*ParsedFile)
54
	visit = func(pf *ParsedFile) {
55
		if visited[pf.Path] {
56
			return
57
		}
58
		visited[pf.Path] = true
59
		for _, inc := range pf.Includes {
60
			visit(inc)
61
		}
62
		res = append(res, pf)
63
	}
64
	for _, pf := range l.files {
65
		visit(pf)
66
	}
67
	return res
68
}
69
70
func (l *Loader) loadFile(fpath string, stack []string) (*ParsedFile, error) {
71
	abs, err := filepath.Abs(fpath)
72
	if err != nil {
73
		return nil, err
74
	}
75
76
	// reuse already loaded
77
	if pf, ok := l.files[abs]; ok {
78
		return pf, nil
79
	}
80
81
	src, err := os.ReadFile(abs)
82
	if err != nil {
83
		return nil, err
84
	}
85
86
	return l.loadBytes(abs, src, stack)
87
}
88
89
func (l *Loader) loadBytes(path string, src []byte, stack []string) (*ParsedFile, error) {
90
	abs, err := filepath.Abs(path)
91
	if err != nil {
92
		return nil, err
93
	}
94
95
	// cycle includes
96
	if slices.Contains(stack, abs) {
97
		return nil, fmt.Errorf("include cycle: %s", strings.Join(append(stack, abs), " → "))
98
	}
99
100
	// reuse already loaded
101
	if pf, ok := l.files[abs]; ok {
102
		return pf, nil
103
	}
104
105
	lex := lexer.New(abs, src)
106
	par := parser.New(lex)
107
	j := par.ParseJournal()
108
109
	pf := &ParsedFile{
110
		Path:     abs,
111
		Src:      src,
112
		Ast:      j,
113
		Includes: []*ParsedFile{},
114
		Errors:   j.Errors,
115
	}
116
	l.files[abs] = pf
117
118
	for _, entry := range j.Entries {
119
		inc, ok := entry.(*ast.IncludeDirective)
120
		if !ok {
121
			continue
122
		}
123
124
		incPath := filepath.Join(filepath.Dir(abs), inc.Path)
125
126
		matches, err := filepath.Glob(incPath)
127
		if err != nil || len(matches) == 0 {
128
			pf.FileErrors = append(pf.FileErrors, &ast.FileError{
129
				Path:    incPath,
130
				Span:    inc.Span,
131
				Message: fmt.Sprintf("include not found: %s", inc.Path),
132
			})
133
			continue
134
		}
135
136
		for _, match := range matches {
137
			child, err := l.loadFile(match, append(stack, abs))
138
			if err != nil {
139
				pf.FileErrors = append(pf.FileErrors, &ast.FileError{
140
					Path:    match,
141
					Span:    inc.Span,
142
					Message: err.Error(),
143
				})
144
				continue
145
			}
146
			pf.Includes = append(pf.Includes, child)
147
		}
148
	}
149
150
	return pf, nil
151
}