all repos

clerk @ f3b258b6c2faf2042ec439726a44238041014d24

missing tooling for ledger/hledger
58 files changed, 3041 insertions(+), 642 deletions(-)
improve analyzer
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-07-29 19:32:29 +0300
Authored at: 2026-07-28 13:45:51 +0300
Change ID: lnrnoykrpskukmvkrxspzzosytmwnmwx
Parent: a1448b5
jump to
A internal/analyzer/analyzer.go
A internal/analyzer/analyzer_test.go
A internal/analyzer/benchmark_test.go
A internal/analyzer/build.go
A internal/analyzer/testdata/account-indexing.txtar
A internal/analyzer/testdata/account-names-sorted.txtar
A internal/analyzer/testdata/account-usage.txtar
A internal/analyzer/testdata/commodity-usage.txtar
A internal/analyzer/testdata/cost-balance.txtar
A internal/analyzer/testdata/dates-dedup.txtar
A internal/analyzer/testdata/dates.txtar
A internal/analyzer/testdata/decimal-marks.txtar
A internal/analyzer/testdata/directives.txtar
A internal/analyzer/testdata/duplicate-transactions.txtar
A internal/analyzer/testdata/empty.txtar
A internal/analyzer/testdata/include.txtar
A internal/analyzer/testdata/journal.txtar
A internal/analyzer/testdata/last-used-dates.txtar
A internal/analyzer/testdata/payee-templates.txtar
A internal/analyzer/testdata/payee-tracking.txtar
A internal/analyzer/testdata/payees.txtar
A internal/analyzer/testdata/periodic-automated.txtar
A internal/analyzer/testdata/posting-types.txtar
A internal/analyzer/testdata/prefix-index.txtar
A internal/analyzer/testdata/tag-names-dedup.txtar
A internal/analyzer/testdata/tag-names.txtar
A internal/analyzer/testdata/tags.txtar
A internal/analyzer/testdata/typed-entries.txtar
M internal/cli/cmd_lint.go
M internal/linter/linter.go
M internal/linter/linter_test.go
M internal/linter/rule_account_depth.go
M internal/linter/rule_duplicated_account.go
M internal/linter/rule_duplicated_commodity.go
M internal/linter/rule_empty_postings.go
M internal/linter/rule_missing_commodity.go
M internal/linter/rule_missing_payee.go
M internal/linter/rule_missing_status.go
M internal/linter/rule_multiple_omitted_amounts.go
M internal/linter/rule_omitted_precision.go
M internal/linter/rule_orderdate.go
M internal/linter/rule_parse_error.go
M internal/linter/rule_unbalanced_transaction.go
M internal/linter/rule_undeclared_account.go
M internal/linter/rule_undeclared_commodity.go
M internal/linter/rule_unused_account.go
M internal/linter/rules.go
M internal/lsp/diagnostics.go
M internal/lsp/server.go
M internal/lsp/textdocument_sync.go
A journal/parser/benchmark_test.go
D journal/semantic/build.go
M journal/semantic/build_test.go → internal/analyzer/build_test.go
D journal/semantic/context.go
D journal/semantic/dump.go
D journal/semantic/testdata/empty.txtar
D journal/semantic/testdata/include.txtar
D journal/semantic/testdata/journal.txtar
A internal/analyzer/analyzer.go
···
        
        1
        +package analyzer

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"olexsmir.xyz/clerk/journal"

      
        
        5
        +	"olexsmir.xyz/clerk/journal/ast"

      
        
        6
        +)

      
        
        7
        +

      
        
        8
        +type Analysis struct {

      
        
        9
        +	Files []*journal.ParsedFile

      
        
        10
        +

      
        
        11
        +	Transactions          []*ast.Transaction

      
        
        12
        +	PeriodicTransactions  []*ast.PeriodicTransaction

      
        
        13
        +	AutomatedTransactions []*ast.AutomatedTransaction

      
        
        14
        +	Directives            []ast.Entry // account, commodity, payee, etc

      
        
        15
        +

      
        
        16
        +	Accounts    map[string]*AccountInfo

      
        
        17
        +	Commodities map[string]*CommodityInfo

      
        
        18
        +	Payees      map[string]*PayeeInfo

      
        
        19
        +

      
        
        20
        +	AccountNames     []string            // sorted for binary search

      
        
        21
        +	PayeeNames       []string            // sorted, all payee names from directives + usage

      
        
        22
        +	AccountsByPrefix map[string][]string // "expenses:" -> ["expenses:food", "expenses:taxi"]

      
        
        23
        +

      
        
        24
        +	// PayeeTemplates holds the last transaction's postings per payee name.

      
        
        25
        +	PayeeTemplates map[string][]PostingTemplate

      
        
        26
        +

      
        
        27
        +	TagNames []string // unique, from tag directives

      
        
        28
        +

      
        
        29
        +	Dates       []ast.Date // unique transaction dats in sorted order

      
        
        30
        +	DateStrings []string   // same order as Dates, "YYYY-MM-DD"

      
        
        31
        +

      
        
        32
        +	// CommodityDecimalMarks decimal mark per commodity, detected from D/amounts.

      
        
        33
        +	CommodityDecimalMarks map[string]byte

      
        
        34
        +

      
        
        35
        +	// TransactionsByKey groups transactions by [TxDuplicateKey] signature.

      
        
        36
        +	TransactionsByKey map[string][]*ast.Transaction

      
        
        37
        +}

      
        
        38
        +

      
        
        39
        +type AccountInfo struct {

      
        
        40
        +	Directives []*ast.AccountDirective

      
        
        41
        +	Usages     []AccountUsage

      
        
        42
        +	UsedCount  int

      
        
        43
        +	LastUsed   ast.Date

      
        
        44
        +}

      
        
        45
        +

      
        
        46
        +type AccountUsage struct {

      
        
        47
        +	FileIndex int

      
        
        48
        +	Posting   *ast.Posting

      
        
        49
        +}

      
        
        50
        +

      
        
        51
        +type CommodityInfo struct {

      
        
        52
        +	Directives []*ast.CommodityDirective

      
        
        53
        +	Usages     []CommodityUsage

      
        
        54
        +	UsedCount  int

      
        
        55
        +	LastUsed   ast.Date

      
        
        56
        +}

      
        
        57
        +

      
        
        58
        +type CommodityUsage struct {

      
        
        59
        +	FileIndex int

      
        
        60
        +	Amount    *ast.Amount

      
        
        61
        +}

      
        
        62
        +

      
        
        63
        +type PayeeInfo struct {

      
        
        64
        +	Directives []*ast.Payee

      
        
        65
        +	Usage      []PayeeUsage

      
        
        66
        +	UsedCount  int

      
        
        67
        +	LastUsed   ast.Date

      
        
        68
        +}

      
        
        69
        +

      
        
        70
        +type PayeeUsage struct {

      
        
        71
        +	FileIndex int

      
        
        72
        +	Payee     *ast.Payee

      
        
        73
        +}

      
        
        74
        +

      
        
        75
        +type PostingTemplate struct {

      
        
        76
        +	Account    string

      
        
        77
        +	Amount     string

      
        
        78
        +	Commodity  string

      
        
        79
        +	IsInferred bool // true if the amount was inferred (auto-balanced)

      
        
        80
        +}

      
A internal/analyzer/analyzer_test.go
···
        
        1
        +package analyzer

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"fmt"

      
        
        5
        +	"io"

      
        
        6
        +	"path/filepath"

      
        
        7
        +	"sort"

      
        
        8
        +	"strings"

      
        
        9
        +	"testing"

      
        
        10
        +

      
        
        11
        +	"olexsmir.xyz/clerk/internal/testutil/golden"

      
        
        12
        +	"olexsmir.xyz/clerk/journal"

      
        
        13
        +	"olexsmir.xyz/clerk/journal/ast"

      
        
        14
        +)

      
        
        15
        +

      
        
        16
        +func TestAnalysis(t *testing.T) {

      
        
        17
        +	tests := []string{

      
        
        18
        +		"empty",

      
        
        19
        +		"include",

      
        
        20
        +		"journal",

      
        
        21
        +		"typed-entries",

      
        
        22
        +		"account-indexing",

      
        
        23
        +		"account-usage",

      
        
        24
        +		"commodity-usage",

      
        
        25
        +		"payees",

      
        
        26
        +		"periodic-automated",

      
        
        27
        +		"directives",

      
        
        28
        +		"cost-balance",

      
        
        29
        +		"posting-types",

      
        
        30
        +		"dates",

      
        
        31
        +		"tags",

      
        
        32
        +		"decimal-marks",

      
        
        33
        +		"duplicate-transactions",

      
        
        34
        +	}

      
        
        35
        +

      
        
        36
        +	for _, tname := range tests {

      
        
        37
        +		t.Run(tname, func(t *testing.T) {

      
        
        38
        +			a := golden.Read(t, tname)

      
        
        39
        +			fsys, err := a.FS()

      
        
        40
        +			if err != nil {

      
        
        41
        +				t.Fatal(err)

      
        
        42
        +			}

      
        
        43
        +

      
        
        44
        +			l := journal.NewLoader()

      
        
        45
        +			if _, err := l.LoadFS(fsys, "in.journal"); err != nil {

      
        
        46
        +				t.Fatalf("failed to load test journal: %v", err)

      
        
        47
        +			}

      
        
        48
        +

      
        
        49
        +			ctx := Build(l.Ordered())

      
        
        50
        +			var b strings.Builder

      
        
        51
        +			fprint(&b, ctx)

      
        
        52
        +			golden.Assert(t, a, b.String())

      
        
        53
        +		})

      
        
        54
        +	}

      
        
        55
        +}

      
        
        56
        +

      
        
        57
        +func fprint(w io.Writer, a *Analysis) {

      
        
        58
        +	fmt.Fprintf(w, "files (%d):\n", len(a.Files))

      
        
        59
        +	for i, pf := range a.Files {

      
        
        60
        +		fmt.Fprintf(w, "  %d: %s\n", i, filepath.Base(pf.Path))

      
        
        61
        +	}

      
        
        62
        +

      
        
        63
        +	// commodities

      
        
        64
        +	csyms := make([]string, 0, len(a.Commodities))

      
        
        65
        +	for sym := range a.Commodities {

      
        
        66
        +		csyms = append(csyms, sym)

      
        
        67
        +	}

      
        
        68
        +	sort.Strings(csyms)

      
        
        69
        +

      
        
        70
        +	fmt.Fprintf(w, "\ncommodities (%d):\n", len(a.Commodities))

      
        
        71
        +	for _, sym := range csyms {

      
        
        72
        +		info := a.Commodities[sym]

      
        
        73
        +		fmt.Fprintf(w, "  %s\n", sym)

      
        
        74
        +		fmt.Fprintf(w, "    directives: %d\n", len(info.Directives))

      
        
        75
        +		fmt.Fprintf(w, "    used: %d\n", info.UsedCount)

      
        
        76
        +		for _, cd := range info.Directives {

      
        
        77
        +			fmt.Fprintf(w, "      commodity %s\n", cd.Commodity)

      
        
        78
        +		}

      
        
        79
        +		fmt.Fprintf(w, "    usages: %d\n", len(info.Usages))

      
        
        80
        +		for _, u := range info.Usages {

      
        
        81
        +			fmt.Fprintf(w, "      file %d: %s (%q)\n", u.FileIndex, u.Amount.Commodity, u.Amount.Quantity.String())

      
        
        82
        +		}

      
        
        83
        +		if info.LastUsed.Year != 0 {

      
        
        84
        +			fmt.Fprintf(w, "    last-used: %d-%02d-%02d\n", info.LastUsed.Year, info.LastUsed.Month, info.LastUsed.Day)

      
        
        85
        +		}

      
        
        86
        +	}

      
        
        87
        +

      
        
        88
        +	// accounts

      
        
        89
        +	anames := make([]string, 0, len(a.Accounts))

      
        
        90
        +	for name := range a.Accounts {

      
        
        91
        +		anames = append(anames, name)

      
        
        92
        +	}

      
        
        93
        +	sort.Strings(anames)

      
        
        94
        +

      
        
        95
        +	fmt.Fprintf(w, "\naccounts (%d):\n", len(a.Accounts))

      
        
        96
        +	for _, name := range anames {

      
        
        97
        +		info := a.Accounts[name]

      
        
        98
        +		fmt.Fprintf(w, "  %s\n", name)

      
        
        99
        +		fmt.Fprintf(w, "    directives: %d\n", len(info.Directives))

      
        
        100
        +		fmt.Fprintf(w, "    used: %d\n", info.UsedCount)

      
        
        101
        +		for _, d := range info.Directives {

      
        
        102
        +			fmt.Fprintf(w, "      account %s\n", d.Account.String())

      
        
        103
        +		}

      
        
        104
        +		fmt.Fprintf(w, "    usages: %d\n", len(info.Usages))

      
        
        105
        +		for _, u := range info.Usages {

      
        
        106
        +			fmt.Fprintf(w, "      file %d: %s\n", u.FileIndex, u.Posting.Account.String())

      
        
        107
        +		}

      
        
        108
        +		if info.LastUsed.Year != 0 {

      
        
        109
        +			fmt.Fprintf(w, "    last-used: %d-%02d-%02d\n", info.LastUsed.Year, info.LastUsed.Month, info.LastUsed.Day)

      
        
        110
        +		}

      
        
        111
        +	}

      
        
        112
        +

      
        
        113
        +	// payees

      
        
        114
        +	pnames := make([]string, 0, len(a.Payees))

      
        
        115
        +	for name := range a.Payees {

      
        
        116
        +		pnames = append(pnames, name)

      
        
        117
        +	}

      
        
        118
        +	sort.Strings(pnames)

      
        
        119
        +

      
        
        120
        +	fmt.Fprintf(w, "\npayees (%d):\n", len(a.Payees))

      
        
        121
        +	for _, name := range pnames {

      
        
        122
        +		info := a.Payees[name]

      
        
        123
        +		fmt.Fprintf(w, "  %s\n", name)

      
        
        124
        +		fmt.Fprintf(w, "    directives: %d\n", len(info.Directives))

      
        
        125
        +		fmt.Fprintf(w, "    used: %d\n", info.UsedCount)

      
        
        126
        +		for _, d := range info.Directives {

      
        
        127
        +			fmt.Fprintf(w, "      payee %s\n", d.Name)

      
        
        128
        +		}

      
        
        129
        +		fmt.Fprintf(w, "    usages: %d\n", len(info.Usage))

      
        
        130
        +		for _, u := range info.Usage {

      
        
        131
        +			fmt.Fprintf(w, "      file %d: %s\n", u.FileIndex, u.Payee.Name)

      
        
        132
        +		}

      
        
        133
        +		if info.LastUsed.Year != 0 {

      
        
        134
        +			fmt.Fprintf(w, "    last-used: %d-%02d-%02d\n", info.LastUsed.Year, info.LastUsed.Month, info.LastUsed.Day)

      
        
        135
        +		}

      
        
        136
        +	}

      
        
        137
        +

      
        
        138
        +	// prefix index

      
        
        139
        +	prefixes := make([]string, 0, len(a.AccountsByPrefix))

      
        
        140
        +	for p := range a.AccountsByPrefix {

      
        
        141
        +		prefixes = append(prefixes, p)

      
        
        142
        +	}

      
        
        143
        +	sort.Strings(prefixes)

      
        
        144
        +	fmt.Fprintf(w, "\nprefixes (%d):\n", len(a.AccountsByPrefix))

      
        
        145
        +	for _, p := range prefixes {

      
        
        146
        +		names := a.AccountsByPrefix[p]

      
        
        147
        +		sort.Strings(names)

      
        
        148
        +		fmt.Fprintf(w, "  %s\n", p)

      
        
        149
        +		for _, n := range names {

      
        
        150
        +			fmt.Fprintf(w, "    %s\n", n)

      
        
        151
        +		}

      
        
        152
        +	}

      
        
        153
        +

      
        
        154
        +	// typed entry counts

      
        
        155
        +	fmt.Fprintf(w, "\ntransactions: %d\n", len(a.Transactions))

      
        
        156
        +	fmt.Fprintf(w, "periodic transactions: %d\n", len(a.PeriodicTransactions))

      
        
        157
        +	fmt.Fprintf(w, "automated transactions: %d\n", len(a.AutomatedTransactions))

      
        
        158
        +

      
        
        159
        +	// directives

      
        
        160
        +	fmt.Fprintf(w, "\ndirectives (%d):\n", len(a.Directives))

      
        
        161
        +	for _, d := range a.Directives {

      
        
        162
        +		switch d := d.(type) {

      
        
        163
        +		case *ast.AccountDirective:

      
        
        164
        +			fmt.Fprintf(w, "  account %s\n", d.Account.String())

      
        
        165
        +		case *ast.CommodityDirective:

      
        
        166
        +			fmt.Fprintf(w, "  commodity %s\n", d.Commodity)

      
        
        167
        +		case *ast.PayeeDirective:

      
        
        168
        +			fmt.Fprintf(w, "  payee %s\n", d.Name)

      
        
        169
        +		case *ast.TagDirective:

      
        
        170
        +			fmt.Fprintf(w, "  tag %s\n", d.Name)

      
        
        171
        +		case *ast.IncludeDirective:

      
        
        172
        +			fmt.Fprintf(w, "  include %s\n", d.Path)

      
        
        173
        +		case *ast.AliasDirective:

      
        
        174
        +			fmt.Fprintf(w, "  alias %s = %s\n", d.From.String(), d.To.String())

      
        
        175
        +		case *ast.YearDirective:

      
        
        176
        +			fmt.Fprintf(w, "  year %d\n", d.Year)

      
        
        177
        +		case *ast.DecimalMarkDirective:

      
        
        178
        +			fmt.Fprintf(w, "  decimal-mark %c\n", d.Mark)

      
        
        179
        +		case *ast.DefaultCommodityDirective:

      
        
        180
        +			fmt.Fprintf(w, "  D %s%s\n", d.Amount.Commodity, d.Amount.Quantity.String())

      
        
        181
        +		case *ast.MarketPriceDirective:

      
        
        182
        +			fmt.Fprintf(w, "  P %s %s\n", d.Commodity, d.Amount.Quantity.String())

      
        
        183
        +		case *ast.ConversionDirective:

      
        
        184
        +			fmt.Fprintf(w, "  C %s%s @@ %s%s\n", d.From.Commodity, d.From.Quantity.String(), d.To.Commodity, d.To.Quantity.String())

      
        
        185
        +		case *ast.ApplyDirective:

      
        
        186
        +			fmt.Fprintf(w, "  apply %s\n", d.Expr)

      
        
        187
        +		case *ast.EndDirective:

      
        
        188
        +			fmt.Fprintf(w, "  end %s\n", d.Expr)

      
        
        189
        +		case *ast.CommentBlockDirective:

      
        
        190
        +			fmt.Fprintf(w, "  comment\n")

      
        
        191
        +		case *ast.IgnoredDirective:

      
        
        192
        +			fmt.Fprintf(w, "  N %s\n", d.Text)

      
        
        193
        +		}

      
        
        194
        +	}

      
        
        195
        +

      
        
        196
        +	// transaction duplicate keys

      
        
        197
        +	keys := make([]string, 0, len(a.TransactionsByKey))

      
        
        198
        +	for k := range a.TransactionsByKey {

      
        
        199
        +		keys = append(keys, k)

      
        
        200
        +	}

      
        
        201
        +	sort.Strings(keys)

      
        
        202
        +	fmt.Fprintf(w, "\ntransaction keys (%d):\n", len(a.TransactionsByKey))

      
        
        203
        +	for _, k := range keys {

      
        
        204
        +		txs := a.TransactionsByKey[k]

      
        
        205
        +		if len(txs) > 1 {

      
        
        206
        +			fmt.Fprintf(w, "  %s (dup: %d)\n", k, len(txs))

      
        
        207
        +		} else {

      
        
        208
        +			fmt.Fprintf(w, "  %s\n", k)

      
        
        209
        +		}

      
        
        210
        +	}

      
        
        211
        +

      
        
        212
        +	// payee templates

      
        
        213
        +	payeeNames := make([]string, 0, len(a.PayeeTemplates))

      
        
        214
        +	for name := range a.PayeeTemplates {

      
        
        215
        +		payeeNames = append(payeeNames, name)

      
        
        216
        +	}

      
        
        217
        +	sort.Strings(payeeNames)

      
        
        218
        +	fmt.Fprintf(w, "\npayee templates (%d):\n", len(a.PayeeTemplates))

      
        
        219
        +	for _, name := range payeeNames {

      
        
        220
        +		templates := a.PayeeTemplates[name]

      
        
        221
        +		fmt.Fprintf(w, "  %s\n", name)

      
        
        222
        +		for _, t := range templates {

      
        
        223
        +			fmt.Fprintf(w, "    %s", t.Account)

      
        
        224
        +			if t.Amount != "" {

      
        
        225
        +				fmt.Fprintf(w, "  %s%s", t.Commodity, t.Amount)

      
        
        226
        +			}

      
        
        227
        +			if t.IsInferred {

      
        
        228
        +				fmt.Fprintf(w, " (inferred)")

      
        
        229
        +			}

      
        
        230
        +			fmt.Fprintln(w)

      
        
        231
        +		}

      
        
        232
        +	}

      
        
        233
        +

      
        
        234
        +	// dates

      
        
        235
        +	fmt.Fprintf(w, "\ndates (%d):\n", len(a.Dates))

      
        
        236
        +	for _, d := range a.Dates {

      
        
        237
        +		fmt.Fprintf(w, "  %d-%02d-%02d\n", d.Year, d.Month, d.Day)

      
        
        238
        +	}

      
        
        239
        +

      
        
        240
        +	// tag names

      
        
        241
        +	fmt.Fprintf(w, "\ntag names (%d):\n", len(a.TagNames))

      
        
        242
        +	for _, t := range a.TagNames {

      
        
        243
        +		fmt.Fprintf(w, "  %s\n", t)

      
        
        244
        +	}

      
        
        245
        +

      
        
        246
        +	// commodity decimal marks

      
        
        247
        +	csyms2 := make([]string, 0, len(a.CommodityDecimalMarks))

      
        
        248
        +	for sym := range a.CommodityDecimalMarks {

      
        
        249
        +		csyms2 = append(csyms2, sym)

      
        
        250
        +	}

      
        
        251
        +	sort.Strings(csyms2)

      
        
        252
        +	fmt.Fprintf(w, "\ncommodity decimal marks (%d):\n", len(a.CommodityDecimalMarks))

      
        
        253
        +	for _, sym := range csyms2 {

      
        
        254
        +		fmt.Fprintf(w, "  %s: %c\n", sym, a.CommodityDecimalMarks[sym])

      
        
        255
        +	}

      
        
        256
        +}

      
