Skip to content

Commit e56a311

Browse files
authored
Merge pull request #26 from microsoft/fixup
Fixup
2 parents bbcffbb + 41d7f14 commit e56a311

4 files changed

Lines changed: 182 additions & 12 deletions

File tree

flowquery-py/src/parsing/parser.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -683,12 +683,7 @@ def _parse_operand(self, expression: Expression) -> bool:
683683
return True
684684
elif (
685685
self.token.is_left_parenthesis()
686-
and self.peek() is not None
687-
and (
688-
self.peek().is_identifier_or_keyword()
689-
or self.peek().is_colon()
690-
or self.peek().is_right_parenthesis()
691-
)
686+
and self._looks_like_node_pattern()
692687
):
693688
# Possible graph pattern expression
694689
pattern = self._parse_pattern_expression()
@@ -779,6 +774,34 @@ def _parse_expression(self) -> Optional[Expression]:
779774
return expression
780775
return None
781776

777+
def _looks_like_node_pattern(self) -> bool:
778+
"""Peek ahead from a left parenthesis to determine whether the
779+
upcoming tokens form a graph-node pattern (e.g. (n:Label), (n),
780+
(:Label), ()) rather than a parenthesised expression (e.g.
781+
(variable.property), (a + b)).
782+
"""
783+
saved_index = self._token_index
784+
self.set_next_token() # skip '('
785+
self._skip_whitespace_and_comments()
786+
787+
if self.token.is_colon() or self.token.is_right_parenthesis():
788+
self._token_index = saved_index
789+
return True
790+
791+
if self.token.is_identifier_or_keyword():
792+
self.set_next_token() # skip identifier
793+
self._skip_whitespace_and_comments()
794+
result = (
795+
self.token.is_colon()
796+
or self.token.is_opening_brace()
797+
or self.token.is_right_parenthesis()
798+
)
799+
self._token_index = saved_index
800+
return result
801+
802+
self._token_index = saved_index
803+
return False
804+
782805
def _parse_is_operator(self) -> ASTNode:
783806
"""Parse IS or IS NOT operator."""
784807
# Current token is IS. Look ahead for NOT to produce IS NOT.

flowquery-py/tests/parsing/test_parser.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,3 +1032,61 @@ def test_where_with_not_ends_with(self):
10321032
"--- Reference (s)"
10331033
)
10341034
assert ast.print() == expected
1035+
1036+
def test_parenthesized_expression_with_addition(self):
1037+
"""Test that (variable + number) is parsed as a parenthesized expression, not a node."""
1038+
parser = Parser()
1039+
ast = parser.parse("WITH 1 AS n RETURN (n + 2)")
1040+
expected = (
1041+
"ASTNode\n"
1042+
"- With\n"
1043+
"-- Expression (n)\n"
1044+
"--- Number (1)\n"
1045+
"- Return\n"
1046+
"-- Expression\n"
1047+
"--- Expression\n"
1048+
"---- Add\n"
1049+
"----- Reference (n)\n"
1050+
"----- Number (2)"
1051+
)
1052+
assert ast.print() == expected
1053+
1054+
def test_parenthesized_expression_with_property_access(self):
1055+
"""Test that (obj.property) is parsed as a parenthesized expression, not a node."""
1056+
parser = Parser()
1057+
ast = parser.parse("WITH {a: 1} AS obj RETURN (obj.a)")
1058+
expected = (
1059+
"ASTNode\n"
1060+
"- With\n"
1061+
"-- Expression (obj)\n"
1062+
"--- AssociativeArray\n"
1063+
"---- KeyValuePair\n"
1064+
"----- String (a)\n"
1065+
"----- Expression\n"
1066+
"------ Number (1)\n"
1067+
"- Return\n"
1068+
"-- Expression\n"
1069+
"--- Expression\n"
1070+
"---- Lookup\n"
1071+
"----- Identifier (a)\n"
1072+
"----- Reference (obj)"
1073+
)
1074+
assert ast.print() == expected
1075+
1076+
def test_parenthesized_expression_with_multiplication(self):
1077+
"""Test that (variable * number) is parsed as a parenthesized expression, not a node."""
1078+
parser = Parser()
1079+
ast = parser.parse("WITH 5 AS x RETURN (x * 3)")
1080+
expected = (
1081+
"ASTNode\n"
1082+
"- With\n"
1083+
"-- Expression (x)\n"
1084+
"--- Number (5)\n"
1085+
"- Return\n"
1086+
"-- Expression\n"
1087+
"--- Expression\n"
1088+
"---- Multiply\n"
1089+
"----- Reference (x)\n"
1090+
"----- Number (3)"
1091+
)
1092+
assert ast.print() == expected

