all repos

clerk @ fc9cae13772b625a39129694607847c27edcf23a

missing tooling for ledger/hledger

clerk/journal/loader.go (view raw)

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