A internal/analyzer/benchmark_test.go
···
        
        1
        +package analyzer

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"testing"

      
        
        5
        +

      
        
        6
        +	"olexsmir.xyz/clerk/journal"

      
        
        7
        +)

      
        
        8
        +

      
        
        9
        +func BenchmarkBuild_BasicJournal(b *testing.B) {

      
        
        10
        +	files := loadJournal("../../journal/testdata/journals/basic.journal")

      
        
        11
        +	b.ResetTimer()

      
        
        12
        +	for b.Loop() {

      
        
        13
        +		Build(files)

      
        
        14
        +	}

      
        
        15
        +}

      
        
        16
        +

      
        
        17
        +func BenchmarkBuild_1kTxns(b *testing.B) {

      
        
        18
        +	files := loadJournal("../../journal/testdata/journals/actual-1ktxns-100accts.journal")

      
        
        19
        +	b.ResetTimer()

      
        
        20
        +	for b.Loop() {

      
        
        21
        +		Build(files)

      
        
        22
        +	}

      
        
        23
        +}

      
        
        24
        +

      
        
        25
        +func BenchmarkBuild_Standard(b *testing.B) {

      
        
        26
        +	files := loadJournal("../../journal/testdata/journals/actual-ledger-input-standard.dat")

      
        
        27
        +	b.ResetTimer()

      
        
        28
        +	for b.Loop() {

      
        
        29
        +		Build(files)

      
        
        30
        +	}

      
        
        31
        +}

      
        
        32
        +

      
        
        33
        +func BenchmarkBuild_Wow(b *testing.B) {

      
        
        34
        +	files := loadJournal("../../journal/testdata/journals/actual-ledger-input-wow.dat")

      
        
        35
        +	b.ResetTimer()

      
        
        36
        +	for b.Loop() {

      
        
        37
        +		Build(files)

      
        
        38
        +	}

      
        
        39
        +}

      
        
        40
        +

      
        
        41
        +func BenchmarkBuild_1kTxns_Allocs(b *testing.B) {

      
        
        42
        +	files := loadJournal("../../journal/testdata/journals/actual-1ktxns-100accts.journal")

      
        
        43
        +	b.ResetTimer()

      
        
        44
        +	b.ReportAllocs()

      
        
        45
        +	for b.Loop() {

      
        
        46
        +		Build(files)

      
        
        47
        +	}

      
        
        48
        +}

      
        
        49
        +

      
        
        50
        +func BenchmarkBuild_Parallel_1kTxns(b *testing.B) {

      
        
        51
        +	files := loadJournal("../../journal/testdata/journals/actual-1ktxns-100accts.journal")

      
        
        52
        +	b.RunParallel(func(pb *testing.PB) {

      
        
        53
        +		for pb.Next() {

      
        
        54
        +			Build(files)

      
        
        55
        +		}

      
        
        56
        +	})

      
        
        57
        +}

      
        
        58
        +

      
        
        59
        +func BenchmarkPrefixLookup_1kTxns(b *testing.B) {

      
        
        60
        +	a := Build(loadJournal("../../journal/testdata/journals/actual-1ktxns-100accts.journal"))

      
        
        61
        +	prefixes := make([]string, 0, len(a.AccountsByPrefix))

      
        
        62
        +	for p := range a.AccountsByPrefix {

      
        
        63
        +		prefixes = append(prefixes, p)

      
        
        64
        +	}

      
        
        65
        +	b.ResetTimer()

      
        
        66
        +	for b.Loop() {

      
        
        67
        +		for _, p := range prefixes {

      
        
        68
        +			_ = a.AccountsByPrefix[p]

      
        
        69
        +		}

      
        
        70
        +	}

      
        
        71
        +}

      
        
        72
        +

      
        
        73
        +func BenchmarkAccountIteration_1kTxns(b *testing.B) {

      
        
        74
        +	a := Build(loadJournal("../../journal/testdata/journals/actual-1ktxns-100accts.journal"))

      
        
        75
        +	names := a.AccountNames

      
        
        76
        +	b.ResetTimer()

      
        
        77
        +	for b.Loop() {

      
        
        78
        +		for _, name := range names {

      
        
        79
        +			_ = a.Accounts[name].UsedCount

      
        
        80
        +		}

      
        
        81
        +	}

      
        
        82
        +}

      
        
        83
        +

      
        
        84
        +func loadJournal(path string) []*journal.ParsedFile {

      
        
        85
        +	ldr := journal.NewLoader()

      
        
        86
        +	if _, err := ldr.Load(path); err != nil {

      
        
        87
        +		panic("loadJournal: " + err.Error())

      
        
        88
        +	}

      
        
        89
        +	return ldr.Ordered()

      
        
        90
        +}

      
A internal/analyzer/build.go
···
        
        1
        +package analyzer

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"fmt"

      
        
        5
        +	"sort"

      
        
        6
        +	"strings"

      
        
        7
        +

      
        
        8
        +	"olexsmir.xyz/clerk/journal"

      
        
        9
        +	"olexsmir.xyz/clerk/journal/ast"

      
        
        10
        +)

      
        
        11
        +

      
        
        12
        +// Build constructs [Analysis] from a list of parsed files.

      
        
        13
        +// Files should be in dependency order (includes before includers).

      
        
        14
        +func Build(files []*journal.ParsedFile) *Analysis {

      
        
        15
        +	a := &Analysis{

      
        
        16
        +		Files:                 files,

      
        
        17
        +		Accounts:              make(map[string]*AccountInfo),

      
        
        18
        +		Commodities:           make(map[string]*CommodityInfo),

      
        
        19
        +		Payees:                make(map[string]*PayeeInfo),

      
        
        20
        +		AccountsByPrefix:      make(map[string][]string),

      
        
        21
        +		PayeeTemplates:        make(map[string][]PostingTemplate),

      
        
        22
        +		CommodityDecimalMarks: make(map[string]byte),

      
        
        23
        +		TransactionsByKey:     make(map[string][]*ast.Transaction),

      
        
        24
        +	}

      
        
        25
        +	for i, pf := range a.Files {

      
        
        26
        +		for _, entry := range pf.Ast.Entries {

      
        
        27
        +			a.addEntry(i, entry)

      
        
        28
        +		}

      
        
        29
        +	}

      
        
        30
        +	a.buildPrefixIndex()

      
        
        31
        +	a.sortAccountNames()

      
        
        32
        +	a.collectPayeeNames()

      
        
        33
        +	a.collectDates()

      
        
        34
        +	a.collectTagNames()

      
        
        35
        +	return a

      
        
        36
        +}

      
        
        37
        +

      
        
        38
        +func TxDuplicateKey(tx *ast.Transaction) string {

      
        
        39
        +	var b strings.Builder

      
        
        40
        +	fmt.Fprintf(&b, "%04d-%02d-%02d|", tx.Date.Year, tx.Date.Month, tx.Date.Day)

      
        
        41
        +	if tx.Payee != nil {

      
        
        42
        +		b.WriteString(tx.Payee.Name)

      
        
        43
        +	}

      
        
        44
        +	b.WriteByte('|')

      
        
        45
        +	for _, p := range tx.Postings {

      
        
        46
        +		b.WriteString(p.Account.String())

      
        
        47
        +		b.WriteByte(',')

      
        
        48
        +	}

      
        
        49
        +	return b.String()

      
        
        50
        +}

      
        
        51
        +

      
        
        52
        +func (a *Analysis) addEntry(fileIndex int, entry ast.Entry) {

      
        
        53
        +	switch e := entry.(type) {

      
        
        54
        +	case *ast.AccountDirective:

      
        
        55
        +		a.addAccountDirective(e)

      
        
        56
        +	case *ast.CommodityDirective:

      
        
        57
        +		a.addCommodityDirective(e)

      
        
        58
        +	case *ast.PayeeDirective:

      
        
        59
        +		a.addPayeeDirective(e)

      
        
        60
        +	case *ast.Transaction:

      
        
        61
        +		a.Transactions = append(a.Transactions, e)

      
        
        62
        +		a.addPostings(fileIndex, e.Postings, &e.Date)

      
        
        63
        +		a.addPayee(fileIndex, e.Payee)

      
        
        64
        +		a.addPayeeTemplate(e)

      
        
        65
        +		key := TxDuplicateKey(e)

      
        
        66
        +		a.TransactionsByKey[key] = append(a.TransactionsByKey[key], e)

      
        
        67
        +	case *ast.PeriodicTransaction:

      
        
        68
        +		a.PeriodicTransactions = append(a.PeriodicTransactions, e)

      
        
        69
        +		a.addPostings(fileIndex, e.Postings, nil)

      
        
        70
        +	case *ast.AutomatedTransaction:

      
        
        71
        +		a.AutomatedTransactions = append(a.AutomatedTransactions, e)

      
        
        72
        +		a.addPostings(fileIndex, e.Postings, nil)

      
        
        73
        +	case *ast.DecimalMarkDirective:

      
        
        74
        +		// Collect commodity decimal marks from directives with explicit format,

      
        
        75
        +		// but DecimalMarkDirective sets the default for all commodities.

      
        
        76
        +		// We don't track a "default" — individual commodities get theirs from amount formatting.

      
        
        77
        +	case *ast.DefaultCommodityDirective:

      
        
        78
        +		if e.Amount.Commodity != "" {

      
        
        79
        +			mark := e.Amount.QuantityFmt.Decimal

      
        
        80
        +			if mark != 0 {

      
        
        81
        +				a.CommodityDecimalMarks[e.Amount.Commodity] = mark

      
        
        82
        +			}

      
        
        83
        +		}

      
        
        84
        +	}

      
        
        85
        +

      
        
        86
        +	// Every directive-like entry goes into Directives.

      
        
        87
        +	switch entry.(type) {

      
        
        88
        +	case *ast.AccountDirective, *ast.CommodityDirective, *ast.PayeeDirective, *ast.TagDirective, *ast.IncludeDirective,

      
        
        89
        +		*ast.AliasDirective, *ast.YearDirective, *ast.DecimalMarkDirective, *ast.DefaultCommodityDirective, *ast.MarketPriceDirective,

      
        
        90
        +		*ast.ConversionDirective, *ast.ApplyDirective, *ast.EndDirective, *ast.CommentBlockDirective, *ast.IgnoredDirective:

      
        
        91
        +		a.Directives = append(a.Directives, entry)

      
        
        92
        +	}

      
        
        93
        +}

      
        
        94
        +

      
        
        95
        +func (a *Analysis) addAccountDirective(ad *ast.AccountDirective) {

      
        
        96
        +	aname := ad.Account.String()

      
        
        97
        +	info, ok := a.Accounts[aname]

      
        
        98
        +	if !ok {

      
        
        99
        +		info = &AccountInfo{}

      
        
        100
        +		a.Accounts[aname] = info

      
        
        101
        +	}

      
        
        102
        +	info.Directives = append(info.Directives, ad)

      
        
        103
        +}

      
        
        104
        +

      
        
        105
        +func (a *Analysis) addPayeeDirective(pd *ast.PayeeDirective) {

      
        
        106
        +	info, ok := a.Payees[pd.Name]

      
        
        107
        +	if !ok {

      
        
        108
        +		info = &PayeeInfo{}

      
        
        109
        +		a.Payees[pd.Name] = info

      
        
        110
        +	}

      
        
        111
        +	info.Directives = append(info.Directives, &ast.Payee{

      
        
        112
        +		Name: pd.Name,

      
        
        113
        +		Span: pd.Span,

      
        
        114
        +	})

      
        
        115
        +}

      
        
        116
        +

      
        
        117
        +func (a *Analysis) addCommodityDirective(cd *ast.CommodityDirective) {

      
        
        118
        +	info, ok := a.Commodities[cd.Commodity]

      
        
        119
        +	if !ok {

      
        
        120
        +		info = &CommodityInfo{}

      
        
        121
        +		a.Commodities[cd.Commodity] = info

      
        
        122
        +	}

      
        
        123
        +	info.Directives = append(info.Directives, cd)

      
        
        124
        +}

      
        
        125
        +

      
        
        126
        +func (a *Analysis) addPayee(fileIndex int, payee *ast.Payee) {

      
        
        127
        +	if payee == nil {

      
        
        128
        +		return

      
        
        129
        +	}

      
        
        130
        +	info, ok := a.Payees[payee.Name]

      
        
        131
        +	if !ok {

      
        
        132
        +		info = &PayeeInfo{}

      
        
        133
        +		a.Payees[payee.Name] = info

      
        
        134
        +	}

      
        
        135
        +	info.Usage = append(info.Usage, PayeeUsage{

      
        
        136
        +		FileIndex: fileIndex,

      
        
        137
        +		Payee:     payee,

      
        
        138
        +	})

      
        
        139
        +	info.UsedCount++

      
        
        140
        +}

      
        
        141
        +

      
        
        142
        +func (a *Analysis) addPostings(fileIndex int, postings []*ast.Posting, date *ast.Date) {

      
        
        143
        +	for _, posting := range postings {

      
        
        144
        +		aname := posting.Account.String()

      
        
        145
        +		info, ok := a.Accounts[aname]

      
        
        146
        +		if !ok {

      
        
        147
        +			info = &AccountInfo{}

      
        
        148
        +			a.Accounts[aname] = info

      
        
        149
        +		}

      
        
        150
        +		info.Usages = append(info.Usages, AccountUsage{

      
        
        151
        +			FileIndex: fileIndex,

      
        
        152
        +			Posting:   posting,

      
        
        153
        +		})

      
        
        154
        +		info.UsedCount++

      
        
        155
        +		if date != nil {

      
        
        156
        +			info.LastUsed = maxDate(info.LastUsed, *date)

      
        
        157
        +		}

      
        
        158
        +

      
        
        159
        +		a.addCommodityUsage(fileIndex, posting.Amount, date)

      
        
        160
        +		if posting.Cost != nil {

      
        
        161
        +			a.addCommodityUsage(fileIndex, &posting.Cost.Amount, date)

      
        
        162
        +		}

      
        
        163
        +		if posting.Balance != nil {

      
        
        164
        +			a.addCommodityUsage(fileIndex, &posting.Balance.Amount, date)

      
        
        165
        +			if posting.Balance.Cost != nil {

      
        
        166
        +				a.addCommodityUsage(fileIndex, &posting.Balance.Cost.Amount, date)

      
        
        167
        +			}

      
        
        168
        +		}

      
        
        169
        +

      
        
        170
        +		// Collect decimal mark from amount formatting.

      
        
        171
        +		if posting.Amount != nil && posting.Amount.Commodity != "" && posting.Amount.QuantityFmt.Decimal != 0 {

      
        
        172
        +			if _, ok := a.CommodityDecimalMarks[posting.Amount.Commodity]; !ok {

      
        
        173
        +				a.CommodityDecimalMarks[posting.Amount.Commodity] = posting.Amount.QuantityFmt.Decimal

      
        
        174
        +			}

      
        
        175
        +		}

      
        
        176
        +	}

      
        
        177
        +}

      
        
        178
        +

      
        
        179
        +func (a *Analysis) addPayeeTemplate(tx *ast.Transaction) {

      
        
        180
        +	payee := ""

      
        
        181
        +	if tx.Payee != nil {

      
        
        182
        +		payee = tx.Payee.Name

      
        
        183
        +	}

      
        
        184
        +	if payee == "" {

      
        
        185
        +		return

      
        
        186
        +	}

      
        
        187
        +

      
        
        188
        +	templates := make([]PostingTemplate, len(tx.Postings))

      
        
        189
        +	for i, p := range tx.Postings {

      
        
        190
        +		t := PostingTemplate{

      
        
        191
        +			Account:    p.Account.String(),

      
        
        192
        +			IsInferred: p.Amount == nil,

      
        
        193
        +		}

      
        
        194
        +		if p.Amount != nil {

      
        
        195
        +			t.Amount = p.Amount.Quantity.String()

      
        
        196
        +			t.Commodity = p.Amount.Commodity

      
        
        197
        +		}

      
        
        198
        +		templates[i] = t

      
        
        199
        +	}

      
        
        200
        +	a.PayeeTemplates[payee] = templates

      
        
        201
        +}

      
        
        202
        +

      
        
        203
        +func (a *Analysis) collectDates() {

      
        
        204
        +	seen := make(map[string]bool)

      
        
        205
        +	for _, tx := range a.Transactions {

      
        
        206
        +		s := formatDate(tx.Date)

      
        
        207
        +		if s != "" && !seen[s] {

      
        
        208
        +			seen[s] = true

      
        
        209
        +			a.Dates = append(a.Dates, tx.Date)

      
        
        210
        +			a.DateStrings = append(a.DateStrings, s)

      
        
        211
        +		}

      
        
        212
        +	}

      
        
        213
        +	sort.Slice(a.Dates, func(i, j int) bool {

      
        
        214
        +		return dateLess(a.Dates[i], a.Dates[j])

      
        
        215
        +	})

      
        
        216
        +	sort.Strings(a.DateStrings)

      
        
        217
        +}

      
        
        218
        +

      
        
        219
        +func (a *Analysis) collectTagNames() {

      
        
        220
        +	seen := make(map[string]bool)

      
        
        221
        +	for _, d := range a.Directives {

      
        
        222
        +		if td, ok := d.(*ast.TagDirective); ok && td.Name != "" && !seen[td.Name] {

      
        
        223
        +			seen[td.Name] = true

      
        
        224
        +			a.TagNames = append(a.TagNames, td.Name)

      
        
        225
        +		}

      
        
        226
        +	}

      
        
        227
        +	sort.Strings(a.TagNames)

      
        
        228
        +}

      
        
        229
        +

      
        
        230
        +func formatDate(d ast.Date) string {

      
        
        231
        +	if d.Year == 0 && d.Month == 0 && d.Day == 0 {

      
        
        232
        +		return ""

      
        
        233
        +	}

      
        
        234
        +	if d.Month < 1 || d.Month > 12 || d.Day < 1 || d.Day > 31 {

      
        
        235
        +		return ""

      
        
        236
        +	}

      
        
        237
        +	return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day)

      
        
        238
        +}

      
        
        239
        +

      
        
        240
        +func dateLess(a, b ast.Date) bool {

      
        
        241
        +	if a.Year != b.Year {

      
        
        242
        +		return a.Year < b.Year

      
        
        243
        +	}

      
        
        244
        +	if a.Month != b.Month {

      
        
        245
        +		return a.Month < b.Month

      
        
        246
        +	}

      
        
        247
        +	return a.Day < b.Day

      
        
        248
        +}

      
        
        249
        +

      
        
        250
        +func (a *Analysis) addCommodityUsage(fileIndex int, am *ast.Amount, date *ast.Date) {

      
        
        251
        +	if am == nil || am.Commodity == "" {

      
        
        252
        +		return

      
        
        253
        +	}

      
        
        254
        +	info, ok := a.Commodities[am.Commodity]

      
        
        255
        +	if !ok {

      
        
        256
        +		info = &CommodityInfo{}

      
        
        257
        +		a.Commodities[am.Commodity] = info

      
        
        258
        +	}

      
        
        259
        +	info.Usages = append(info.Usages, CommodityUsage{

      
        
        260
        +		FileIndex: fileIndex,

      
        
        261
        +		Amount:    am,

      
        
        262
        +	})

      
        
        263
        +	info.UsedCount++

      
        
        264
        +	if date != nil {

      
        
        265
        +		info.LastUsed = maxDate(info.LastUsed, *date)

      
        
        266
        +	}

      
        
        267
        +}

      
        
        268
        +

      
        
        269
        +func (a *Analysis) buildPrefixIndex() {

      
        
        270
        +	for name := range a.Accounts {

      
        
        271
        +		parts := strings.Split(name, ":")

      
        
        272
        +		for i := 1; i < len(parts); i++ {

      
        
        273
        +			prefix := strings.Join(parts[:i], ":") + ":"

      
        
        274
        +			a.AccountsByPrefix[prefix] = append(a.AccountsByPrefix[prefix], name)

      
        
        275
        +		}

      
        
        276
        +	}

      
        
        277
        +}

      
        
        278
        +

      
        
        279
        +func (a *Analysis) collectPayeeNames() {

      
        
        280
        +	names := make([]string, 0, len(a.Payees))

      
        
        281
        +	for name := range a.Payees {

      
        
        282
        +		names = append(names, name)

      
        
        283
        +	}

      
        
        284
        +	sort.Strings(names)

      
        
        285
        +	a.PayeeNames = names

      
        
        286
        +}

      
        
        287
        +

      
        
        288
        +func (a *Analysis) sortAccountNames() {

      
        
        289
        +	names := make([]string, 0, len(a.Accounts))

      
        
        290
        +	for name := range a.Accounts {

      
        
        291
        +		names = append(names, name)

      
        
        292
        +	}

      
        
        293
        +	sort.Strings(names)

      
        
        294
        +	a.AccountNames = names

      
        
        295
        +}

      
        
        296
        +

      
        
        297
        +func maxDate(a, b ast.Date) ast.Date {

      
        
        298
        +	if a.Year > b.Year ||

      
        
        299
        +		(a.Year == b.Year && (a.Month > b.Month ||

      
        
        300
        +			(a.Year == b.Year && a.Month == b.Month && a.Day > b.Day))) {

      
        
        301
        +		return a

      
        
        302
        +	}

      
        
        303
        +	return b

      
        
        304
        +}

      
A internal/analyzer/testdata/account-indexing.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +2024-01-01 groceries

      
        
        3
        +    expenses:food:groceries  $10

      
        
        4
        +    expenses:transport:fuel  $5

      
        
        5
        +    assets:bank:checking  $15

      
        
        6
        +

      
        
        7
        +2024-01-02 transport

      
        
        8
        +    expenses:transport:fuel  $20

      
        
        9
        +    assets:cash  $-20

      
        
        10
        +

      
        
        11
        +-- expect --

      
        
        12
        +files (1):

      
        
        13
        +  0: in.journal

      
        
        14
        +

      
        
        15
        +commodities (1):

      
        
        16
        +  $

      
        
        17
        +    directives: 0

      
        
        18
        +    used: 5

      
        
        19
        +    usages: 5

      
        
        20
        +      file 0: $ ("10")

      
        
        21
        +      file 0: $ ("5")

      
        
        22
        +      file 0: $ ("15")

      
        
        23
        +      file 0: $ ("20")

      
        
        24
        +      file 0: $ ("-20")

      
        
        25
        +    last-used: 2024-01-02

      
        
        26
        +

      
        
        27
        +accounts (4):

      
        
        28
        +  assets:bank:checking

      
        
        29
        +    directives: 0

      
        
        30
        +    used: 1

      
        
        31
        +    usages: 1

      
        
        32
        +      file 0: assets:bank:checking

      
        
        33
        +    last-used: 2024-01-01

      
        
        34
        +  assets:cash

      
        
        35
        +    directives: 0

      
        
        36
        +    used: 1

      
        
        37
        +    usages: 1

      
        
        38
        +      file 0: assets:cash

      
        
        39
        +    last-used: 2024-01-02

      
        
        40
        +  expenses:food:groceries

      
        
        41
        +    directives: 0

      
        
        42
        +    used: 1

      
        
        43
        +    usages: 1

      
        
        44
        +      file 0: expenses:food:groceries

      
        
        45
        +    last-used: 2024-01-01

      
        
        46
        +  expenses:transport:fuel

      
        
        47
        +    directives: 0

      
        
        48
        +    used: 2

      
        
        49
        +    usages: 2

      
        
        50
        +      file 0: expenses:transport:fuel

      
        
        51
        +      file 0: expenses:transport:fuel

      
        
        52
        +    last-used: 2024-01-02

      
        
        53
        +

      
        
        54
        +payees (2):

      
        
        55
        +  groceries

      
        
        56
        +    directives: 0

      
        
        57
        +    used: 1

      
        
        58
        +    usages: 1

      
        
        59
        +      file 0: groceries

      
        
        60
        +  transport

      
        
        61
        +    directives: 0

      
        
        62
        +    used: 1

      
        
        63
        +    usages: 1

      
        
        64
        +      file 0: transport

      
        
        65
        +

      
        
        66
        +prefixes (5):

      
        
        67
        +  assets:

      
        
        68
        +    assets:bank:checking

      
        
        69
        +    assets:cash

      
        
        70
        +  assets:bank:

      
        
        71
        +    assets:bank:checking

      
        
        72
        +  expenses:

      
        
        73
        +    expenses:food:groceries

      
        
        74
        +    expenses:transport:fuel

      
        
        75
        +  expenses:food:

      
        
        76
        +    expenses:food:groceries

      
        
        77
        +  expenses:transport:

      
        
        78
        +    expenses:transport:fuel

      
        
        79
        +

      
        
        80
        +transactions: 2

      
        
        81
        +periodic transactions: 0

      
        
        82
        +automated transactions: 0

      
        
        83
        +

      
        
        84
        +directives (0):

      
        
        85
        +

      
        
        86
        +transaction keys (2):

      
        
        87
        +  2024-01-01|groceries|expenses:food:groceries,expenses:transport:fuel,assets:bank:checking,

      
        
        88
        +  2024-01-02|transport|expenses:transport:fuel,assets:cash,

      
        
        89
        +

      
        
        90
        +payee templates (2):

      
        
        91
        +  groceries

      
        
        92
        +    expenses:food:groceries  $10

      
        
        93
        +    expenses:transport:fuel  $5

      
        
        94
        +    assets:bank:checking  $15

      
        
        95
        +  transport

      
        
        96
        +    expenses:transport:fuel  $20

      
        
        97
        +    assets:cash  $-20

      
        
        98
        +

      
        
        99
        +dates (2):

      
        
        100
        +  2024-01-01

      
        
        101
        +  2024-01-02

      
        
        102
        +

      
        
        103
        +tag names (0):

      
        
        104
        +

      
        
        105
        +commodity decimal marks (1):

      
        
        106
        +  $: .

      
