Skip to content

Commit e44e031

Browse files
authored
Merge pull request #43 from jg-rp/fix-42
Fix unbalanced parentheses
2 parents e82a9b0 + 3b360e6 commit e44e031

File tree

7 files changed

+79
-30
lines changed

7 files changed

+79
-30
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# JSON P3 Change Log
22

3+
## Version 2.2.2
4+
5+
**Fixes**
6+
7+
- Fixed an issue where we'd get syntax errors claiming "unbalanced parentheses" when the query has balanced brackets.
8+
39
## Version 2.2.1
410

511
**Fixes**

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-p3",
3-
"version": "2.2.1",
3+
"version": "2.2.2",
44
"author": "James Prior",
55
"license": "MIT",
66
"description": "JSONPath, JSON Pointer and JSON Patch",

src/path/lex.ts

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@ class Lexer {
3434
* If the stack is empty, we are not in a function call. Remember that
3535
* function arguments can use arbitrarily nested in parentheses.
3636
*/
37-
public parenStack: number[] = [];
37+
public funcCallStack: number[] = [];
38+
39+
/**
40+
* A stack of parentheses and square brackets used to check for balanced
41+
* brackets.
42+
*/
43+
public bracketStack: Array<[string, number]> = [];
3844

3945
/** Tokens resulting from tokenizing a JSONPath query. */
4046
public tokens: Token[] = [];
@@ -205,12 +211,26 @@ export function tokenize(
205211
): Token[] {
206212
const [lexer, tokens] = lex(environment, path);
207213
lexer.run();
214+
215+
// If there's an error, it will be the last token with kind set to ERROR.
208216
if (tokens.length && tokens[tokens.length - 1].kind === TokenKind.ERROR) {
209217
throw new JSONPathSyntaxError(
210218
tokens[tokens.length - 1].value,
211219
tokens[tokens.length - 1],
212220
);
213221
}
222+
223+
// If the bracket stack is not empty, we hav unbalanced brackets.
224+
// This might not be reachable.
225+
if (lexer.bracketStack.length !== 0) {
226+
const [ch, index] = lexer.bracketStack[lexer.bracketStack.length - 1];
227+
const msg = "unbalanced brackets";
228+
throw new JSONPathSyntaxError(
229+
msg,
230+
new Token(TokenKind.ERROR, ch, index, path),
231+
);
232+
}
233+
214234
return tokens;
215235
}
216236

@@ -242,6 +262,7 @@ function lexSegment(l: Lexer): StateFn | null {
242262
}
243263
return lexDotSelector;
244264
case "[":
265+
l.bracketStack.push(["[", l.start]);
245266
l.emit(TokenKind.LBRACKET);
246267
return lexInsideBracketedSelection;
247268
default:
@@ -300,6 +321,7 @@ function lexDescendantSelection(l: Lexer): StateFn | null {
300321
l.emit(TokenKind.WILD);
301322
return lexSegment;
302323
case "[":
324+
l.bracketStack.push(["[", l.start]);
303325
l.emit(TokenKind.LBRACKET);
304326
return lexInsideBracketedSelection;
305327
default:
@@ -391,6 +413,16 @@ function lexInsideBracketedSelection(l: Lexer): StateFn | null {
391413
const ch = l.next();
392414
switch (ch) {
393415
case "]":
416+
if (
417+
l.bracketStack.length === 0 ||
418+
l.bracketStack[l.bracketStack.length - 1][0] !== "["
419+
) {
420+
l.backup();
421+
l.error("unbalanced brackets");
422+
return null;
423+
}
424+
425+
l.bracketStack.pop();
394426
l.emit(TokenKind.RBRACKET);
395427
return lexSegment;
396428
case "":
@@ -432,36 +464,44 @@ function lexInsideFilter(l: Lexer): StateFn | null {
432464
return null;
433465
case "]":
434466
l.filterLevel -= 1;
435-
if (l.parenStack.length === 1) {
436-
l.error("unbalanced parentheses");
437-
return null;
438-
}
439467
l.backup();
440468
return lexInsideBracketedSelection;
441469
case ",":
442470
l.emit(TokenKind.COMMA);
443471
// If we have unbalanced parens, we are inside a function call and a
444472
// comma separates arguments. Otherwise a comma separates selectors.
445-
if (l.parenStack.length) continue;
473+
if (l.funcCallStack.length) continue;
446474
l.filterLevel -= 1;
447475
return lexInsideBracketedSelection;
448476
case "'":
449477
return lexSingleQuoteStringInsideFilterExpression;
450478
case '"':
451479
return lexDoubleQuoteStringInsideFilterExpression;
452480
case "(":
481+
l.bracketStack.push(["(", l.start]);
453482
l.emit(TokenKind.LPAREN);
454483
// Are we in a function call? If so, a function argument contains parens.
455-
if (l.parenStack.length) l.parenStack[l.parenStack.length - 1] += 1;
484+
if (l.funcCallStack.length)
485+
l.funcCallStack[l.funcCallStack.length - 1] += 1;
456486
continue;
457487
case ")":
488+
if (
489+
l.bracketStack.length === 0 ||
490+
l.bracketStack[l.bracketStack.length - 1][0] !== "("
491+
) {
492+
l.backup();
493+
l.error("unbalanced brackets");
494+
return null;
495+
}
496+
497+
l.bracketStack.pop();
458498
l.emit(TokenKind.RPAREN);
459499
// Are we closing a function call or a parenthesized expression?
460-
if (l.parenStack.length) {
461-
if (l.parenStack[l.parenStack.length - 1] === 1) {
462-
l.parenStack.pop();
500+
if (l.funcCallStack.length) {
501+
if (l.funcCallStack[l.funcCallStack.length - 1] === 1) {
502+
l.funcCallStack.pop();
463503
} else {
464-
l.parenStack[l.parenStack.length - 1] -= 1;
504+
l.funcCallStack[l.funcCallStack.length - 1] -= 1;
465505
}
466506
}
467507
continue;
@@ -562,8 +602,9 @@ function lexInsideFilter(l: Lexer): StateFn | null {
562602
// functions
563603
if (l.acceptMatchRun(functionNamePattern) && l.peek() === "(") {
564604
// Keep track of parentheses for this function call.
565-
l.parenStack.push(1);
605+
l.funcCallStack.push(1);
566606
l.emit(TokenKind.FUNCTION);
607+
l.bracketStack.push(["(", l.start]);
567608
l.next();
568609
l.ignore();
569610
continue;

tests/path/errors.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { JSONValue } from "../../src";
1+
import { compile, JSONValue } from "../../src";
22
import { JSONPathEnvironment } from "../../src/path/environment";
33
import {
44
JSONPathIndexError,
@@ -49,7 +49,7 @@ describe("syntax error", () => {
4949
const query = "$[?((@.foo)]";
5050
expect(() => env.query(query, {})).toThrow(JSONPathSyntaxError);
5151
expect(() => env.query(query, {})).toThrow(
52-
"expected an expression, found ']' ('((@.foo)]':11)",
52+
"unbalanced brackets ('((@.foo)]':11)",
5353
);
5454
});
5555
});
@@ -183,4 +183,10 @@ describe("escape sequence decode errors", () => {
183183
"invalid \\uXXXX escape sequence ('$['ab\\u26':3)",
184184
);
185185
});
186+
187+
test("well-typed nested functions, unbalanced parens", () => {
188+
expect(() => compile("$.values[?match(@.a, value($..['regex'])]")).toThrow(
189+
JSONPathSyntaxError,
190+
);
191+
});
186192
});

tests/path/issues.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { query } from "../../src/path";
1+
import { query, compile, JSONPathQuery } from "../../src";
22

33
describe("issues", () => {
44
test("issue 40", () => {
@@ -10,4 +10,11 @@ describe("issues", () => {
1010
"d449f7a5-9153-4f39-a05d-dca1c35538ec",
1111
]);
1212
});
13+
14+
test("issue 42", () => {
15+
// This was failing with an "unbalanced parentheses" syntax error.
16+
expect(compile("$[? count(@.likes[? @.location]) > 3]")).toBeInstanceOf(
17+
JSONPathQuery,
18+
);
19+
});
1320
});

tests/path/parse.test.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import {
2-
JSONPathEnvironment,
3-
JSONPathSyntaxError,
4-
compile,
5-
query,
6-
} from "../../src/path";
1+
import { JSONPathEnvironment, query } from "../../src/path";
72

83
type TestCase = {
94
description: string;
@@ -79,10 +74,4 @@ describe("parse", () => {
7974
const rv = query("$.values[?match(@.a, value($..['regex']))]", data);
8075
expect(rv.values()).toStrictEqual([{ a: "ab" }]);
8176
});
82-
83-
test("well-typed nested functions, unbalanced parens", () => {
84-
expect(() => compile("$.values[?match(@.a, value($..['regex'])]")).toThrow(
85-
JSONPathSyntaxError,
86-
);
87-
});
8877
});

0 commit comments

Comments
 (0)