修复: byte/rune 长度混淆及错误位置丢失

This commit is contained in:
2026-04-01 10:43:16 +08:00
parent 1b5b6aff8f
commit 2f9d81dc17
8 changed files with 603 additions and 93 deletions

View File

@@ -61,7 +61,7 @@ func (e *Executor) walk(ctx *Context, ph *Placeholder, sql *strings.Builder, arg
val, ok := ctx.Get(n.Name)
if !ok {
if e.strict {
return fmt.Errorf("line %d, col %d: undefined variable %q", n.Pos.Line, n.Pos.Col, n.Name)
return PosErrorf(n.Pos.Line, n.Pos.Col, "undefined variable %q", n.Name)
}
val = nil
}
@@ -72,7 +72,7 @@ func (e *Executor) walk(ctx *Context, ph *Placeholder, sql *strings.Builder, arg
val, ok := ctx.Get(n.Name)
if !ok {
if e.strict {
return fmt.Errorf("line %d, col %d: undefined variable %q", n.Pos.Line, n.Pos.Col, n.Name)
return PosErrorf(n.Pos.Line, n.Pos.Col, "undefined variable %q", n.Name)
}
val = ""
}
@@ -103,11 +103,11 @@ func (e *Executor) walk(ctx *Context, ph *Placeholder, sql *strings.Builder, arg
// skip
case *UseNode:
if e.blocks == nil {
return fmt.Errorf("line %d, col %d: @use(\"%s\") no blocks available", n.Pos.Line, n.Pos.Col, n.Name)
return PosErrorf(n.Pos.Line, n.Pos.Col, "@use(\"%s\") no blocks available", n.Name)
}
blockNodes, ok := e.blocks[n.Name]
if !ok {
return fmt.Errorf("line %d, col %d: @use(\"%s\") block not found", n.Pos.Line, n.Pos.Col, n.Name)
return PosErrorf(n.Pos.Line, n.Pos.Col, "@use(\"%s\") block not found", n.Name)
}
if err := e.walk(ctx, ph, sql, args, blockNodes); err != nil {
return err
@@ -159,7 +159,7 @@ func (e *Executor) walkFor(ctx *Context, ph *Placeholder, sql *strings.Builder,
case reflect.Slice, reflect.Array:
length = rv.Len()
default:
return fmt.Errorf("line %d, col %d: @for requires a slice or array, got %T", n.Pos.Line, n.Pos.Col, listVal)
return PosErrorf(n.Pos.Line, n.Pos.Col, "@for requires a slice or array, got %T", listVal)
}
for i := 0; i < length; i++ {

View File

@@ -127,7 +127,7 @@ func (p *ExprParser) parseUnary() (*Expr, error) {
func (p *ExprParser) parsePrimary() (*Expr, error) {
p.skipSpaces()
if p.pos >= len(p.input) {
return nil, fmt.Errorf("line %d, col %d: unexpected end of expression", p.line, p.col)
return nil, PosErrorf(p.line, p.col, "unexpected end of expression")
}
ch := p.input[p.pos]
@@ -152,12 +152,12 @@ func (p *ExprParser) parsePrimary() (*Expr, error) {
}
p.skipSpaces()
if p.pos >= len(p.input) || p.input[p.pos] != ')' {
return nil, fmt.Errorf("line %d, col %d: expected ')'", p.line, p.col)
return nil, PosErrorf(p.line, p.col, "expected ')'")
}
p.skip(1)
return expr, nil
}
return nil, fmt.Errorf("line %d, col %d: unexpected character %q", p.line, p.col, string(ch))
return nil, PosErrorf(p.line, p.col, "unexpected character %q", string(ch))
}
func (p *ExprParser) parseIdentOrKeyword() (*Expr, error) {
@@ -186,7 +186,7 @@ func (p *ExprParser) parseIdentOrKeyword() (*Expr, error) {
for p.pos < len(p.input) && p.input[p.pos] == '.' {
p.skip(1)
if p.pos >= len(p.input) || !isIdentStart(p.input[p.pos]) {
return nil, fmt.Errorf("line %d, col %d: expected identifier after '.'", p.line, p.col)
return nil, PosErrorf(p.line, p.col, "expected identifier after '.'")
}
segStart := p.pos
for p.pos < len(p.input) && isIdentPart(p.input[p.pos]) {
@@ -221,7 +221,7 @@ func (p *ExprParser) parseFuncCall(name string) (*Expr, error) {
}
p.skipSpaces()
if p.pos >= len(p.input) || p.input[p.pos] != ')' {
return nil, fmt.Errorf("line %d, col %d: expected ')' after function call", p.line, p.col)
return nil, PosErrorf(p.line, p.col, "expected ')' after function call")
}
p.skip(1)
return &Expr{Pos: Pos{Line: p.line, Col: p.col}, ExprType: ExprFuncCall, FuncName: name, FuncArgs: args}, nil
@@ -252,7 +252,7 @@ func (p *ExprParser) parseStringLit() (*Expr, error) {
p.pos++
}
if p.pos >= len(p.input) {
return nil, fmt.Errorf("line %d, col %d: unterminated string", p.line, p.col)
return nil, PosErrorf(p.line, p.col, "unterminated string")
}
p.skip(1)
return &Expr{Pos: Pos{Line: p.line, Col: p.col}, ExprType: ExprLiteral, Value: buf.String()}, nil
@@ -266,7 +266,7 @@ func (p *ExprParser) parseSingleQuoteStringLit() (*Expr, error) {
p.pos++
}
if p.pos >= len(p.input) {
return nil, fmt.Errorf("line %d, col %d: unterminated string", p.line, p.col)
return nil, PosErrorf(p.line, p.col, "unterminated string")
}
p.skip(1)
return &Expr{Pos: Pos{Line: p.line, Col: p.col}, ExprType: ExprLiteral, Value: buf.String()}, nil
@@ -292,13 +292,13 @@ func (p *ExprParser) parseNumberLit() (*Expr, error) {
if isFloat {
v, err := strconv.ParseFloat(text, 64)
if err != nil {
return nil, fmt.Errorf("line %d, col %d: invalid number %q", p.line, p.col, text)
return nil, PosErrorf(p.line, p.col, "invalid number %q", text)
}
return &Expr{Pos: Pos{Line: p.line, Col: p.col}, ExprType: ExprLiteral, Value: v}, nil
}
v, err := strconv.ParseInt(text, 10, 64)
if err != nil {
return nil, fmt.Errorf("line %d, col %d: invalid number %q", p.line, p.col, text)
return nil, PosErrorf(p.line, p.col, "invalid number %q", text)
}
return &Expr{Pos: Pos{Line: p.line, Col: p.col}, ExprType: ExprLiteral, Value: int(v)}, nil
}
@@ -359,7 +359,7 @@ func Eval(expr *Expr, ctx *Context) (any, error) {
case ExprFuncCall:
return evalFuncCall(expr, ctx)
default:
return nil, fmt.Errorf("line %d, col %d: unknown expression type", expr.Pos.Line, expr.Pos.Col)
return nil, PosErrorf(expr.Pos.Line, expr.Pos.Col, "unknown expression type")
}
}
@@ -372,7 +372,7 @@ func evalUnary(expr *Expr, ctx *Context) (any, error) {
case "!":
return !isTruthy(val), nil
default:
return nil, fmt.Errorf("line %d, col %d: unknown unary operator %q", expr.Pos.Line, expr.Pos.Col, expr.UnaryOp)
return nil, PosErrorf(expr.Pos.Line, expr.Pos.Col, "unknown unary operator %q", expr.UnaryOp)
}
}
@@ -421,14 +421,14 @@ func evalBinary(expr *Expr, ctx *Context) (any, error) {
case ">=":
return compareOrder(left, right, expr.Op)
default:
return nil, fmt.Errorf("line %d, col %d: unknown operator %q", expr.Pos.Line, expr.Pos.Col, expr.Op)
return nil, PosErrorf(expr.Pos.Line, expr.Pos.Col, "unknown operator %q", expr.Op)
}
}
func evalFuncCall(expr *Expr, ctx *Context) (any, error) {
fn, ok := LookupBuiltin(expr.FuncName)
if !ok {
return nil, fmt.Errorf("line %d, col %d: unknown function %q", expr.Pos.Line, expr.Pos.Col, expr.FuncName)
return nil, PosErrorf(expr.Pos.Line, expr.Pos.Col, "unknown function %q", expr.FuncName)
}
var args []any
for _, a := range expr.FuncArgs {
@@ -440,7 +440,7 @@ func evalFuncCall(expr *Expr, ctx *Context) (any, error) {
}
result, ok := fn(args)
if !ok {
return nil, fmt.Errorf("line %d, col %d: function %q call failed", expr.Pos.Line, expr.Pos.Col, expr.FuncName)
return nil, PosErrorf(expr.Pos.Line, expr.Pos.Col, "function %q call failed", expr.FuncName)
}
return result, nil
}

View File

@@ -3,6 +3,7 @@ package internal
import (
"fmt"
"strings"
"unicode/utf8"
)
type Parser struct {
@@ -39,7 +40,7 @@ func (p *Parser) Parse() ([]Node, error) {
continue
}
if hasTpl && !isWhitespace(tok.Value) {
return nil, fmt.Errorf("line %d, col %d: top-level text is not allowed in templates with @tpl blocks", tok.Pos.Line, tok.Pos.Col)
return nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "top-level text is not allowed in templates with @tpl blocks")
}
nodes = append(nodes, &TextNode{Pos: tok.Pos, Text: tok.Value})
@@ -96,7 +97,7 @@ func (p *Parser) Parse() ([]Node, error) {
}
nodes = append(nodes, node)
case TokUseStart:
case TokUseStart:
node, err := p.parseUse(tok)
if err != nil {
return nil, err
@@ -104,7 +105,7 @@ func (p *Parser) Parse() ([]Node, error) {
nodes = append(nodes, node)
case TokElse:
return nil, fmt.Errorf("line %d, col %d: unexpected else", tok.Pos.Line, tok.Pos.Col)
return nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "unexpected else")
case TokComment:
p.pos++
@@ -248,7 +249,7 @@ func (p *Parser) consumeTokensForRuneRange(_, runeEnd int) {
// Text tokens store Pos as the end position; other tokens store Pos as the start.
tokRuneStart := runePosFromLineCol(p.input, tok.Pos.Line, tok.Pos.Col)
if tok.Type == TokText {
tokRuneStart -= len(tok.Value)
tokRuneStart -= utf8.RuneCountInString(tok.Value)
}
if tokRuneStart >= runeEnd {
break
@@ -256,11 +257,12 @@ func (p *Parser) consumeTokensForRuneRange(_, runeEnd int) {
// For text tokens, check if the token extends past runeEnd.
// If so, split it: keep the remainder as a new text token.
if tok.Type == TokText && len(tok.Value) > 0 {
tokRuneEnd := tokRuneStart + len(tok.Value)
runes := []rune(tok.Value)
tokRuneEnd := tokRuneStart + len(runes)
if tokRuneEnd > runeEnd {
overlap := runeEnd - tokRuneStart
if overlap > 0 && overlap < len(tok.Value) {
remainder := tok.Value[overlap:]
if overlap > 0 && overlap < len(runes) {
remainder := string(runes[overlap:])
// Replace current token with the remainder
p.tokens[p.pos] = Token{
Type: TokText,
@@ -279,11 +281,11 @@ func (p *Parser) parseParam(tok Token) (*ParamNode, error) {
runePos := p.runePosFromToken(tok)
content, endPos, err := p.readUntilBrace(runePos)
if err != nil {
return nil, fmt.Errorf("line %d, col %d: unterminated param, expected '}'", tok.Pos.Line, tok.Pos.Col)
return nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "unterminated param, expected '}'")
}
content = strings.TrimSpace(content)
if content == "" {
return nil, fmt.Errorf("line %d, col %d: empty param name", tok.Pos.Line, tok.Pos.Col)
return nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "empty param name")
}
p.consumeTokensForRuneRange(runePos, endPos+1)
return &ParamNode{Pos: tok.Pos, Name: content}, nil
@@ -293,11 +295,11 @@ func (p *Parser) parseRaw(tok Token) (*RawNode, error) {
runePos := p.runePosFromToken(tok)
content, endPos, err := p.readUntilBrace(runePos)
if err != nil {
return nil, fmt.Errorf("line %d, col %d: unterminated raw, expected '}'", tok.Pos.Line, tok.Pos.Col)
return nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "unterminated raw, expected '}'")
}
content = strings.TrimSpace(content)
if content == "" {
return nil, fmt.Errorf("line %d, col %d: empty raw name", tok.Pos.Line, tok.Pos.Col)
return nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "empty raw name")
}
p.consumeTokensForRuneRange(runePos, endPos+1)
return &RawNode{Pos: tok.Pos, Name: content}, nil
@@ -307,7 +309,7 @@ func (p *Parser) parseIf(tok Token) (*IfNode, error) {
runePos := p.runePosFromToken(tok)
exprStr, parenClose, err := p.readUntilParen(runePos)
if err != nil {
return nil, fmt.Errorf("line %d, col %d: unterminated @if, expected ')'", tok.Pos.Line, tok.Pos.Col)
return nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "unterminated @if, expected ')'")
}
expr, err := NewExprParser(strings.TrimSpace(exprStr), tok.Pos.Line, tok.Pos.Col).Parse()
@@ -318,7 +320,7 @@ func (p *Parser) parseIf(tok Token) (*IfNode, error) {
// Find '{' after ')' in raw input
braceOpen := findChar(p.input[parenClose+1:], '{')
if braceOpen < 0 {
return nil, fmt.Errorf("line %d, col %d: expected '{' after @if condition", tok.Pos.Line, tok.Pos.Col)
return nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "expected '{' after @if condition")
}
braceOpen = parenClose + 1 + braceOpen
@@ -344,12 +346,12 @@ func (p *Parser) parseElseIfBranch(tok Token) (*ElseIfBranch, error) {
runePos := runePosFromLineCol(p.input, tok.Pos.Line, tok.Pos.Col)
idx := strings.Index(tok.Value, prefix)
if idx < 0 {
return nil, fmt.Errorf("line %d, col %d: expected @elseif(", tok.Pos.Line, tok.Pos.Col)
return nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "expected @elseif(")
}
exprStart := runePos + idx + len(prefix)
exprStr, closePos, err := p.readUntilParen(exprStart)
if err != nil {
return nil, fmt.Errorf("line %d, col %d: unterminated @elseif, expected ')'", tok.Pos.Line, tok.Pos.Col)
return nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "unterminated @elseif, expected ')'")
}
expr, err := NewExprParser(strings.TrimSpace(exprStr), tok.Pos.Line, tok.Pos.Col).Parse()
@@ -376,19 +378,19 @@ func (p *Parser) parseFor(tok Token) (*ForNode, error) {
runePos := p.runePosFromToken(tok)
content, parenClose, err := p.readUntilParen(runePos)
if err != nil {
return nil, fmt.Errorf("line %d, col %d: unterminated @for, expected ')'", tok.Pos.Line, tok.Pos.Col)
return nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "unterminated @for, expected ')'")
}
braceOpen := findChar(p.input[parenClose+1:], '{')
if braceOpen < 0 {
return nil, fmt.Errorf("line %d, col %d: expected '{' after @for", tok.Pos.Line, tok.Pos.Col)
return nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "expected '{' after @for")
}
braceOpen = parenClose + 1 + braceOpen
p.consumeTokensForRuneRange(runePos, braceOpen+1)
keyVar, valVar, listExprStr, err := parseForHeader(content)
if err != nil {
return nil, fmt.Errorf("line %d, col %d: %s", tok.Pos.Line, tok.Pos.Col, err.Error())
return nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "%s", err.Error())
}
listExpr, err := NewExprParser(strings.TrimSpace(listExprStr), tok.Pos.Line, tok.Pos.Col).Parse()
@@ -451,14 +453,14 @@ func (p *Parser) parseTpl(tok Token, tplNames map[string]bool) (*BlockNode, erro
runePos := p.runePosFromToken(tok)
name, quotePos, err := p.readUntilQuote(runePos)
if err != nil {
return nil, fmt.Errorf("line %d, col %d: unterminated @tpl name", tok.Pos.Line, tok.Pos.Col)
return nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "unterminated @tpl name")
}
name = strings.TrimSpace(name)
if name == "" {
return nil, fmt.Errorf("line %d, col %d: empty @tpl block name", tok.Pos.Line, tok.Pos.Col)
return nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "empty @tpl block name")
}
if tplNames[name] {
return nil, fmt.Errorf("line %d, col %d: duplicate @tpl block name %q", tok.Pos.Line, tok.Pos.Col, name)
return nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "duplicate @tpl block name %q", name)
}
tplNames[name] = true
@@ -470,7 +472,7 @@ func (p *Parser) parseTpl(tok Token, tplNames map[string]bool) (*BlockNode, erro
// find '{' and consume tokens up to past it
braceOpen := findChar(p.input[endPos:], '{')
if braceOpen < 0 {
return nil, fmt.Errorf("line %d, col %d: expected '{' after @tpl", tok.Pos.Line, tok.Pos.Col)
return nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "expected '{' after @tpl")
}
braceOpen = endPos + braceOpen
p.consumeTokensForRuneRange(runePos, braceOpen+1)
@@ -488,23 +490,11 @@ func (p *Parser) parseTpl(tok Token, tplNames map[string]bool) (*BlockNode, erro
}
func (p *Parser) parseInclude(tok Token) (*IncludeNode, error) {
runePos := p.runePosFromToken(tok)
path, quotePos, err := p.readUntilQuote(runePos)
name, err := p.parseQuotedName(tok, "@include path")
if err != nil {
return nil, fmt.Errorf("line %d, col %d: unterminated @include path", tok.Pos.Line, tok.Pos.Col)
return nil, err
}
path = strings.TrimSpace(path)
endPos := quotePos + 1
if endPos < len(p.input) && p.input[endPos] == ')' {
endPos++
}
p.consumeTokensForRuneRange(runePos, endPos)
return &IncludeNode{
Pos: tok.Pos,
Path: path,
}, nil
return &IncludeNode{Pos: tok.Pos, Path: name}, nil
}
func (p *Parser) expandInclude(tok Token) ([]Node, error) {
@@ -513,11 +503,11 @@ func (p *Parser) expandInclude(tok Token) ([]Node, error) {
return nil, err
}
if p.includeMgr == nil {
return nil, fmt.Errorf("line %d, col %d: @include used but no include resolver configured", incNode.Pos.Line, incNode.Pos.Col)
return nil, PosErrorf(incNode.Pos.Line, incNode.Pos.Col, "@include used but no include resolver configured")
}
expanded, err := p.includeMgr.Resolve(incNode.Path)
if err != nil {
return nil, fmt.Errorf("line %d, col %d: %s", incNode.Pos.Line, incNode.Pos.Col, err.Error())
return nil, PosErrorf(incNode.Pos.Line, incNode.Pos.Col, "%s", err.Error())
}
subLexer := NewLexer(expanded)
subTokens, err := subLexer.Tokenize()
@@ -528,11 +518,13 @@ func (p *Parser) expandInclude(tok Token) ([]Node, error) {
return subParser.Parse()
}
func (p *Parser) parseUse(tok Token) (*UseNode, error) {
// parseQuotedName reads a quoted name from the directive token position.
// Shared by @include, @use, and @namespace.
func (p *Parser) parseQuotedName(tok Token, desc string) (string, error) {
runePos := p.runePosFromToken(tok)
name, quotePos, err := p.readUntilQuote(runePos)
if err != nil {
return nil, fmt.Errorf("line %d, col %d: unterminated @use name", tok.Pos.Line, tok.Pos.Col)
return "", PosErrorf(tok.Pos.Line, tok.Pos.Col, "unterminated %s", desc)
}
name = strings.TrimSpace(name)
@@ -542,30 +534,26 @@ func (p *Parser) parseUse(tok Token) (*UseNode, error) {
}
p.consumeTokensForRuneRange(runePos, endPos)
return &UseNode{
Pos: tok.Pos,
Name: name,
}, nil
return name, nil
}
func (p *Parser) parseUse(tok Token) (*UseNode, error) {
name, err := p.parseQuotedName(tok, "@use name")
if err != nil {
return nil, err
}
return &UseNode{Pos: tok.Pos, Name: name}, nil
}
func (p *Parser) parseNamespace(tok Token, nodeCount int) (*NamespaceNode, error) {
if nodeCount > 0 {
return nil, fmt.Errorf("line %d, col %d: @namespace must be at the top of the file", tok.Pos.Line, tok.Pos.Col)
return nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "@namespace must be at the top of the file")
}
runePos := p.runePosFromToken(tok)
name, quotePos, err := p.readUntilQuote(runePos)
name, err := p.parseQuotedName(tok, "@namespace name")
if err != nil {
return nil, fmt.Errorf("line %d, col %d: unterminated @namespace name", tok.Pos.Line, tok.Pos.Col)
return nil, err
}
name = strings.TrimSpace(name)
endPos := quotePos + 1
if endPos < len(p.input) && p.input[endPos] == ')' {
endPos++
}
p.consumeTokensForRuneRange(runePos, endPos)
return &NamespaceNode{
Pos: tok.Pos,
Name: name,
@@ -584,7 +572,7 @@ func (p *Parser) skipToBraceOpen() {
startRune := runePosFromLineCol(p.input, tok.Pos.Line, tok.Pos.Col)
// Text tokens store Pos as the end position; adjust to start.
if tok.Type == TokText {
startRune -= len(tok.Value)
startRune -= utf8.RuneCountInString(tok.Value)
}
// Find '{' in raw input
@@ -649,7 +637,7 @@ func (p *Parser) parseBlockBodyWithElse(blockType string) ([]Node, []Node, []*El
for p.pos < len(p.tokens) {
tok := p.cur()
if tok.Type == TokEOF {
return nil, nil, nil, fmt.Errorf("line %d, col %d: unterminated %s block", tok.Pos.Line, tok.Pos.Col, blockType)
return nil, nil, nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "unterminated %s block", blockType)
}
if tok.Type == TokText {
@@ -715,21 +703,21 @@ func (p *Parser) parseBlockBodyWithElse(blockType string) ([]Node, []Node, []*El
}
body = append(body, node)
case TokTplStart:
return nil, nil, nil, fmt.Errorf("line %d, col %d: @tpl blocks cannot be nested", tok.Pos.Line, tok.Pos.Col)
return nil, nil, nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "@tpl blocks cannot be nested")
case TokIncludeStart:
subNodes, err := p.expandInclude(tok)
if err != nil {
return nil, nil, nil, err
}
body = append(body, subNodes...)
case TokUseStart:
case TokUseStart:
node, err := p.parseUse(tok)
if err != nil {
return nil, nil, nil, err
}
body = append(body, node)
case TokNamespaceStart:
return nil, nil, nil, fmt.Errorf("line %d, col %d: @namespace must be at file top level", tok.Pos.Line, tok.Pos.Col)
return nil, nil, nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "@namespace must be at file top level")
}
}
@@ -782,7 +770,7 @@ func (p *Parser) handleTokElse(tok Token, body []Node) ([]Node, []Node, []*ElseI
}
// It's a plain else — find '{' in text tokens and split
if !p.splitTextAtBrace() {
return nil, nil, nil, fmt.Errorf("line %d, col %d: expected '{' after else", tok.Pos.Line, tok.Pos.Col)
return nil, nil, nil, PosErrorf(tok.Pos.Line, tok.Pos.Col, "expected '{' after else")
}
elseBody, _, _, err := p.parseBlockBodyWithElse("else")
if err != nil {
@@ -854,7 +842,7 @@ func (p *Parser) parseElseIfFromText(body []Node, next Token) ([]Node, []Node, [
// Find "if" and skip to the opening paren
ifIdx := strings.Index(trimmed, "if")
if ifIdx < 0 {
return nil, nil, nil, fmt.Errorf("line %d, col %d: expected 'if' in else-if condition", next.Pos.Line, next.Pos.Col)
return nil, nil, nil, PosErrorf(next.Pos.Line, next.Pos.Col, "expected 'if' in else-if condition")
}
afterIf := trimmed[ifIdx+2:]
// skip whitespace between "if" and "("
@@ -863,12 +851,12 @@ func (p *Parser) parseElseIfFromText(body []Node, next Token) ([]Node, []Node, [
parenLocalOffset++
}
if parenLocalOffset >= len(afterIf) || afterIf[parenLocalOffset] != '(' {
return nil, nil, nil, fmt.Errorf("line %d, col %d: expected '(' after 'else if'", next.Pos.Line, next.Pos.Col)
return nil, nil, nil, PosErrorf(next.Pos.Line, next.Pos.Col, "expected '(' after 'else if'")
}
// Compute rune position of '(' in the raw input.
// The text token's Pos marks the END of the token text; the start is Pos - len(Value).
runePos := runePosFromLineCol(p.input, next.Pos.Line, next.Pos.Col) - len(next.Value)
// The text token's Pos marks the END of the token text; the start is Pos - rune count.
runePos := runePosFromLineCol(p.input, next.Pos.Line, next.Pos.Col) - utf8.RuneCountInString(next.Value)
// Walk through next.Value byte-by-byte to find the '(' that corresponds to the condition.
// We know trimmed starts at some offset into next.Value; find "if" in the raw value.
@@ -878,7 +866,7 @@ func (p *Parser) parseElseIfFromText(body []Node, next Token) ([]Node, []Node, [
rawParenOffset++
}
if rawParenOffset >= len(next.Value) || next.Value[rawParenOffset] != '(' {
return nil, nil, nil, fmt.Errorf("line %d, col %d: expected '(' after 'else if'", next.Pos.Line, next.Pos.Col)
return nil, nil, nil, PosErrorf(next.Pos.Line, next.Pos.Col, "expected '(' after 'else if'")
}
// parenRunePos points to '(' in the raw input
@@ -886,7 +874,7 @@ func (p *Parser) parseElseIfFromText(body []Node, next Token) ([]Node, []Node, [
// readUntilParen expects the position AFTER '(' — it reads content between start and closing ')'
exprStr, parenClose, err := p.readUntilParen(parenRunePos + 1)
if err != nil {
return nil, nil, nil, fmt.Errorf("line %d, col %d: unterminated else-if condition, expected ')'", next.Pos.Line, next.Pos.Col)
return nil, nil, nil, PosErrorf(next.Pos.Line, next.Pos.Col, "unterminated else-if condition, expected ')'")
}
expr, err := NewExprParser(strings.TrimSpace(exprStr), next.Pos.Line, next.Pos.Col).Parse()
@@ -900,7 +888,7 @@ func (p *Parser) parseElseIfFromText(body []Node, next Token) ([]Node, []Node, [
// Find '{' after ')' in raw input
braceOpen := findChar(p.input[parenClose+1:], '{')
if braceOpen < 0 {
return nil, nil, nil, fmt.Errorf("line %d, col %d: expected '{' after else-if condition", next.Pos.Line, next.Pos.Col)
return nil, nil, nil, PosErrorf(next.Pos.Line, next.Pos.Col, "expected '{' after else-if condition")
}
braceOpen = parenClose + 1 + braceOpen

19
internal/poserror.go Normal file
View File

@@ -0,0 +1,19 @@
package internal
import "fmt"
// PosError is an internal error type that carries source position information.
// Lexer and parser return this so that the public wrapParseError can extract the position.
type PosError struct {
Line int
Col int
Message string
}
func (e *PosError) Error() string {
return fmt.Sprintf("line %d, col %d: %s", e.Line, e.Col, e.Message)
}
func PosErrorf(line, col int, format string, args ...any) *PosError {
return &PosError{Line: line, Col: col, Message: fmt.Sprintf(format, args...)}
}