A internal/analyzer/testdata/account-names-sorted.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +2024-01-01 groceries

      
        
        3
        +    expenses:food  $10

      
        
        4
        +    assets:cash  $-10

      
        
        5
        +

      
        
        6
        +2024-01-02 transport

      
        
        7
        +    expenses:transport:fuel  $20

      
        
        8
        +    assets:cash  $-20

      
        
        9
        +

      
        
        10
        +-- expect --

      
        
        11
        +files (1):

      
        
        12
        +  0: in.journal

      
        
        13
        +

      
        
        14
        +commodities (1):

      
        
        15
        +  $

      
        
        16
        +    directives: 0

      
        
        17
        +    used: 4

      
        
        18
        +    usages: 4

      
        
        19
        +      file 0: $ ("10")

      
        
        20
        +      file 0: $ ("-10")

      
        
        21
        +      file 0: $ ("20")

      
        
        22
        +      file 0: $ ("-20")

      
        
        23
        +    last-used: 2024-01-02

      
        
        24
        +

      
        
        25
        +accounts (3):

      
        
        26
        +  assets:cash

      
        
        27
        +    directives: 0

      
        
        28
        +    used: 2

      
        
        29
        +    usages: 2

      
        
        30
        +      file 0: assets:cash

      
        
        31
        +      file 0: assets:cash

      
        
        32
        +    last-used: 2024-01-02

      
        
        33
        +  expenses:food

      
        
        34
        +    directives: 0

      
        
        35
        +    used: 1

      
        
        36
        +    usages: 1

      
        
        37
        +      file 0: expenses:food

      
        
        38
        +    last-used: 2024-01-01

      
        
        39
        +  expenses:transport:fuel

      
        
        40
        +    directives: 0

      
        
        41
        +    used: 1

      
        
        42
        +    usages: 1

      
        
        43
        +      file 0: expenses:transport:fuel

      
        
        44
        +    last-used: 2024-01-02

      
        
        45
        +

      
        
        46
        +payees (2):

      
        
        47
        +  groceries

      
        
        48
        +    directives: 0

      
        
        49
        +    used: 1

      
        
        50
        +    usages: 1

      
        
        51
        +      file 0: groceries

      
        
        52
        +  transport

      
        
        53
        +    directives: 0

      
        
        54
        +    used: 1

      
        
        55
        +    usages: 1

      
        
        56
        +      file 0: transport

      
        
        57
        +

      
        
        58
        +prefixes (3):

      
        
        59
        +  assets:

      
        
        60
        +    assets:cash

      
        
        61
        +  expenses:

      
        
        62
        +    expenses:food

      
        
        63
        +    expenses:transport:fuel

      
        
        64
        +  expenses:transport:

      
        
        65
        +    expenses:transport:fuel

      
        
        66
        +

      
        
        67
        +transactions: 2

      
        
        68
        +periodic transactions: 0

      
        
        69
        +automated transactions: 0

      
        
        70
        +

      
        
        71
        +directives (0):

      
        
        72
        +

      
        
        73
        +transaction keys (2):

      
        
        74
        +  2024-01-01|groceries|expenses:food,assets:cash,

      
        
        75
        +  2024-01-02|transport|expenses:transport:fuel,assets:cash,

      
        
        76
        +

      
        
        77
        +payee templates (2):

      
        
        78
        +  groceries

      
        
        79
        +    expenses:food  $10

      
        
        80
        +    assets:cash  $-10

      
        
        81
        +  transport

      
        
        82
        +    expenses:transport:fuel  $20

      
        
        83
        +    assets:cash  $-20

      
        
        84
        +

      
        
        85
        +dates (2):

      
        
        86
        +  2024-01-01

      
        
        87
        +  2024-01-02

      
        
        88
        +

      
        
        89
        +tag names (0):

      
        
        90
        +

      
        
        91
        +commodity decimal marks (1):

      
        
        92
        +  $: .

      
A internal/analyzer/testdata/account-usage.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +2024-01-01 groceries

      
        
        3
        +    expenses:food  $10

      
        
        4
        +    assets:cash

      
        
        5
        +

      
        
        6
        +2024-01-02 lunch

      
        
        7
        +    expenses:food  $5

      
        
        8
        +    assets:cash

      
        
        9
        +

      
        
        10
        +2024-01-15 dinner

      
        
        11
        +    expenses:food  $20

      
        
        12
        +    assets:cash

      
        
        13
        +

      
        
        14
        +-- expect --

      
        
        15
        +files (1):

      
        
        16
        +  0: in.journal

      
        
        17
        +

      
        
        18
        +commodities (1):

      
        
        19
        +  $

      
        
        20
        +    directives: 0

      
        
        21
        +    used: 3

      
        
        22
        +    usages: 3

      
        
        23
        +      file 0: $ ("10")

      
        
        24
        +      file 0: $ ("5")

      
        
        25
        +      file 0: $ ("20")

      
        
        26
        +    last-used: 2024-01-15

      
        
        27
        +

      
        
        28
        +accounts (2):

      
        
        29
        +  assets:cash

      
        
        30
        +    directives: 0

      
        
        31
        +    used: 3

      
        
        32
        +    usages: 3

      
        
        33
        +      file 0: assets:cash

      
        
        34
        +      file 0: assets:cash

      
        
        35
        +      file 0: assets:cash

      
        
        36
        +    last-used: 2024-01-15

      
        
        37
        +  expenses:food

      
        
        38
        +    directives: 0

      
        
        39
        +    used: 3

      
        
        40
        +    usages: 3

      
        
        41
        +      file 0: expenses:food

      
        
        42
        +      file 0: expenses:food

      
        
        43
        +      file 0: expenses:food

      
        
        44
        +    last-used: 2024-01-15

      
        
        45
        +

      
        
        46
        +payees (3):

      
        
        47
        +  dinner

      
        
        48
        +    directives: 0

      
        
        49
        +    used: 1

      
        
        50
        +    usages: 1

      
        
        51
        +      file 0: dinner

      
        
        52
        +  groceries

      
        
        53
        +    directives: 0

      
        
        54
        +    used: 1

      
        
        55
        +    usages: 1

      
        
        56
        +      file 0: groceries

      
        
        57
        +  lunch

      
        
        58
        +    directives: 0

      
        
        59
        +    used: 1

      
        
        60
        +    usages: 1

      
        
        61
        +      file 0: lunch

      
        
        62
        +

      
        
        63
        +prefixes (2):

      
        
        64
        +  assets:

      
        
        65
        +    assets:cash

      
        
        66
        +  expenses:

      
        
        67
        +    expenses:food

      
        
        68
        +

      
        
        69
        +transactions: 3

      
        
        70
        +periodic transactions: 0

      
        
        71
        +automated transactions: 0

      
        
        72
        +

      
        
        73
        +directives (0):

      
        
        74
        +

      
        
        75
        +transaction keys (3):

      
        
        76
        +  2024-01-01|groceries|expenses:food,assets:cash,

      
        
        77
        +  2024-01-02|lunch|expenses:food,assets:cash,

      
        
        78
        +  2024-01-15|dinner|expenses:food,assets:cash,

      
        
        79
        +

      
        
        80
        +payee templates (3):

      
        
        81
        +  dinner

      
        
        82
        +    expenses:food  $20

      
        
        83
        +    assets:cash (inferred)

      
        
        84
        +  groceries

      
        
        85
        +    expenses:food  $10

      
        
        86
        +    assets:cash (inferred)

      
        
        87
        +  lunch

      
        
        88
        +    expenses:food  $5

      
        
        89
        +    assets:cash (inferred)

      
        
        90
        +

      
        
        91
        +dates (3):

      
        
        92
        +  2024-01-01

      
        
        93
        +  2024-01-02

      
        
        94
        +  2024-01-15

      
        
        95
        +

      
        
        96
        +tag names (0):

      
        
        97
        +

      
        
        98
        +commodity decimal marks (1):

      
        
        99
        +  $: .

      
A internal/analyzer/testdata/commodity-usage.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +2024-01-01 groceries

      
        
        3
        +    expenses:food  $10

      
        
        4
        +    assets:cash

      
        
        5
        +

      
        
        6
        +2024-01-02 exchange

      
        
        7
        +    expenses:food  EUR 5

      
        
        8
        +    assets:cash

      
        
        9
        +

      
        
        10
        +2024-01-03 market

      
        
        11
        +    expenses:food  10 AAPL

      
        
        12
        +    assets:cash

      
        
        13
        +

      
        
        14
        +-- expect --

      
        
        15
        +files (1):

      
        
        16
        +  0: in.journal

      
        
        17
        +

      
        
        18
        +commodities (3):

      
        
        19
        +  $

      
        
        20
        +    directives: 0

      
        
        21
        +    used: 1

      
        
        22
        +    usages: 1

      
        
        23
        +      file 0: $ ("10")

      
        
        24
        +    last-used: 2024-01-01

      
        
        25
        +  AAPL

      
        
        26
        +    directives: 0

      
        
        27
        +    used: 1

      
        
        28
        +    usages: 1

      
        
        29
        +      file 0: AAPL ("10")

      
        
        30
        +    last-used: 2024-01-03

      
        
        31
        +  EUR

      
        
        32
        +    directives: 0

      
        
        33
        +    used: 1

      
        
        34
        +    usages: 1

      
        
        35
        +      file 0: EUR ("5")

      
        
        36
        +    last-used: 2024-01-02

      
        
        37
        +

      
        
        38
        +accounts (2):

      
        
        39
        +  assets:cash

      
        
        40
        +    directives: 0

      
        
        41
        +    used: 3

      
        
        42
        +    usages: 3

      
        
        43
        +      file 0: assets:cash

      
        
        44
        +      file 0: assets:cash

      
        
        45
        +      file 0: assets:cash

      
        
        46
        +    last-used: 2024-01-03

      
        
        47
        +  expenses:food

      
        
        48
        +    directives: 0

      
        
        49
        +    used: 3

      
        
        50
        +    usages: 3

      
        
        51
        +      file 0: expenses:food

      
        
        52
        +      file 0: expenses:food

      
        
        53
        +      file 0: expenses:food

      
        
        54
        +    last-used: 2024-01-03

      
        
        55
        +

      
        
        56
        +payees (3):

      
        
        57
        +  exchange

      
        
        58
        +    directives: 0

      
        
        59
        +    used: 1

      
        
        60
        +    usages: 1

      
        
        61
        +      file 0: exchange

      
        
        62
        +  groceries

      
        
        63
        +    directives: 0

      
        
        64
        +    used: 1

      
        
        65
        +    usages: 1

      
        
        66
        +      file 0: groceries

      
        
        67
        +  market

      
        
        68
        +    directives: 0

      
        
        69
        +    used: 1

      
        
        70
        +    usages: 1

      
        
        71
        +      file 0: market

      
        
        72
        +

      
        
        73
        +prefixes (2):

      
        
        74
        +  assets:

      
        
        75
        +    assets:cash

      
        
        76
        +  expenses:

      
        
        77
        +    expenses:food

      
        
        78
        +

      
        
        79
        +transactions: 3

      
        
        80
        +periodic transactions: 0

      
        
        81
        +automated transactions: 0

      
        
        82
        +

      
        
        83
        +directives (0):

      
        
        84
        +

      
        
        85
        +transaction keys (3):

      
        
        86
        +  2024-01-01|groceries|expenses:food,assets:cash,

      
        
        87
        +  2024-01-02|exchange|expenses:food,assets:cash,

      
        
        88
        +  2024-01-03|market|expenses:food,assets:cash,

      
        
        89
        +

      
        
        90
        +payee templates (3):

      
        
        91
        +  exchange

      
        
        92
        +    expenses:food  EUR5

      
        
        93
        +    assets:cash (inferred)

      
        
        94
        +  groceries

      
        
        95
        +    expenses:food  $10

      
        
        96
        +    assets:cash (inferred)

      
        
        97
        +  market

      
        
        98
        +    expenses:food  AAPL10

      
        
        99
        +    assets:cash (inferred)

      
        
        100
        +

      
        
        101
        +dates (3):

      
        
        102
        +  2024-01-01

      
        
        103
        +  2024-01-02

      
        
        104
        +  2024-01-03

      
        
        105
        +

      
        
        106
        +tag names (0):

      
        
        107
        +

      
        
        108
        +commodity decimal marks (3):

      
        
        109
        +  $: .

      
        
        110
        +  AAPL: .

      
        
        111
        +  EUR: .

      
A internal/analyzer/testdata/cost-balance.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +2024-01-01 * buy stock

      
        
        3
        +    assets:stocks  10 AAPL @ $150.00

      
        
        4
        +    assets:bank  $-1500.00

      
        
        5
        +    assets:stocks  = 100 AAPL

      
        
        6
        +

      
        
        7
        +-- expect --

      
        
        8
        +files (1):

      
        
        9
        +  0: in.journal

      
        
        10
        +

      
        
        11
        +commodities (2):

      
        
        12
        +  $

      
        
        13
        +    directives: 0

      
        
        14
        +    used: 2

      
        
        15
        +    usages: 2

      
        
        16
        +      file 0: $ ("150")

      
        
        17
        +      file 0: $ ("-1500")

      
        
        18
        +    last-used: 2024-01-01

      
        
        19
        +  AAPL

      
        
        20
        +    directives: 0

      
        
        21
        +    used: 2

      
        
        22
        +    usages: 2

      
        
        23
        +      file 0: AAPL ("10")

      
        
        24
        +      file 0: AAPL ("100")

      
        
        25
        +    last-used: 2024-01-01

      
        
        26
        +

      
        
        27
        +accounts (2):

      
        
        28
        +  assets:bank

      
        
        29
        +    directives: 0

      
        
        30
        +    used: 1

      
        
        31
        +    usages: 1

      
        
        32
        +      file 0: assets:bank

      
        
        33
        +    last-used: 2024-01-01

      
        
        34
        +  assets:stocks

      
        
        35
        +    directives: 0

      
        
        36
        +    used: 2

      
        
        37
        +    usages: 2

      
        
        38
        +      file 0: assets:stocks

      
        
        39
        +      file 0: assets:stocks

      
        
        40
        +    last-used: 2024-01-01

      
        
        41
        +

      
        
        42
        +payees (1):

      
        
        43
        +  buy stock

      
        
        44
        +    directives: 0

      
        
        45
        +    used: 1

      
        
        46
        +    usages: 1

      
        
        47
        +      file 0: buy stock

      
        
        48
        +

      
        
        49
        +prefixes (1):

      
        
        50
        +  assets:

      
        
        51
        +    assets:bank

      
        
        52
        +    assets:stocks

      
        
        53
        +

      
        
        54
        +transactions: 1

      
        
        55
        +periodic transactions: 0

      
        
        56
        +automated transactions: 0

      
        
        57
        +

      
        
        58
        +directives (0):

      
        
        59
        +

      
        
        60
        +transaction keys (1):

      
        
        61
        +  2024-01-01|buy stock|assets:stocks,assets:bank,assets:stocks,

      
        
        62
        +

      
        
        63
        +payee templates (1):

      
        
        64
        +  buy stock

      
        
        65
        +    assets:stocks  AAPL10

      
        
        66
        +    assets:bank  $-1500

      
        
        67
        +    assets:stocks (inferred)

      
        
        68
        +

      
        
        69
        +dates (1):

      
        
        70
        +  2024-01-01

      
        
        71
        +

      
        
        72
        +tag names (0):

      
        
        73
        +

      
        
        74
        +commodity decimal marks (2):

      
        
        75
        +  $: .

      
        
        76
        +  AAPL: .

      
A internal/analyzer/testdata/dates-dedup.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +2024-01-01 groceries

      
        
        3
        +    expenses:food  $10

      
        
        4
        +    assets:cash

      
        
        5
        +

      
        
        6
        +2024-01-01 lunch

      
        
        7
        +    expenses:food  $5

      
        
        8
        +    assets:cash

      
        
        9
        +

      
        
        10
        +-- expect --

      
        
        11
        +files (1):

      
        
        12
        +  0: in.journal

      
        
        13
        +

      
        
        14
        +commodities (1):

      
        
        15
        +  $

      
        
        16
        +    directives: 0

      
        
        17
        +    used: 2

      
        
        18
        +    usages: 2

      
        
        19
        +      file 0: $ ("10")

      
        
        20
        +      file 0: $ ("5")

      
        
        21
        +    last-used: 2024-01-01

      
        
        22
        +

      
        
        23
        +accounts (2):

      
        
        24
        +  assets:cash

      
        
        25
        +    directives: 0

      
        
        26
        +    used: 2

      
        
        27
        +    usages: 2

      
        
        28
        +      file 0: assets:cash

      
        
        29
        +      file 0: assets:cash

      
        
        30
        +    last-used: 2024-01-01

      
        
        31
        +  expenses:food

      
        
        32
        +    directives: 0

      
        
        33
        +    used: 2

      
        
        34
        +    usages: 2

      
        
        35
        +      file 0: expenses:food

      
        
        36
        +      file 0: expenses:food

      
        
        37
        +    last-used: 2024-01-01

      
        
        38
        +

      
        
        39
        +payees (2):

      
        
        40
        +  groceries

      
        
        41
        +    directives: 0

      
        
        42
        +    used: 1

      
        
        43
        +    usages: 1

      
        
        44
        +      file 0: groceries

      
        
        45
        +  lunch

      
        
        46
        +    directives: 0

      
        
        47
        +    used: 1

      
        
        48
        +    usages: 1

      
        
        49
        +      file 0: lunch

      
        
        50
        +

      
        
        51
        +prefixes (2):

      
        
        52
        +  assets:

      
        
        53
        +    assets:cash

      
        
        54
        +  expenses:

      
        
        55
        +    expenses:food

      
        
        56
        +

      
        
        57
        +transactions: 2

      
        
        58
        +periodic transactions: 0

      
        
        59
        +automated transactions: 0

      
        
        60
        +

      
        
        61
        +directives (0):

      
        
        62
        +

      
        
        63
        +transaction keys (2):

      
        
        64
        +  2024-01-01|groceries|expenses:food,assets:cash,

      
        
        65
        +  2024-01-01|lunch|expenses:food,assets:cash,

      
        
        66
        +

      
        
        67
        +payee templates (2):

      
        
        68
        +  groceries

      
        
        69
        +    expenses:food  $10

      
        
        70
        +    assets:cash (inferred)

      
        
        71
        +  lunch

      
        
        72
        +    expenses:food  $5

      
        
        73
        +    assets:cash (inferred)

      
        
        74
        +

      
        
        75
        +dates (1):

      
        
        76
        +  2024-01-01

      
        
        77
        +

      
        
        78
        +tag names (0):

      
        
        79
        +

      
        
        80
        +commodity decimal marks (1):

      
        
        81
        +  $: .

      
A internal/analyzer/testdata/dates.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +2024-01-03 third

      
        
        3
        +    expenses:food  $10

      
        
        4
        +    assets:cash

      
        
        5
        +

      
        
        6
        +2024-01-01 first

      
        
        7
        +    expenses:food  $5

      
        
        8
        +    assets:cash

      
        
        9
        +

      
        
        10
        +2024-01-01 also-first

      
        
        11
        +    expenses:food  $5

      
        
        12
        +    assets:cash

      
        
        13
        +

      
        
        14
        +2024-01-02 second

      
        
        15
        +    expenses:food  $20

      
        
        16
        +    assets:cash

      
        
        17
        +

      
        
        18
        +-- expect --

      
        
        19
        +files (1):

      
        
        20
        +  0: in.journal

      
        
        21
        +

      
        
        22
        +commodities (1):

      
        
        23
        +  $

      
        
        24
        +    directives: 0

      
        
        25
        +    used: 4

      
        
        26
        +    usages: 4

      
        
        27
        +      file 0: $ ("10")

      
        
        28
        +      file 0: $ ("5")

      
        
        29
        +      file 0: $ ("5")

      
        
        30
        +      file 0: $ ("20")

      
        
        31
        +    last-used: 2024-01-03

      
        
        32
        +

      
        
        33
        +accounts (2):

      
        
        34
        +  assets:cash

      
        
        35
        +    directives: 0

      
        
        36
        +    used: 4

      
        
        37
        +    usages: 4

      
        
        38
        +      file 0: assets:cash

      
        
        39
        +      file 0: assets:cash

      
        
        40
        +      file 0: assets:cash

      
        
        41
        +      file 0: assets:cash

      
        
        42
        +    last-used: 2024-01-03

      
        
        43
        +  expenses:food

      
        
        44
        +    directives: 0

      
        
        45
        +    used: 4

      
        
        46
        +    usages: 4

      
        
        47
        +      file 0: expenses:food

      
        
        48
        +      file 0: expenses:food

      
        
        49
        +      file 0: expenses:food

      
        
        50
        +      file 0: expenses:food

      
        
        51
        +    last-used: 2024-01-03

      
        
        52
        +

      
        
        53
        +payees (4):

      
        
        54
        +  also-first

      
        
        55
        +    directives: 0

      
        
        56
        +    used: 1

      
        
        57
        +    usages: 1

      
        
        58
        +      file 0: also-first

      
        
        59
        +  first

      
        
        60
        +    directives: 0

      
        
        61
        +    used: 1

      
        
        62
        +    usages: 1

      
        
        63
        +      file 0: first

      
        
        64
        +  second

      
        
        65
        +    directives: 0

      
        
        66
        +    used: 1

      
        
        67
        +    usages: 1

      
        
        68
        +      file 0: second

      
        
        69
        +  third

      
        
        70
        +    directives: 0

      
        
        71
        +    used: 1

      
        
        72
        +    usages: 1

      
        
        73
        +      file 0: third

      
        
        74
        +

      
        
        75
        +prefixes (2):

      
        
        76
        +  assets:

      
        
        77
        +    assets:cash

      
        
        78
        +  expenses:

      
        
        79
        +    expenses:food

      
        
        80
        +

      
        
        81
        +transactions: 4

      
        
        82
        +periodic transactions: 0

      
        
        83
        +automated transactions: 0

      
        
        84
        +

      
        
        85
        +directives (0):

      
        
        86
        +

      
        
        87
        +transaction keys (4):

      
        
        88
        +  2024-01-01|also-first|expenses:food,assets:cash,

      
        
        89
        +  2024-01-01|first|expenses:food,assets:cash,

      
        
        90
        +  2024-01-02|second|expenses:food,assets:cash,

      
        
        91
        +  2024-01-03|third|expenses:food,assets:cash,

      
        
        92
        +

      
        
        93
        +payee templates (4):

      
        
        94
        +  also-first

      
        
        95
        +    expenses:food  $5

      
        
        96
        +    assets:cash (inferred)

      
        
        97
        +  first

      
        
        98
        +    expenses:food  $5

      
        
        99
        +    assets:cash (inferred)

      
        
        100
        +  second

      
        
        101
        +    expenses:food  $20

      
        
        102
        +    assets:cash (inferred)

      
        
        103
        +  third

      
        
        104
        +    expenses:food  $10

      
        
        105
        +    assets:cash (inferred)

      
        
        106
        +

      
        
        107
        +dates (3):

      
        
        108
        +  2024-01-01

      
        
        109
        +  2024-01-02

      
        
        110
        +  2024-01-03

      
        
        111
        +

      
        
        112
        +tag names (0):

      
        
        113
        +

      
        
        114
        +commodity decimal marks (1):

      
        
        115
        +  $: .

      
