31 files changed,
98 insertions(+),
157 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-09-29 19:57:41 +0300
Authored at:
2026-09-26 20:44:18 +0300
Change ID:
kkryxnumzpuorvvzsvqlpnsuzuupmvvv
Parent:
25c8f80
jump to
M
internal/analyzer/build.go
··· 57 57 return b.String() 58 58 } 59 59 60 -// PayeeTemplates returns the last Transactions's postings per payee name. 60 +// PayeeTemplates returns the last transaction's postings per payee name. 61 61 func (a *Analysis) PayeeTemplates() map[string][]PostingTemplate { 62 62 templates := make(map[string][]PostingTemplate) 63 63 for _, tx := range a.Transactions {
M
internal/decimal/decimal.go
··· 165 165 return sb.String() 166 166 } 167 167 168 -// WriteFixed writes a string representation with exactly places digits 169 -// after the decimal point directly into sb. Pads with zeros or truncates 170 -// as needed. decSep and thousandsSep control formatting; zero values mean 171 -// no custom separator. 168 +// WriteFixed is [Decimal.StringFixed] writing into sb instead of returning a string. 172 169 func (d Decimal) WriteFixed(sb *strings.Builder, places int, decSep, thousandsSep byte) { 173 170 if d.IsZero() { 174 171 sb.WriteByte('0')
M
internal/linter/rule_duplicated_transaction.go
··· 8 8 9 9 const DuplicatedTransactionID = "duplicated-transaction" 10 10 11 -// DuplicatedTransaction flags idnetical transactions. 11 +// DuplicatedTransaction flags identical transactions. 12 12 type DuplicatedTransaction struct{} 13 13 14 14 func (DuplicatedTransaction) ID() RuleID { return DuplicatedTransactionID }
M
internal/lsp/lifecycle.go
··· 44 44 45 45 // lifecycle gates the connection on the initialize request: before it, only 46 46 // initialize and exit are served; everything else answers ServerNotInitialized 47 -// and notifications are dropped. Every later message is served — like gopls, 47 +// and notifications are dropped. Every later message is served: like gopls, 48 48 // the server trusts its client, so shutdown and exit leave subsequent requests 49 49 // to fail naturally. The handlers themselves enforce initialize-once and the 50 50 // exit code.
M
internal/lsp/lsp.go
··· 88 88 return os.OpenFile(filepath.Join(dir, "lsp.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) 89 89 } 90 90 91 -// lspCodec mirros go.lsp.dev/protocol wire codec, so Run can install the lifecycle guard. 91 +// lspCodec mirrors go.lsp.dev/protocol wire codec, so Run can install the lifecycle guard. 92 92 type lspCodec struct{} 93 93 94 94 func (lspCodec) Marshal(v any) ([]byte, error) {
M
internal/lsp/lsputil/lineindex.go
··· 16 16 starts []int // byte offset of each line's first byte; starts[0] == 0 17 17 } 18 18 19 -// NewLineIndex builds the line-start table for content. 20 19 func NewLineIndex(content string) *LineIndex { 21 20 starts := make([]int, 1, len(content)/20+1) 22 21 for i := 0; i < len(content); i++ { ··· 80 79 for off < lineEnd && units < col { 81 80 r, size := utf8.DecodeRuneInString(l.content[off:lineEnd]) 82 81 off += size 83 - units += utf16Len(r) 82 + units += Utf16LenRune(r) 84 83 } 85 84 return off 86 85 }
M
internal/lsp/lsputil/utf16.go
··· 32 32 if r == utf8.RuneError && size <= 1 { 33 33 break 34 34 } 35 - col += utf16Len(r) 35 + col += Utf16LenRune(r) 36 36 i += size 37 37 } 38 38 return col ··· 55 55 if r == utf8.RuneError && size <= 1 { 56 56 break 57 57 } 58 - col += utf16Len(r) 58 + col += Utf16LenRune(r) 59 59 i += size 60 60 } 61 61 return col ··· 88 88 if r == utf8.RuneError && size <= 1 { 89 89 break 90 90 } 91 - n += utf16Len(r) 91 + n += Utf16LenRune(r) 92 92 i += size 93 93 } 94 94 return n ··· 126 126 for off < lineEnd && units < col { 127 127 r, size := utf8.DecodeRuneInString(content[off:]) 128 128 off += size 129 - units += utf16Len(r) 129 + units += Utf16LenRune(r) 130 130 } 131 131 return off 132 132 } ··· 188 188 return protocol.Position{Line: uint32(line), Character: uint32(col)} 189 189 } 190 190 191 -// utf16Len returns the number of UTF-16 code units for a rune. 192 -func utf16Len(r rune) int { 191 +// Utf16LenRune returns the number of UTF-16 code units for a rune. 192 +func Utf16LenRune(r rune) int { 193 193 if r >= 0x10000 && r <= 0x10FFFF { 194 194 return 2 195 195 }
M
internal/lsp/server.go
··· 27 27 conn jsonrpc2.Conn // set in Run; Exit closes it to end the session 28 28 29 29 stateMu sync.Mutex 30 - state serverState // [serverState] 30 + state serverState 31 31 version, name string 32 32 33 33 settings settings.Settings 34 34 loader *journal.Loader 35 35 36 - mu sync.RWMutex 37 - openDocs map[uri.URI]docState 38 - diagCancel context.CancelFunc 39 - dynFileWather bool 40 - configPath string 36 + mu sync.RWMutex 37 + openDocs map[uri.URI]docState 38 + diagCancel context.CancelFunc 39 + dynFileWatcher bool 40 + configPath string 41 41 } 42 42 43 -// parsedFileFor returns the parsed file for path within the analysis, or nil. 44 43 func parsedFileFor(an *analyzer.Analysis, path string) *journal.ParsedFile { 45 44 for _, pf := range an.Files { 46 45 if pf.Path == path { ··· 86 85 func (s *server) Initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) { 87 86 if w := params.Capabilities.Workspace; w != nil { 88 87 if wf := w.DidChangeWatchedFiles; wf != nil { 89 - s.dynFileWather = wf.DynamicRegistration != nil && *wf.DynamicRegistration 88 + s.dynFileWatcher = wf.DynamicRegistration != nil && *wf.DynamicRegistration 90 89 } 91 90 } 92 91 ··· 150 149 } 151 150 152 151 func (s *server) Initialized(ctx context.Context, params *protocol.InitializedParams) error { 153 - if s.dynFileWather { 152 + if s.dynFileWatcher { 154 153 go s.registerFileWatchers(context.Background()) 155 154 } 156 155 s.applyConfigFile(ctx) ··· 188 187 return nil 189 188 } 190 189 191 -// Exit records the exit code the LSP spec prescribes — 0 after a shutdown 192 -// request, 1 otherwise — and closes the connection to end the session. The 193 -// close runs in its own goroutine: called from inside the exit-notification 194 -// handler it cannot block on the connection draining (see [jsonrpc2.Conn.Close]). 195 -// Exit honors an exit that never saw initialize, and is safe to call 196 -// concurrently from the exit notification and watchParent: the stateMu guard 197 -// below makes the transition a once-only, and an exit that lost the race to 198 -// another exit is a no-op. 199 190 func (s *server) Exit(context.Context) error { 200 191 s.stateMu.Lock() 201 192 if s.state < stateExited { ··· 260 251 return 261 252 } 262 253 var raw map[string]any 263 - if err := toml.Unmarshal(data, &raw); err != nil { 264 - s.reportConfigError(ctx, err) 254 + if uerr := toml.Unmarshal(data, &raw); uerr != nil { 255 + s.reportConfigError(ctx, uerr) 265 256 return 266 257 } 267 258 s.mu.Lock()
M
internal/lsp/textdocument_completion.go
··· 136 136 } 137 137 138 138 func cmplHeaderCtx(content string, cursor int, toks []token.Token) (cmplCtx, int) { 139 - // cursor inside date token 140 139 if cursor >= toks[0].Span.Start.Offset && cursor <= toks[0].Span.End.Offset { 141 140 return cmplDate, toks[0].Span.Start.Offset 142 141 } ··· 195 194 return cmplNone, cursor 196 195 } 197 196 198 -// cmplDirectiveContext classifies a directive line. keyword completion before the keyword ends, symbol completion in the value field after 197 +// cmplDirectiveContext classifies a directive line: keyword completion up to the end of 198 +// the keyword, symbol completion in the value field after it. 199 199 func cmplDirectiveContext(cursor, lineStart int, toks []token.Token) (cmplCtx, int) { 200 200 kwEnd := toks[0].Span.End.Offset 201 201 if cursor <= kwEnd { ··· 290 290 return cmplCommodity, cursor 291 291 } 292 292 293 -// commentStart completes tag names before ':' of the current tag and tag values after it 293 +// cmplTagContext completes tag names before the ':' of the current tag, and tag values after it. 294 294 func cmplTagContext(content string, commentStart, cursor int) (cmplCtx, int) { 295 295 prefix := content[commentStart:cursor] 296 296 segStart := commentStart ··· 310 310 return cmplTagName, keyStart 311 311 } 312 312 313 -// tagKeyAt returns the key of tag whose value region starts at start 313 +// tagKeyAt returns the key of the tag whose value region starts at start. 314 314 func tagKeyAt(content string, start int) (string, bool) { 315 315 lineStart, _ := lineBounds(content, start) 316 316 segStart := lineStart ··· 376 376 rank int // lower sorts first among equal scores; 0 except for date completions 377 377 } 378 378 379 -// cmplItems ranks candidates for the content against typed pattern 379 +// cmplItems ranks candidates for the content against the typed pattern. 380 380 func cmplItems( 381 381 a *analyzer.Analysis, 382 382 ctx cmplCtx, ··· 579 579 return int64(era)*146097 + int64(doe) - 719468 580 580 } 581 581 582 -// dateStyle returns reparator and yesr-ness of the most recent history date. 583 -// Defualts to '-'/true when history is empty. 582 +// dateStyle returns the separator and year-ness of the most recent history date. 583 +// Defaults to '-' with a year when history is empty. 584 584 func dateStyle(history []string) (sep byte, hasYear bool) { 585 585 sep, hasYear = '-', true 586 586 if n := len(history); n > 0 {
M
internal/lsp/textdocument_definition.go
··· 271 271 return entries[idx] 272 272 } 273 273 274 -// entrySpan returns the span covering an entry. 275 274 func entrySpan(e ast.Entry) token.Span { 276 275 switch e := e.(type) { 277 276 case *ast.BlankLine: ··· 319 318 } 320 319 321 320 func spanEndClamped(content string, end int) int { 322 - for end > 0 { 323 - switch content[end-1] { 324 - case ' ', '\t', '\r', '\n': 325 - end-- 326 - default: 327 - return end 328 - } 321 + for end > 0 && isSpanSpace(content[end-1]) { 322 + end-- 329 323 } 330 324 return end 331 325 }
M
internal/lsp/textdocument_hover.go
··· 80 80 if spanContains(content, e.Date.Span, cursor) { 81 81 return &hoverElement{kind: hoverDate, span: e.Date.Span, tx: e} 82 82 } 83 - if el := hoverTagInComment(content, e.Comment, cursor); el != nil { 84 - return el 85 - } 86 - for _, c := range e.HeaderComments { 87 - if el := hoverTagInComment(content, c, cursor); el != nil { 88 - return el 89 - } 90 - } 91 - return hoverInPostings(content, e.Postings, cursor) 83 + return hoverInTransaction(content, e.Comment, e.HeaderComments, e.Postings, cursor) 92 84 case *ast.PeriodicTransaction: 93 - if el := hoverTagInComment(content, e.Comment, cursor); el != nil { 94 - return el 95 - } 96 - for _, c := range e.HeaderComments { 97 - if el := hoverTagInComment(content, c, cursor); el != nil { 98 - return el 99 - } 100 - } 101 - return hoverInPostings(content, e.Postings, cursor) 85 + return hoverInTransaction(content, e.Comment, e.HeaderComments, e.Postings, cursor) 102 86 case *ast.AutomatedTransaction: 103 - if el := hoverTagInComment(content, e.Comment, cursor); el != nil { 104 - return el 105 - } 106 - for _, c := range e.HeaderComments { 107 - if el := hoverTagInComment(content, c, cursor); el != nil { 108 - return el 109 - } 110 - } 111 - return hoverInPostings(content, e.Postings, cursor) 87 + return hoverInTransaction(content, e.Comment, e.HeaderComments, e.Postings, cursor) 112 88 case *ast.Comment: 113 89 return hoverTagInComment(content, e, cursor) 114 90 case *ast.AccountDirective: ··· 143 119 } 144 120 } 145 121 return nil 122 +} 123 + 124 +func hoverInTransaction(content string, comment *ast.Comment, header []*ast.Comment, postings []ast.Posting, cursor int) *hoverElement { 125 + if el := hoverTagInComment(content, comment, cursor); el != nil { 126 + return el 127 + } 128 + for _, c := range header { 129 + if el := hoverTagInComment(content, c, cursor); el != nil { 130 + return el 131 + } 132 + } 133 + return hoverInPostings(content, postings, cursor) 146 134 } 147 135 148 136 func hoverInPostings(content string, postings []ast.Posting, cursor int) *hoverElement {
M
internal/lsp/textdocument_rename.go
··· 97 97 if e.Payee != "" && spanContains(content, e.PayeeSpan, cursor) { 98 98 return &symbolRef{symbolPayee, e.Payee, e.PayeeSpan} 99 99 } 100 - if ref := tagRefInComment(content, e.Comment, cursor); ref != nil { 101 - return ref 102 - } 103 - for _, c := range e.HeaderComments { 104 - if ref := tagRefInComment(content, c, cursor); ref != nil { 105 - return ref 106 - } 107 - } 108 - return symbolInPostings(content, e.Postings, cursor) 100 + return symbolInTransaction(content, e.Comment, e.HeaderComments, e.Postings, cursor) 109 101 case *ast.PeriodicTransaction: 110 - if ref := tagRefInComment(content, e.Comment, cursor); ref != nil { 111 - return ref 112 - } 113 - for _, c := range e.HeaderComments { 114 - if ref := tagRefInComment(content, c, cursor); ref != nil { 115 - return ref 116 - } 117 - } 118 - return symbolInPostings(content, e.Postings, cursor) 102 + return symbolInTransaction(content, e.Comment, e.HeaderComments, e.Postings, cursor) 119 103 case *ast.AutomatedTransaction: 120 - if ref := tagRefInComment(content, e.Comment, cursor); ref != nil { 121 - return ref 122 - } 123 - for _, c := range e.HeaderComments { 124 - if ref := tagRefInComment(content, c, cursor); ref != nil { 125 - return ref 126 - } 127 - } 128 - return symbolInPostings(content, e.Postings, cursor) 104 + return symbolInTransaction(content, e.Comment, e.HeaderComments, e.Postings, cursor) 129 105 case *ast.Comment: 130 106 return tagRefInComment(content, e, cursor) 131 107 case *ast.AccountDirective: ··· 217 193 return nil 218 194 } 219 195 return &symbolRef{symbolCommodity, am.Commodity, am.CommoditySpan} 196 +} 197 + 198 +func symbolInTransaction(content string, comment *ast.Comment, header []*ast.Comment, postings []ast.Posting, cursor int) *symbolRef { 199 + if ref := tagRefInComment(content, comment, cursor); ref != nil { 200 + return ref 201 + } 202 + for _, c := range header { 203 + if ref := tagRefInComment(content, c, cursor); ref != nil { 204 + return ref 205 + } 206 + } 207 + return symbolInPostings(content, postings, cursor) 220 208 } 221 209 222 210 func symbolInPostings(content string, postings []ast.Posting, cursor int) *symbolRef { ··· 433 421 changes[u] = dedup 434 422 } 435 423 } 436 - 437 -// Validation 438 424 439 425 func validateAccountName(name string) error { return validateRenameName(name, "account", ";") } 440 426 func validateCommodityName(name string) error { return validateRenameName(name, "commodity", ";") }
M
internal/lsp/textdocument_semantic_tokens.go
··· 239 239 return rawToSemanticTokens(content, raw) 240 240 } 241 241 242 -// rawSpan is a source span tagged with semantic token 242 +// rawSpan is a source span carrying a semantic token type and modifiers 243 243 type rawSpan struct { 244 244 span token.Span 245 245 tok, mods uint32 ··· 384 384 continue 385 385 } 386 386 cursor += size 387 - col += utf16Units(r) 387 + col += lsputil.Utf16LenRune(r) 388 388 } 389 389 } 390 390 for i, t := range raw { ··· 403 403 return out 404 404 } 405 405 406 -func utf16Units(r rune) int { 407 - if r >= 0x10000 && r <= 0x10FFFF { 408 - return 2 409 - } 410 - return 1 411 -} 412 - 413 406 func visitEntry(content string, e ast.Entry, emit semEmitFunc) { 414 407 switch e := e.(type) { 415 408 case *ast.Transaction: ··· 481 474 if e.DateTime.Time != nil { 482 475 emit(e.DateTime.Time.Span, semDate, 0) 483 476 } 484 - // commodity: text between the date (or time) and the amount 485 477 commStart := e.DateTime.Date.Span.End.Offset 486 478 if e.DateTime.Time != nil { 487 479 commStart = e.DateTime.Time.Span.End.Offset ··· 494 486 case *ast.ConversionDirective: 495 487 emit(directiveKeyword(e.Span, "C"), semDirective, 0) 496 488 semEmitAmount(content, &e.From, emit) 497 - // = operator: text between the two amounts 498 489 if op, ok := betweenSpan(content, e.Span.File, e.From.Span.End.Offset, e.To.Span.Start.Offset); ok { 499 490 emit(op, semOperator, 0) 500 491 } ··· 598 589 emit(p.StatusSpan, semStatus, 0) 599 590 } 600 591 601 - // virtual brackets 602 592 if p.Type == ast.PostingVirtualUnbalanced || p.Type == ast.PostingVirtualBalanced { 603 - // opening bracket 604 593 for off := p.Span.Start.Offset; off < p.Account.Span.Start.Offset && off < p.Span.End.Offset; off++ { 605 594 if content[off] == '(' || content[off] == '[' { 606 595 emit(offsetSpan(p.Span.File, off, off+1), semOperator, modifierAbstract)
M
internal/lsp/textdocument_semantic_tokens_test.go
··· 329 329 ar := golden.Read(t, tt) 330 330 in, edited := ar.Get("in.journal"), ar.Get("edited.journal") 331 331 332 - // golden: the server's incremental result equals a full tokenization 333 332 golden.Assert(t, ar, renderSemanticTokens(serverSemTokensAfterEdit(t, newServer(t).server, string(in), string(edited)))) 334 333 335 - // engage: the incremental path actually runs, not a silent full rebuild 334 + // the incremental path has to actually run, not silently rebuild 336 335 ev := partialChangeFromDiff(string(in), string(edited))[0].(*protocol.TextDocumentContentChangePartial) 337 336 start, oldEnd, newEnd, delta := editRegion(string(in), ev) 338 337 _, entries := computeSemTokens(string(in), parseJournalStr(string(in))) ··· 361 360 } 362 361 } 363 362 364 -// BenchmarkSemanticTokensDelta measures the cost of one dela response after an edit. 363 +// BenchmarkSemanticTokensDelta measures the cost of one delta response after an edit. 365 364 func BenchmarkSemanticTokensDelta(b *testing.B) { 366 365 content := openJournal(b, "../../journal/testdata/journals/actual-1ktxns-100accts.journal") 367 366 prev := encodeSemTokens(tokenizeForSemantics(content, parseJournalStr(content)))
M
internal/settings/settings.go
··· 54 54 return s, warns, err 55 55 } 56 56 57 -// Apply merges raw setting from a config file into Settings object. 57 +// Apply merges settings from a config file into s. 58 58 func (s *Settings) Apply(raw map[string]any) ([]string, error) { 59 59 return applyMap(raw, s.applyFileField) 60 60 } ··· 70 70 } 71 71 } 72 72 73 -// ApplyLSP merges raw settings from lsp server config into Settings object. 73 +// ApplyLSP merges settings from the LSP server configuration into s. 74 74 func (s *Settings) ApplyLSP(raw map[string]any) ([]string, error) { 75 75 return applyMap(raw, s.applyLSPField) 76 76 }
M
journal/journal_test.go
··· 14 14 15 15 var tests = map[string]test{ 16 16 "actual-1ktxns-100accts.journal": {desc: "hledger: stress test: 1000 transactions, 100 accounts, number-only account names"}, 17 - "actual-accounttypes.journal": {desc: "hledger: account type annotations (type:A, type:L) via comments"}, // todo: tags are not supported yet 17 + "actual-accounttypes.journal": {desc: "hledger: account type annotations (type:A, type:L) via comments"}, // TODO: tags are not supported yet 18 18 "actual-alias.journal": {desc: "hledger: account alias directives for renaming"}, 19 19 "actual-borrowing.journal": {desc: "hledger: borrowing/lending example with liabilities"}, 20 20 "actual-business.journal": {desc: "hledger: simple business transactions with commodities"}, 21 21 "actual-goal-budget.journal": {desc: "hledger: goal budget using periodic transactions"}, 22 22 "actual-i18n-en.journal": {desc: "hledger: internationalization with account types in English"}, 23 - "actual-ledger-baseline-opt-lots-basis.dat": {err: true, desc: "ledger: G/S prefixes"}, // no lot support yet 23 + "actual-ledger-baseline-opt-lots-basis.dat": {err: true, desc: "ledger: G/S prefixes"}, // TODO: no lot support yet 24 24 "actual-ledger-input-divzero.dat": {desc: "ledger: fuzz corpus, designed to cause divide-by-zero"}, 25 25 "actual-ledger-input-parsing.dat": {desc: "ledger: fuzz corpus, tests EOF without newline"}, 26 26 "actual-ledger-input-sample.dat": {desc: "ledger: fuzz corpus, default commodity directive"}, 27 27 "actual-ledger-input-standard.dat": {desc: "ledger: fuzz corpus, standard ledger format"}, 28 28 "actual-ledger-input-transfer.dat": {desc: "ledger: fuzz corpus, byte quantity (non-monetary)"}, 29 - "actual-ledger-input-wow.dat": {err: true, desc: "ledger-cli: fuzz corpus, World of Warcraft currency (1G=100s)"}, // no lot support ye 29 + "actual-ledger-input-wow.dat": {err: true, desc: "ledger-cli: fuzz corpus, World of Warcraft currency (1G=100s)"}, // TOOD: no lot support yet 30 30 "actual-multicurrency.journal": {desc: "hledger: multi-currency transactions with HRK/EUR"}, 31 31 "actual-personal.journal": {desc: "hledger: simple personal finance example"}, 32 32 "actual-quickstart.journal": {desc: "hledger: quickstart guide with commodity directive"},
M
journal/lexer/lexer.go
··· 120 120 return tok 121 121 case l.ch == ';' || l.ch == '#' || l.ch == '%': 122 122 l.mode = modeComment 123 - return l.lexSingle(token.SEMICOLON) // todo: ?? 123 + return l.lexSingle(token.SEMICOLON) 124 124 case l.ch == '*': // * at col 0 == comment 125 125 l.mode = modeComment 126 126 return l.lexSingle(token.STAR) ··· 237 237 case '"', '\'': 238 238 return l.lexString() 239 239 default: // description / payee 240 - if l.isDate() { // secondsry date after = 240 + if l.isDate() { // secondary date after = 241 241 return l.lexDate() 242 242 } 243 243 return l.lexText() ··· 482 482 return token.Token{Type: token.TEXT, Literal: lit, Span: l.span(s)} 483 483 } 484 484 485 -// lexAccountNameDirective reads accout name in directive context. 485 +// lexAccountNameDirective reads an account name in directive context. 486 486 // stops at any whitespace, supports multi-word names("Taxi Fare"). 487 487 func (l *Lexer) lexAccountNameDirective() token.Token { 488 488 s := l.save() ··· 574 574 } 575 575 lit := l.lit(s) 576 576 kind := l.keyword(lit) 577 - if kind == token.ILLEGAL { // todo: report an error ?? 577 + if kind == token.ILLEGAL { 578 578 kind = token.TEXT 579 579 } else { 580 580 l.mode = modeDirective
M
journal/lexer/lexer_test.go
··· 30 30 "transaction with code", 31 31 "transaction with unicode commodity symbols", 32 32 "transaction with virtual accounts", 33 - "transaction, accounts with uppercase latters", 33 + "transaction, accounts with uppercase letters", 34 34 } 35 35 for _, tt := range tests { 36 36 t.Run(tt, func(t *testing.T) { ··· 82 82 for range maxTokens { 83 83 tok := l.Next() 84 84 85 - // Monotonic span 86 85 if tok.Span.Start.Offset < prevEnd { 87 86 t.Fatalf("non-monotonic span: prevEnd=%d current=%s %d", 88 87 prevEnd, tok.Type, tok.Span.Start.Offset) 89 88 } 90 89 91 - // Token type in range (no garbage from memory corruption) 92 90 if tok.Type < 0 || tok.Type > maxKnownTokenType { 93 91 t.Fatalf("token type out of range: %d", tok.Type) 94 92 } 95 93 96 - // Span in bounds (EOF/NEWLINE sentinels may extend one past input) 94 + // EOF and NEWLINE sentinels may extend one past the input 97 95 maxEnd := len(data) 98 96 if tok.Type == token.NEWLINE || tok.Type == token.EOF { 99 97 maxEnd = len(data) + 1 ··· 124 122 t.Fatalf("token consumed beyond input: end=%d len=%d", prevEnd, len(data)) 125 123 } 126 124 127 - // Pass 2: re-lex the same input — token stream must be identical 125 + // pass 2: re-lex the same input, the token stream must be identical 128 126 l2 := New("j", data) 129 127 for _, expected := range tokens { 130 128 tok := l2.Next()
M
journal/loader.go
··· 61 61 ast *ast.Journal 62 62 } 63 63 64 -// Loader include-aware journal parsing caching. 64 +// Loader parses a journal and its includes, caching both by canonical path. 65 65 type Loader struct { 66 66 mu sync.RWMutex 67 67 contentCache map[string][]byte // canonical path: normalised content ··· 164 164 l.mu.Unlock() 165 165 } 166 166 167 -// Evict drops a file's cached content. 167 +// Evict drops a file's cached content and every parse cached for it. 168 168 func (l *Loader) Evict(fpath string) { 169 169 canon := CanonicalPath(fpath) 170 170 l.mu.Lock() ··· 211 211 return bytes.ReplaceAll(content, []byte("\r"), []byte("\n")) 212 212 } 213 213 214 -// resolveOccurrence recursively parses one occurrence and its includes 214 +// resolveOccurrence recursively parses one occurrence and its includes. 215 215 func (l *Loader) resolveOccurrence(rj *ResolvedJournal, parent *ParsedFile, fpath string, src []byte, defaultYear int, stack []string) { 216 216 // cycle detection uses canonical paths to catch cycles through symlinks. 217 217 canon := CanonicalPath(fpath) ··· 316 316 return target, nil 317 317 } 318 318 319 -// CanonicalPath resolvea path to it's canonical form: absolute, symlinks evaluated, cleaned. 319 +// CanonicalPath resolves a path to its canonical form: absolute, symlinks evaluated, cleaned. 320 320 func CanonicalPath(path string) string { 321 321 abs, err := filepath.Abs(path) 322 322 if err != nil {
M
journal/parser/parser.go
··· 1198 1198 } 1199 1199 1200 1200 func (p *Parser) expectNewline() { 1201 - if p.got(token.NEWLINE) || p.got(token.EOF) { 1202 - if p.got(token.NEWLINE) { 1203 - p.advance() 1204 - } 1205 - return 1201 + switch { 1202 + case p.got(token.NEWLINE): 1203 + p.advance() 1204 + case p.got(token.EOF): 1205 + default: 1206 + p.errorf("expected %s, got %s", token.NEWLINE, p.cur.Type) 1206 1207 } 1207 - p.errorf("expected %s, got %s", token.NEWLINE, p.cur.Type) 1208 1208 } 1209 1209 1210 1210 func (p *Parser) advance() token.Token { ··· 1417 1417 return ast.QuantityFormat{Decimal: dec, Thousands: thou, Precision: prec} 1418 1418 } 1419 1419 1420 -// parseSimpleDate parses full YYYY/MM/DD date literal embedded in free text. 1420 +// parseSimpleDate parses full YYYY/MM/DD date literal embedded in free text. 1421 1421 func parseSimpleDate(s string) ast.Date { 1422 1422 year, month, day, sep, err := ParseDateLiteral(s) 1423 1423 if err != nil { ··· 1471 1471 return 0 1472 1472 } 1473 1473 1474 -// parseCommentTags extacts tags from comment text. 1474 +// parseCommentTags extracts tags from comment text. 1475 1475 // A tag is a word immediately followed by a ':', with an optional value that ends at a comma or the end of a line. 1476 1476 // https://hledger.org/1.52/hledger.html?highlight=tags#tags 1477 1477 func parseCommentTags(text string, base token.Span) []ast.Tag {
M
journal/parser/parser_test.go
··· 28 28 "P directive quoted commodity", 29 29 "N directive", 30 30 "N directive quoted", 31 - "inclue directive", 32 - "inclue directive with comment", 33 - "inclue directive digit path", 31 + "include directive", 32 + "include directive with comment", 33 + "include directive digit path", 34 34 "apply tag directive", 35 35 "apply fixed directive", 36 36 "end apply directive", ··· 44 44 "account directive with comment", 45 45 "account with subdirectives", 46 46 "account subdirectives types", 47 - "comodity directive", 48 - "comodity directive word", 49 - "comodity directive no space", 47 + "commodity directive", 48 + "commodity directive word", 49 + "commodity directive no space", 50 50 "commodity quantity first", 51 51 "commodity quantity after", 52 52 "commodity with subdirectives", ··· 59 59 "account with spaces", 60 60 "transaction with multiword payee", 61 61 "transaction pending", 62 - "transaction clearerd", 62 + "transaction cleared", 63 63 "transaction with note", 64 64 "transaction with comment", 65 65 "transaction with secondary date",