Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions parser/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -2942,7 +2942,7 @@ func (q *QueryParam) Pos() Pos {
}

func (q *QueryParam) End() Pos {
return q.RBracePos
return q.RBracePos + 1
}

func (q *QueryParam) Accept(visitor ASTVisitor) error {
Expand Down Expand Up @@ -3015,7 +3015,7 @@ func (f *FunctionExpr) Pos() Pos {
}

func (f *FunctionExpr) End() Pos {
return f.Params.RightParenPos
return f.Params.RightParenPos + 1
Comment on lines 3017 to +3018

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Extend function spans through parametric arguments

For parametric aggregate calls, Params.ColumnArgList contains the second argument list, but this end position still stops after the first list. For example, parsing SELECT quantile(0.9)(x) reports the function and root query ending after quantile(0.9), excluding (x), so source-rewriting consumers receive an incomplete span. Use the trailing ColumnArgList end when it is present.

Useful? React with 👍 / 👎.

}

func (f *FunctionExpr) Accept(visitor ASTVisitor) error {
Expand Down
14 changes: 10 additions & 4 deletions parser/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,18 @@ func (e *ParseError) renderCaret(b *strings.Builder) {
line := e.starts.lineText(e.input, e.Line)
b.WriteString(line)
b.WriteByte('\n')
for i := 1; i < e.Column; i++ {
b.WriteByte(' ')
lineStart := e.starts[int(e.Line)-1]
offset := int(e.Pos) - lineStart
if offset < 0 {
offset = 0
}
if offset > len(line) {
offset = len(line)
}
b.WriteString(strings.Repeat(" ", displayWidth(line[:offset])))
width := 1
if e.Got != nil && len(e.Got.String) > width {
width = len(e.Got.String)
if e.Got != nil && displayWidth(e.Got.String) > width {
width = displayWidth(e.Got.String)
Comment on lines +83 to +84

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Size string-token carets from the complete source span

After string token positions were changed to include their quotes, Got.String still contains only the unquoted literal contents. Consequently, an error on 'abc' now starts the caret at the opening quote but emits only three carets instead of covering the five-character token; escaped or doubled quotes produce similarly inaccurate diagnostics. Compute the underline width from the token's source span, including delimiters.

Useful? React with 👍 / 👎.

}
b.WriteString(strings.Repeat("^", width))
b.WriteByte('\n')
Expand Down
9 changes: 9 additions & 0 deletions parser/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ func TestParseError_LexicalFailure(t *testing.T) {
}
}

func TestParseError_CaretUsesDisplayWidth(t *testing.T) {
_, err := NewParser("SELECT\t中文 FROM 123").ParseStmts()
require.Error(t, err)
var pe *ParseError
require.ErrorAs(t, err, &pe)
require.Equal(t, 1, pe.Line)
require.Contains(t, pe.Error(), "SELECT\t中文 FROM 123\n ^")
}

func TestParser_TokenConsumptionError(t *testing.T) {
for _, tc := range []struct {
name string
Expand Down
4 changes: 2 additions & 2 deletions parser/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,8 @@ func (l *Lexer) consumeString() error {
l.currentToken = &Token{
Kind: TokenKindString,
String: l.slice(1, i),
Pos: Pos(l.offset + 1),
End: Pos(l.offset + i),
Pos: Pos(l.offset),
End: Pos(l.offset + i + 1),
}
l.skipN(i + 1)
return nil
Expand Down
10 changes: 6 additions & 4 deletions parser/parser_column.go
Original file line number Diff line number Diff line change
Expand Up @@ -633,10 +633,11 @@ func (p *Parser) parseColumnExpr(pos Pos) (Expr, error) { //nolint:funlen
return p.parseNumber(p.Pos())
case p.matchTokenKind(TokenKindQuestionMark):
// Placeholder `?`
curToken := p.current()
_ = p.lexer.consumeToken()
return &PlaceHolder{
PlaceholderPos: pos,
PlaceHolderEnd: pos,
PlaceholderPos: curToken.Pos,
PlaceHolderEnd: curToken.End,
Type: string(TokenKindQuestionMark),
}, nil
default:
Expand Down Expand Up @@ -1884,12 +1885,13 @@ func (p *Parser) parseEnumValueExpr(pos Pos) (*EnumValue, error) {
}

func (p *Parser) parseColumnStar(pos Pos) (*Ident, error) {
curToken := p.current()
if err := p.expectTokenKind("*"); err != nil {
return nil, err
}
return &Ident{
NamePos: pos,
NameEnd: pos,
NamePos: curToken.Pos,
NameEnd: curToken.End,
Name: "*",
}, nil
}
Expand Down
4 changes: 4 additions & 0 deletions parser/parser_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -1148,12 +1148,14 @@ func (p *Parser) parseSetOperation(selectStmt *SelectQuery) error {
return err
}
selectStmt.UnionAll = unionAllExpr
selectStmt.StatementEnd = unionAllExpr.End()
case p.tryConsumeKeywords(KeywordDistinct):
unionDistinctExpr, err := p.parseSelectQuery(p.Pos())
if err != nil {
return err
}
selectStmt.UnionDistinct = unionDistinctExpr
selectStmt.StatementEnd = unionDistinctExpr.End()
default:
return fmt.Errorf("expected ALL or DISTINCT, got %s", p.currentTokenKind())
}
Expand All @@ -1163,12 +1165,14 @@ func (p *Parser) parseSetOperation(selectStmt *SelectQuery) error {
return err
}
selectStmt.Except = exceptExpr
selectStmt.StatementEnd = exceptExpr.End()
case p.tryConsumeKeywords(KeywordIntersect):
intersectExpr, err := p.parseSelectQuery(p.Pos())
if err != nil {
return err
}
selectStmt.Intersect = intersectExpr
selectStmt.StatementEnd = intersectExpr.End()
}

return nil
Expand Down
40 changes: 37 additions & 3 deletions parser/position_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ func TestKeywordArgFunctionPositions(t *testing.T) {
stmt := parseOneStmt(t, sql).(*SelectQuery)
fn := stmt.SelectItems[0].Expr.(*FunctionExpr)
require.Equal(t, Pos(7), fn.Pos())
require.Equal(t, Pos(len(sql)-1), fn.End(), "ends on the closing paren, as any call does")
// Spans are complete half-open source ranges, so the closing paren is included.
require.Equal(t, Pos(len(sql)), fn.End())

from := fn.Params.Items.Items[0].(*ColumnExpr).Expr.(*BinaryOperation)
require.Equal(t, "BOTH", sql[12:16])
Expand All @@ -110,8 +111,8 @@ func TestKeywordArgFunctionPositions(t *testing.T) {
modifier := from.LeftExpr.(*UnaryExpr)
require.Equal(t, Pos(12), modifier.Pos())
require.Equal(t, modifier.Expr.End(), modifier.End())
require.Equal(t, Pos(18), modifier.Expr.Pos())
require.Equal(t, Pos(19), modifier.Expr.End())
require.Equal(t, Pos(17), modifier.Expr.Pos())
require.Equal(t, Pos(20), modifier.Expr.End())
}

func TestJoinLocalityIsInsideTheJoinSpan(t *testing.T) {
Expand All @@ -123,3 +124,36 @@ func TestJoinLocalityIsInsideTheJoinSpan(t *testing.T) {
require.Equal(t, Pos(17), join.Pos())
require.Equal(t, Pos(len(sql)), join.End())
}

func TestSourceSpansCoverReportedNodes(t *testing.T) {
for _, tc := range []struct {
sql string
wantSlice string
get func(Expr) Expr
}{
{"SELECT *", "*", func(e Expr) Expr { return e.(*SelectQuery).SelectItems[0].Expr }},
{"SELECT ?", "?", func(e Expr) Expr { return e.(*SelectQuery).SelectItems[0].Expr }},
{"SELECT 'abc'", "'abc'", func(e Expr) Expr { return e.(*SelectQuery).SelectItems[0].Expr }},
{"SELECT f(1)", "f(1)", func(e Expr) Expr { return e.(*SelectQuery).SelectItems[0].Expr }},
} {
t.Run(tc.sql, func(t *testing.T) {
stmt := parseOneStmt(t, tc.sql)
node := tc.get(stmt)
require.GreaterOrEqual(t, node.Pos(), Pos(0))
require.LessOrEqual(t, node.Pos(), node.End())
require.LessOrEqual(t, node.End(), Pos(len(tc.sql)))
require.Equal(t, tc.wantSlice, tc.sql[node.Pos():node.End()])
})
}
}

func TestSelectQuerySpanContainsSetOperations(t *testing.T) {
sql := "SELECT 1 INTERSECT SELECT 2 UNION ALL SELECT 3"
root := parseOneStmt(t, sql).(*SelectQuery)
require.Equal(t, Pos(len(sql)), root.End())
require.NotNil(t, root.Intersect)
require.NotNil(t, root.Intersect.UnionAll)
require.LessOrEqual(t, root.Pos(), root.Intersect.Pos())
require.LessOrEqual(t, root.Intersect.End(), root.End())
require.LessOrEqual(t, root.Intersect.UnionAll.End(), root.End())
}
32 changes: 31 additions & 1 deletion parser/source.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package parser

import "sort"
import (
"sort"
"unicode"
"unicode/utf8"
)

// lineStarts holds the byte offset where each line of the input begins
// (lineStarts[i] is the start of 0-based line i). It is built once so that
Expand Down Expand Up @@ -45,3 +49,29 @@ func (s lineStarts) lineText(input string, line int) string {
}
return input[start:end]
}

func displayWidth(text string) int {
width := 0
for len(text) > 0 {
r, size := utf8.DecodeRuneInString(text)
switch {
case r == '\t':
width += 8 - width%8
case unicode.Is(unicode.Mn, r) || unicode.Is(unicode.Me, r):
case eastAsianWide(r):
width += 2
default:
width++
}
text = text[size:]
}
return width
}

func eastAsianWide(r rune) bool {
return r >= 0x1100 && (r <= 0x115f || r == 0x2329 || r == 0x232a ||
(r >= 0x2e80 && r <= 0xa4cf) || (r >= 0xac00 && r <= 0xd7a3) ||
(r >= 0xf900 && r <= 0xfaff) || (r >= 0xfe10 && r <= 0xfe19) ||
(r >= 0xfe30 && r <= 0xfe6f) || (r >= 0xff00 && r <= 0xff60) ||
(r >= 0xffe0 && r <= 0xffe6))
Comment on lines +71 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include supplementary wide runes in caret widths

The new width table stops at BMP ranges, so supplementary characters that occupy two terminal cells—such as CJK Extension B rune 𠀀 and common emoji—fall through to width one. When such a rune appears before a later syntax error, for example in SELECT '𠀀' 123, the rendered caret is shifted one column left. Use a complete terminal-width implementation or include the applicable supplementary wide ranges.

Useful? React with 👍 / 👎.

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[
{
"SelectPos": 0,
"StatementEnd": 43,
"StatementEnd": 44,
"With": null,
"Top": null,
"HasDistinct": false,
Expand Down
6 changes: 3 additions & 3 deletions parser/testdata/basic/output/set_statement.sql.golden.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"SetPos": 81,
"Settings": {
"SettingsPos": 85,
"ListEnd": 132,
"ListEnd": 133,
"Items": [
{
"SettingsPos": 85,
Expand All @@ -61,8 +61,8 @@
"NameEnd": 124
},
"Expr": {
"LiteralPos": 128,
"LiteralEnd": 132,
"LiteralPos": 127,
"LiteralEnd": 133,
"Literal": "true"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"SetPos": 91,
"Settings": {
"SettingsPos": 100,
"ListEnd": 147,
"ListEnd": 148,
"Items": [
{
"SettingsPos": 100,
Expand All @@ -61,8 +61,8 @@
"NameEnd": 139
},
"Expr": {
"LiteralPos": 143,
"LiteralEnd": 147,
"LiteralPos": 142,
"LiteralEnd": 148,
"Literal": "true"
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[
{
"SelectPos": 0,
"StatementEnd": 34,
"StatementEnd": 35,
"With": null,
"Top": null,
"HasDistinct": false,
Expand Down
Loading
Loading