A internal/analyzer/testdata/decimal-marks.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +D $1,000.00

      
        
        3
        +

      
        
        4
        +2024-01-01 groceries

      
        
        5
        +    expenses:food  $10.50

      
        
        6
        +    assets:cash

      
        
        7
        +

      
        
        8
        +-- expect --

      
        
        9
        +files (1):

      
        
        10
        +  0: in.journal

      
        
        11
        +

      
        
        12
        +commodities (1):

      
        
        13
        +  $

      
        
        14
        +    directives: 0

      
        
        15
        +    used: 1

      
        
        16
        +    usages: 1

      
        
        17
        +      file 0: $ ("10.5")

      
        
        18
        +    last-used: 2024-01-01

      
        
        19
        +

      
        
        20
        +accounts (2):

      
        
        21
        +  assets:cash

      
        
        22
        +    directives: 0

      
        
        23
        +    used: 1

      
        
        24
        +    usages: 1

      
        
        25
        +      file 0: assets:cash

      
        
        26
        +    last-used: 2024-01-01

      
        
        27
        +  expenses:food

      
        
        28
        +    directives: 0

      
        
        29
        +    used: 1

      
        
        30
        +    usages: 1

      
        
        31
        +      file 0: expenses:food

      
        
        32
        +    last-used: 2024-01-01

      
        
        33
        +

      
        
        34
        +payees (1):

      
        
        35
        +  groceries

      
        
        36
        +    directives: 0

      
        
        37
        +    used: 1

      
        
        38
        +    usages: 1

      
        
        39
        +      file 0: groceries

      
        
        40
        +

      
        
        41
        +prefixes (2):

      
        
        42
        +  assets:

      
        
        43
        +    assets:cash

      
        
        44
        +  expenses:

      
        
        45
        +    expenses:food

      
        
        46
        +

      
        
        47
        +transactions: 1

      
        
        48
        +periodic transactions: 0

      
        
        49
        +automated transactions: 0

      
        
        50
        +

      
        
        51
        +directives (1):

      
        
        52
        +  D $1000

      
        
        53
        +

      
        
        54
        +transaction keys (1):

      
        
        55
        +  2024-01-01|groceries|expenses:food,assets:cash,

      
        
        56
        +

      
        
        57
        +payee templates (1):

      
        
        58
        +  groceries

      
        
        59
        +    expenses:food  $10.5

      
        
        60
        +    assets:cash (inferred)

      
        
        61
        +

      
        
        62
        +dates (1):

      
        
        63
        +  2024-01-01

      
        
        64
        +

      
        
        65
        +tag names (0):

      
        
        66
        +

      
        
        67
        +commodity decimal marks (1):

      
        
        68
        +  $: .

      
A internal/analyzer/testdata/directives.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +account expenses:food

      
        
        3
        +commodity $

      
        
        4
        +payee TestPayee

      
        
        5
        +tag mytag

      
        
        6
        +include other.journal

      
        
        7
        +alias expenses:food = expenses:food:old

      
        
        8
        +year 2024

      
        
        9
        +D $1,000.00

      
        
        10
        +P 2024-01-01 AAPL $150.00

      
        
        11
        +C $100.00 @@ EUR 90.00

      
        
        12
        +apply tag foo

      
        
        13
        +end tag

      
        
        14
        +comment

      
        
        15
        +some ignored text

      
        
        16
        +N ignored

      
        
        17
        +

      
        
        18
        +-- expect --

      
        
        19
        +files (1):

      
        
        20
        +  0: in.journal

      
        
        21
        +

      
        
        22
        +commodities (1):

      
        
        23
        +  $

      
        
        24
        +    directives: 1

      
        
        25
        +    used: 0

      
        
        26
        +      commodity $

      
        
        27
        +    usages: 0

      
        
        28
        +

      
        
        29
        +accounts (1):

      
        
        30
        +  expenses:food

      
        
        31
        +    directives: 1

      
        
        32
        +    used: 0

      
        
        33
        +      account expenses:food

      
        
        34
        +    usages: 0

      
        
        35
        +

      
        
        36
        +payees (1):

      
        
        37
        +  

      
        
        38
        +    directives: 1

      
        
        39
        +    used: 0

      
        
        40
        +      payee 

      
        
        41
        +    usages: 0

      
        
        42
        +

      
        
        43
        +prefixes (1):

      
        
        44
        +  expenses:

      
        
        45
        +    expenses:food

      
        
        46
        +

      
        
        47
        +transactions: 0

      
        
        48
        +periodic transactions: 0

      
        
        49
        +automated transactions: 0

      
        
        50
        +

      
        
        51
        +directives (13):

      
        
        52
        +  account expenses:food

      
        
        53
        +  commodity $

      
        
        54
        +  payee 

      
        
        55
        +  tag mytag

      
        
        56
        +  include other.journal

      
        
        57
        +  alias expenses:food = expenses:food:old

      
        
        58
        +  year 2024

      
        
        59
        +  D $1000

      
        
        60
        +  P AAPL 150

      
        
        61
        +  C $100 @@ 0

      
        
        62
        +  apply tag foo

      
        
        63
        +  end tag

      
        
        64
        +  comment

      
        
        65
        +

      
        
        66
        +transaction keys (0):

      
        
        67
        +

      
        
        68
        +payee templates (0):

      
        
        69
        +

      
        
        70
        +dates (0):

      
        
        71
        +

      
        
        72
        +tag names (1):

      
        
        73
        +  mytag

      
        
        74
        +

      
        
        75
        +commodity decimal marks (1):

      
        
        76
        +  $: .

      
A internal/analyzer/testdata/duplicate-transactions.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +2024-01-01 groceries

      
        
        3
        +    expenses:food  $10

      
        
        4
        +    assets:cash

      
        
        5
        +

      
        
        6
        +2024-01-01 groceries

      
        
        7
        +    expenses:food  $10

      
        
        8
        +    assets:cash

      
        
        9
        +

      
        
        10
        +-- expect --

      
        
        11
        +files (1):

      
        
        12
        +  0: in.journal

      
        
        13
        +

      
        
        14
        +commodities (1):

      
        
        15
        +  $

      
        
        16
        +    directives: 0

      
        
        17
        +    used: 2

      
        
        18
        +    usages: 2

      
        
        19
        +      file 0: $ ("10")

      
        
        20
        +      file 0: $ ("10")

      
        
        21
        +    last-used: 2024-01-01

      
        
        22
        +

      
        
        23
        +accounts (2):

      
        
        24
        +  assets:cash

      
        
        25
        +    directives: 0

      
        
        26
        +    used: 2

      
        
        27
        +    usages: 2

      
        
        28
        +      file 0: assets:cash

      
        
        29
        +      file 0: assets:cash

      
        
        30
        +    last-used: 2024-01-01

      
        
        31
        +  expenses:food

      
        
        32
        +    directives: 0

      
        
        33
        +    used: 2

      
        
        34
        +    usages: 2

      
        
        35
        +      file 0: expenses:food

      
        
        36
        +      file 0: expenses:food

      
        
        37
        +    last-used: 2024-01-01

      
        
        38
        +

      
        
        39
        +payees (1):

      
        
        40
        +  groceries

      
        
        41
        +    directives: 0

      
        
        42
        +    used: 2

      
        
        43
        +    usages: 2

      
        
        44
        +      file 0: groceries

      
        
        45
        +      file 0: groceries

      
        
        46
        +

      
        
        47
        +prefixes (2):

      
        
        48
        +  assets:

      
        
        49
        +    assets:cash

      
        
        50
        +  expenses:

      
        
        51
        +    expenses:food

      
        
        52
        +

      
        
        53
        +transactions: 2

      
        
        54
        +periodic transactions: 0

      
        
        55
        +automated transactions: 0

      
        
        56
        +

      
        
        57
        +directives (0):

      
        
        58
        +

      
        
        59
        +transaction keys (1):

      
        
        60
        +  2024-01-01|groceries|expenses:food,assets:cash, (dup: 2)

      
        
        61
        +

      
        
        62
        +payee templates (1):

      
        
        63
        +  groceries

      
        
        64
        +    expenses:food  $10

      
        
        65
        +    assets:cash (inferred)

      
        
        66
        +

      
        
        67
        +dates (1):

      
        
        68
        +  2024-01-01

      
        
        69
        +

      
        
        70
        +tag names (0):

      
        
        71
        +

      
        
        72
        +commodity decimal marks (1):

      
        
        73
        +  $: .

      
A internal/analyzer/testdata/empty.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +

      
        
        3
        +-- expect --

      
        
        4
        +files (1):

      
        
        5
        +  0: in.journal

      
        
        6
        +

      
        
        7
        +commodities (0):

      
        
        8
        +

      
        
        9
        +accounts (0):

      
        
        10
        +

      
        
        11
        +payees (0):

      
        
        12
        +

      
        
        13
        +prefixes (0):

      
        
        14
        +

      
        
        15
        +transactions: 0

      
        
        16
        +periodic transactions: 0

      
        
        17
        +automated transactions: 0

      
        
        18
        +

      
        
        19
        +directives (0):

      
        
        20
        +

      
        
        21
        +transaction keys (0):

      
        
        22
        +

      
        
        23
        +payee templates (0):

      
        
        24
        +

      
        
        25
        +dates (0):

      
        
        26
        +

      
        
        27
        +tag names (0):

      
        
        28
        +

      
        
        29
        +commodity decimal marks (0):

      
A internal/analyzer/testdata/include.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +include accounts.journal

      
        
        3
        +

      
        
        4
        +2024/01/01 ! test

      
        
        5
        +    expenses:food  $10

      
        
        6
        +    assets:checking

      
        
        7
        +

      
        
        8
        +-- accounts.journal --

      
        
        9
        +account expenses:food

      
        
        10
        +

      
        
        11
        +-- expect --

      
        
        12
        +files (2):

      
        
        13
        +  0: accounts.journal

      
        
        14
        +  1: in.journal

      
        
        15
        +

      
        
        16
        +commodities (1):

      
        
        17
        +  $

      
        
        18
        +    directives: 0

      
        
        19
        +    used: 1

      
        
        20
        +    usages: 1

      
        
        21
        +      file 1: $ ("10")

      
        
        22
        +    last-used: 2024-01-01

      
        
        23
        +

      
        
        24
        +accounts (2):

      
        
        25
        +  assets:checking

      
        
        26
        +    directives: 0

      
        
        27
        +    used: 1

      
        
        28
        +    usages: 1

      
        
        29
        +      file 1: assets:checking

      
        
        30
        +    last-used: 2024-01-01

      
        
        31
        +  expenses:food

      
        
        32
        +    directives: 1

      
        
        33
        +    used: 1

      
        
        34
        +      account expenses:food

      
        
        35
        +    usages: 1

      
        
        36
        +      file 1: expenses:food

      
        
        37
        +    last-used: 2024-01-01

      
        
        38
        +

      
        
        39
        +payees (1):

      
        
        40
        +  test

      
        
        41
        +    directives: 0

      
        
        42
        +    used: 1

      
        
        43
        +    usages: 1

      
        
        44
        +      file 1: test

      
        
        45
        +

      
        
        46
        +prefixes (2):

      
        
        47
        +  assets:

      
        
        48
        +    assets:checking

      
        
        49
        +  expenses:

      
        
        50
        +    expenses:food

      
        
        51
        +

      
        
        52
        +transactions: 1

      
        
        53
        +periodic transactions: 0

      
        
        54
        +automated transactions: 0

      
        
        55
        +

      
        
        56
        +directives (2):

      
        
        57
        +  account expenses:food

      
        
        58
        +  include accounts.journal

      
        
        59
        +

      
        
        60
        +transaction keys (1):

      
        
        61
        +  2024-01-01|test|expenses:food,assets:checking,

      
        
        62
        +

      
        
        63
        +payee templates (1):

      
        
        64
        +  test

      
        
        65
        +    expenses:food  $10

      
        
        66
        +    assets:checking (inferred)

      
        
        67
        +

      
        
        68
        +dates (1):

      
        
        69
        +  2024-01-01

      
        
        70
        +

      
        
        71
        +tag names (0):

      
        
        72
        +

      
        
        73
        +commodity decimal marks (1):

      
        
        74
        +  $: .

      
A internal/analyzer/testdata/journal.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +account expenses:food

      
        
        3
        +account expenses:food

      
        
        4
        +account expenses:transport

      
        
        5
        +

      
        
        6
        +commodity $

      
        
        7
        +commodity $

      
        
        8
        +commodity EUR

      
        
        9
        +

      
        
        10
        +~ monthly

      
        
        11
        +    expenses:transport  $50

      
        
        12
        +    assets:checking

      
        
        13
        +

      
        
        14
        += expr:big

      
        
        15
        +    expenses:food  $100

      
        
        16
        +    assets:credit

      
        
        17
        +

      
        
        18
        +2024/01/01 groceries

      
        
        19
        +    expenses:food  $10

      
        
        20
        +    assets:checking

      
        
        21
        +

      
        
        22
        +2024/01/02 lunch

      
        
        23
        +    expenses:food  $5

      
        
        24
        +    assets:cash

      
        
        25
        +

      
        
        26
        +2024/01/04 aws

      
        
        27
        +    expenses:food  1000000.00 USD

      
        
        28
        +    assets:cash

      
        
        29
        +

      
        
        30
        +-- expect --

      
        
        31
        +files (1):

      
        
        32
        +  0: in.journal

      
        
        33
        +

      
        
        34
        +commodities (3):

      
        
        35
        +  $

      
        
        36
        +    directives: 2

      
        
        37
        +    used: 4

      
        
        38
        +      commodity $

      
        
        39
        +      commodity $

      
        
        40
        +    usages: 4

      
        
        41
        +      file 0: $ ("50")

      
        
        42
        +      file 0: $ ("100")

      
        
        43
        +      file 0: $ ("10")

      
        
        44
        +      file 0: $ ("5")

      
        
        45
        +    last-used: 2024-01-02

      
        
        46
        +  EUR

      
        
        47
        +    directives: 1

      
        
        48
        +    used: 0

      
        
        49
        +      commodity EUR

      
        
        50
        +    usages: 0

      
        
        51
        +  USD

      
        
        52
        +    directives: 0

      
        
        53
        +    used: 1

      
        
        54
        +    usages: 1

      
        
        55
        +      file 0: USD ("1000000")

      
        
        56
        +    last-used: 2024-01-04

      
        
        57
        +

      
        
        58
        +accounts (5):

      
        
        59
        +  assets:cash

      
        
        60
        +    directives: 0

      
        
        61
        +    used: 2

      
        
        62
        +    usages: 2

      
        
        63
        +      file 0: assets:cash

      
        
        64
        +      file 0: assets:cash

      
        
        65
        +    last-used: 2024-01-04

      
        
        66
        +  assets:checking

      
        
        67
        +    directives: 0

      
        
        68
        +    used: 2

      
        
        69
        +    usages: 2

      
        
        70
        +      file 0: assets:checking

      
        
        71
        +      file 0: assets:checking

      
        
        72
        +    last-used: 2024-01-01

      
        
        73
        +  assets:credit

      
        
        74
        +    directives: 0

      
        
        75
        +    used: 1

      
        
        76
        +    usages: 1

      
        
        77
        +      file 0: assets:credit

      
        
        78
        +  expenses:food

      
        
        79
        +    directives: 2

      
        
        80
        +    used: 4

      
        
        81
        +      account expenses:food

      
        
        82
        +      account expenses:food

      
        
        83
        +    usages: 4

      
        
        84
        +      file 0: expenses:food

      
        
        85
        +      file 0: expenses:food

      
        
        86
        +      file 0: expenses:food

      
        
        87
        +      file 0: expenses:food

      
        
        88
        +    last-used: 2024-01-04

      
        
        89
        +  expenses:transport

      
        
        90
        +    directives: 1

      
        
        91
        +    used: 1

      
        
        92
        +      account expenses:transport

      
        
        93
        +    usages: 1

      
        
        94
        +      file 0: expenses:transport

      
        
        95
        +

      
        
        96
        +payees (3):

      
        
        97
        +  aws

      
        
        98
        +    directives: 0

      
        
        99
        +    used: 1

      
        
        100
        +    usages: 1

      
        
        101
        +      file 0: aws

      
        
        102
        +  groceries

      
        
        103
        +    directives: 0

      
        
        104
        +    used: 1

      
        
        105
        +    usages: 1

      
        
        106
        +      file 0: groceries

      
        
        107
        +  lunch

      
        
        108
        +    directives: 0

      
        
        109
        +    used: 1

      
        
        110
        +    usages: 1

      
        
        111
        +      file 0: lunch

      
        
        112
        +

      
        
        113
        +prefixes (2):

      
        
        114
        +  assets:

      
        
        115
        +    assets:cash

      
        
        116
        +    assets:checking

      
        
        117
        +    assets:credit

      
        
        118
        +  expenses:

      
        
        119
        +    expenses:food

      
        
        120
        +    expenses:transport

      
        
        121
        +

      
        
        122
        +transactions: 3

      
        
        123
        +periodic transactions: 1

      
        
        124
        +automated transactions: 1

      
        
        125
        +

      
        
        126
        +directives (6):

      
        
        127
        +  account expenses:food

      
        
        128
        +  account expenses:food

      
        
        129
        +  account expenses:transport

      
        
        130
        +  commodity $

      
        
        131
        +  commodity $

      
        
        132
        +  commodity EUR

      
        
        133
        +

      
        
        134
        +transaction keys (3):

      
        
        135
        +  2024-01-01|groceries|expenses:food,assets:checking,

      
        
        136
        +  2024-01-02|lunch|expenses:food,assets:cash,

      
        
        137
        +  2024-01-04|aws|expenses:food,assets:cash,

      
        
        138
        +

      
        
        139
        +payee templates (3):

      
        
        140
        +  aws

      
        
        141
        +    expenses:food  USD1000000

      
        
        142
        +    assets:cash (inferred)

      
        
        143
        +  groceries

      
        
        144
        +    expenses:food  $10

      
        
        145
        +    assets:checking (inferred)

      
        
        146
        +  lunch

      
        
        147
        +    expenses:food  $5

      
        
        148
        +    assets:cash (inferred)

      
        
        149
        +

      
        
        150
        +dates (3):

      
        
        151
        +  2024-01-01

      
        
        152
        +  2024-01-02

      
        
        153
        +  2024-01-04

      
        
        154
        +

      
        
        155
        +tag names (0):

      
        
        156
        +

      
        
        157
        +commodity decimal marks (2):

      
        
        158
        +  $: .

      
        
        159
        +  USD: .

      
A internal/analyzer/testdata/last-used-dates.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +2024-01-01 groceries

      
        
        3
        +    expenses:food  $10

      
        
        4
        +    assets:cash

      
        
        5
        +

      
        
        6
        +2024-01-15 lunch

      
        
        7
        +    expenses:food  $5

      
        
        8
        +    assets:cash

      
        
        9
        +

      
        
        10
        +-- expect --

      
        
        11
        +files (1):

      
        
        12
        +  0: in.journal

      
        
        13
        +

      
        
        14
        +commodities (1):

      
        
        15
        +  $

      
        
        16
        +    directives: 0

      
        
        17
        +    used: 2

      
        
        18
        +    usages: 2

      
        
        19
        +      file 0: $ ("10")

      
        
        20
        +      file 0: $ ("5")

      
        
        21
        +    last-used: 2024-01-15

      
        
        22
        +

      
        
        23
        +accounts (2):

      
        
        24
        +  assets:cash

      
        
        25
        +    directives: 0

      
        
        26
        +    used: 2

      
        
        27
        +    usages: 2

      
        
        28
        +      file 0: assets:cash

      
        
        29
        +      file 0: assets:cash

      
        
        30
        +    last-used: 2024-01-15

      
        
        31
        +  expenses:food

      
        
        32
        +    directives: 0

      
        
        33
        +    used: 2

      
        
        34
        +    usages: 2

      
        
        35
        +      file 0: expenses:food

      
        
        36
        +      file 0: expenses:food

      
        
        37
        +    last-used: 2024-01-15

      
        
        38
        +

      
        
        39
        +payees (2):

      
        
        40
        +  groceries

      
        
        41
        +    directives: 0

      
        
        42
        +    used: 1

      
        
        43
        +    usages: 1

      
        
        44
        +      file 0: groceries

      
        
        45
        +  lunch

      
        
        46
        +    directives: 0

      
        
        47
        +    used: 1

      
        
        48
        +    usages: 1

      
        
        49
        +      file 0: lunch

      
        
        50
        +

      
        
        51
        +prefixes (2):

      
        
        52
        +  assets:

      
        
        53
        +    assets:cash

      
        
        54
        +  expenses:

      
        
        55
        +    expenses:food

      
        
        56
        +

      
        
        57
        +transactions: 2

      
        
        58
        +periodic transactions: 0

      
        
        59
        +automated transactions: 0

      
        
        60
        +

      
        
        61
        +directives (0):

      
        
        62
        +

      
        
        63
        +transaction keys (2):

      
        
        64
        +  2024-01-01|groceries|expenses:food,assets:cash,

      
        
        65
        +  2024-01-15|lunch|expenses:food,assets:cash,

      
        
        66
        +

      
        
        67
        +payee templates (2):

      
        
        68
        +  groceries

      
        
        69
        +    expenses:food  $10

      
        
        70
        +    assets:cash (inferred)

      
        
        71
        +  lunch

      
        
        72
        +    expenses:food  $5

      
        
        73
        +    assets:cash (inferred)

      
        
        74
        +

      
        
        75
        +dates (2):

      
        
        76
        +  2024-01-01

      
        
        77
        +  2024-01-15

      
        
        78
        +

      
        
        79
        +tag names (0):

      
        
        80
        +

      
        
        81
        +commodity decimal marks (1):

      
        
        82
        +  $: .

      
A internal/analyzer/testdata/payee-templates.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +2024-01-01 groceries

      
        
        3
        +    expenses:food  $10

      
        
        4
        +    assets:checking

      
        
        5
        +

      
        
        6
        +2024-01-02 lunch

      
        
        7
        +    expenses:food  $5

      
        
        8
        +    assets:cash

      
        
        9
        +

      
        
        10
        +2024-01-03 groceries

      
        
        11
        +    expenses:food  $20

      
        
        12
        +    assets:checking

      
        
        13
        +

      
        
        14
        +-- expect --

      
        
        15
        +files (1):

      
        
        16
        +  0: in.journal

      
        
        17
        +

      
        
        18
        +commodities (1):

      
        
        19
        +  $

      
        
        20
        +    directives: 0

      
        
        21
        +    used: 3

      
        
        22
        +    usages: 3

      
        
        23
        +      file 0: $ ("10")

      
        
        24
        +      file 0: $ ("5")

      
        
        25
        +      file 0: $ ("20")

      
        
        26
        +    last-used: 2024-01-03

      
        
        27
        +

      
        
        28
        +accounts (3):

      
        
        29
        +  assets:cash

      
        
        30
        +    directives: 0

      
        
        31
        +    used: 1

      
        
        32
        +    usages: 1

      
        
        33
        +      file 0: assets:cash

      
        
        34
        +    last-used: 2024-01-02

      
        
        35
        +  assets:checking

      
        
        36
        +    directives: 0

      
        
        37
        +    used: 2

      
        
        38
        +    usages: 2

      
        
        39
        +      file 0: assets:checking

      
        
        40
        +      file 0: assets:checking

      
        
        41
        +    last-used: 2024-01-03

      
        
        42
        +  expenses:food

      
        
        43
        +    directives: 0

      
        
        44
        +    used: 3

      
        
        45
        +    usages: 3

      
        
        46
        +      file 0: expenses:food

      
        
        47
        +      file 0: expenses:food

      
        
        48
        +      file 0: expenses:food

      
        
        49
        +    last-used: 2024-01-03

      
        
        50
        +

      
        
        51
        +payees (2):

      
        
        52
        +  groceries

      
        
        53
        +    directives: 0

      
        
        54
        +    used: 2

      
        
        55
        +    usages: 2

      
        
        56
        +      file 0: groceries

      
        
        57
        +      file 0: groceries

      
        
        58
        +  lunch

      
        
        59
        +    directives: 0

      
        
        60
        +    used: 1

      
        
        61
        +    usages: 1

      
        
        62
        +      file 0: lunch

      
        
        63
        +

      
        
        64
        +prefixes (2):

      
        
        65
        +  assets:

      
        
        66
        +    assets:cash

      
        
        67
        +    assets:checking

      
        
        68
        +  expenses:

      
        
        69
        +    expenses:food

      
        
        70
        +

      
        
        71
        +transactions: 3

      
        
        72
        +periodic transactions: 0

      
        
        73
        +automated transactions: 0

      
        
        74
        +

      
        
        75
        +directives (0):

      
        
        76
        +

      
        
        77
        +transaction keys (3):

      
        
        78
        +  2024-01-01|groceries|expenses:food,assets:checking,

      
        
        79
        +  2024-01-02|lunch|expenses:food,assets:cash,

      
        
        80
        +  2024-01-03|groceries|expenses:food,assets:checking,

      
        
        81
        +

      
        
        82
        +payee templates (2):

      
        
        83
        +  groceries

      
        
        84
        +    expenses:food  $20

      
        
        85
        +    assets:checking (inferred)

      
        
        86
        +  lunch

      
        
        87
        +    expenses:food  $5

      
        
        88
        +    assets:cash (inferred)

      
        
        89
        +

      
        
        90
        +dates (3):

      
        
        91
        +  2024-01-01

      
        
        92
        +  2024-01-02

      
        
        93
        +  2024-01-03

      
        
        94
        +

      
        
        95
        +tag names (0):

      
        
        96
        +

      
        
        97
        +commodity decimal marks (1):

      
        
        98
        +  $: .

      
