all repos

clerk @ 4481c7d

missing tooling for ledger/hledger
1 files changed, 24 insertions(+), 65 deletions(-)
parser: some refactoring
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-09-10 16:55:48 +0300
Authored at: 2026-09-10 14:45:46 +0300
Change ID: vmsmwxvkquvtsktzrqyzszlkzlsmwmzy
Parent: d3fcad3
M journal/parser/parser.go
···
        208
        208
         	p.skipWhitespace()

      
        209
        209
         

      
        210
        210
         	pt := &ast.PeriodicTransaction{}

      
        211
        
        -

      
        212
        211
         	pt.Period = p.parsePeriod()

      
        213
        
        -

      
        214
        212
         	if desc, dspan := p.parseOptPeriodicDescription(); desc != "" {

      
        215
        213
         		pt.Description = desc

      
        216
        214
         		pt.DescriptionSpan = dspan

      
        217
        215
         	}

      
        218
        216
         

      
        219
        
        -	comment := p.parseOptInlineComment()

      
        
        217
        +	pt.Comment = p.parseOptInlineComment()

      
        220
        218
         	p.expectNewline()

      
        221
        219
         

      
        222
        220
         	pt.HeaderComments, pt.Postings = p.parseHeaderCommentsAndPostings()

      
        223
        
        -

      
        224
        221
         	pt.Span = p.span(s)

      
        225
        
        -	pt.Comment = comment

      
        226
        222
         	return pt

      
        227
        223
         }

      
        228
        224
         

      ···
        234
        230
         	at := &ast.AutomatedTransaction{}

      
        235
        231
         

      
        236
        232
         	// expression

      
        237
        
        -	sd := p.cur.Span

      
        
        233
        +	expSpan := p.cur.Span

      
        238
        234
         	expr := p.parseDirectiveExpr()

      
        239
        235
         	at.Expr = expr

      
        240
        
        -	at.ExprSpan = p.span(sd)

      
        
        236
        +	at.ExprSpan = p.span(expSpan)

      
        241
        237
         	at.Comment = p.parseOptInlineComment()

      
        242
        238
         	p.expectNewline()

      
        243
        239
         

      ···
        267
        263
         	s := p.cur.Span

      
        268
        264
         

      
        269
        265
         	var periodBuf strings.Builder

      
        270
        
        -

      
        271
        266
         	for !p.got(token.NEWLINE) && !p.got(token.EOF) &&

      
        272
        267
         		!p.got(token.SEMICOLON) && !p.got(token.HASH) && !p.got(token.PERCENT) && !p.got(token.STAR) {

      
        273
        268
         

      ···
        289
        284
         

      
        290
        285
         	str := periodBuf.String()

      
        291
        286
         	period := ast.Period{Raw: str, Span: p.span(s)}

      
        292
        
        -

      
        293
        287
         	if _, after, ok := strings.Cut(str, " from "); ok {

      
        294
        288
         		end := strings.Index(after, " ")

      
        295
        289
         		dateStr := after

      ···
        318
        312
         	return period

      
        319
        313
         }

      
        320
        314
         

      
        321
        
        -// periodDateSpan returns the source span of dateStr, which occurs in the

      
        322
        
        -// period text at or after searchFrom. The period span and text cover the same

      
        323
        
        -// bytes, so offsets line up 1:1.

      
        324
        315
         func periodDateSpan(period ast.Period, text, dateStr string, searchFrom int) token.Span {

      
        325
        316
         	off := strings.Index(text[searchFrom:], dateStr)

      
        326
        317
         	abs := period.Span.Start.Offset + searchFrom + off

      ···
        482
        473
         			p.expectNewline()

      
        483
        474
         			blockComments = append(blockComments, c)

      
        484
        475
         		case p.got(token.TEXT) && isCommentMarker(p.cur.Literal):

      
        485
        
        -			// '#', '%' and '*' lex as TEXT in directive mode; treat them as comment lines

      
        486
        476
         			blockComments = append(blockComments, p.parseTextComment())

      
        487
        477
         		case p.got(token.TEXT):

      
        488
        478
         			p.errorf("unknown subdirective %q", p.cur.Literal)

      ···
        493
        483
         		}

      
        494
        484
         	}

      
        495
        485
         

      
        496
        
        -	cd := &ast.CommodityDirective{

      
        
        486
        +	return &ast.CommodityDirective{

      
        497
        487
         		Commodity:     commodity,

      
        498
        488
         		CommoditySpan: commoditySpan,

      
        499
        489
         		FormatSub:     format,

      ···
        501
        491
         		Comment:       comment,

      
        502
        492
         		Span:          p.span(s),

      
        503
        493
         	}

      
        504
        
        -	return cd

      
        505
        494
         }

      
        506
        495
         

      
        507
        496
         func (p *Parser) parseIncludeDirective() *ast.IncludeDirective {

      ···
        510
        499
         	p.skipWhitespace()

      
        511
        500
         

      
        512
        501
         	id := &ast.IncludeDirective{}

      
        513
        
        -

      
        514
        502
         	if p.got(token.TEXT) {

      
        515
        503
         		id.Path = p.cur.Literal

      
        516
        504
         		p.advance()

      
        517
        505
         	} else {

      
        518
        506
         		p.errorf("expected file path, got %s", p.cur.Type)

      
        519
        507
         	}

      
        520
        
        -

      
        521
        508
         	id.Comment = p.parseOptInlineComment()

      
        522
        509
         	p.expectNewline()

      
        523
        510
         	id.Span = p.span(s)

      ···
        545
        532
         	p.expect(token.PAYEE)

      
        546
        533
         	p.skipWhitespace()

      
        547
        534
         

      
        548
        
        -	name, nameSpan := "", token.Span{}

      
        
        535
        +	pd := &ast.PayeeDirective{}

      
        549
        536
         	if p.got(token.TEXT) || p.got(token.STRING) || p.got(token.COMMODITYMARK) {

      
        550
        
        -		name, nameSpan = p.parsePayee()

      
        
        537
        +		pd.Name, pd.NameSpan = p.parsePayee()

      
        551
        538
         	}

      
        552
        
        -

      
        553
        
        -	comment := p.parseOptInlineComment()

      
        
        539
        +	pd.Comment = p.parseOptInlineComment()

      
        554
        540
         	p.expectNewline()

      
        555
        
        -

      
        556
        
        -	return &ast.PayeeDirective{

      
        557
        
        -		Name:     name,

      
        558
        
        -		NameSpan: nameSpan,

      
        559
        
        -		Comment:  comment,

      
        560
        
        -		Span:     p.span(s),

      
        561
        
        -	}

      
        
        541
        +	pd.Span = p.span(s)

      
        
        542
        +	return pd

      
        562
        543
         }

      
        563
        544
         

      
        564
        545
         func (p *Parser) parseTagDirective() *ast.TagDirective {

      ···
        566
        547
         	p.expect(token.TAG)

      
        567
        548
         	p.skipWhitespace()

      
        568
        549
         

      
        569
        
        -	name := ""

      
        
        550
        +	td := &ast.TagDirective{}

      
        570
        551
         	if p.got(token.TEXT) || p.got(token.COMMODITYMARK) || p.got(token.STRING) {

      
        571
        
        -		name = unquote(p.cur.Literal)

      
        
        552
        +		td.Name = unquote(p.cur.Literal)

      
        572
        553
         		p.advance()

      
        573
        554
         	}

      
        574
        
        -

      
        575
        
        -	comment := p.parseOptInlineComment()

      
        
        555
        +	td.Comment = p.parseOptInlineComment()

      
        576
        556
         	p.expectNewline()

      
        577
        
        -

      
        578
        
        -	return &ast.TagDirective{

      
        579
        
        -		Name:    name,

      
        580
        
        -		Comment: comment,

      
        581
        
        -		Span:    p.span(s),

      
        582
        
        -	}

      
        
        557
        +	td.Span = p.span(s)

      
        
        558
        +	return td

      
        583
        559
         }

      
        584
        560
         

      
        585
        561
         func (p *Parser) parseYearDirective() *ast.YearDirective {

      ···
        1098
        1074
         	s := p.cur.Span

      
        1099
        1075
         	acc := ast.Account{Name: make([]ast.SubAccount, 0, 6)}

      
        1100
        1076
         

      
        1101
        
        -	sub, ok := p.readAccountSegment()

      
        
        1077
        +	seg, ok := p.readAccountSegment()

      
        1102
        1078
         	if !ok {

      
        1103
        1079
         		p.errorf("expected account, got %s", p.cur.Type)

      
        1104
        1080
         		return ast.Account{}

      
        1105
        1081
         	}

      
        1106
        
        -	acc.Name = append(acc.Name, sub)

      
        
        1082
        +	acc.Name = append(acc.Name, seg)

      
        1107
        1083
         

      
        1108
        1084
         	for p.got(token.COLON) {

      
        1109
        1085
         		p.advance()

      
        1110
        
        -		sub, ok := p.readAccountSegment()

      
        
        1086
        +		seg, ok := p.readAccountSegment()

      
        1111
        1087
         		if !ok {

      
        1112
        1088
         			break

      
        1113
        1089
         		}

      
        1114
        
        -		acc.Name = append(acc.Name, sub)

      
        
        1090
        +		acc.Name = append(acc.Name, seg)

      
        1115
        1091
         	}

      
        1116
        1092
         

      
        1117
        1093
         	acc.Span = p.span(s)

      ···
        1145
        1121
         	return p.parseCommentRest(p.cur.Span)

      
        1146
        1122
         }

      
        1147
        1123
         

      
        1148
        
        -// parseCommentRest consumes a comment marker at p.cur, then optional text;

      
        1149
        
        -// s anchors the span at the marker's start.

      
        1150
        1124
         func (p *Parser) parseCommentRest(s token.Span) *ast.Comment {

      
        1151
        
        -	marker := p.cur.Literal[0]

      
        
        1125
        +	c := &ast.Comment{}

      
        
        1126
        +	c.Marker = p.cur.Literal[0]

      
        1152
        1127
         	p.advance()

      
        1153
        1128
         	p.skipWhitespace()

      
        1154
        
        -

      
        1155
        
        -	var tags []ast.Tag

      
        1156
        
        -	text := ""

      
        1157
        1129
         	if p.got(token.TEXT) {

      
        1158
        
        -		text = p.cur.Literal

      
        1159
        
        -		tags = parseCommentTags(text, p.cur.Span)

      
        
        1130
        +		c.Text = p.cur.Literal

      
        
        1131
        +		c.Tags = parseCommentTags(c.Text, p.cur.Span)

      
        1160
        1132
         		p.advance()

      
        1161
        1133
         	}

      
        1162
        
        -

      
        1163
        
        -	return &ast.Comment{

      
        1164
        
        -		Marker: marker,

      
        1165
        
        -		Tags:   tags,

      
        1166
        
        -		Text:   text,

      
        1167
        
        -		Span:   p.span(s),

      
        1168
        
        -	}

      
        
        1134
        +	c.Span = p.span(s)

      
        
        1135
        +	return c

      
        1169
        1136
         }

      
        1170
        1137
         

      
        1171
        1138
         func (p *Parser) parseOptPeriodicDescription() (string, token.Span) {

      ···
        1174
        1141
         	}

      
        1175
        1142
         

      
        1176
        1143
         	p.skipWhitespace()

      
        1177
        
        -

      
        1178
        1144
         	if p.cur.Type != token.TEXT {

      
        1179
        1145
         		return "", token.Span{}

      
        1180
        1146
         	}

      ···
        1211
        1177
         	lit := p.cur.Literal

      
        1212
        1178
         	p.advance()

      
        1213
        1179
         

      
        1214
        
        -	// detect format metadata before normalizing

      
        1215
        1180
         	amt.QuantityFmt = detectFormat(lit)

      
        1216
        
        -

      
        1217
        
        -	// normalize for decimal.NewFromString

      
        1218
        
        -	// remove thousands separators, replace decimal mark with '.'

      
        1219
        1181
         	normalized := normalizeLiteral(lit, amt.QuantityFmt.Thousands, amt.QuantityFmt.Decimal)

      
        1220
        
        -

      
        1221
        1182
         	q, err := decimal.FromString(normalized)

      
        1222
        1183
         	if err != nil {

      
        1223
        1184
         		p.errorf("invalid quantity %q: %v", lit, err)

      ···
        1255
        1216
         

      
        1256
        1217
         func (p *Parser) got(kind token.Type) bool     { return p.cur.Type == kind }

      
        1257
        1218
         func (p *Parser) willGet(kind token.Type) bool { return p.peek.Type == kind }

      
        1258
        
        -

      
        1259
        1219
         func (p *Parser) expect(kind token.Type) (token.Token, bool) {

      
        1260
        1220
         	if p.got(kind) {

      
        1261
        1221
         		return p.advance(), true

      ···
        1271
        1231
         	})

      
        1272
        1232
         }

      
        1273
        1233
         

      
        1274
        
        -// errorfAt records a parse error pointing at the start of span.

      
        1275
        1234
         func (p *Parser) errorfAt(span token.Span, format string, args ...any) {

      
        1276
        1235
         	p.errors = append(p.errors, &ast.ParseError{

      
        1277
        1236
         		Span:    token.Span{File: span.File, Start: span.Start, End: span.Start},