src/parsing/parser.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -798,12 +798,7 @@ class Parser extends BaseParser {
798798
expression.addNode(lookup);
799799
return true;
800800
}
801-
} else if (
802-
this.token.isLeftParenthesis() &&
803-
(this.peek()?.isIdentifierOrKeyword() ||
804-
this.peek()?.isColon() ||
805-
this.peek()?.isRightParenthesis())
806-
) {
801+
} else if (this.token.isLeftParenthesis() && this.looksLikeNodePattern()) {
807802
// Possible graph pattern expression
808803
const pattern = this.parsePatternExpression();
809804
if (pattern !== null) {
@@ -865,6 +860,42 @@ class Parser extends BaseParser {
865860
return false;
866861
}
867862

863+
/**
864+
* Peeks ahead from a left parenthesis to determine whether the
865+
* upcoming tokens form a graph-node pattern (e.g. (n:Label), (n),
866+
* (:Label), ()) rather than a parenthesised expression (e.g.
867+
* (variable.property), (a + b)).
868+
*
869+
* The heuristic is:
870+
* • ( followed by `:` or `)` → node pattern
871+
* • ( identifier, then `:` or `{` or `)` → node pattern
872+
* • anything else → parenthesised expression
873+
*/
874+
private looksLikeNodePattern(): boolean {
875+
const savedIndex = this.tokenIndex;
876+
this.setNextToken(); // skip '('
877+
this.skipWhitespaceAndComments();
878+
879+
if (this.token.isColon() || this.token.isRightParenthesis()) {
880+
this.tokenIndex = savedIndex;
881+
return true;
882+
}
883+
884+
if (this.token.isIdentifierOrKeyword()) {
885+
this.setNextToken(); // skip identifier
886+
this.skipWhitespaceAndComments();
887+
const result =
888+
this.token.isColon() ||
889+
this.token.isOpeningBrace() ||
890+
this.token.isRightParenthesis();
891+
this.tokenIndex = savedIndex;
892+
return result;
893+
}
894+
895+
this.tokenIndex = savedIndex;
896+
return false;
897+
}
898+
868899
private parseExpression(): Expression | null {
869900
const expression = new Expression();
870901
while (true) {

tests/parsing/parser.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,3 +1098,61 @@ test("Test WHERE with NOT ENDS WITH", () => {
10981098
"--- Reference (s)"
10991099
);
11001100
});
1101+
1102+
test("Test parenthesized expression with addition", () => {
1103+
const parser = new Parser();
1104+
const ast = parser.parse("WITH 1 AS n RETURN (n + 2)");
1105+
// prettier-ignore
1106+
expect(ast.print()).toBe(
1107+
"ASTNode\n" +
1108+
"- With\n" +
1109+
"-- Expression (n)\n" +
1110+
"--- Number (1)\n" +
1111+
"- Return\n" +
1112+
"-- Expression\n" +
1113+
"--- Expression\n" +
1114+
"---- Add\n" +
1115+
"----- Reference (n)\n" +
1116+
"----- Number (2)"
1117+
);
1118+
});
1119+
1120+
test("Test parenthesized expression with property access", () => {
1121+
const parser = new Parser();
1122+
const ast = parser.parse("WITH {a: 1} AS obj RETURN (obj.a)");
1123+
// prettier-ignore
1124+
expect(ast.print()).toBe(
1125+
"ASTNode\n" +
1126+
"- With\n" +
1127+
"-- Expression (obj)\n" +
1128+
"--- AssociativeArray\n" +
1129+
"---- KeyValuePair\n" +
1130+
"----- String (a)\n" +
1131+
"----- Expression\n" +
1132+
"------ Number (1)\n" +
1133+
"- Return\n" +
1134+
"-- Expression\n" +
1135+
"--- Expression\n" +
1136+
"---- Lookup\n" +
1137+
"----- Identifier (a)\n" +
1138+
"----- Reference (obj)"
1139+
);
1140+
});
1141+
1142+
test("Test parenthesized expression with multiplication", () => {
1143+
const parser = new Parser();
1144+
const ast = parser.parse("WITH 5 AS x RETURN (x * 3)");
1145+
// prettier-ignore
1146+
expect(ast.print()).toBe(
1147+
"ASTNode\n" +
1148+
"- With\n" +
1149+
"-- Expression (x)\n" +
1150+
"--- Number (5)\n" +
1151+
"- Return\n" +
1152+
"-- Expression\n" +
1153+
"--- Expression\n" +
1154+
"---- Multiply\n" +
1155+
"----- Reference (x)\n" +
1156+
"----- Number (3)"
1157+
);
1158+
});

0 commit comments

Comments
 (0)