A internal/analyzer/testdata/payee-tracking.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +2024-01-01 groceries

      
        
        3
        +    expenses:food  $10

      
        
        4
        +    assets:cash

      
        
        5
        +

      
        
        6
        +-- expect --

      
        
        7
        +files (1):

      
        
        8
        +  0: in.journal

      
        
        9
        +

      
        
        10
        +commodities (1):

      
        
        11
        +  $

      
        
        12
        +    directives: 0

      
        
        13
        +    used: 1

      
        
        14
        +    usages: 1

      
        
        15
        +      file 0: $ ("10")

      
        
        16
        +    last-used: 2024-01-01

      
        
        17
        +

      
        
        18
        +accounts (2):

      
        
        19
        +  assets:cash

      
        
        20
        +    directives: 0

      
        
        21
        +    used: 1

      
        
        22
        +    usages: 1

      
        
        23
        +      file 0: assets:cash

      
        
        24
        +    last-used: 2024-01-01

      
        
        25
        +  expenses:food

      
        
        26
        +    directives: 0

      
        
        27
        +    used: 1

      
        
        28
        +    usages: 1

      
        
        29
        +      file 0: expenses:food

      
        
        30
        +    last-used: 2024-01-01

      
        
        31
        +

      
        
        32
        +payees (1):

      
        
        33
        +  groceries

      
        
        34
        +    directives: 0

      
        
        35
        +    used: 1

      
        
        36
        +    usages: 1

      
        
        37
        +      file 0: groceries

      
        
        38
        +

      
        
        39
        +prefixes (2):

      
        
        40
        +  assets:

      
        
        41
        +    assets:cash

      
        
        42
        +  expenses:

      
        
        43
        +    expenses:food

      
        
        44
        +

      
        
        45
        +transactions: 1

      
        
        46
        +periodic transactions: 0

      
        
        47
        +automated transactions: 0

      
        
        48
        +

      
        
        49
        +directives (0):

      
        
        50
        +

      
        
        51
        +transaction keys (1):

      
        
        52
        +  2024-01-01|groceries|expenses:food,assets:cash,

      
        
        53
        +

      
        
        54
        +payee templates (1):

      
        
        55
        +  groceries

      
        
        56
        +    expenses:food  $10

      
        
        57
        +    assets:cash (inferred)

      
        
        58
        +

      
        
        59
        +dates (1):

      
        
        60
        +  2024-01-01

      
        
        61
        +

      
        
        62
        +tag names (0):

      
        
        63
        +

      
        
        64
        +commodity decimal marks (1):

      
        
        65
        +  $: .

      
A internal/analyzer/testdata/payees.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +2024-01-01 groceries

      
        
        3
        +    expenses:food  $10

      
        
        4
        +    assets:checking

      
        
        5
        +

      
        
        6
        +2024-01-02 lunch

      
        
        7
        +    expenses:food  $5

      
        
        8
        +    assets:cash

      
        
        9
        +

      
        
        10
        +2024-01-03 groceries

      
        
        11
        +    expenses:food  $20

      
        
        12
        +    assets:checking

      
        
        13
        +

      
        
        14
        +-- expect --

      
        
        15
        +files (1):

      
        
        16
        +  0: in.journal

      
        
        17
        +

      
        
        18
        +commodities (1):

      
        
        19
        +  $

      
        
        20
        +    directives: 0

      
        
        21
        +    used: 3

      
        
        22
        +    usages: 3

      
        
        23
        +      file 0: $ ("10")

      
        
        24
        +      file 0: $ ("5")

      
        
        25
        +      file 0: $ ("20")

      
        
        26
        +    last-used: 2024-01-03

      
        
        27
        +

      
        
        28
        +accounts (3):

      
        
        29
        +  assets:cash

      
        
        30
        +    directives: 0

      
        
        31
        +    used: 1

      
        
        32
        +    usages: 1

      
        
        33
        +      file 0: assets:cash

      
        
        34
        +    last-used: 2024-01-02

      
        
        35
        +  assets:checking

      
        
        36
        +    directives: 0

      
        
        37
        +    used: 2

      
        
        38
        +    usages: 2

      
        
        39
        +      file 0: assets:checking

      
        
        40
        +      file 0: assets:checking

      
        
        41
        +    last-used: 2024-01-03

      
        
        42
        +  expenses:food

      
        
        43
        +    directives: 0

      
        
        44
        +    used: 3

      
        
        45
        +    usages: 3

      
        
        46
        +      file 0: expenses:food

      
        
        47
        +      file 0: expenses:food

      
        
        48
        +      file 0: expenses:food

      
        
        49
        +    last-used: 2024-01-03

      
        
        50
        +

      
        
        51
        +payees (2):

      
        
        52
        +  groceries

      
        
        53
        +    directives: 0

      
        
        54
        +    used: 2

      
        
        55
        +    usages: 2

      
        
        56
        +      file 0: groceries

      
        
        57
        +      file 0: groceries

      
        
        58
        +  lunch

      
        
        59
        +    directives: 0

      
        
        60
        +    used: 1

      
        
        61
        +    usages: 1

      
        
        62
        +      file 0: lunch

      
        
        63
        +

      
        
        64
        +prefixes (2):

      
        
        65
        +  assets:

      
        
        66
        +    assets:cash

      
        
        67
        +    assets:checking

      
        
        68
        +  expenses:

      
        
        69
        +    expenses:food

      
        
        70
        +

      
        
        71
        +transactions: 3

      
        
        72
        +periodic transactions: 0

      
        
        73
        +automated transactions: 0

      
        
        74
        +

      
        
        75
        +directives (0):

      
        
        76
        +

      
        
        77
        +transaction keys (3):

      
        
        78
        +  2024-01-01|groceries|expenses:food,assets:checking,

      
        
        79
        +  2024-01-02|lunch|expenses:food,assets:cash,

      
        
        80
        +  2024-01-03|groceries|expenses:food,assets:checking,

      
        
        81
        +

      
        
        82
        +payee templates (2):

      
        
        83
        +  groceries

      
        
        84
        +    expenses:food  $20

      
        
        85
        +    assets:checking (inferred)

      
        
        86
        +  lunch

      
        
        87
        +    expenses:food  $5

      
        
        88
        +    assets:cash (inferred)

      
        
        89
        +

      
        
        90
        +dates (3):

      
        
        91
        +  2024-01-01

      
        
        92
        +  2024-01-02

      
        
        93
        +  2024-01-03

      
        
        94
        +

      
        
        95
        +tag names (0):

      
        
        96
        +

      
        
        97
        +commodity decimal marks (1):

      
        
        98
        +  $: .

      
A internal/analyzer/testdata/periodic-automated.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +~ monthly

      
        
        3
        +    expenses:food  $100

      
        
        4
        +    assets:cash

      
        
        5
        +

      
        
        6
        += expr:big

      
        
        7
        +    expenses:food  $200

      
        
        8
        +    assets:cash

      
        
        9
        +

      
        
        10
        +-- expect --

      
        
        11
        +files (1):

      
        
        12
        +  0: in.journal

      
        
        13
        +

      
        
        14
        +commodities (1):

      
        
        15
        +  $

      
        
        16
        +    directives: 0

      
        
        17
        +    used: 2

      
        
        18
        +    usages: 2

      
        
        19
        +      file 0: $ ("100")

      
        
        20
        +      file 0: $ ("200")

      
        
        21
        +

      
        
        22
        +accounts (2):

      
        
        23
        +  assets:cash

      
        
        24
        +    directives: 0

      
        
        25
        +    used: 2

      
        
        26
        +    usages: 2

      
        
        27
        +      file 0: assets:cash

      
        
        28
        +      file 0: assets:cash

      
        
        29
        +  expenses:food

      
        
        30
        +    directives: 0

      
        
        31
        +    used: 2

      
        
        32
        +    usages: 2

      
        
        33
        +      file 0: expenses:food

      
        
        34
        +      file 0: expenses:food

      
        
        35
        +

      
        
        36
        +payees (0):

      
        
        37
        +

      
        
        38
        +prefixes (2):

      
        
        39
        +  assets:

      
        
        40
        +    assets:cash

      
        
        41
        +  expenses:

      
        
        42
        +    expenses:food

      
        
        43
        +

      
        
        44
        +transactions: 0

      
        
        45
        +periodic transactions: 1

      
        
        46
        +automated transactions: 1

      
        
        47
        +

      
        
        48
        +directives (0):

      
        
        49
        +

      
        
        50
        +transaction keys (0):

      
        
        51
        +

      
        
        52
        +payee templates (0):

      
        
        53
        +

      
        
        54
        +dates (0):

      
        
        55
        +

      
        
        56
        +tag names (0):

      
        
        57
        +

      
        
        58
        +commodity decimal marks (1):

      
        
        59
        +  $: .

      
A internal/analyzer/testdata/posting-types.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +2024-01-01 test

      
        
        3
        +    (virtual:unbalanced)  $10

      
        
        4
        +    [virtual:balanced]    $20

      
        
        5
        +    expenses:food         $30

      
        
        6
        +

      
        
        7
        +-- expect --

      
        
        8
        +files (1):

      
        
        9
        +  0: in.journal

      
        
        10
        +

      
        
        11
        +commodities (1):

      
        
        12
        +  $

      
        
        13
        +    directives: 0

      
        
        14
        +    used: 3

      
        
        15
        +    usages: 3

      
        
        16
        +      file 0: $ ("10")

      
        
        17
        +      file 0: $ ("20")

      
        
        18
        +      file 0: $ ("30")

      
        
        19
        +    last-used: 2024-01-01

      
        
        20
        +

      
        
        21
        +accounts (3):

      
        
        22
        +  expenses:food

      
        
        23
        +    directives: 0

      
        
        24
        +    used: 1

      
        
        25
        +    usages: 1

      
        
        26
        +      file 0: expenses:food

      
        
        27
        +    last-used: 2024-01-01

      
        
        28
        +  virtual:balanced

      
        
        29
        +    directives: 0

      
        
        30
        +    used: 1

      
        
        31
        +    usages: 1

      
        
        32
        +      file 0: virtual:balanced

      
        
        33
        +    last-used: 2024-01-01

      
        
        34
        +  virtual:unbalanced

      
        
        35
        +    directives: 0

      
        
        36
        +    used: 1

      
        
        37
        +    usages: 1

      
        
        38
        +      file 0: virtual:unbalanced

      
        
        39
        +    last-used: 2024-01-01

      
        
        40
        +

      
        
        41
        +payees (1):

      
        
        42
        +  test

      
        
        43
        +    directives: 0

      
        
        44
        +    used: 1

      
        
        45
        +    usages: 1

      
        
        46
        +      file 0: test

      
        
        47
        +

      
        
        48
        +prefixes (2):

      
        
        49
        +  expenses:

      
        
        50
        +    expenses:food

      
        
        51
        +  virtual:

      
        
        52
        +    virtual:balanced

      
        
        53
        +    virtual:unbalanced

      
        
        54
        +

      
        
        55
        +transactions: 1

      
        
        56
        +periodic transactions: 0

      
        
        57
        +automated transactions: 0

      
        
        58
        +

      
        
        59
        +directives (0):

      
        
        60
        +

      
        
        61
        +transaction keys (1):

      
        
        62
        +  2024-01-01|test|virtual:unbalanced,virtual:balanced,expenses:food,

      
        
        63
        +

      
        
        64
        +payee templates (1):

      
        
        65
        +  test

      
        
        66
        +    virtual:unbalanced  $10

      
        
        67
        +    virtual:balanced  $20

      
        
        68
        +    expenses:food  $30

      
        
        69
        +

      
        
        70
        +dates (1):

      
        
        71
        +  2024-01-01

      
        
        72
        +

      
        
        73
        +tag names (0):

      
        
        74
        +

      
        
        75
        +commodity decimal marks (1):

      
        
        76
        +  $: .

      
A internal/analyzer/testdata/prefix-index.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +2024-01-01 groceries

      
        
        3
        +    expenses:food:groceries  $10

      
        
        4
        +    expenses:transport:fuel  $5

      
        
        5
        +    assets:bank:checking  $15

      
        
        6
        +

      
        
        7
        +-- expect --

      
        
        8
        +files (1):

      
        
        9
        +  0: in.journal

      
        
        10
        +

      
        
        11
        +commodities (1):

      
        
        12
        +  $

      
        
        13
        +    directives: 0

      
        
        14
        +    used: 3

      
        
        15
        +    usages: 3

      
        
        16
        +      file 0: $ ("10")

      
        
        17
        +      file 0: $ ("5")

      
        
        18
        +      file 0: $ ("15")

      
        
        19
        +    last-used: 2024-01-01

      
        
        20
        +

      
        
        21
        +accounts (3):

      
        
        22
        +  assets:bank:checking

      
        
        23
        +    directives: 0

      
        
        24
        +    used: 1

      
        
        25
        +    usages: 1

      
        
        26
        +      file 0: assets:bank:checking

      
        
        27
        +    last-used: 2024-01-01

      
        
        28
        +  expenses:food:groceries

      
        
        29
        +    directives: 0

      
        
        30
        +    used: 1

      
        
        31
        +    usages: 1

      
        
        32
        +      file 0: expenses:food:groceries

      
        
        33
        +    last-used: 2024-01-01

      
        
        34
        +  expenses:transport:fuel

      
        
        35
        +    directives: 0

      
        
        36
        +    used: 1

      
        
        37
        +    usages: 1

      
        
        38
        +      file 0: expenses:transport:fuel

      
        
        39
        +    last-used: 2024-01-01

      
        
        40
        +

      
        
        41
        +payees (1):

      
        
        42
        +  groceries

      
        
        43
        +    directives: 0

      
        
        44
        +    used: 1

      
        
        45
        +    usages: 1

      
        
        46
        +      file 0: groceries

      
        
        47
        +

      
        
        48
        +prefixes (5):

      
        
        49
        +  assets:

      
        
        50
        +    assets:bank:checking

      
        
        51
        +  assets:bank:

      
        
        52
        +    assets:bank:checking

      
        
        53
        +  expenses:

      
        
        54
        +    expenses:food:groceries

      
        
        55
        +    expenses:transport:fuel

      
        
        56
        +  expenses:food:

      
        
        57
        +    expenses:food:groceries

      
        
        58
        +  expenses:transport:

      
        
        59
        +    expenses:transport:fuel

      
        
        60
        +

      
        
        61
        +transactions: 1

      
        
        62
        +periodic transactions: 0

      
        
        63
        +automated transactions: 0

      
        
        64
        +

      
        
        65
        +directives (0):

      
        
        66
        +

      
        
        67
        +transaction keys (1):

      
        
        68
        +  2024-01-01|groceries|expenses:food:groceries,expenses:transport:fuel,assets:bank:checking,

      
        
        69
        +

      
        
        70
        +payee templates (1):

      
        
        71
        +  groceries

      
        
        72
        +    expenses:food:groceries  $10

      
        
        73
        +    expenses:transport:fuel  $5

      
        
        74
        +    assets:bank:checking  $15

      
        
        75
        +

      
        
        76
        +dates (1):

      
        
        77
        +  2024-01-01

      
        
        78
        +

      
        
        79
        +tag names (0):

      
        
        80
        +

      
        
        81
        +commodity decimal marks (1):

      
        
        82
        +  $: .

      
A internal/analyzer/testdata/tag-names-dedup.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +tag project-xyz

      
        
        3
        +tag project-xyz

      
        
        4
        +

      
        
        5
        +-- expect --

      
        
        6
        +files (1):

      
        
        7
        +  0: in.journal

      
        
        8
        +

      
        
        9
        +commodities (0):

      
        
        10
        +

      
        
        11
        +accounts (0):

      
        
        12
        +

      
        
        13
        +payees (0):

      
        
        14
        +

      
        
        15
        +prefixes (0):

      
        
        16
        +

      
        
        17
        +transactions: 0

      
        
        18
        +periodic transactions: 0

      
        
        19
        +automated transactions: 0

      
        
        20
        +

      
        
        21
        +directives (2):

      
        
        22
        +  tag project-xyz

      
        
        23
        +  tag project-xyz

      
        
        24
        +

      
        
        25
        +transaction keys (0):

      
        
        26
        +

      
        
        27
        +payee templates (0):

      
        
        28
        +

      
        
        29
        +dates (0):

      
        
        30
        +

      
        
        31
        +tag names (1):

      
        
        32
        +  project-xyz

      
        
        33
        +

      
        
        34
        +commodity decimal marks (0):

      
A internal/analyzer/testdata/tag-names.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +tag project-xyz

      
        
        3
        +tag personal

      
        
        4
        +

      
        
        5
        +-- expect --

      
        
        6
        +files (1):

      
        
        7
        +  0: in.journal

      
        
        8
        +

      
        
        9
        +commodities (0):

      
        
        10
        +

      
        
        11
        +accounts (0):

      
        
        12
        +

      
        
        13
        +payees (0):

      
        
        14
        +

      
        
        15
        +prefixes (0):

      
        
        16
        +

      
        
        17
        +transactions: 0

      
        
        18
        +periodic transactions: 0

      
        
        19
        +automated transactions: 0

      
        
        20
        +

      
        
        21
        +directives (2):

      
        
        22
        +  tag project-xyz

      
        
        23
        +  tag personal

      
        
        24
        +

      
        
        25
        +transaction keys (0):

      
        
        26
        +

      
        
        27
        +payee templates (0):

      
        
        28
        +

      
        
        29
        +dates (0):

      
        
        30
        +

      
        
        31
        +tag names (2):

      
        
        32
        +  personal

      
        
        33
        +  project-xyz

      
        
        34
        +

      
        
        35
        +commodity decimal marks (0):

      
A internal/analyzer/testdata/tags.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +tag project-xyz

      
        
        3
        +tag personal

      
        
        4
        +tag project-xyz

      
        
        5
        +

      
        
        6
        +-- expect --

      
        
        7
        +files (1):

      
        
        8
        +  0: in.journal

      
        
        9
        +

      
        
        10
        +commodities (0):

      
        
        11
        +

      
        
        12
        +accounts (0):

      
        
        13
        +

      
        
        14
        +payees (0):

      
        
        15
        +

      
        
        16
        +prefixes (0):

      
        
        17
        +

      
        
        18
        +transactions: 0

      
        
        19
        +periodic transactions: 0

      
        
        20
        +automated transactions: 0

      
        
        21
        +

      
        
        22
        +directives (3):

      
        
        23
        +  tag project-xyz

      
        
        24
        +  tag personal

      
        
        25
        +  tag project-xyz

      
        
        26
        +

      
        
        27
        +transaction keys (0):

      
        
        28
        +

      
        
        29
        +payee templates (0):

      
        
        30
        +

      
        
        31
        +dates (0):

      
        
        32
        +

      
        
        33
        +tag names (2):

      
        
        34
        +  personal

      
        
        35
        +  project-xyz

      
        
        36
        +

      
        
        37
        +commodity decimal marks (0):

      
A internal/analyzer/testdata/typed-entries.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +2024-01-01 groceries

      
        
        3
        +    expenses:food  $10

      
        
        4
        +    assets:cash

      
        
        5
        +

      
        
        6
        +~ monthly

      
        
        7
        +    expenses:transport  $50

      
        
        8
        +    assets:checking

      
        
        9
        +

      
        
        10
        += expr:big

      
        
        11
        +    expenses:food  $100

      
        
        12
        +    assets:credit

      
        
        13
        +

      
        
        14
        +account expenses:food

      
        
        15
        +

      
        
        16
        +-- expect --

      
        
        17
        +files (1):

      
        
        18
        +  0: in.journal

      
        
        19
        +

      
        
        20
        +commodities (1):

      
        
        21
        +  $

      
        
        22
        +    directives: 0

      
        
        23
        +    used: 3

      
        
        24
        +    usages: 3

      
        
        25
        +      file 0: $ ("10")

      
        
        26
        +      file 0: $ ("50")

      
        
        27
        +      file 0: $ ("100")

      
        
        28
        +    last-used: 2024-01-01

      
        
        29
        +

      
        
        30
        +accounts (5):

      
        
        31
        +  assets:cash

      
        
        32
        +    directives: 0

      
        
        33
        +    used: 1

      
        
        34
        +    usages: 1

      
        
        35
        +      file 0: assets:cash

      
        
        36
        +    last-used: 2024-01-01

      
        
        37
        +  assets:checking

      
        
        38
        +    directives: 0

      
        
        39
        +    used: 1

      
        
        40
        +    usages: 1

      
        
        41
        +      file 0: assets:checking

      
        
        42
        +  assets:credit

      
        
        43
        +    directives: 0

      
        
        44
        +    used: 1

      
        
        45
        +    usages: 1

      
        
        46
        +      file 0: assets:credit

      
        
        47
        +  expenses:food

      
        
        48
        +    directives: 1

      
        
        49
        +    used: 2

      
        
        50
        +      account expenses:food

      
        
        51
        +    usages: 2

      
        
        52
        +      file 0: expenses:food

      
        
        53
        +      file 0: expenses:food

      
        
        54
        +    last-used: 2024-01-01

      
        
        55
        +  expenses:transport

      
        
        56
        +    directives: 0

      
        
        57
        +    used: 1

      
        
        58
        +    usages: 1

      
        
        59
        +      file 0: expenses:transport

      
        
        60
        +

      
        
        61
        +payees (1):

      
        
        62
        +  groceries

      
        
        63
        +    directives: 0

      
        
        64
        +    used: 1

      
        
        65
        +    usages: 1

      
        
        66
        +      file 0: groceries

      
        
        67
        +

      
        
        68
        +prefixes (2):

      
        
        69
        +  assets:

      
        
        70
        +    assets:cash

      
        
        71
        +    assets:checking

      
        
        72
        +    assets:credit

      
        
        73
        +  expenses:

      
        
        74
        +    expenses:food

      
        
        75
        +    expenses:transport

      
        
        76
        +

      
        
        77
        +transactions: 1

      
        
        78
        +periodic transactions: 1

      
        
        79
        +automated transactions: 1

      
        
        80
        +

      
        
        81
        +directives (1):

      
        
        82
        +  account expenses:food

      
        
        83
        +

      
        
        84
        +transaction keys (1):

      
        
        85
        +  2024-01-01|groceries|expenses:food,assets:cash,

      
        
        86
        +

      
        
        87
        +payee templates (1):

      
        
        88
        +  groceries

      
        
        89
        +    expenses:food  $10

      
        
        90
        +    assets:cash (inferred)

      
        
        91
        +

      
        
        92
        +dates (1):

      
        
        93
        +  2024-01-01

      
        
        94
        +

      
        
        95
        +tag names (0):

      
        
        96
        +

      
        
        97
        +commodity decimal marks (1):

      
        
        98
        +  $: .

      
