12 files changed,
57 insertions(+),
63 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-09-02 19:12:11 +0300
Authored at:
2026-09-02 18:39:10 +0300
Change ID:
rkulqspkupnrqoouxzqrorqkuookmmtx
Parent:
747f2da
jump to
M
internal/analyzer/build.go
··· 230 230 info.UsedCount++ 231 231 } 232 232 233 -func (a *Analysis) addPostings(fileIndex int, postings []*ast.Posting, names []string, date *ast.Date) { 233 +func (a *Analysis) addPostings(fileIndex int, postings []ast.Posting, names []string, date *ast.Date) { 234 234 if names == nil { 235 235 names = make([]string, len(postings)) 236 236 for i, p := range postings { ··· 246 246 } 247 247 info.Usages = append(info.Usages, AccountUsage{ 248 248 FileIndex: fileIndex, 249 - Posting: posting, 249 + Posting: &postings[i], 250 250 }) 251 251 info.UsedCount++ 252 252 if date != nil {
M
internal/linter/rule_missing_commodity.go
··· 43 43 return finds 44 44 } 45 45 46 -func (m *MissingCommodity) checkPostings(finds *[]Find, postings []*ast.Posting) { 46 +func (m *MissingCommodity) checkPostings(finds *[]Find, postings []ast.Posting) { 47 47 for _, posting := range postings { 48 48 if posting.Amount == nil { 49 49 continue
M
internal/linter/rule_multiple_omitted_amounts.go
··· 25 25 return finds 26 26 } 27 27 28 -func (m *MultipleOmittedAmounts) check(postings []*ast.Posting) []Find { 28 +func (m *MultipleOmittedAmounts) check(postings []ast.Posting) []Find { 29 29 var finds []Find 30 30 for _, p := range postings { 31 31 if p.Amount == nil && p.Balance == nil {
M
internal/linter/rule_unbalanced_transaction.go
··· 29 29 return finds 30 30 } 31 31 32 -func (u *UnbalancedTransaction) check(postings []*ast.Posting, span token.Span) []Find { 32 +func (u *UnbalancedTransaction) check(postings []ast.Posting, span token.Span) []Find { 33 33 var hasExpr, hasCost bool 34 34 var realPostings []*ast.Posting 35 35 autoBalancingPostings := 0 36 36 37 - for _, posting := range postings { 37 + for i := range postings { 38 + posting := &postings[i] 38 39 if posting.Type != ast.PostingReal { 39 40 continue 40 41 }
M
internal/lsp/textdocument_folding_range.go
··· 84 84 } 85 85 86 86 // postingsFold folds the posting block of a transaction, leaving the header visible. 87 -func postingsFold(postings []*ast.Posting) *protocol.FoldingRange { 87 +func postingsFold(postings []ast.Posting) *protocol.FoldingRange { 88 88 if len(postings) < 2 { 89 89 return nil 90 90 }
M
internal/lsp/textdocument_hover.go
··· 147 147 return nil 148 148 } 149 149 150 -func hoverInPostings(content string, postings []*ast.Posting, cursor int) *hoverElement { 150 +func hoverInPostings(content string, postings []ast.Posting, cursor int) *hoverElement { 151 151 for _, p := range postings { 152 152 if spanContains(content, p.Account.Span, cursor) { 153 153 return &hoverElement{kind: hoverAccount, span: p.Account.Span, name: p.Account.String()}
M
internal/lsp/textdocument_rename.go
··· 211 211 return &symbolRef{symbolCommodity, am.Commodity, am.CommoditySpan} 212 212 } 213 213 214 -func symbolInPostings(content string, postings []*ast.Posting, cursor int) *symbolRef { 214 +func symbolInPostings(content string, postings []ast.Posting, cursor int) *symbolRef { 215 215 for _, p := range postings { 216 216 if spanContains(content, p.Account.Span, cursor) { 217 217 return &symbolRef{symbolAccount, p.Account.String(), p.Account.Span}
M
internal/lsp/textdocument_semantic_tokens.go
··· 384 384 emitComment(t.HeaderComments[i], emit) 385 385 } 386 386 for _, p := range t.Postings { 387 - visitPosting(content, p, emit) 387 + visitPosting(content, &p, emit) 388 388 } 389 389 } 390 390 ··· 421 421 emitComment(pt.HeaderComments[i], emit) 422 422 } 423 423 for _, p := range pt.Postings { 424 - visitPosting(content, p, emit) 424 + visitPosting(content, &p, emit) 425 425 } 426 426 } 427 427 ··· 437 437 emitComment(at.HeaderComments[i], emit) 438 438 } 439 439 for _, p := range at.Postings { 440 - visitPosting(content, p, emit) 440 + visitPosting(content, &p, emit) 441 441 } 442 442 } 443 443
M
journal/ast/dump.go
··· 154 154 dumpComment(b, c, depth+2) 155 155 } 156 156 } 157 - for _, p := range t.Postings { 158 - dumpPosting(b, p, depth+1) 157 + for i := range t.Postings { 158 + dumpPosting(b, &t.Postings[i], depth+1) 159 159 } 160 160 } 161 161 ··· 172 172 dumpComment(b, c, depth+2) 173 173 } 174 174 } 175 - for _, p := range t.Postings { 176 - dumpPosting(b, p, depth+1) 175 + for i := range t.Postings { 176 + dumpPosting(b, &t.Postings[i], depth+1) 177 177 } 178 178 } 179 179 ··· 210 210 dumpComment(b, c, depth+2) 211 211 } 212 212 } 213 - for _, p := range t.Postings { 214 - dumpPosting(b, p, depth+1) 213 + for i := range t.Postings { 214 + dumpPosting(b, &t.Postings[i], depth+1) 215 215 } 216 216 } 217 217
M
journal/ast/entries.go
··· 18 18 Note *Note // part after | 19 19 Comment *Comment // inline ; on header line 20 20 HeaderComments []*Comment // indented ; lines before first posting 21 - Postings []*Posting 21 + Postings []Posting 22 22 Span token.Span 23 23 } 24 24 ··· 40 40 Description *Description // optional description 41 41 Comment *Comment // optional inline comment 42 42 HeaderComments []*Comment 43 - Postings []*Posting 43 + Postings []Posting 44 44 Span token.Span 45 45 } 46 46 ··· 48 48 49 49 type AutomatedTransaction struct { 50 50 Expr Expr 51 - Postings []*Posting 51 + Postings []Posting 52 52 Comment *Comment // inline ; on header line 53 53 HeaderComments []*Comment // indented ; lines before first posting 54 54 Span token.Span
M
journal/parser/parser.go
··· 243 243 return at 244 244 } 245 245 246 -func (p *Parser) parseHeaderCommentsAndPostings() (comments []*ast.Comment, postings []*ast.Posting) { 246 +func (p *Parser) parseHeaderCommentsAndPostings() (comments []*ast.Comment, postings []ast.Posting) { 247 247 for p.got(token.INDENT) && p.willGet(token.SEMICOLON) { 248 248 p.advance() // consume indent 249 249 comments = append(comments, p.parseComment()) 250 250 } 251 251 252 + postings = make([]ast.Posting, 0, 2) // most transactions have 2 postings, small optimization 252 253 for p.got(token.INDENT) { 253 - if posting := p.parsePosting(); posting != nil { 254 + if posting, ok := p.parsePosting(); ok { 254 255 postings = append(postings, posting) 255 256 } 256 257 } ··· 423 424 amt.CommoditySpan = commoditySpan 424 425 amt.CommodityPos = ast.CommodityBefore 425 426 amt.HasSpace = hadSpace 426 - format = &ast.FormatSubDirective{Amount: *amt} 427 + format = &ast.FormatSubDirective{Amount: amt} 427 428 } 428 429 case token.INT, token.DECIMAL: 429 430 amt := p.parseAmount() 430 431 commodity = amt.Commodity 431 432 commoditySpan = amt.CommoditySpan 432 - format = &ast.FormatSubDirective{Amount: *amt} 433 + format = &ast.FormatSubDirective{Amount: amt} 433 434 default: 434 435 p.errorf("expected commodity name or amount, got %s", p.cur.Type) 435 436 } ··· 471 472 } 472 473 c := p.parseOptInlineComment() 473 474 p.expectNewline() 474 - format = &ast.FormatSubDirective{KeywordSpan: kw, Amount: *amt, Comment: c} 475 + format = &ast.FormatSubDirective{KeywordSpan: kw, Amount: amt, Comment: c} 475 476 case p.got(token.SEMICOLON): // comment line 476 477 c := p.parseCommentRest(p.cur.Span) 477 478 p.expectNewline() ··· 622 623 com := &ast.DefaultCommodityDirective{} 623 624 p.expect(token.D) 624 625 p.skipWhitespace() 625 - com.Amount = *p.parseAmount() 626 + com.Amount = p.parseAmount() 626 627 com.Comment = p.parseOptInlineComment() 627 628 p.expectNewline() 628 629 com.Span = p.span(s) ··· 636 637 p.skipWhitespace() 637 638 638 639 if p.isAmountStart() { 639 - cd.From = *p.parseAmount() 640 + cd.From = p.parseAmount() 640 641 } else { 641 642 p.errorf("expected amount, got %s", p.cur.Type) 642 643 } ··· 646 647 p.advance() 647 648 p.skipWhitespace() 648 649 if p.isAmountStart() { 649 - cd.To = *p.parseAmount() 650 + cd.To = p.parseAmount() 650 651 } else { 651 652 p.errorf("expected amount, got %s", p.cur.Type) 652 653 } ··· 697 698 } 698 699 p.skipWhitespace() 699 700 700 - mp.Amount = *p.parseAmount() 701 + mp.Amount = p.parseAmount() 701 702 702 703 mp.Comment = p.parseOptInlineComment() 703 704 ··· 846 847 } 847 848 } 848 849 849 -func (p *Parser) parseAmount() *ast.Amount { 850 +func (p *Parser) parseAmount() ast.Amount { 850 851 s := p.cur.Span 851 - amt := &ast.Amount{ 852 - QuantityFmt: ast.QuantityFormat{}, 853 - } 854 - defer func() { 855 - // The span covers from the first token to the start of the next unconsumed token. 856 - // Since parseQuantityInto (and possible commodity consumption) advanced past the last 857 - // amount token, p.cur points to the next token after the amount — which is the correct end. 858 - amt.Span = p.span(s) 859 - }() 852 + amt := ast.Amount{QuantityFmt: ast.QuantityFormat{}} 860 853 861 - p.parseAmountSign(amt) 854 + p.parseAmountSign(&amt) 862 855 p.skipWhitespace() 863 856 864 857 // commodity before quantity: $10.00, eur 10.00 ··· 875 868 } 876 869 877 870 // optional sign after commodity: $ -10 878 - p.parseAmountSign(amt) 871 + p.parseAmountSign(&amt) 879 872 p.skipWhitespace() 880 873 881 - p.parseQuantityInto(amt) 874 + p.parseQuantityInto(&amt) 882 875 883 876 // commodity after quantity: 10.00 UAH, 10.00 "EUR" (only if not set) 884 877 if amt.Commodity == "" { ··· 902 895 } 903 896 } 904 897 898 + amt.Span = p.span(s) 905 899 return amt 906 900 } 907 901 ··· 916 910 } 917 911 } 918 912 919 -func (p *Parser) parseAmountWithOptExpr() *ast.Amount { 913 +func (p *Parser) parseAmountWithOptExpr() ast.Amount { 920 914 if p.got(token.STAR) { 921 915 p.advance() 922 916 p.skipWhitespace() 923 917 amt := p.parseAmount() 924 - if amt != nil { 925 - amt.IsExpr = true 926 - } 918 + amt.IsExpr = true 927 919 return amt 928 920 } 929 921 if p.got(token.PARENEXPR) { 930 922 lit := p.cur.Literal 931 - amt := &ast.Amount{ 923 + amt := ast.Amount{ 932 924 IsExpr: true, 933 925 QuantityFmt: ast.QuantityFormat{}, 934 926 } ··· 942 934 return p.parseAmount() 943 935 } 944 936 945 -func (p *Parser) parsePosting() *ast.Posting { 937 +func (p *Parser) parsePosting() (ast.Posting, bool) { 946 938 s := p.cur.Span 947 - posting := &ast.Posting{} 939 + posting := ast.Posting{} 948 940 p.expect(token.INDENT) 949 941 950 942 // exit if it's empty line 951 943 if p.got(token.NEWLINE) || p.got(token.EOF) { 952 944 p.syncToNextline() 953 - return nil 945 + return ast.Posting{}, false 954 946 } 955 947 956 948 // optional status, outside of brackets, '! (account)' ··· 975 967 if p.cur.Type != token.TEXT { 976 968 p.errorf("expected account name, got %s", p.cur.Type) 977 969 p.syncToNextline() 978 - return nil 970 + return ast.Posting{}, false 979 971 } 980 972 981 973 posting.Account = p.parseAccount() ··· 992 984 if p.got(token.WHITESPACE) { 993 985 p.skipWhitespace() 994 986 if p.isAmountStart() { 995 - posting.Amount = p.parseAmountWithOptExpr() 987 + amt := p.parseAmountWithOptExpr() 988 + posting.Amount = &amt 996 989 } 997 990 } 998 991 ··· 1023 1016 } 1024 1017 1025 1018 posting.Span = p.span(s) 1026 - return posting 1019 + return posting, true 1027 1020 } 1028 1021 1029 1022 func (p *Parser) parseCost() *ast.Cost { ··· 1033 1026 p.skipWhitespace() 1034 1027 return &ast.Cost{ 1035 1028 IsTotal: isTotal, 1036 - Amount: *p.parseAmount(), 1029 + Amount: p.parseAmount(), 1037 1030 Span: p.span(s), 1038 1031 } 1039 1032 } ··· 1055 1048 p.advance() 1056 1049 p.skipWhitespace() 1057 1050 1058 - ba.Amount = *p.parseAmount() 1051 + ba.Amount = p.parseAmount() 1059 1052 p.skipWhitespace() 1060 1053 if p.got(token.AT) || p.got(token.ATAT) { 1061 1054 c := p.parseCost() ··· 1097 1090 1098 1091 func (p *Parser) parseAccount() ast.Account { 1099 1092 s := p.cur.Span 1100 - acc := ast.Account{} 1093 + acc := ast.Account{Name: make([]ast.SubAccount, 0, 2)} 1101 1094 1102 1095 sub, ok := p.readAccountSegment() 1103 1096 if !ok {
M
journal/printer/transaction_postings.go
··· 8 8 "olexsmir.xyz/clerk/journal/ast" 9 9 ) 10 10 11 -func (p *printer) writePostings(postings []*ast.Posting) { 11 +func (p *printer) writePostings(postings []ast.Posting) { 12 12 if len(postings) == 0 { 13 13 return 14 14 } ··· 21 21 } 22 22 } 23 23 24 -func (p *printer) writePostingsTwoSpaces(postings []*ast.Posting, maxAcct int) { 24 +func (p *printer) writePostingsTwoSpaces(postings []ast.Posting, maxAcct int) { 25 25 for _, pt := range postings { 26 - p.writePostingLine(pt, maxAcct) 26 + p.writePostingLine(&pt, maxAcct) 27 27 p.buf.WriteByte('\n') 28 28 for _, c := range pt.Comments { 29 29 p.buf.WriteString(p.indent) ··· 33 33 } 34 34 } 35 35 36 -func (p *printer) writePostingsTabbed(postings []*ast.Posting, maxAcct int) { 36 +func (p *printer) writePostingsTabbed(postings []ast.Posting, maxAcct int) { 37 37 var tmp strings.Builder 38 38 tw := tabwriter.NewWriter(&tmp, 0, 0, 2, ' ', tabwriter.StripEscape) 39 39 40 40 lp := &printer{cfg: p.cfg, indent: p.indent} 41 41 for _, pt := range postings { 42 42 lp.buf.Reset() 43 - lp.writePostingLine(pt, maxAcct) 43 + lp.writePostingLine(&pt, maxAcct) 44 44 _, _ = fmt.Fprintln(tw, lp.buf.String()) 45 45 for _, c := range pt.Comments { 46 46 lp.buf.Reset() ··· 128 128 } 129 129 } 130 130 131 -func measureTxAccts(postings []*ast.Posting) int { 131 +func measureTxAccts(postings []ast.Posting) int { 132 132 maxAcct := 0 133 133 for _, p := range postings { 134 134 n := len(p.Account.Name)