Skip to content

Commit 24c555e

Browse files
authored
feat: Add parser error messages (#292)
* feat: Add parser error messages - prep for selector support * Formatting * Update tests and add error messager to exception * Fix formatting * Fix test formatting as well * Undo traversal error changes * Fix compiler error * Rename error to message
1 parent c7bb73c commit 24c555e

File tree

4 files changed

+165
-59
lines changed

4 files changed

+165
-59
lines changed

src/parser.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable camelcase */
22
import {tryConstantEvaluate} from './evaluator'
33
import {type GroqFunctionArity, namespaces, pipeFunctions} from './evaluator/functions'
4-
import {type Mark, MarkProcessor, type MarkVisitor} from './markProcessor'
4+
import {MarkProcessor, type MarkVisitor} from './markProcessor'
55
import type {
66
ArrayElementNode,
77
ExprNode,
@@ -873,8 +873,8 @@ class GroqSyntaxError extends Error {
873873
public position: number
874874
public override name = 'GroqSyntaxError'
875875

876-
constructor(position: number) {
877-
super(`Syntax error in GROQ query at position ${position}`)
876+
constructor(position: number, detail: string) {
877+
super(`Syntax error in GROQ query at position ${position}: ${detail}`)
878878
this.position = position
879879
}
880880
}
@@ -885,8 +885,8 @@ class GroqSyntaxError extends Error {
885885
export function parse(input: string, options: ParseOptions = {}): ExprNode {
886886
const result = rawParse(input)
887887
if (result.type === 'error') {
888-
throw new GroqSyntaxError(result.position)
888+
throw new GroqSyntaxError(result.position, result.message)
889889
}
890-
const processor = new MarkProcessor(input, result.marks as Mark[], options)
890+
const processor = new MarkProcessor(input, result.marks, options)
891891
return processor.process(EXPR_BUILDER)
892892
}

src/rawParser.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ export declare function parse(input: string):
1313
| {
1414
type: 'error'
1515
position: number
16+
message: string
1617
}

src/rawParser.js

Lines changed: 84 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function parse(str) {
3232
if (result.failPosition) {
3333
pos = result.failPosition - 1
3434
}
35-
return {type: 'error', position: pos}
35+
return {type: 'error', message: 'Unexpected end of query', position: pos}
3636
}
3737
delete result.position
3838
delete result.failPosition
@@ -48,7 +48,7 @@ function parseExpr(str, pos, level) {
4848
// while handling the RHS of the multiplication in `1 + 2 * 3 + 4` we only parse `3`.
4949
//
5050
// `lhsLevel` is the precedence level of the currently parsed expression on
51-
// the left-hand side. This is mainly used to handle non-associcativeness.
51+
// the left-hand side. This is mainly used to handle non-associativeness.
5252

5353
// This means that you'll see code like:
5454
// - `if (level > PREC_XXX) break`: Operator is at this precedence level.
@@ -77,34 +77,12 @@ function parseExpr(str, pos, level) {
7777
break
7878
}
7979
case '(': {
80-
let rhs = parseExpr(str, skipWS(str, pos + 1), 0)
81-
if (rhs.type === 'error') return rhs
82-
pos = skipWS(str, rhs.position)
83-
switch (str[pos]) {
84-
case ',': {
85-
// Tuples
86-
marks = [{name: 'tuple', position: startPos}].concat(rhs.marks)
87-
pos = skipWS(str, pos + 1)
88-
while (true) {
89-
rhs = parseExpr(str, pos, 0)
90-
if (rhs.type === 'error') return rhs
91-
pos = skipWS(str, rhs.position)
92-
if (str[pos] !== ',') break
93-
pos = skipWS(str, pos + 1)
94-
}
95-
if (str[pos] !== ')') return {type: 'error', position: pos}
96-
pos++
97-
marks.push({name: 'tuple_end', position: pos})
98-
break
99-
}
100-
case ')': {
101-
pos++
102-
marks = [{name: 'group', position: startPos}].concat(rhs.marks)
103-
break
104-
}
105-
default:
106-
return {type: 'error', position: pos}
107-
}
80+
let result = parseGroupOrTuple(str, pos)
81+
if (result.type === 'error') return result
82+
83+
pos = result.position
84+
marks = result.marks
85+
10886
break
10987
}
11088
case '!': {
@@ -147,7 +125,7 @@ function parseExpr(str, pos, level) {
147125
pos++
148126
marks.push({name: 'array_end', position: pos})
149127
} else {
150-
return {type: 'error', position: pos}
128+
return {type: 'error', message: 'Expected "]" after array expression', position: pos}
151129
}
152130

153131
break
@@ -210,7 +188,7 @@ function parseExpr(str, pos, level) {
210188
pos++
211189
}
212190
let expLen = parseRegex(str, pos, NUM)
213-
if (!expLen) return {type: 'error', position: pos}
191+
if (!expLen) return {type: 'error', message: 'Exponent must be a number', position: pos}
214192
pos += expLen
215193
}
216194

@@ -249,7 +227,7 @@ function parseExpr(str, pos, level) {
249227
}
250228

251229
if (!marks) {
252-
return {type: 'error', position: pos}
230+
return {type: 'error', message: 'Expected expression', position: pos}
253231
}
254232

255233
let lhsLevel = 12
@@ -272,7 +250,7 @@ function parseExpr(str, pos, level) {
272250
}
273251
marks.push({name: 'traversal_end', position: pos})
274252
continue
275-
}
253+
} // ignore if type === 'error'
276254

277255
let token = str[innerPos]
278256
switch (token) {
@@ -401,7 +379,7 @@ function parseExpr(str, pos, level) {
401379
// pipe call
402380
let identPos = skipWS(str, innerPos + 1)
403381
let identLen = parseRegex(str, identPos, IDENT)
404-
if (!identLen) return {type: 'error', position: identPos}
382+
if (!identLen) return {type: 'error', message: 'Expected identifier', position: identPos}
405383
pos = identPos + identLen
406384
if (str[pos] === '(' || str[pos] === ':') {
407385
let result = parseFuncCall(str, identPos, pos)
@@ -440,7 +418,7 @@ function parseExpr(str, pos, level) {
440418
break
441419
}
442420
case 'd': {
443-
// asc
421+
// desc
444422
if (str.slice(innerPos, innerPos + 4) !== 'desc') break loop
445423
if (level > PREC_ORDER || lhsLevel < PREC_ORDER) break loop
446424
marks.unshift({name: 'desc', position: startPos})
@@ -502,7 +480,8 @@ function parseExpr(str, pos, level) {
502480

503481
if (isGroup) {
504482
pos = skipWS(str, pos)
505-
if (str[pos] !== ')') return {type: 'error', position: pos}
483+
if (str[pos] !== ')')
484+
return {type: 'error', message: 'Expected ")" in group', position: pos}
506485
pos++
507486
}
508487

@@ -534,14 +513,57 @@ function parseExpr(str, pos, level) {
534513
return {type: 'success', marks, position: pos, failPosition}
535514
}
536515

516+
function parseGroupOrTuple(str, pos) {
517+
const startPos = pos
518+
let marks
519+
let rhs = parseExpr(str, skipWS(str, pos + 1), 0)
520+
if (rhs.type === 'error') return rhs
521+
pos = skipWS(str, rhs.position)
522+
switch (str[pos]) {
523+
case ',': {
524+
// Tuples
525+
marks = [{name: 'tuple', position: startPos}].concat(rhs.marks)
526+
pos = skipWS(str, pos + 1)
527+
while (true) {
528+
rhs = parseExpr(str, pos, 0)
529+
if (rhs.type === 'error') return rhs
530+
marks.push(...rhs.marks)
531+
pos = skipWS(str, rhs.position)
532+
if (str[pos] !== ',') break
533+
pos = skipWS(str, pos + 1)
534+
}
535+
if (str[pos] !== ')')
536+
return {type: 'error', message: 'Expected ")" after tuple expression', position: pos}
537+
pos++
538+
marks.push({name: 'tuple_end', position: pos})
539+
break
540+
}
541+
case ')': {
542+
pos++
543+
marks = [{name: 'group', position: startPos}].concat(rhs.marks)
544+
break
545+
}
546+
default:
547+
return {type: 'error', message: `Unexpected character "${str[pos]}"`, position: pos}
548+
}
549+
550+
return {type: 'success', marks, position: pos}
551+
}
552+
537553
function parseTraversal(str, pos) {
538554
let startPos = pos
539555
switch (str[pos]) {
540556
case '.': {
541557
pos = skipWS(str, pos + 1)
558+
559+
// TODO: allow tuples/groups in a traversal for selectors
560+
// if (str[pos] === '(') {
561+
// return parseGroupOrTuple(str, pos)
562+
// }
563+
542564
let identStart = pos
543565
let identLen = parseRegex(str, pos, IDENT)
544-
if (!identLen) return {type: 'error', position: pos}
566+
if (!identLen) return {type: 'error', message: 'Expected identifier after "."', position: pos}
545567
pos += identLen
546568

547569
return {
@@ -555,7 +577,8 @@ function parseTraversal(str, pos) {
555577
}
556578
}
557579
case '-':
558-
if (str[pos + 1] !== '>') return {type: 'error', position: pos}
580+
if (str[pos + 1] !== '>')
581+
return {type: 'error', message: 'Expected ">" in reference', position: pos}
559582
// ->
560583

561584
let marks = [{name: 'deref', position: startPos}]
@@ -607,7 +630,8 @@ function parseTraversal(str, pos) {
607630
let rhs = parseExpr(str, pos, 0)
608631
if (rhs.type === 'error') return rhs
609632
pos = skipWS(str, rhs.position)
610-
if (str[pos] !== ']') return {type: 'error', position: pos}
633+
if (str[pos] !== ']')
634+
return {type: 'error', message: 'Expected "]" after array expression', position: pos}
611635

612636
return {
613637
type: 'success',
@@ -619,7 +643,8 @@ function parseTraversal(str, pos) {
619643
}
620644
}
621645

622-
if (str[pos] !== ']') return {type: 'error', position: pos}
646+
if (str[pos] !== ']')
647+
return {type: 'error', message: 'Expected "]" after array expression', position: pos}
623648

624649
return {
625650
type: 'success',
@@ -645,7 +670,7 @@ function parseTraversal(str, pos) {
645670
}
646671
}
647672

648-
return {type: 'error', position: pos}
673+
return {type: 'error', message: 'Unexpected character in traversal', position: pos}
649674
}
650675

651676
function parseFuncCall(str, startPos, pos) {
@@ -658,10 +683,11 @@ function parseFuncCall(str, startPos, pos) {
658683
marks.push({name: 'ident', position: startPos}, {name: 'ident_end', position: pos})
659684
pos = skipWS(str, pos + 2)
660685
let nameLen = parseRegex(str, pos, IDENT)
661-
if (!nameLen) return {type: 'error', position: pos}
686+
if (!nameLen) return {type: 'error', message: 'Expected function name', position: pos}
662687
marks.push({name: 'ident', position: pos}, {name: 'ident_end', position: pos + nameLen})
663688
pos = skipWS(str, pos + nameLen)
664-
if (str[pos] !== '(') return {type: 'error', position: pos}
689+
if (str[pos] !== '(')
690+
return {type: 'error', message: 'Expected "(" after function name', position: pos}
665691
pos++
666692
// Consume any whitespace in front of the function argument.
667693
pos = skipWS(str, pos)
@@ -679,6 +705,17 @@ function parseFuncCall(str, startPos, pos) {
679705
marks = marks.concat(result.marks)
680706
lastPos = result.position
681707
pos = skipWS(str, result.position)
708+
709+
// TODO: allow traversals in function arguments for selectors
710+
// if (str[pos] === '.') {
711+
// result = parseTraversal(str, pos)
712+
// if (result.type === 'error') return result
713+
// TODO: what to do with type === 'warning'?
714+
// marks = marks.concat(result.marks)
715+
// lastPos = result.position
716+
// pos = skipWS(str, result.position)
717+
// }
718+
682719
if (str[pos] !== ',') break
683720
pos = skipWS(str, pos + 1)
684721
// Also allow trailing commas
@@ -687,7 +724,7 @@ function parseFuncCall(str, startPos, pos) {
687724
}
688725

689726
if (str[pos] !== ')') {
690-
return {type: 'error', position: pos}
727+
return {type: 'error', message: 'Expected ")" after function arguments', position: pos}
691728
}
692729

693730
// NOTE: a bit arbitrary the func_args_end points comes before the whitespace.
@@ -739,7 +776,7 @@ function parseObject(str, pos) {
739776
}
740777

741778
if (str[pos] !== '}') {
742-
return {type: 'error', position: pos}
779+
return {type: 'error', message: 'Expected "}" after object', position: pos}
743780
}
744781

745782
pos++
@@ -752,7 +789,7 @@ function parseString(str, pos) {
752789
pos = pos + 1
753790
const marks = [{name: 'str', position: pos}]
754791
str: for (; ; pos++) {
755-
if (pos > str.length) return {type: 'error', position: pos}
792+
if (pos > str.length) return {type: 'error', message: 'Unexpected end of query', position: pos}
756793

757794
switch (str[pos]) {
758795
case token: {

0 commit comments

Comments
 (0)