M internal/cli/cmd_lint.go
···
        8
        8
         

      
        9
        9
         	"github.com/urfave/cli/v3"

      
        10
        10
         

      
        
        11
        +	"olexsmir.xyz/clerk/internal/analyzer"

      
        11
        12
         	"olexsmir.xyz/clerk/internal/linter"

      
        12
        13
         	"olexsmir.xyz/clerk/journal"

      
        13
        
        -	"olexsmir.xyz/clerk/journal/semantic"

      
        14
        14
         )

      
        15
        15
         

      
        16
        16
         func (c *Cli) lintAction(ctx context.Context, cmd *cli.Command) error {

      ···
        44
        44
         	}

      
        45
        45
         

      
        46
        46
         	for _, root := range loader.Roots() {

      
        47
        
        -		reporter.Collect(lint.Run(semantic.Build(journal.CollectFiles(root))))

      
        
        47
        +		reporter.Collect(lint.Run(analyzer.Build(journal.CollectFiles(root))))

      
        48
        48
         	}

      
        49
        49
         

      
        50
        50
         	if err := reporter.Flush(format); err != nil {

      ···
        64
        64
         	}

      
        65
        65
         

      
        66
        66
         	for _, root := range loader.Roots() {

      
        67
        
        -		reporter.Collect(lint.Run(semantic.Build(journal.CollectFiles(root))))

      
        
        67
        +		reporter.Collect(lint.Run(analyzer.Build(journal.CollectFiles(root))))

      
        68
        68
         	}

      
        69
        69
         

      
        70
        70
         	if err := reporter.Flush(format); err != nil {

      
M internal/linter/linter.go
···
        1
        1
         package linter

      
        2
        2
         

      
        3
        3
         import (

      
        
        4
        +	"olexsmir.xyz/clerk/internal/analyzer"

      
        4
        5
         	"olexsmir.xyz/clerk/journal/token"

      
        5
        
        -

      
        6
        
        -	"olexsmir.xyz/clerk/journal/semantic"

      
        7
        6
         )

      
        8
        7
         

      
        9
        8
         // A Find represents a single lint finding.

      ···
        24
        23
         	return &Linter{rules: rules}

      
        25
        24
         }

      
        26
        25
         

      
        27
        
        -// Run runs all rules against the semantic context.

      
        28
        
        -func (l *Linter) Run(ctx *semantic.Context) []Find {

      
        
        26
        +// Run runs all rules against the analysis context.

      
        
        27
        +func (l *Linter) Run(a *analyzer.Analysis) []Find {

      
        29
        28
         	var finds []Find

      
        30
        29
         

      
        31
        
        -	// per-entry traversal

      
        32
        
        -	for _, pf := range ctx.Files {

      
        33
        
        -		for _, entry := range pf.Ast.Entries {

      
        34
        
        -			for _, rule := range l.rules {

      
        35
        
        -				if ec, ok := rule.(EntryChecker); ok {

      
        36
        
        -					finds = append(finds, ec.CheckEntry(entry)...)

      
        37
        
        -				}

      
        38
        
        -			}

      
        39
        
        -		}

      
        40
        
        -	}

      
        41
        
        -

      
        42
        
        -	// post-traversal

      
        43
        30
         	for _, rule := range l.rules {

      
        44
        
        -		if jc, ok := rule.(JournalChecker); ok {

      
        45
        
        -			finds = append(finds, jc.CheckJournal(ctx)...)

      
        
        31
        +		if jc, ok := rule.(Rule); ok {

      
        
        32
        +			finds = append(finds, jc.CheckJournal(a)...)

      
        46
        33
         		}

      
        47
        34
         	}

      
        48
        35
         

      
M internal/linter/linter_test.go
···
        4
        4
         	"strings"

      
        5
        5
         	"testing"

      
        6
        6
         

      
        
        7
        +	"olexsmir.xyz/clerk/internal/analyzer"

      
        7
        8
         	"olexsmir.xyz/clerk/internal/testutil/golden"

      
        8
        9
         	"olexsmir.xyz/clerk/journal"

      
        9
        
        -	"olexsmir.xyz/clerk/journal/semantic"

      
        10
        10
         )

      
        11
        11
         

      
        12
        12
         var tests = map[string][]Rule{

      ···
        42
        42
         				t.Fatalf("failed to load test journal: %v", err)

      
        43
        43
         			}

      
        44
        44
         

      
        45
        
        -			ctx := semantic.Build(l.Ordered())

      
        
        45
        +			ctx := analyzer.Build(l.Ordered())

      
        46
        46
         			finds := NewLinter(trules).Run(ctx)

      
        47
        47
         

      
        48
        48
         			var b strings.Builder

      ···
        61
        61
         		b.Fatalf("failed to load benchmark journal: %v", err)

      
        62
        62
         	}

      
        63
        63
         

      
        64
        
        -	ctx := semantic.Build(ldr.Ordered())

      
        
        64
        +	ctx := analyzer.Build(ldr.Ordered())

      
        65
        65
         	l := NewLinter(Rules)

      
        66
        66
         

      
        67
        67
         	b.ResetTimer()

      
M internal/linter/rule_account_depth.go
···
        3
        3
         import (

      
        4
        4
         	"fmt"

      
        5
        5
         

      
        
        6
        +	"olexsmir.xyz/clerk/internal/analyzer"

      
        6
        7
         	"olexsmir.xyz/clerk/journal/ast"

      
        7
        8
         )

      
        8
        9
         

      ···
        11
        12
         	MaxDepth int

      
        12
        13
         }

      
        13
        14
         

      
        14
        
        -func (AccountDepthLimit) ID() RuleID          { return "account-depth" }

      
        15
        
        -func (AccountDepthLimit) Severity() Severity  { return SeverityWarning }

      
        16
        
        -func (a *AccountDepthLimit) CheckEntry(entry ast.Entry) []Find {

      
        
        15
        +func (AccountDepthLimit) ID() RuleID         { return "account-depth" }

      
        
        16
        +func (AccountDepthLimit) Severity() Severity { return SeverityWarning }

      
        
        17
        +func (a *AccountDepthLimit) CheckJournal(an *analyzer.Analysis) []Find {

      
        17
        18
         	var finds []Find

      
        18
        
        -	switch e := entry.(type) {

      
        19
        
        -	case *ast.Transaction:

      
        20
        
        -		for _, p := range e.Postings {

      
        
        19
        +

      
        
        20
        +	for _, txn := range an.Transactions {

      
        
        21
        +		for _, p := range txn.Postings {

      
        21
        22
         			a.check(&finds, p.Account)

      
        22
        23
         		}

      
        23
        
        -	case *ast.PeriodicTransaction:

      
        24
        
        -		for _, p := range e.Postings {

      
        
        24
        +	}

      
        
        25
        +	for _, ptx := range an.PeriodicTransactions {

      
        
        26
        +		for _, p := range ptx.Postings {

      
        25
        27
         			a.check(&finds, p.Account)

      
        26
        28
         		}

      
        27
        
        -	case *ast.AutomatedTransaction:

      
        28
        
        -		for _, p := range e.Postings {

      
        
        29
        +	}

      
        
        30
        +	for _, atx := range an.AutomatedTransactions {

      
        
        31
        +		for _, p := range atx.Postings {

      
        29
        32
         			a.check(&finds, p.Account)

      
        30
        33
         		}

      
        31
        
        -	case *ast.AccountDirective:

      
        32
        
        -		a.check(&finds, e.Account)

      
        33
        
        -	case *ast.AliasDirective:

      
        34
        
        -		a.check(&finds, e.From)

      
        35
        
        -		a.check(&finds, e.To)

      
        
        34
        +	}

      
        
        35
        +

      
        
        36
        +	for _, d := range an.Directives {

      
        
        37
        +		switch e := d.(type) {

      
        
        38
        +		case *ast.AccountDirective:

      
        
        39
        +			a.check(&finds, e.Account)

      
        
        40
        +		case *ast.AliasDirective:

      
        
        41
        +			a.check(&finds, e.From)

      
        
        42
        +			a.check(&finds, e.To)

      
        
        43
        +		}

      
        36
        44
         	}

      
        
        45
        +

      
        37
        46
         	return finds

      
        38
        47
         }

      
        39
        48
         

      
M internal/linter/rule_duplicated_account.go
···
        3
        3
         import (

      
        4
        4
         	"fmt"

      
        5
        5
         

      
        6
        
        -	"olexsmir.xyz/clerk/journal/semantic"

      
        
        6
        +	"olexsmir.xyz/clerk/internal/analyzer"

      
        7
        7
         )

      
        8
        8
         

      
        9
        9
         // DuplicatedAccount flags account declarations that appear more than once.

      ···
        11
        11
         

      
        12
        12
         func (DuplicatedAccount) ID() RuleID         { return "duplicated-account" }

      
        13
        13
         func (DuplicatedAccount) Severity() Severity { return SeverityWarning }

      
        14
        
        -func (d *DuplicatedAccount) CheckJournal(ctx *semantic.Context) []Find {

      
        
        14
        +func (d *DuplicatedAccount) CheckJournal(an *analyzer.Analysis) []Find {

      
        15
        15
         	var finds []Find

      
        16
        
        -	for _, info := range ctx.Accounts {

      
        
        16
        +	for _, info := range an.Accounts {

      
        17
        17
         		if len(info.Directives) <= 1 {

      
        18
        18
         			continue

      
        19
        19
         		}

      
M internal/linter/rule_duplicated_commodity.go
···
        3
        3
         import (

      
        4
        4
         	"fmt"

      
        5
        5
         

      
        6
        
        -	"olexsmir.xyz/clerk/journal/semantic"

      
        
        6
        +	"olexsmir.xyz/clerk/internal/analyzer"

      
        7
        7
         )

      
        8
        8
         

      
        9
        9
         // DuplicatedCommodity flags commodity declarations that appear more than once.

      ···
        11
        11
         

      
        12
        12
         func (DuplicatedCommodity) ID() RuleID         { return "duplicated-commodity" }

      
        13
        13
         func (DuplicatedCommodity) Severity() Severity { return SeverityWarning }

      
        14
        
        -func (d *DuplicatedCommodity) CheckJournal(ctx *semantic.Context) []Find {

      
        
        14
        +func (d *DuplicatedCommodity) CheckJournal(an *analyzer.Analysis) []Find {

      
        15
        15
         	var finds []Find

      
        16
        
        -	for sym, info := range ctx.Commodities {

      
        
        16
        +	for sym, info := range an.Commodities {

      
        17
        17
         		if len(info.Directives) <= 1 {

      
        18
        18
         			continue

      
        19
        19
         		}

      
M internal/linter/rule_empty_postings.go
···
        1
        1
         package linter

      
        2
        2
         

      
        3
        
        -import "olexsmir.xyz/clerk/journal/ast"

      
        
        3
        +import "olexsmir.xyz/clerk/internal/analyzer"

      
        4
        4
         

      
        5
        5
         // EmptyPostings flags transactions that have no postings.

      
        6
        6
         type EmptyPostings struct{}

      
        7
        7
         

      
        8
        
        -func (EmptyPostings) ID() RuleID          { return "empty-postings" }

      
        9
        
        -func (EmptyPostings) Severity() Severity  { return SeverityError }

      
        10
        
        -func (e *EmptyPostings) CheckEntry(entry ast.Entry) []Find {

      
        11
        
        -	txn, ok := entry.(*ast.Transaction)

      
        12
        
        -	if !ok {

      
        13
        
        -		return nil

      
        
        8
        +func (EmptyPostings) ID() RuleID         { return "empty-postings" }

      
        
        9
        +func (EmptyPostings) Severity() Severity { return SeverityError }

      
        
        10
        +func (e *EmptyPostings) CheckJournal(an *analyzer.Analysis) []Find {

      
        
        11
        +	var finds []Find

      
        
        12
        +	for _, txn := range an.Transactions {

      
        
        13
        +		if len(txn.Postings) == 0 {

      
        
        14
        +			finds = append(finds, Find{

      
        
        15
        +				Code:     e.ID(),

      
        
        16
        +				Severity: e.Severity(),

      
        
        17
        +				Message:  "transaction has no postings",

      
        
        18
        +				Span:     txn.Span,

      
        
        19
        +			})

      
        
        20
        +		}

      
        14
        21
         	}

      
        15
        
        -	if len(txn.Postings) == 0 {

      
        16
        
        -		return []Find{{

      
        17
        
        -			Code:     e.ID(),

      
        18
        
        -			Severity: e.Severity(),

      
        19
        
        -			Message:  "transaction has no postings",

      
        20
        
        -			Span:     txn.Span,

      
        21
        
        -		}}

      
        22
        
        -	}

      
        23
        
        -	return nil

      
        
        22
        +	return finds

      
        24
        23
         }

      
M internal/linter/rule_missing_commodity.go
···
        1
        1
         package linter

      
        2
        2
         

      
        3
        
        -import "olexsmir.xyz/clerk/journal/ast"

      
        
        3
        +import (

      
        
        4
        +	"olexsmir.xyz/clerk/internal/analyzer"

      
        
        5
        +	"olexsmir.xyz/clerk/journal/ast"

      
        
        6
        +)

      
        4
        7
         

      
        5
        8
         // MissingCommodity flags amounts with a missing commodity.

      
        6
        9
         type MissingCommodity struct{}

      
        7
        10
         

      
        8
        11
         func (MissingCommodity) ID() RuleID         { return "missing-commodity" }

      
        9
        12
         func (MissingCommodity) Severity() Severity { return SeverityWarning }

      
        10
        
        -func (m *MissingCommodity) CheckEntry(entry ast.Entry) []Find {

      
        
        13
        +func (m *MissingCommodity) CheckJournal(an *analyzer.Analysis) []Find {

      
        11
        14
         	var finds []Find

      
        12
        
        -	switch e := entry.(type) {

      
        13
        
        -	case *ast.Transaction:

      
        14
        
        -		m.checkPostings(&finds, e.Postings)

      
        15
        
        -	case *ast.PeriodicTransaction:

      
        16
        
        -		m.checkPostings(&finds, e.Postings)

      
        17
        
        -	case *ast.AutomatedTransaction:

      
        18
        
        -		m.checkPostings(&finds, e.Postings)

      
        19
        
        -	case *ast.ConversionDirective:

      
        20
        
        -		m.check(&finds, e.From)

      
        21
        
        -		m.check(&finds, e.To)

      
        22
        
        -	case *ast.CommodityDirective:

      
        23
        
        -		if e.Format.Commodity != "" {

      
        24
        
        -			m.check(&finds, e.Format)

      
        
        15
        +

      
        
        16
        +	for _, txn := range an.Transactions {

      
        
        17
        +		m.checkPostings(&finds, txn.Postings)

      
        
        18
        +	}

      
        
        19
        +	for _, ptx := range an.PeriodicTransactions {

      
        
        20
        +		m.checkPostings(&finds, ptx.Postings)

      
        
        21
        +	}

      
        
        22
        +	for _, atx := range an.AutomatedTransactions {

      
        
        23
        +		m.checkPostings(&finds, atx.Postings)

      
        
        24
        +	}

      
        
        25
        +

      
        
        26
        +	for _, d := range an.Directives {

      
        
        27
        +		switch e := d.(type) {

      
        
        28
        +		case *ast.ConversionDirective:

      
        
        29
        +			m.check(&finds, e.From)

      
        
        30
        +			m.check(&finds, e.To)

      
        
        31
        +		case *ast.CommodityDirective:

      
        
        32
        +			if e.Format.Commodity != "" {

      
        
        33
        +				m.check(&finds, e.Format)

      
        
        34
        +			}

      
        
        35
        +		case *ast.DefaultCommodityDirective:

      
        
        36
        +			m.check(&finds, e.Amount)

      
        
        37
        +		case *ast.MarketPriceDirective:

      
        
        38
        +			m.check(&finds, e.Amount)

      
        25
        39
         		}

      
        26
        
        -	case *ast.DefaultCommodityDirective:

      
        27
        
        -		m.check(&finds, e.Amount)

      
        28
        
        -	case *ast.MarketPriceDirective:

      
        29
        
        -		m.check(&finds, e.Amount)

      
        30
        40
         	}

      
        
        41
        +

      
        31
        42
         	return finds

      
        32
        43
         }

      
        33
        44
         

      
M internal/linter/rule_missing_payee.go
···
        1
        1
         package linter

      
        2
        2
         

      
        3
        
        -import "olexsmir.xyz/clerk/journal/ast"

      
        
        3
        +import "olexsmir.xyz/clerk/internal/analyzer"

      
        4
        4
         

      
        5
        5
         // MissingPayee flags transactions with missing payee.

      
        6
        6
         type MissingPayee struct{}

      
        7
        7
         

      
        8
        8
         func (MissingPayee) ID() RuleID         { return "missing-payee" }

      
        9
        9
         func (MissingPayee) Severity() Severity { return SeverityWarning }

      
        10
        
        -func (m *MissingPayee) CheckEntry(entry ast.Entry) []Find {

      
        11
        
        -	txn, ok := entry.(*ast.Transaction)

      
        12
        
        -	if !ok {

      
        13
        
        -		return nil

      
        
        10
        +func (m *MissingPayee) CheckJournal(an *analyzer.Analysis) []Find {

      
        
        11
        +	var finds []Find

      
        
        12
        +	for _, txn := range an.Transactions {

      
        
        13
        +		if txn.Payee == nil {

      
        
        14
        +			finds = append(finds, Find{

      
        
        15
        +				Code:     m.ID(),

      
        
        16
        +				Severity: m.Severity(),

      
        
        17
        +				Message:  "transaction has no payee",

      
        
        18
        +				Span:     txn.Date.Span,

      
        
        19
        +			})

      
        
        20
        +		}

      
        14
        21
         	}

      
        15
        
        -	if txn.Payee == nil {

      
        16
        
        -		return []Find{{

      
        17
        
        -			Code:     m.ID(),

      
        18
        
        -			Severity: m.Severity(),

      
        19
        
        -			Message:  "transaction has no payee",

      
        20
        
        -			Span:     txn.Date.Span,

      
        21
        
        -		}}

      
        22
        
        -	}

      
        23
        
        -	return nil

      
        
        22
        +	return finds

      
        24
        23
         }

      
M internal/linter/rule_missing_status.go
···
        1
        1
         package linter

      
        2
        2
         

      
        3
        
        -import "olexsmir.xyz/clerk/journal/ast"

      
        
        3
        +import (

      
        
        4
        +	"olexsmir.xyz/clerk/internal/analyzer"

      
        
        5
        +	"olexsmir.xyz/clerk/journal/ast"

      
        
        6
        +)

      
        4
        7
         

      
        5
        8
         // MissingStatus flags transactions with missing status.

      
        6
        9
         type MissingStatus struct{}

      
        7
        10
         

      
        8
        11
         func (MissingStatus) ID() RuleID         { return "missing-status" }

      
        9
        12
         func (MissingStatus) Severity() Severity { return SeverityWarning }

      
        10
        
        -func (m *MissingStatus) CheckEntry(entry ast.Entry) []Find {

      
        11
        
        -	tnx, ok := entry.(*ast.Transaction)

      
        12
        
        -	if !ok {

      
        13
        
        -		return nil

      
        
        13
        +func (m *MissingStatus) CheckJournal(an *analyzer.Analysis) []Find {

      
        
        14
        +	var finds []Find

      
        
        15
        +	for _, txn := range an.Transactions {

      
        
        16
        +		if txn.Status.Value == ast.StatusNone {

      
        
        17
        +			finds = append(finds, Find{

      
        
        18
        +				Code:     m.ID(),

      
        
        19
        +				Severity: m.Severity(),

      
        
        20
        +				Message:  "transaction has no status",

      
        
        21
        +				Span:     txn.Status.Span,

      
        
        22
        +			})

      
        
        23
        +		}

      
        14
        24
         	}

      
        15
        
        -

      
        16
        
        -	if tnx.Status.Value == ast.StatusNone {

      
        17
        
        -		return []Find{{

      
        18
        
        -			Code:     m.ID(),

      
        19
        
        -			Severity: m.Severity(),

      
        20
        
        -			Message:  "transaction has no status",

      
        21
        
        -			Span:     tnx.Status.Span,

      
        22
        
        -		}}

      
        23
        
        -	}

      
        24
        
        -	return nil

      
        
        25
        +	return finds

      
        25
        26
         }

      
M internal/linter/rule_multiple_omitted_amounts.go
···
        1
        1
         package linter

      
        2
        2
         

      
        3
        
        -import "olexsmir.xyz/clerk/journal/ast"

      
        
        3
        +import (

      
        
        4
        +	"olexsmir.xyz/clerk/internal/analyzer"

      
        
        5
        +	"olexsmir.xyz/clerk/journal/ast"

      
        
        6
        +)

      
        4
        7
         

      
        5
        8
         // MultipleOmittedAmounts flags entries where more than one posting has an ommited amount.

      
        6
        9
         type MultipleOmittedAmounts struct{}

      
        7
        10
         

      
        8
        11
         func (MultipleOmittedAmounts) ID() RuleID         { return "multiple-omitted-amounts" }

      
        9
        12
         func (MultipleOmittedAmounts) Severity() Severity { return SeverityError }

      
        10
        
        -func (m *MultipleOmittedAmounts) CheckEntry(entry ast.Entry) []Find {

      
        11
        
        -	switch e := entry.(type) {

      
        12
        
        -	case *ast.Transaction:

      
        13
        
        -		return m.check(e.Postings)

      
        14
        
        -	case *ast.PeriodicTransaction:

      
        15
        
        -		return m.check(e.Postings)

      
        16
        
        -	case *ast.AutomatedTransaction:

      
        17
        
        -		return m.check(e.Postings)

      
        18
        
        -	default:

      
        19
        
        -		return nil

      
        
        13
        +func (m *MultipleOmittedAmounts) CheckJournal(an *analyzer.Analysis) []Find {

      
        
        14
        +	var finds []Find

      
        
        15
        +	for _, txn := range an.Transactions {

      
        
        16
        +		finds = append(finds, m.check(txn.Postings)...)

      
        
        17
        +	}

      
        
        18
        +	for _, ptx := range an.PeriodicTransactions {

      
        
        19
        +		finds = append(finds, m.check(ptx.Postings)...)

      
        
        20
        +	}

      
        
        21
        +	for _, atx := range an.AutomatedTransactions {

      
        
        22
        +		finds = append(finds, m.check(atx.Postings)...)

      
        20
        23
         	}

      
        
        24
        +	return finds

      
        21
        25
         }

      
        22
        26
         

      
        23
        27
         func (m *MultipleOmittedAmounts) check(postings []*ast.Posting) []Find {

      
        24
        28
         	var finds []Find

      
        25
        29
         	for _, p := range postings {

      
        26
        
        -		// skipping postings with balance assertion, since those are often used as reconciliation entries

      
        27
        30
         		if p.Amount == nil && p.Balance == nil {

      
        28
        31
         			finds = append(finds, Find{

      
        29
        32
         				Code:     m.ID(),

      
M internal/linter/rule_omitted_precision.go
···
        1
        1
         package linter

      
        2
        2
         

      
        3
        
        -import "olexsmir.xyz/clerk/journal/ast"

      
        
        3
        +import "olexsmir.xyz/clerk/internal/analyzer"

      
        4
        4
         

      
        5
        5
         // OmittedPrecision flags amounts with insufficient decimal precision (<2 digits).

      
        6
        6
         type OmittedPrecision struct{}

      
        7
        7
         

      
        8
        8
         func (OmittedPrecision) ID() RuleID         { return "omitted-precision" }

      
        9
        9
         func (OmittedPrecision) Severity() Severity { return SeverityWarning }

      
        10
        
        -func (o *OmittedPrecision) CheckEntry(entry ast.Entry) []Find {

      
        11
        
        -	txn, ok := entry.(*ast.Transaction)

      
        12
        
        -	if !ok || (txn.Postings != nil && len(txn.Postings) == 0) {

      
        13
        
        -		return nil

      
        14
        
        -	}

      
        15
        
        -

      
        
        10
        +func (o *OmittedPrecision) CheckJournal(an *analyzer.Analysis) []Find {

      
        16
        11
         	var finds []Find

      
        17
        
        -	for _, posting := range txn.Postings {

      
        18
        
        -		if posting.Amount == nil {

      
        19
        
        -			continue

      
        20
        
        -		}

      
        21
        
        -		if posting.Amount.QuantityFmt.Precision < 2 {

      
        22
        
        -			finds = append(finds, Find{

      
        23
        
        -				Code:     o.ID(),

      
        24
        
        -				Severity: o.Severity(),

      
        25
        
        -				Message:  "amount has insufficient precision",

      
        26
        
        -				Span:     posting.Amount.Span,

      
        27
        
        -			})

      
        
        12
        +	for _, txn := range an.Transactions {

      
        
        13
        +		for _, posting := range txn.Postings {

      
        
        14
        +			if posting.Amount == nil {

      
        
        15
        +				continue

      
        
        16
        +			}

      
        
        17
        +			if posting.Amount.QuantityFmt.Precision < 2 {

      
        
        18
        +				finds = append(finds, Find{

      
        
        19
        +					Code:     o.ID(),

      
        
        20
        +					Severity: o.Severity(),

      
        
        21
        +					Message:  "amount has insufficient precision",

      
        
        22
        +					Span:     posting.Amount.Span,

      
        
        23
        +				})

      
        
        24
        +			}

      
        28
        25
         		}

      
        29
        26
         	}

      
        30
        27
         	return finds

      
M internal/linter/rule_orderdate.go
···
        3
        3
         import (

      
        4
        4
         	"fmt"

      
        5
        5
         

      
        
        6
        +	"olexsmir.xyz/clerk/internal/analyzer"

      
        6
        7
         	"olexsmir.xyz/clerk/journal/ast"

      
        7
        
        -	"olexsmir.xyz/clerk/journal/semantic"

      
        8
        8
         )

      
        9
        9
         

      
        10
        10
         // OrderDate checks that transactions are in chronological order by date.

      ···
        12
        12
         

      
        13
        13
         func (OrderDate) ID() RuleID         { return "orderdate" }

      
        14
        14
         func (OrderDate) Severity() Severity { return SeverityWarning }

      
        15
        
        -func (r *OrderDate) CheckJournal(ctx *semantic.Context) []Find {

      
        
        15
        +func (r *OrderDate) CheckJournal(an *analyzer.Analysis) []Find {

      
        16
        16
         	var finds []Find

      
        17
        17
         	var anchor *ast.Date

      
        18
        
        -	for _, pf := range ctx.Files {

      
        
        18
        +	for _, pf := range an.Files {

      
        19
        19
         		for _, entry := range pf.Ast.Entries {

      
        20
        20
         			txn, ok := entry.(*ast.Transaction)

      
        21
        21
         			if !ok {

      
M internal/linter/rule_parse_error.go
···
        1
        1
         package linter

      
        2
        2
         

      
        3
        
        -import "olexsmir.xyz/clerk/journal/semantic"

      
        
        3
        +import "olexsmir.xyz/clerk/internal/analyzer"

      
        4
        4
         

      
        5
        5
         // ParseError wraps parser errors into lint findings.

      
        6
        6
         type ParseError struct{}

      
        7
        7
         

      
        8
        8
         func (ParseError) ID() RuleID         { return "parse-error" }

      
        9
        9
         func (ParseError) Severity() Severity { return SeverityError }

      
        10
        
        -func (e *ParseError) CheckJournal(ctx *semantic.Context) []Find {

      
        
        10
        +func (e *ParseError) CheckJournal(an *analyzer.Analysis) []Find {

      
        11
        11
         	var finds []Find

      
        12
        
        -	for _, pf := range ctx.Files {

      
        
        12
        +	for _, pf := range an.Files {

      
        13
        13
         		for _, err := range pf.Errors {

      
        14
        14
         			finds = append(finds, Find{

      
        15
        15
         				Code:     e.ID(),

      
M internal/linter/rule_unbalanced_transaction.go
···
        3
        3
         import (

      
        4
        4
         	"fmt"

      
        5
        5
         

      
        
        6
        +	"olexsmir.xyz/clerk/internal/analyzer"

      
        6
        7
         	"olexsmir.xyz/clerk/internal/decimal"

      
        7
        8
         	"olexsmir.xyz/clerk/journal/ast"

      
        8
        9
         	"olexsmir.xyz/clerk/journal/token"

      ···
        13
        14
         

      
        14
        15
         func (UnbalancedTransaction) ID() RuleID         { return "unbalanced-transaction" }

      
        15
        16
         func (UnbalancedTransaction) Severity() Severity { return SeverityError }

      
        16
        
        -func (r *UnbalancedTransaction) CheckEntry(entry ast.Entry) []Find {

      
        17
        
        -	switch e := entry.(type) {

      
        18
        
        -	case *ast.Transaction:

      
        19
        
        -		return r.check(e.Postings, e.Span)

      
        20
        
        -	case *ast.PeriodicTransaction:

      
        21
        
        -		return r.check(e.Postings, e.Span)

      
        22
        
        -	case *ast.AutomatedTransaction:

      
        23
        
        -		return r.check(e.Postings, e.Span)

      
        24
        
        -	default:

      
        25
        
        -		return nil

      
        
        17
        +func (r *UnbalancedTransaction) CheckJournal(an *analyzer.Analysis) []Find {

      
        
        18
        +	var finds []Find

      
        
        19
        +	for _, txn := range an.Transactions {

      
        
        20
        +		finds = append(finds, r.check(txn.Postings, txn.Span)...)

      
        
        21
        +	}

      
        
        22
        +	for _, ptx := range an.PeriodicTransactions {

      
        
        23
        +		finds = append(finds, r.check(ptx.Postings, ptx.Span)...)

      
        
        24
        +	}

      
        
        25
        +	for _, atx := range an.AutomatedTransactions {

      
        
        26
        +		finds = append(finds, r.check(atx.Postings, atx.Span)...)

      
        26
        27
         	}

      
        
        28
        +	return finds

      
        27
        29
         }

      
        28
        30
         

      
        29
        31
         func (r *UnbalancedTransaction) check(postings []*ast.Posting, span token.Span) []Find {

      ···
        50
        52
         

      
        51
        53
         	if len(realPostings) == 0 ||

      
        52
        54
         		autoBalancingPostings == 1 ||

      
        53
        
        -		// multiple auto-balancing postings is alreayd handled by [MultipleOmittedAmounts]

      
        54
        55
         		autoBalancingPostings > 1 ||

      
        55
        
        -		// too complex too verify

      
        56
        56
         		hasExpr || hasCost {

      
        57
        57
         		return nil

      
        58
        58
         	}

      
        59
        59
         

      
        60
        
        -	// sum amounts per commodity

      
        61
        60
         	sums := make(map[string]decimal.Decimal)

      
        62
        61
         	for _, posting := range realPostings {

      
        63
        62
         		if posting.Amount == nil {

      
M internal/linter/rule_undeclared_account.go
···
        3
        3
         import (

      
        4
        4
         	"fmt"

      
        5
        5
         

      
        6
        
        -	"olexsmir.xyz/clerk/journal/semantic"

      
        
        6
        +	"olexsmir.xyz/clerk/internal/analyzer"

      
        7
        7
         )

      
        8
        8
         

      
        9
        9
         // UndeclaredAccount flags postings that reference an account not declared via `account` directive.

      ···
        11
        11
         

      
        12
        12
         func (UndeclaredAccount) ID() RuleID         { return "undeclared-account" }

      
        13
        13
         func (UndeclaredAccount) Severity() Severity { return SeverityWarning }

      
        14
        
        -func (r *UndeclaredAccount) CheckJournal(ctx *semantic.Context) []Find {

      
        
        14
        +func (r *UndeclaredAccount) CheckJournal(an *analyzer.Analysis) []Find {

      
        15
        15
         	var finds []Find

      
        16
        
        -	for name, info := range ctx.Accounts {

      
        
        16
        +	for name, info := range an.Accounts {

      
        17
        17
         		if len(info.Directives) > 0 {

      
        18
        18
         			continue

      
        19
        19
         		}

      
M internal/linter/rule_undeclared_commodity.go
···
        3
        3
         import (

      
        4
        4
         	"fmt"

      
        5
        5
         

      
        6
        
        -	"olexsmir.xyz/clerk/journal/semantic"

      
        
        6
        +	"olexsmir.xyz/clerk/internal/analyzer"

      
        7
        7
         )

      
        8
        8
         

      
        9
        9
         // UndeclaredCommodity flags amounts that reference a commodity not declared via `commodity` directive.

      ···
        11
        11
         

      
        12
        12
         func (UndeclaredCommodity) ID() RuleID         { return "undeclared-commodity" }

      
        13
        13
         func (UndeclaredCommodity) Severity() Severity { return SeverityWarning }

      
        14
        
        -func (r *UndeclaredCommodity) CheckJournal(ctx *semantic.Context) []Find {

      
        
        14
        +func (r *UndeclaredCommodity) CheckJournal(an *analyzer.Analysis) []Find {

      
        15
        15
         	var finds []Find

      
        16
        
        -	for name, info := range ctx.Commodities {

      
        
        16
        +	for name, info := range an.Commodities {

      
        17
        17
         		if len(info.Directives) > 0 {

      
        18
        18
         			continue

      
        19
        19
         		}

      
M internal/linter/rule_unused_account.go
···
        3
        3
         import (

      
        4
        4
         	"fmt"

      
        5
        5
         

      
        6
        
        -	"olexsmir.xyz/clerk/journal/semantic"

      
        
        6
        +	"olexsmir.xyz/clerk/internal/analyzer"

      
        7
        7
         )

      
        8
        8
         

      
        9
        9
         // UnusedAccount flags declared accounts that are not used.

      ···
        11
        11
         

      
        12
        12
         func (UnusedAccount) ID() RuleID         { return "unused-account" }

      
        13
        13
         func (UnusedAccount) Severity() Severity { return SeverityWarning }

      
        14
        
        -func (a *UnusedAccount) CheckJournal(ctx *semantic.Context) []Find {

      
        
        14
        +func (a *UnusedAccount) CheckJournal(an *analyzer.Analysis) []Find {

      
        15
        15
         	var finds []Find

      
        16
        
        -	for name, info := range ctx.Accounts {

      
        
        16
        +	for name, info := range an.Accounts {

      
        17
        17
         		if len(info.Directives) == 0 {

      
        18
        18
         			continue

      
        19
        19
         		}

      
M internal/linter/rules.go
···
        1
        1
         package linter

      
        2
        2
         

      
        3
        
        -import (

      
        4
        
        -	"olexsmir.xyz/clerk/journal/ast"

      
        5
        
        -

      
        6
        
        -	"olexsmir.xyz/clerk/journal/semantic"

      
        7
        
        -)

      
        
        3
        +import "olexsmir.xyz/clerk/internal/analyzer"

      
        8
        4
         

      
        9
        5
         type RuleID string

      
        10
        6
         

      ···
        12
        8
         type Rule interface {

      
        13
        9
         	ID() RuleID

      
        14
        10
         	Severity() Severity

      
        15
        
        -}

      
        16
        
        -

      
        17
        
        -// EntryChecker implements per-entry linting.

      
        18
        
        -type EntryChecker interface {

      
        19
        
        -	Rule

      
        20
        
        -	CheckEntry(entry ast.Entry) []Find

      
        21
        
        -}

      
        22
        
        -

      
        23
        
        -// JournalChecker implements whole-journal linting using the semantic context.

      
        24
        
        -type JournalChecker interface {

      
        25
        
        -	Rule

      
        26
        
        -	CheckJournal(ctx *semantic.Context) []Find

      
        
        11
        +	CheckJournal(an *analyzer.Analysis) []Find

      
        27
        12
         }

      
        28
        13
         

      
        29
        14
         // Rules is list of all available rules.

      ···
        42
        27
         	&UndeclaredCommodity{},

      
        43
        28
         	&UndeclaredAccount{},

      
        44
        29
         	&UnbalancedTransaction{},

      
        45
        
        -	&UnusedAccount{},

      
        
        30
        +	// &UnusedAccount{},

      
        46
        31
         }

      
M internal/lsp/diagnostics.go
···
        8
        8
         	"go.lsp.dev/jsonrpc2"

      
        9
        9
         	"go.lsp.dev/protocol"

      
        10
        10
         	"go.lsp.dev/uri"

      
        
        11
        +

      
        
        12
        +	"olexsmir.xyz/clerk/internal/analyzer"

      
        11
        13
         	"olexsmir.xyz/clerk/internal/linter"

      
        12
        14
         	"olexsmir.xyz/clerk/journal"

      
        13
        
        -	"olexsmir.xyz/clerk/journal/semantic"

      
        14
        15
         	"olexsmir.xyz/clerk/journal/token"

      
        15
        16
         )

      
        16
        17
         

      
        17
        
        -func (s *server) rebuildWorkspace(ctx context.Context) {

      
        18
        
        -	s.log.Debug("rebuilding workspace")

      
        
        18
        +const diagDebounce = 200 * time.Millisecond

      
        
        19
        +

      
        
        20
        +func (s *server) scheduleDiagnostics(ctx context.Context) {

      
        
        21
        +	s.diagMu.Lock()

      
        
        22
        +	defer s.diagMu.Unlock()

      
        
        23
        +

      
        
        24
        +	if s.diagCancel != nil {

      
        
        25
        +		s.diagCancel()

      
        
        26
        +	}

      
        
        27
        +

      
        
        28
        +	ctx, cancel := context.WithCancel(context.Background())

      
        
        29
        +	s.diagCancel = cancel

      
        
        30
        +

      
        
        31
        +	time.AfterFunc(diagDebounce, func() {

      
        
        32
        +		if ctx.Err() != nil {

      
        
        33
        +			return

      
        
        34
        +		}

      
        
        35
        +		s.publishDiagnostics(jsonrpc2.DetachContext(ctx))

      
        
        36
        +	})

      
        
        37
        +}

      
        
        38
        +

      
        
        39
        +func (s *server) publishDiagnostics(ctx context.Context) {

      
        
        40
        +	s.log.Debug("publishing diagnostics")

      
        19
        41
         

      
        20
        42
         	s.mu.Lock()

      
        21
        
        -	defer s.mu.Unlock()

      
        
        43
        +	openDocs := make(map[uri.URI]docState, len(s.openDocs))

      
        
        44
        +	for u, st := range s.openDocs {

      
        
        45
        +		openDocs[u] = st

      
        
        46
        +	}

      
        
        47
        +	ordered := s.loader.Ordered()

      
        
        48
        +	s.mu.Unlock()

      
        22
        49
         

      
        23
        
        -	if len(s.openDocs) == 0 {

      
        
        50
        +	if ctx.Err() != nil {

      
        
        51
        +		return

      
        
        52
        +	}

      
        
        53
        +

      
        
        54
        +	if len(openDocs) == 0 {

      
        24
        55
         		return

      
        25
        56
         	}

      
        26
        57
         

      
        27
        
        -	for uri, state := range s.openDocs {

      
        
        58
        +	for uri, state := range openDocs {

      
        28
        59
         		path := uri.Path()

      
        29
        60
         		if _, err := s.loader.Reload(path, []byte(state.text)); err != nil {

      
        30
        
        -			s.log.Warn("filed to reload open document", "uri", uri, "err", err)

      
        
        61
        +			s.log.Warn("failed to reload open document", "uri", uri, "err", err)

      
        31
        62
         		}

      
        32
        63
         	}

      
        33
        64
         

      
        34
        
        -	roots := s.loader.Roots()

      
        35
        
        -	if len(roots) == 0 {

      
        
        65
        +	if ctx.Err() != nil {

      
        
        66
        +		return

      
        
        67
        +	}

      
        
        68
        +

      
        
        69
        +	ordered = s.loader.Ordered()

      
        
        70
        +	if len(ordered) == 0 {

      
        36
        71
         		s.log.Debug("no files in workspace")

      
        37
        72
         		return

      
        38
        73
         	}

      
        39
        74
         

      
        40
        
        -	var allFiles []*journal.ParsedFile

      
        41
        
        -	var finds []linter.Find

      
        42
        
        -	for _, root := range roots {

      
        43
        
        -		files := journal.CollectFiles(root)

      
        44
        
        -		allFiles = append(allFiles, files...)

      
        45
        
        -		c := semantic.Build(files)

      
        46
        
        -		finds = append(finds, s.linter.Run(c)...)

      
        
        75
        +	if ctx.Err() != nil {

      
        
        76
        +		return

      
        
        77
        +	}

      
        
        78
        +

      
        
        79
        +	a := analyzer.Build(ordered)

      
        
        80
        +	finds := dedupFinds(s.linter.Run(a))

      
        
        81
        +

      
        
        82
        +	s.mu.Lock()

      
        
        83
        +	s.current = a

      
        
        84
        +	s.mu.Unlock()

      
        47
        85
         

      
        
        86
        +	if ctx.Err() != nil {

      
        
        87
        +		return

      
        48
        88
         	}

      
        49
        89
         

      
        50
        
        -	finds = dedupFinds(finds)

      
        51
        
        -	s.semanticCtx = semantic.Build(allFiles)

      
        
        90
        +	diagsByFile := s.groupFindsByFile(finds)

      
        
        91
        +

      
        
        92
        +	s.mu.Lock()

      
        
        93
        +	activeFiles := s.activeFileSet(openDocs, ordered)

      
        
        94
        +	s.mu.Unlock()

      
        52
        95
         

      
        53
        
        -	diagsByFile := s.groupFindsByFile(finds)

      
        54
        
        -	activeFiles := s.activeFileSet(s.openDocs, s.loader.Ordered())

      
        55
        96
         	for fpath := range activeFiles {

      
        
        97
        +		if ctx.Err() != nil {

      
        
        98
        +			return

      
        
        99
        +		}

      
        56
        100
         		if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{

      
        57
        101
         			URI:         uri.File(fpath),

      
        58
        102
         			Diagnostics: diagsByFile[fpath],

      ···
        61
        105
         		}

      
        62
        106
         	}

      
        63
        107
         

      
        64
        
        -	s.log.Debug("workspace rebuild complete",

      
        65
        
        -		"files", len(s.loader.Ordered()), "findings", len(finds))

      
        66
        
        -}

      
        67
        
        -

      
        68
        
        -func (s *server) scheduleWorkspaceRebuild(ctx context.Context) {

      
        69
        
        -	s.mu.Lock()

      
        70
        
        -	defer s.mu.Unlock()

      
        71
        
        -	if s.debouncer != nil {

      
        72
        
        -		s.debouncer.Stop()

      
        73
        
        -	}

      
        74
        
        -	s.debouncer = time.AfterFunc(200*time.Millisecond, func() {

      
        75
        
        -		s.rebuildWorkspace(jsonrpc2.DetachContext(ctx))

      
        76
        
        -	})

      
        77
        
        -}

      
        78
        
        -

      
        79
        
        -func (s *server) cancelWorkspaceRebuild() {

      
        80
        
        -	s.mu.Lock()

      
        81
        
        -	defer s.mu.Unlock()

      
        82
        
        -	if s.debouncer != nil {

      
        83
        
        -		s.debouncer.Stop()

      
        84
        
        -		s.debouncer = nil

      
        85
        
        -	}

      
        
        108
        +	s.log.Debug("diagnostics published", "files", len(ordered), "findings", len(finds))

      
        86
        109
         }

      
        87
        110
         

      
        88
        111
         func (s *server) activeFileSet(docs map[uri.URI]docState, ordered []*journal.ParsedFile) map[string]bool {

      
M internal/lsp/server.go
···
        4
        4
         	"context"

      
        5
        5
         	"log/slog"

      
        6
        6
         	"sync"

      
        7
        
        -	"time"

      
        8
        7
         

      
        9
        8
         	"go.lsp.dev/protocol"

      
        10
        9
         	"go.lsp.dev/uri"

      
        11
        10
         

      
        
        11
        +	"olexsmir.xyz/clerk/internal/analyzer"

      
        12
        12
         	"olexsmir.xyz/clerk/internal/linter"

      
        13
        13
         	"olexsmir.xyz/clerk/journal"

      
        14
        14
         	"olexsmir.xyz/clerk/journal/printer"

      
        15
        
        -	"olexsmir.xyz/clerk/journal/semantic"

      
        16
        15
         )

      
        17
        16
         

      
        18
        17
         type server struct {

      ···
        23
        22
         

      
        24
        23
         	version, name string

      
        25
        24
         

      
        26
        
        -	linter    *linter.Linter

      
        27
        
        -	loader    *journal.Loader

      
        28
        
        -	printer   *printer.Config

      
        29
        
        -	debouncer *time.Timer

      
        
        25
        +	linter  *linter.Linter

      
        
        26
        +	loader  *journal.Loader

      
        
        27
        +	printer *printer.Config

      
        30
        28
         

      
        31
        
        -	mu          sync.Mutex

      
        32
        
        -	openDocs    map[uri.URI]docState

      
        33
        
        -	semanticCtx *semantic.Context

      
        
        29
        +	mu       sync.Mutex

      
        
        30
        +	openDocs map[uri.URI]docState

      
        
        31
        +	current  *analyzer.Analysis

      
        
        32
        +

      
        
        33
        +	diagMu     sync.Mutex

      
        
        34
        +	diagCancel context.CancelFunc

      
        34
        35
         }

      
        35
        36
         

      
        36
        37
         type docState struct {

      ···
        56
        57
         }

      
        57
        58
         

      
        58
        59
         func (s *server) Initialized(ctx context.Context, params *protocol.InitializedParams) error {

      
        59
        
        -	s.scheduleWorkspaceRebuild(ctx)

      
        
        60
        +	s.scheduleDiagnostics(ctx)

      
        60
        61
         	return nil

      
        61
        62
         }

      
        62
        63
         

      
        63
        64
         func (s *server) Shutdown(ctx context.Context) error {

      
        64
        
        -	s.cancelWorkspaceRebuild()

      
        65
        65
         	return nil

      
        66
        66
         }

      
        67
        67
         

      
M internal/lsp/textdocument_sync.go
···
        9
        9
         

      
        10
        10
         func (s *server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {

      
        11
        11
         	s.openDoc(params.TextDocument.URI, params.TextDocument.Text, params.TextDocument.Version, params.TextDocument.LanguageID)

      
        12
        
        -	s.scheduleWorkspaceRebuild(ctx)

      
        
        12
        +	s.scheduleDiagnostics(ctx)

      
        13
        13
         	return nil

      
        14
        14
         }

      
        15
        15
         

      
        16
        16
         func (s *server) DidChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error {

      
        17
        17
         	s.updateDoc(params.TextDocument.URI, params.TextDocument.Version, params.ContentChanges)

      
        18
        
        -	s.scheduleWorkspaceRebuild(ctx)

      
        
        18
        +	s.scheduleDiagnostics(ctx)

      
        19
        19
         	return nil

      
        20
        20
         }

      
        21
        21
         

      
        22
        22
         func (s *server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {

      
        23
        23
         	s.closeDoc(params.TextDocument.URI)

      
        24
        
        -	s.scheduleWorkspaceRebuild(ctx)

      
        
        24
        +	s.scheduleDiagnostics(ctx)

      
        25
        25
         	return nil

      
        26
        26
         }

      
        27
        27
         

      
A journal/parser/benchmark_test.go
···
        
        1
        +package parser

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"os"

      
        
        5
        +	"testing"

      
        
        6
        +

      
        
        7
        +	"olexsmir.xyz/clerk/journal/lexer"

      
        
        8
        +	"olexsmir.xyz/clerk/journal/token"

      
        
        9
        +)

      
        
        10
        +

      
        
        11
        +func loadTestdata(path string) []byte {

      
        
        12
        +	src, err := os.ReadFile(path)

      
        
        13
        +	if err != nil {

      
        
        14
        +		panic("loadTestdata: " + err.Error())

      
        
        15
        +	}

      
        
        16
        +	return src

      
        
        17
        +}

      
        
        18
        +

      
        
        19
        +var (

      
        
        20
        +	basicSrc     = loadTestdata("../../journal/testdata/journals/basic.journal")

      
        
        21
        +	personalSrc  = loadTestdata("../../journal/testdata/journals/actual-personal.journal")

      
        
        22
        +	sampleSrc    = loadTestdata("../../journal/testdata/journals/actual-sample.journal")

      
        
        23
        +	standardSrc  = loadTestdata("../../journal/testdata/journals/actual-ledger-input-standard.dat")

      
        
        24
        +	wowSrc       = loadTestdata("../../journal/testdata/journals/actual-ledger-input-wow.dat")

      
        
        25
        +	benchmarkSrc = loadTestdata("../../journal/testdata/journals/actual-1ktxns-100accts.journal")

      
        
        26
        +)

      
        
        27
        +

      
        
        28
        +func BenchmarkLexer_Basic(b *testing.B) {

      
        
        29
        +	for b.Loop() {

      
        
        30
        +		l := lexer.New("bench", basicSrc)

      
        
        31
        +		for l.Next().Type != token.EOF {

      
        
        32
        +		}

      
        
        33
        +	}

      
        
        34
        +}

      
        
        35
        +

      
        
        36
        +func BenchmarkLexer_Personal(b *testing.B) {

      
        
        37
        +	for b.Loop() {

      
        
        38
        +		l := lexer.New("bench", personalSrc)

      
        
        39
        +		for l.Next().Type != token.EOF {

      
        
        40
        +		}

      
        
        41
        +	}

      
        
        42
        +}

      
        
        43
        +

      
        
        44
        +func BenchmarkLexer_Sample(b *testing.B) {

      
        
        45
        +	for b.Loop() {

      
        
        46
        +		l := lexer.New("bench", sampleSrc)

      
        
        47
        +		for l.Next().Type != token.EOF {

      
        
        48
        +		}

      
        
        49
        +	}

      
        
        50
        +}

      
        
        51
        +

      
        
        52
        +func BenchmarkLexer_Standard(b *testing.B) {

      
        
        53
        +	for b.Loop() {

      
        
        54
        +		l := lexer.New("bench", standardSrc)

      
        
        55
        +		for l.Next().Type != token.EOF {

      
        
        56
        +		}

      
        
        57
        +	}

      
        
        58
        +}

      
        
        59
        +

      
        
        60
        +func BenchmarkLexer_Wow(b *testing.B) {

      
        
        61
        +	for b.Loop() {

      
        
        62
        +		l := lexer.New("bench", wowSrc)

      
        
        63
        +		for l.Next().Type != token.EOF {

      
        
        64
        +		}

      
        
        65
        +	}

      
        
        66
        +}

      
        
        67
        +

      
        
        68
        +func BenchmarkLexer_1kTxns(b *testing.B) {

      
        
        69
        +	for b.Loop() {

      
        
        70
        +		l := lexer.New("bench", benchmarkSrc)

      
        
        71
        +		for l.Next().Type != token.EOF {

      
        
        72
        +		}

      
        
        73
        +	}

      
        
        74
        +}

      
        
        75
        +

      
        
        76
        +func BenchmarkParser_Basic(b *testing.B) {

      
        
        77
        +	for b.Loop() {

      
        
        78
        +		l := lexer.New("bench", basicSrc)

      
        
        79
        +		New(l).ParseJournal()

      
        
        80
        +	}

      
        
        81
        +}

      
        
        82
        +

      
        
        83
        +func BenchmarkParser_Personal(b *testing.B) {

      
        
        84
        +	for b.Loop() {

      
        
        85
        +		l := lexer.New("bench", personalSrc)

      
        
        86
        +		New(l).ParseJournal()

      
        
        87
        +	}

      
        
        88
        +}

      
        
        89
        +

      
        
        90
        +func BenchmarkParser_Sample(b *testing.B) {

      
        
        91
        +	for b.Loop() {

      
        
        92
        +		l := lexer.New("bench", sampleSrc)

      
        
        93
        +		New(l).ParseJournal()

      
        
        94
        +	}

      
        
        95
        +}

      
        
        96
        +

      
        
        97
        +func BenchmarkParser_Standard(b *testing.B) {

      
        
        98
        +	for b.Loop() {

      
        
        99
        +		l := lexer.New("bench", standardSrc)

      
        
        100
        +		New(l).ParseJournal()

      
        
        101
        +	}

      
        
        102
        +}

      
        
        103
        +

      
        
        104
        +func BenchmarkParser_Wow(b *testing.B) {

      
        
        105
        +	for b.Loop() {

      
        
        106
        +		l := lexer.New("bench", wowSrc)

      
        
        107
        +		New(l).ParseJournal()

      
        
        108
        +	}

      
        
        109
        +}

      
        
        110
        +

      
        
        111
        +func BenchmarkParser_1kTxns(b *testing.B) {

      
        
        112
        +	for b.Loop() {

      
        
        113
        +		l := lexer.New("bench", benchmarkSrc)

      
        
        114
        +		New(l).ParseJournal()

      
        
        115
        +	}

      
        
        116
        +}

      
        
        117
        +

      
        
        118
        +func BenchmarkParser_1kTxns_Allocs(b *testing.B) {

      
        
        119
        +	b.ReportAllocs()

      
        
        120
        +	for b.Loop() {

      
        
        121
        +		l := lexer.New("bench", benchmarkSrc)

      
        
        122
        +		New(l).ParseJournal()

      
        
        123
        +	}

      
        
        124
        +}

      
        
        125
        +

      
        
        126
        +func BenchmarkParser_Parallel_1kTxns(b *testing.B) {

      
        
        127
        +	b.RunParallel(func(pb *testing.PB) {

      
        
        128
        +		for pb.Next() {

      
        
        129
        +			l := lexer.New("bench", benchmarkSrc)

      
        
        130
        +			New(l).ParseJournal()

      
        
        131
        +		}

      
        
        132
        +	})

      
        
        133
        +}

      
D journal/semantic/build.go
···
        1
        
        -package semantic

      
        2
        
        -

      
        3
        
        -import (

      
        4
        
        -	"olexsmir.xyz/clerk/journal"

      
        5
        
        -	"olexsmir.xyz/clerk/journal/ast"

      
        6
        
        -)

      
        7
        
        -

      
        8
        
        -// Build constructs [Context] from a list of parsed files.

      
        9
        
        -// Files should be in dependency order(includes before includers).

      
        10
        
        -func Build(files []*journal.ParsedFile) *Context {

      
        11
        
        -	c := &Context{

      
        12
        
        -		Files:       files,

      
        13
        
        -		Accounts:    make(map[string]*AccountInfo),

      
        14
        
        -		Commodities: make(map[string]*CommodityInfo),

      
        15
        
        -		Payees:      make(map[string]*PayeeInfo),

      
        16
        
        -	}

      
        17
        
        -	for i, pf := range files {

      
        18
        
        -		for _, entry := range pf.Ast.Entries {

      
        19
        
        -			switch e := entry.(type) {

      
        20
        
        -			case *ast.AccountDirective:

      
        21
        
        -				c.addAccountDirective(e)

      
        22
        
        -			case *ast.CommodityDirective:

      
        23
        
        -				c.addCommodityDirective(e)

      
        24
        
        -			case *ast.PayeeDirective:

      
        25
        
        -				c.addPayeeDirective(e)

      
        26
        
        -			case *ast.Transaction:

      
        27
        
        -				c.addPostings(i, e.Postings)

      
        28
        
        -				c.addPayee(i, e.Payee)

      
        29
        
        -			case *ast.PeriodicTransaction:

      
        30
        
        -				c.addPostings(i, e.Postings)

      
        31
        
        -			case *ast.AutomatedTransaction:

      
        32
        
        -				c.addPostings(i, e.Postings)

      
        33
        
        -			}

      
        34
        
        -		}

      
        35
        
        -	}

      
        36
        
        -	return c

      
        37
        
        -}

      
        38
        
        -

      
        39
        
        -func (c *Context) addAccountDirective(ad *ast.AccountDirective) {

      
        40
        
        -	aname := ad.Account.String()

      
        41
        
        -	info, ok := c.Accounts[aname]

      
        42
        
        -	if !ok {

      
        43
        
        -		info = &AccountInfo{}

      
        44
        
        -		c.Accounts[aname] = info

      
        45
        
        -	}

      
        46
        
        -	info.Directives = append(info.Directives, ad)

      
        47
        
        -}

      
        48
        
        -

      
        49
        
        -func (c *Context) addPayeeDirective(pd *ast.PayeeDirective) {

      
        50
        
        -	info, ok := c.Payees[pd.Name]

      
        51
        
        -	if !ok {

      
        52
        
        -		info = &PayeeInfo{}

      
        53
        
        -		c.Payees[pd.Name] = info

      
        54
        
        -	}

      
        55
        
        -	info.Directives = append(info.Directives, &ast.Payee{

      
        56
        
        -		Name: pd.Name,

      
        57
        
        -		Span: pd.Span,

      
        58
        
        -	})

      
        59
        
        -}

      
        60
        
        -

      
        61
        
        -func (c *Context) addCommodityDirective(cd *ast.CommodityDirective) {

      
        62
        
        -	info, ok := c.Commodities[cd.Commodity]

      
        63
        
        -	if !ok {

      
        64
        
        -		info = &CommodityInfo{}

      
        65
        
        -		c.Commodities[cd.Commodity] = info

      
        66
        
        -	}

      
        67
        
        -	info.Directives = append(info.Directives, cd)

      
        68
        
        -}

      
        69
        
        -

      
        70
        
        -func (c *Context) addPayee(fileIndex int, payee *ast.Payee) {

      
        71
        
        -	if payee == nil {

      
        72
        
        -		return

      
        73
        
        -	}

      
        74
        
        -	info, ok := c.Payees[payee.Name]

      
        75
        
        -	if !ok {

      
        76
        
        -		info = &PayeeInfo{}

      
        77
        
        -		c.Payees[payee.Name] = info

      
        78
        
        -	}

      
        79
        
        -	info.Usage = append(info.Usage, PayeeUsage{

      
        80
        
        -		FileIndex: fileIndex,

      
        81
        
        -		Payee:     payee,

      
        82
        
        -	})

      
        83
        
        -}

      
        84
        
        -

      
        85
        
        -func (c *Context) addPostings(fileIndex int, postings []*ast.Posting) {

      
        86
        
        -	for _, posting := range postings {

      
        87
        
        -		// Account tracking

      
        88
        
        -		aname := posting.Account.String()

      
        89
        
        -		info, ok := c.Accounts[aname]

      
        90
        
        -		if !ok {

      
        91
        
        -			info = &AccountInfo{}

      
        92
        
        -			c.Accounts[aname] = info

      
        93
        
        -		}

      
        94
        
        -		info.Usages = append(info.Usages, AccountUsage{

      
        95
        
        -			FileIndex: fileIndex,

      
        96
        
        -			Posting:   posting,

      
        97
        
        -		})

      
        98
        
        -

      
        99
        
        -		// Commodity tracking

      
        100
        
        -		c.addCommodityUsage(fileIndex, posting.Amount)

      
        101
        
        -		if posting.Cost != nil {

      
        102
        
        -			c.addCommodityUsage(fileIndex, &posting.Cost.Amount)

      
        103
        
        -		}

      
        104
        
        -		if posting.Balance != nil {

      
        105
        
        -			c.addCommodityUsage(fileIndex, &posting.Balance.Amount)

      
        106
        
        -			if posting.Balance.Cost != nil {

      
        107
        
        -				c.addCommodityUsage(fileIndex, &posting.Balance.Cost.Amount)

      
        108
        
        -			}

      
        109
        
        -		}

      
        110
        
        -	}

      
        111
        
        -}

      
        112
        
        -

      
        113
        
        -func (c *Context) addCommodityUsage(fileIndex int, am *ast.Amount) {

      
        114
        
        -	if am == nil || am.Commodity == "" {

      
        115
        
        -		return

      
        116
        
        -	}

      
        117
        
        -	info, ok := c.Commodities[am.Commodity]

      
        118
        
        -	if !ok {

      
        119
        
        -		info = &CommodityInfo{}

      
        120
        
        -		c.Commodities[am.Commodity] = info

      
        121
        
        -	}

      
        122
        
        -	info.Usages = append(info.Usages, CommodityUsage{

      
        123
        
        -		FileIndex: fileIndex,

      
        124
        
        -		Amount:    am,

      
        125
        
        -	})

      
        126
        
        -}

      
M journal/semantic/build_test.gointernal/analyzer/build_test.go
···
        1
        
        -package semantic

      
        
        1
        +package analyzer

      
        2
        2
         

      
        3
        3
         import (

      
        4
        4
         	"bytes"

      
D journal/semantic/context.go
···
        1
        
        -package semantic

      
        2
        
        -

      
        3
        
        -import (

      
        4
        
        -	"olexsmir.xyz/clerk/journal"

      
        5
        
        -	"olexsmir.xyz/clerk/journal/ast"

      
        6
        
        -)

      
        7
        
        -

      
        8
        
        -// Context holds workspace-level semantic data built from parsed journal files.

      
        9
        
        -type Context struct {

      
        10
        
        -	// Files in dependency order (includes before includers).

      
        11
        
        -	Files []*journal.ParsedFile

      
        12
        
        -

      
        13
        
        -	Accounts    map[string]*AccountInfo

      
        14
        
        -	Commodities map[string]*CommodityInfo

      
        15
        
        -	Payees      map[string]*PayeeInfo

      
        16
        
        -}

      
        17
        
        -

      
        18
        
        -type AccountInfo struct {

      
        19
        
        -	// len == 0 =  account is used but never declared (undeclared)

      
        20
        
        -	// len  > 1 = account is declared more than once (duplicated)

      
        21
        
        -	// len  > 0 = account appears in a posting

      
        22
        
        -	Directives []*ast.AccountDirective

      
        23
        
        -	Usages     []AccountUsage

      
        24
        
        -}

      
        25
        
        -

      
        26
        
        -// AccountUsage is a single posting that references an account.

      
        27
        
        -type AccountUsage struct {

      
        28
        
        -	FileIndex int

      
        29
        
        -	Posting   *ast.Posting

      
        30
        
        -}

      
        31
        
        -

      
        32
        
        -// CommodityInfo tracks all declarations and usages for one commodity.

      
        33
        
        -type CommodityInfo struct {

      
        34
        
        -	Directives []*ast.CommodityDirective

      
        35
        
        -	Usages     []CommodityUsage

      
        36
        
        -}

      
        37
        
        -

      
        38
        
        -// CommodityUsage is a single amount that references a commodity.

      
        39
        
        -type CommodityUsage struct {

      
        40
        
        -	FileIndex int

      
        41
        
        -	Amount    *ast.Amount

      
        42
        
        -}

      
        43
        
        -

      
        44
        
        -type PayeeInfo struct {

      
        45
        
        -	Directives []*ast.Payee

      
        46
        
        -	Usage      []PayeeUsage

      
        47
        
        -}

      
        48
        
        -

      
        49
        
        -type PayeeUsage struct {

      
        50
        
        -	FileIndex int

      
        51
        
        -	Payee     *ast.Payee

      
        52
        
        -}

      
D journal/semantic/dump.go
···
        1
        
        -package semantic

      
        2
        
        -

      
        3
        
        -import (

      
        4
        
        -	"fmt"

      
        5
        
        -	"io"

      
        6
        
        -	"path/filepath"

      
        7
        
        -	"sort"

      
        8
        
        -)

      
        9
        
        -

      
        10
        
        -func fprint(w io.Writer, ctx *Context) {

      
        11
        
        -	// Files

      
        12
        
        -	fmt.Fprintf(w, "files (%d):\n", len(ctx.Files))

      
        13
        
        -	for i, pf := range ctx.Files {

      
        14
        
        -		fmt.Fprintf(w, "  %d: %s\n", i, filepath.Base(pf.Path))

      
        15
        
        -	}

      
        16
        
        -

      
        17
        
        -	// Commodities

      
        18
        
        -	csyms := make([]string, 0, len(ctx.Commodities))

      
        19
        
        -	for sym := range ctx.Commodities {

      
        20
        
        -		csyms = append(csyms, sym)

      
        21
        
        -	}

      
        22
        
        -	sort.Strings(csyms)

      
        23
        
        -

      
        24
        
        -	fmt.Fprintf(w, "\ncommodities (%d):\n", len(ctx.Commodities))

      
        25
        
        -	for _, sym := range csyms {

      
        26
        
        -		info := ctx.Commodities[sym]

      
        27
        
        -		fmt.Fprintf(w, "  %s\n", sym)

      
        28
        
        -		fmt.Fprintf(w, "    directives: %d\n", len(info.Directives))

      
        29
        
        -		for _, cd := range info.Directives {

      
        30
        
        -			fmt.Fprintf(w, "      commodity %s\n", cd.Commodity)

      
        31
        
        -		}

      
        32
        
        -		fmt.Fprintf(w, "    usages: %d\n", len(info.Usages))

      
        33
        
        -		for _, u := range info.Usages {

      
        34
        
        -			fmt.Fprintf(w, "      file %d: %s (%q)\n", u.FileIndex, u.Amount.Commodity, u.Amount.Quantity.String())

      
        35
        
        -		}

      
        36
        
        -	}

      
        37
        
        -

      
        38
        
        -	// Accounts

      
        39
        
        -	anames := make([]string, 0, len(ctx.Accounts))

      
        40
        
        -	for name := range ctx.Accounts {

      
        41
        
        -		anames = append(anames, name)

      
        42
        
        -	}

      
        43
        
        -	sort.Strings(anames)

      
        44
        
        -

      
        45
        
        -	fmt.Fprintf(w, "\naccounts (%d):\n", len(ctx.Accounts))

      
        46
        
        -	for _, name := range anames {

      
        47
        
        -		info := ctx.Accounts[name]

      
        48
        
        -		fmt.Fprintf(w, "  %s\n", name)

      
        49
        
        -		fmt.Fprintf(w, "    directives: %d\n", len(info.Directives))

      
        50
        
        -		for _, d := range info.Directives {

      
        51
        
        -			fmt.Fprintf(w, "      account %s\n", d.Account.String())

      
        52
        
        -		}

      
        53
        
        -		fmt.Fprintf(w, "    usages: %d\n", len(info.Usages))

      
        54
        
        -		for _, u := range info.Usages {

      
        55
        
        -			fmt.Fprintf(w, "      file %d: %s\n", u.FileIndex, u.Posting.Account.String())

      
        56
        
        -		}

      
        57
        
        -	}

      
        58
        
        -

      
        59
        
        -	// Payees

      
        60
        
        -	pnames := make([]string, 0, len(ctx.Payees))

      
        61
        
        -	for name := range ctx.Payees {

      
        62
        
        -		pnames = append(pnames, name)

      
        63
        
        -	}

      
        64
        
        -	sort.Strings(pnames)

      
        65
        
        -

      
        66
        
        -	fmt.Fprintf(w, "\npayees (%d):\n", len(ctx.Payees))

      
        67
        
        -	for _, name := range pnames {

      
        68
        
        -		info := ctx.Payees[name]

      
        69
        
        -		fmt.Fprintf(w, "  %s\n", name)

      
        70
        
        -		fmt.Fprintf(w, "    directives: %d\n", len(info.Directives))

      
        71
        
        -		for _, d := range info.Directives {

      
        72
        
        -			fmt.Fprintf(w, "      payee %s\n", d.Name)

      
        73
        
        -		}

      
        74
        
        -		fmt.Fprintf(w, "    usages: %d\n", len(info.Usage))

      
        75
        
        -		for _, u := range info.Usage {

      
        76
        
        -			fmt.Fprintf(w, "      file %d: %s\n", u.FileIndex, u.Payee.Name)

      
        77
        
        -		}

      
        78
        
        -	}

      
        79
        
        -}

      
D journal/semantic/testdata/empty.txtar
···
        1
        
        --- in.journal --

      
        2
        
        -

      
        3
        
        --- expect --

      
        4
        
        -files (1):

      
        5
        
        -  0: in.journal

      
        6
        
        -

      
        7
        
        -commodities (0):

      
        8
        
        -

      
        9
        
        -accounts (0):

      
        10
        
        -

      
        11
        
        -payees (0):

      
D journal/semantic/testdata/include.txtar
···
        1
        
        --- accounts.journal --

      
        2
        
        -account expenses:food

      
        3
        
        -

      
        4
        
        --- in.journal --

      
        5
        
        -include accounts.journal

      
        6
        
        -

      
        7
        
        -2024/01/01 ! test

      
        8
        
        -    expenses:food  $10

      
        9
        
        -    assets:checking

      
        10
        
        -

      
        11
        
        --- expect --

      
        12
        
        -files (2):

      
        13
        
        -  0: accounts.journal

      
        14
        
        -  1: in.journal

      
        15
        
        -

      
        16
        
        -commodities (1):

      
        17
        
        -  $

      
        18
        
        -    directives: 0

      
        19
        
        -    usages: 1

      
        20
        
        -      file 1: $ ("10")

      
        21
        
        -

      
        22
        
        -accounts (2):

      
        23
        
        -  assets:checking

      
        24
        
        -    directives: 0

      
        25
        
        -    usages: 1

      
        26
        
        -      file 1: assets:checking

      
        27
        
        -  expenses:food

      
        28
        
        -    directives: 1

      
        29
        
        -      account expenses:food

      
        30
        
        -    usages: 1

      
        31
        
        -      file 1: expenses:food

      
        32
        
        -

      
        33
        
        -payees (1):

      
        34
        
        -  test

      
        35
        
        -    directives: 0

      
        36
        
        -    usages: 1

      
        37
        
        -      file 1: test

      
D journal/semantic/testdata/journal.txtar
···
        1
        
        --- in.journal --

      
        2
        
        -account expenses:food

      
        3
        
        -account expenses:food

      
        4
        
        -account expenses:transport

      
        5
        
        -

      
        6
        
        -commodity $

      
        7
        
        -commodity $

      
        8
        
        -commodity EUR

      
        9
        
        -

      
        10
        
        -~ monthly

      
        11
        
        -    expenses:transport  $50

      
        12
        
        -    assets:checking

      
        13
        
        -

      
        14
        
        -= expr:big

      
        15
        
        -    expenses:food  $100

      
        16
        
        -    assets:credit

      
        17
        
        -

      
        18
        
        -2024/01/01 groceries

      
        19
        
        -    expenses:food  $10

      
        20
        
        -    assets:checking

      
        21
        
        -

      
        22
        
        -2024/01/02 lunch

      
        23
        
        -    expenses:food  $5

      
        24
        
        -    assets:cash

      
        25
        
        -

      
        26
        
        -2024/01/04 aws

      
        27
        
        -    expenses:food  1000000.00 USD

      
        28
        
        -    assets:cash

      
        29
        
        -

      
        30
        
        --- expect --

      
        31
        
        -files (1):

      
        32
        
        -  0: in.journal

      
        33
        
        -

      
        34
        
        -commodities (3):

      
        35
        
        -  $

      
        36
        
        -    directives: 2

      
        37
        
        -      commodity $

      
        38
        
        -      commodity $

      
        39
        
        -    usages: 4

      
        40
        
        -      file 0: $ ("50")

      
        41
        
        -      file 0: $ ("100")

      
        42
        
        -      file 0: $ ("10")

      
        43
        
        -      file 0: $ ("5")

      
        44
        
        -  EUR

      
        45
        
        -    directives: 1

      
        46
        
        -      commodity EUR

      
        47
        
        -    usages: 0

      
        48
        
        -  USD

      
        49
        
        -    directives: 0

      
        50
        
        -    usages: 1

      
        51
        
        -      file 0: USD ("1000000")

      
        52
        
        -

      
        53
        
        -accounts (5):

      
        54
        
        -  assets:cash

      
        55
        
        -    directives: 0

      
        56
        
        -    usages: 2

      
        57
        
        -      file 0: assets:cash

      
        58
        
        -      file 0: assets:cash

      
        59
        
        -  assets:checking

      
        60
        
        -    directives: 0

      
        61
        
        -    usages: 2

      
        62
        
        -      file 0: assets:checking

      
        63
        
        -      file 0: assets:checking

      
        64
        
        -  assets:credit

      
        65
        
        -    directives: 0

      
        66
        
        -    usages: 1

      
        67
        
        -      file 0: assets:credit

      
        68
        
        -  expenses:food

      
        69
        
        -    directives: 2

      
        70
        
        -      account expenses:food

      
        71
        
        -      account expenses:food

      
        72
        
        -    usages: 4

      
        73
        
        -      file 0: expenses:food

      
        74
        
        -      file 0: expenses:food

      
        75
        
        -      file 0: expenses:food

      
        76
        
        -      file 0: expenses:food

      
        77
        
        -  expenses:transport

      
        78
        
        -    directives: 1

      
        79
        
        -      account expenses:transport

      
        80
        
        -    usages: 1

      
        81
        
        -      file 0: expenses:transport

      
        82
        
        -

      
        83
        
        -payees (3):

      
        84
        
        -  aws

      
        85
        
        -    directives: 0

      
        86
        
        -    usages: 1

      
        87
        
        -      file 0: aws

      
        88
        
        -  groceries

      
        89
        
        -    directives: 0

      
        90
        
        -    usages: 1

      
        91
        
        -      file 0: groceries

      
        92
        
        -  lunch

      
        93
        
        -    directives: 0

      
        94
        
        -    usages: 1

      
        95
        
        -      file 0: lunch