Skip to content

Commit 0f07f4f

Browse files
committed
fix: detect unconsumed tokens after parse to catch unbalanced braces (#445)
ANTLR's default error recovery silently absorbed certain malformed queries (e.g. missing opening braces) without ever calling the syntaxError listener, producing a structurally different but syntactically valid parse tree and leaving trailing tokens unconsumed. Fix by comparing the document tree's stop-token index against the last non-EOF token after parsing; throw ParseError if any tokens were skipped. Also fix two test queries that had a genuine extra `}` that was previously silently ignored: entities-query in federation_tests and later-fragments-do-not-override-earlier in fragments_tests.
1 parent cbdfeb6 commit 0f07f4f

4 files changed

Lines changed: 43 additions & 6 deletions

File tree

src/com/walmartlabs/lacinia/parser/antlr.clj

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
(java.util.concurrent ConcurrentHashMap)
66
(org.antlr.v4.runtime ANTLRErrorListener
77
CharStreams CommonTokenStream Lexer Parser
8-
RecognitionException)
8+
ParserRuleContext RecognitionException Token)
99
(org.antlr.v4.runtime.tree ParseTree Tree)))
1010

1111
(def ^ConcurrentHashMap fast-keyword-cache
@@ -132,7 +132,8 @@
132132
(.removeErrorListeners)
133133
(.addErrorListener error-listener))
134134

135-
parser (parser ap (CommonTokenStream. lexer))
135+
token-stream (CommonTokenStream. lexer)
136+
parser (parser ap token-stream)
136137
_ (doto parser
137138
(.removeErrorListeners)
138139
(.addErrorListener error-listener))
@@ -142,5 +143,27 @@
142143
(when-let [errors @error-listener]
143144
(throw (parse-error errors tree)))
144145

146+
;; ANTLR's error recovery can silently produce a valid parse tree even when
147+
;; there are unconsumed tokens (e.g. unbalanced braces). Detect this case by
148+
;; checking that every non-EOF token was consumed by the document rule.
149+
(.fill token-stream)
150+
(let [tokens (.getTokens token-stream)
151+
n (count tokens)
152+
;; tokens is [...real-tokens... EOF]; last-real-idx is the index of the
153+
;; last non-EOF token (n-2), or -1 when the input is empty.
154+
last-real-idx (- n 2)
155+
stop-token (.getStop ^ParserRuleContext tree)
156+
stop-idx (if stop-token (.getTokenIndex ^Token stop-token) -1)]
157+
(when (< stop-idx last-real-idx)
158+
(let [first-unconsumed (.get token-stream (inc stop-idx))
159+
line (.getLine ^Token first-unconsumed)
160+
col (inc (.getCharPositionInLine ^Token first-unconsumed))]
161+
(throw (parse-error [{:line line
162+
:char col
163+
:message (str "extraneous input '"
164+
(.getText ^Token first-unconsumed)
165+
"' expecting EOF")}]
166+
tree)))))
167+
145168
{:tree tree
146169
:parser parser}))

test/com/walmartlabs/lacinia/federation_tests.clj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ query($reps : [_Any!]!) {
5252
... on User { id name }
5353
5454
... on Account { acct_number name }
55-
}
5655
}
5756
}")
5857

test/com/walmartlabs/lacinia/fragments_tests.clj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,7 @@
9090
}
9191
}
9292
}
93-
}
94-
}"))))
93+
}"))))
9594

9695
(deftest named-fragments
9796
(is (= {:data {:characters [{:name "R2-D2"

test/com/walmartlabs/lacinia/parser/query_test.clj

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
e (is (thrown? Throwable
9696
(parser/parse-query schema "query [hero]")))]
9797
(when e
98-
(is (= "Failed to parse GraphQL query." (.getMessage e)))
98+
(is (= "Failed to parse GraphQL query." (ex-message e)))
9999
;; TODO: See if we can get a proper column number here!
100100
(is (= {:errors [{:locations [{:column nil
101101
:line 1}]
@@ -105,6 +105,22 @@
105105
:message "mismatched input ']' expecting {'(', '{', '@'}"}]}
106106
(ex-data e))))))
107107

108+
(deftest unbalanced-braces
109+
;; Regression test for https://github.com/walmartlabs/lacinia/issues/445
110+
;; ANTLR's error recovery can silently produce a valid parse tree for
111+
;; certain malformed queries with unbalanced/missing braces.
112+
(testing "missing opening brace after query keyword"
113+
(let [e (is (thrown? Throwable
114+
(parse-query "query\n myQuery {\n name\n }\n}")))]
115+
(when e
116+
(is (= "Failed to parse GraphQL query." (ex-message e))))))
117+
118+
(testing "missing opening brace for nested field"
119+
(let [e (is (thrown? Throwable
120+
(parse-query "query {\n myQuery\n name\n }\n}")))]
121+
(when e
122+
(is (= "Failed to parse GraphQL query." (ex-message e)))))))
123+
108124
(deftest requires-compiled-schema
109125
(is (thrown-with-msg? IllegalStateException
110126
#"The provided schema has not been compiled"

0 commit comments

Comments
 (0)