7 files changed,
94 insertions(+),
13 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-06-27 12:41:04 +0300
Authored at:
2026-06-26 17:40:03 +0300
Change ID:
zpkkkzprtusnqnmlyrwkvomqvpslyuxy
Parent:
5d20567
M
internal/linter/linter_test.go
··· 21 21 "orderdate": {&OrderDate{}}, 22 22 "duplicated-account": {&DuplicatedAccount{}}, 23 23 "duplicated-commodity": {&DuplicatedCommodity{}}, 24 + "undeclared-commodity": {&UndeclaredCommodity{}}, 24 25 "undeclared-account": {&UndeclaredAccount{}}, 25 26 } 26 27
A
internal/linter/rule_undeclared_commodity.go
··· 1 +package linter 2 + 3 +import ( 4 + "fmt" 5 + 6 + "olexsmir.xyz/clerk/journal/semantic" 7 +) 8 + 9 +// UndeclaredCommodity flags amounts that reference a commodity not declared via `commodity` directive. 10 +type UndeclaredCommodity struct{} 11 + 12 +func (UndeclaredCommodity) ID() RuleID { return "undeclared-commodity" } 13 +func (UndeclaredCommodity) Severity() Severity { return SeverityWarning } 14 +func (r *UndeclaredCommodity) CheckJournal(ctx *semantic.Context) []Find { 15 + var finds []Find 16 + for name, info := range ctx.Commodities { 17 + if len(info.Directives) > 0 { 18 + continue 19 + } 20 + for _, usage := range info.Usages { 21 + finds = append(finds, Find{ 22 + Code: r.ID(), 23 + Severity: r.Severity(), 24 + Span: usage.Amount.Span, 25 + Message: fmt.Sprintf("undeclared commodity: %s", name), 26 + }) 27 + } 28 + } 29 + return finds 30 +}
A
internal/linter/testdata/undeclared-commodity.golden
··· 1 +undeclared-commodity.journal:13:22: undeclared-commodity: undeclared commodity: GBP
A
internal/linter/testdata/undeclared-commodity.input
··· 1 +commodity USD 2 +commodity EUR 3 + 4 +2026-01-15 * "Purchase" 5 + Expenses:Food 50.00 USD 6 + Assets:Cash 7 + 8 +2026-01-16 * "Refund" 9 + Assets:Cash 20.00 EUR 10 + Expenses:Food 11 + 12 +2026-01-17 * "Transfer" 13 + Assets:Cash 100 GBP 14 + Assets:Bank
M
lint.go
··· 47 47 continue 48 48 } 49 49 if info.IsDir() { 50 - filepath.Walk(path, func(fpath string, finfo os.FileInfo, err error) error { 51 - if err != nil || finfo.IsDir() || !journal.IsJournalFile(fpath) { 52 - return nil 53 - } 54 - finds, fatal := lintFile(l, fpath) 55 - reporter.Collect(finds) 56 - if fatal { 57 - exitCode = 1 58 - } 59 - return nil 60 - }) 50 + exitCode = lintDir(l, reporter, path) 61 51 continue 62 52 } 63 53 finds, fatal := lintFile(l, path) ··· 70 60 os.Exit(exitCode) 71 61 } 72 62 63 +func lintDir(l *linter.Linter, reporter *linter.Reporter, dir string) int { 64 + ldr := journal.NewLoader() 65 + var paths []string 66 + filepath.Walk(dir, func(fpath string, finfo os.FileInfo, err error) error { 67 + if err != nil || finfo.IsDir() || !journal.IsJournalFile(fpath) { 68 + return nil 69 + } 70 + paths = append(paths, fpath) 71 + return nil 72 + }) 73 + if len(paths) == 0 { 74 + return 0 75 + } 76 + var lastErr error 77 + for _, fpath := range paths { 78 + src, err := os.ReadFile(fpath) 79 + if err != nil { 80 + fmt.Fprintf(os.Stderr, "error: %s: %v\n", fpath, err) 81 + lastErr = err 82 + continue 83 + } 84 + if _, err := ldr.LoadBytes(fpath, src); err != nil { 85 + fmt.Fprintf(os.Stderr, "error: %s: %v\n", fpath, err) 86 + lastErr = err 87 + } 88 + } 89 + ctx := semantic.Build(ldr.Ordered()) 90 + finds := l.Run(ctx) 91 + reporter.Collect(finds) 92 + if lastErr != nil { 93 + return 1 94 + } 95 + if hasFatal(finds) { 96 + return 1 97 + } 98 + return 0 99 +} 100 + 73 101 func lintFile(l *linter.Linter, path string) ([]linter.Find, bool) { 74 102 src, err := os.ReadFile(path) 75 103 if err != nil { 76 104 fmt.Fprintf(os.Stderr, "error: %s: %v\n", path, err) 77 105 return nil, true 78 106 } 79 - pf, err := journal.NewLoader().LoadBytes(path, src) 107 + ldr := journal.NewLoader() 108 + pf, err := ldr.LoadBytes(path, src) 80 109 if err != nil { 81 110 fmt.Fprintf(os.Stderr, "error: %s: %v\n", path, err) 82 111 return nil, true ··· 93 122 fmt.Fprintf(os.Stderr, "error reading stdin: %v\n", err) 94 123 os.Exit(1) 95 124 } 96 - pf, err := journal.NewLoader().LoadBytes("stdin", src) 125 + ldr := journal.NewLoader() 126 + _, err = ldr.LoadBytes("stdin", src) 97 127 if err != nil { 98 128 fmt.Fprintf(os.Stderr, "error: %v\n", err) 99 129 os.Exit(1)