14 files changed,
68 insertions(+),
26 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-06-22 18:00:13 +0300
Authored at:
2026-06-17 16:50:16 +0300
Change ID:
srowyvkmpwwszlnzqmwzvwqkxtyrlnqr
Parent:
3bf8ce6
jump to
A
internal/linter/rule_missing_status.go
··· 1 +package linter 2 + 3 +import "olexsmir.xyz/clerk/journal/ast" 4 + 5 +// MissingStatus flags transactions with missing status. 6 +type MissingStatus struct{} 7 + 8 +func (MissingStatus) ID() RuleID { return "missing-status" } 9 +func (MissingStatus) Severity() Severity { return SeverityWarning } 10 +func (MissingStatus) Description() string { return "transaction has no status" } 11 +func (m *MissingStatus) CheckEntry(entry ast.Entry) []Find { 12 + tnx, ok := entry.(*ast.Transaction) 13 + if !ok { 14 + return nil 15 + } 16 + 17 + if tnx.Status.Value == ast.StatusNone { 18 + return []Find{{ 19 + Code: m.ID(), 20 + Severity: m.Severity(), 21 + Message: m.Description(), 22 + Span: tnx.Status.Span, 23 + }} 24 + } 25 + return nil 26 +}
A
internal/linter/testdata/missing-status.golden
··· 1 +missing-status.journal:1:12: missing-status: transaction has no status
A
internal/linter/testdata/missing-status.input
··· 1 +2024-05-01 "groceries" 2 + expenses:food 50.00 USD 3 + assets:cash
M
journal/ast/dump.go
··· 128 128 indent(b, depth+1) 129 129 fmt.Fprintf(b, "SecondDate: %s\n", dumpDate(*t.SecondDate)) 130 130 } 131 - if t.Status != nil { 131 + if t.Status.Value != StatusNone { 132 132 indent(b, depth+1) 133 133 fmt.Fprintf(b, "State: %q\n", t.Status.Value) 134 134 } ··· 188 188 indent(b, depth+1) 189 189 fmt.Fprintf(b, "To: %s\n", dumpDate(*t.Period.To)) 190 190 } 191 - if t.Status != nil { 191 + if t.Status.Value != StatusNone { 192 192 indent(b, depth+1) 193 193 fmt.Fprintf(b, "Status: %q\n", t.Status.Value) 194 194 } ··· 220 220 indent(b, depth+1) 221 221 fmt.Fprintf(b, "Type: %s\n", p.Type) 222 222 } 223 - if p.Status != nil { 223 + if p.Status.Value != StatusNone { 224 224 indent(b, depth+1) 225 225 fmt.Fprintf(b, "Status: %q\n", p.Status.Value) 226 226 }
M
journal/ast/entries.go
··· 11 11 12 12 type Transaction struct { 13 13 Date Date 14 - SecondDate *Date // optional =2026-05-18 date 15 - Status *Status // optional */! status 16 - Code *string // optional (123) code 17 - Payee *Payee // optional payee 18 - Note *string // part after | 14 + SecondDate *Date // optional =2026-05-18 date 15 + Status Status // optional */! status 16 + Code *string // optional (123) code 17 + Payee *Payee // optional payee 18 + Note *string // part after | 19 19 Comment *Comment // inline ; on header line 20 20 HeaderComments []*Comment // indented ; lines before first posting 21 21 Postings []*Posting ··· 35 35 36 36 type PeriodicTransaction struct { 37 37 Period Period // period-expr 38 - Status *Status // optional */! status 38 + Status Status // optional */! status 39 39 Code *string // optional (123) code 40 40 Description *string // optional description 41 41 Comment *Comment // optional inline comment ··· 79 79 80 80 type Posting struct { 81 81 Type PostingType 82 - Status *Status 82 + Status Status 83 83 Account Account 84 84 Amount *Amount // nil == auto-balancing 85 85 Cost *Cost // @ @@
M
journal/parser/parser.go
··· 752 752 } 753 753 } 754 754 755 -func (p *Parser) parseStatus() *ast.Status { 756 - if p.got(token.STAR) || p.got(token.BANG) { 757 - status := ast.StatusPending 758 - if p.cur.Literal[0] == '*' { 759 - status = ast.StatusCleared 760 - } 761 - st := &ast.Status{Value: status, Span: p.cur.Span} 755 +func (p *Parser) parseStatus() ast.Status { 756 + s := p.cur.Span 757 + st := ast.Status{} 758 + switch p.cur.Type { 759 + case token.STAR: 760 + p.advance() 761 + p.skipWhitespace() 762 + st.Value = ast.StatusCleared 763 + case token.BANG: 762 764 p.advance() 763 765 p.skipWhitespace() 764 - return st 766 + st.Value = ast.StatusPending 767 + default: 768 + st.Value = ast.StatusNone 765 769 } 766 - return nil 770 + st.Span = p.span(s) 771 + return st 767 772 } 768 773 769 774 func (p *Parser) isAmountStart() bool {
M
journal/parser/testdata/period_transaction_expressions.golden
··· 2 2 BlankLine j:2:0-2:1 3 3 PeriodicTransaction j:2:1-2:3 4 4 Period: "monthly" 5 + Status: "*" 5 6 Posting j:3:1-4:1 6 7 Account j:3:5-3:18 7 8 SubAccount: "expenses" j:3:5-3:13 ··· 24 25 Period: "monthly from 2023-04-15 to 2023-06-16" 25 26 From: 2023-04-15 26 27 To: 2023-06-16 28 + Status: "*" 27 29 Posting j:7:1-8:1 28 30 Account j:7:5-7:23 29 31 SubAccount: "expenses" j:7:5-7:13 ··· 44 46 BlankLine j:10:0-10:1 45 47 PeriodicTransaction j:10:1-10:3 46 48 Period: "every 2 months" 49 + Status: "*" 47 50 Description: "in 2023, we will review" 48 51 Posting j:11:1-12:1 49 52 Account j:11:5-11:23 ··· 65 68 BlankLine j:14:0-14:1 66 69 PeriodicTransaction j:14:1-14:3 67 70 Period: "monthly" 71 + Status: "*" 68 72 Description: "Next year blah blah" 69 73 Posting j:15:1-16:1 70 74 Account j:15:5-15:18 ··· 85 89 BlankLine j:18:0-18:1 86 90 PeriodicTransaction j:18:1-18:3 87 91 Period: "monthly from 2018/6" 92 + Status: "*" 88 93 Comment j:18:23-19:0 89 94 Marker: ";" 90 95 Text: "In 2019 we will change this"
M
journal/printer/transaction.go
··· 15 15 } 16 16 17 17 // status 18 - if t.Status != nil && t.Status.Value != ast.StatusNone { 18 + if t.Status.Value != ast.StatusNone { 19 19 p.buf.WriteByte(' ') 20 20 p.buf.WriteString(t.Status.Value.String()) 21 21 } ··· 63 63 } 64 64 65 65 // status 66 - if t.Status != nil && t.Status.Value != ast.StatusNone { 66 + if t.Status.Value != ast.StatusNone { 67 67 p.buf.WriteByte(' ') 68 68 p.buf.WriteString(t.Status.Value.String()) 69 69 }
M
journal/printer/transaction_postings.go
··· 62 62 func (p *printer) writePostingLine(pt *ast.Posting, maxAcct int) { 63 63 p.buf.WriteString(p.indent) 64 64 65 - if pt.Status != nil && pt.Status.Value != ast.StatusNone { 65 + if pt.Status.Value != ast.StatusNone { 66 66 p.buf.WriteString(pt.Status.Value.String()) 67 67 p.buf.WriteByte(' ') 68 68 } ··· 82 82 p.buf.WriteString(" ") 83 83 case AlignRight: 84 84 current := len(p.indent) + len(acct) 85 - if pt.Status != nil && pt.Status.Value != ast.StatusNone { 85 + if pt.Status.Value != ast.StatusNone { 86 86 current++ 87 87 } 88 88 if pad := p.cfg.AlignColumn - current; pad > 0 { ··· 91 91 p.buf.WriteByte('\v') 92 92 case AlignTab: 93 93 current := len(p.indent) + len(acct) 94 - if pt.Status != nil && pt.Status.Value != ast.StatusNone { 94 + if pt.Status.Value != ast.StatusNone { 95 95 current++ 96 96 } 97 97 if pad := maxAcct + len(p.indent) - current; pad > 0 {