From d45b8852e37b8aea7cc464a92a6b816f7fe0f86f Mon Sep 17 00:00:00 2001 From: Daniel Bodart Date: Tue, 1 Sep 2026 23:14:43 +0100 Subject: [PATCH] inject() should accept bound variables inject() was the only variadic-generic step whose grammar used genericLiteralVarargs rather than genericArgumentVarargs, so it rejected a bound variable that every sibling step (V, E, within, without, hasId, hasValue, property) already accepts. Change both inject productions to genericArgumentVarargs and route the two inject visitors through ArgumentVisitor.parseObjectVarargs, mirroring V()/E(). Adapt the Groovy translators (Java and JavaScript) to the flatter varargs tree shape. This is a strict superset: genericArgument includes genericLiteral, so every existing inject(...) call is unchanged and only inject() becomes newly valid. --- CHANGELOG.asciidoc | 1 + .../grammar/TraversalMethodVisitor.java | 2 +- .../TraversalSourceSpawnMethodVisitor.java | 2 +- .../translator/GroovyTranslateVisitor.java | 40 +++++++++---------- .../grammar/GremlinQueryParserTest.java | 5 +++ .../translator/GremlinTranslatorTest.java | 9 +++++ .../translator/GroovyTranslateVisitor.ts | 36 +++++++---------- gremlin-language/src/main/antlr4/Gremlin.g4 | 4 +- .../language/grammar/BasicGrammarTest.java | 10 +++++ 9 files changed, 62 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index d94822198f3..3ce1b813175 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -25,6 +25,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima [[release-4-0-0]] === TinkerPop 4.0.0 (Release Date: NOT OFFICIALLY RELEASED YET) +* Allowed `inject()` to accept bound variables, aligning it with the other variadic steps (`V()`, `E()`, `within()`, `without()`, etc.) which already accept a `genericArgument`; previously `inject()` was the only such step restricted to literals. * Fixed `gremlin-go` to report a malformed or truncated GraphBinary response as a deserialization error rather than a bare decoder message. * Made `TinkerGraph` an interface and renamed the in-memory implementation to `TinkerMemoryGraph`; `TinkerGraph.open()` and `gremlin.graph=...TinkerGraph` behave as before. *(breaking)* * Renamed `TinkerTransactionGraph` to `TinkerStorageGraph`. *(breaking)* diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java index 9c12ef900de..d93471b2d8d 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java @@ -1037,7 +1037,7 @@ public GraphTraversal visitTraversalMethod_inV(final GremlinParser.TraversalMeth */ @Override public GraphTraversal visitTraversalMethod_inject(final GremlinParser.TraversalMethod_injectContext ctx) { - return graphTraversal.inject(antlr.genericVisitor.parseObjectVarargs(ctx.genericLiteralVarargs())); + return graphTraversal.inject(antlr.argumentVisitor.parseObjectVarargs(ctx.genericArgumentVarargs())); } @Override diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java index ae8d0fba34d..090d4029ad6 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalSourceSpawnMethodVisitor.java @@ -143,7 +143,7 @@ public GraphTraversal visitTraversalSourceSpawnMethod_V(final GremlinParser.Trav */ @Override public GraphTraversal visitTraversalSourceSpawnMethod_inject(final GremlinParser.TraversalSourceSpawnMethod_injectContext ctx) { - return this.traversalSource.inject(antlr.genericVisitor.parseObjectVarargs(ctx.genericLiteralVarargs())); + return this.traversalSource.inject(antlr.argumentVisitor.parseObjectVarargs(ctx.genericArgumentVarargs())); } /** diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GroovyTranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GroovyTranslateVisitor.java index 76b369d2f10..137bff827d2 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GroovyTranslateVisitor.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GroovyTranslateVisitor.java @@ -20,7 +20,6 @@ import org.antlr.v4.runtime.ParserRuleContext; import org.apache.tinkerpop.gremlin.language.grammar.GremlinParser; -import org.apache.tinkerpop.gremlin.language.grammar.GremlinParser.GenericLiteralVarargsContext; import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceVertex; /** @@ -267,29 +266,26 @@ public Void visitTraversalMethod_hasLabel_String_String(final GremlinParser.Trav * inject() ends up being ambiguous with groovy's jdk extension of inject(Object initialValue, Closure closure) */ private Void handleInject(final ParserRuleContext ctx) { - if (ctx.getChildCount() > 3 && ctx.getChild(2) instanceof GremlinParser.GenericLiteralVarargsContext) { - final GremlinParser.GenericLiteralVarargsContext varArgs = (GremlinParser.GenericLiteralVarargsContext) ctx.getChild(2); - if (varArgs.getChildCount() == 1 && varArgs.getChild(0) instanceof GremlinParser.GenericLiteralExprContext) { - GremlinParser.GenericLiteralExprContext injectArgs = (GremlinParser.GenericLiteralExprContext) varArgs.getChild(0); - if (injectArgs.getChildCount() > 2 && "null".equals(injectArgs.getChild(2).getText())) { - sb.append(ctx.getChild(0).getText()); - sb.append("("); - for (int i = 0; i < injectArgs.getChildCount(); i += 2) { - if (i == 2) { - sb.append("(Object) null"); - } else { - visit(injectArgs.getChild(i)); - } - - if (i < injectArgs.getChildCount() - 1) { - sb.append(", "); - } - } - - sb.append(")"); - return null; + // ctx is always K_INJECT LPAREN genericArgumentVarargs RPAREN, so child(2) is the varargs node. + final GremlinParser.GenericArgumentVarargsContext injectArgs = + (GremlinParser.GenericArgumentVarargsContext) ctx.getChild(2); + if (injectArgs.getChildCount() > 2 && "null".equals(injectArgs.getChild(2).getText())) { + sb.append(ctx.getChild(0).getText()); + sb.append("("); + for (int i = 0; i < injectArgs.getChildCount(); i += 2) { + if (i == 2) { + sb.append("(Object) null"); + } else { + visit(injectArgs.getChild(i)); + } + + if (i < injectArgs.getChildCount() - 1) { + sb.append(", "); } } + + sb.append(")"); + return null; } return visitChildren(ctx); diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GremlinQueryParserTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GremlinQueryParserTest.java index cd16ef308c4..b0f12302e60 100644 --- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GremlinQueryParserTest.java +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GremlinQueryParserTest.java @@ -67,6 +67,11 @@ public void shouldParseVariablesInVarargs() { t = (GraphTraversal) GremlinQueryParser.parse("g.V(x, y, 300).has('name',gt(z))", gremlinAntlrToJava); assertEquals(g.V(100, 200, 300).has("name", P.gt(50)).asAdmin().getGremlinLang(), t.asAdmin().getGremlinLang()); + + // inject() accepts bound variables like the other varargs steps (V, within, etc.) + t = (GraphTraversal) GremlinQueryParser.parse("g.inject(x, y, 300)", gremlinAntlrToJava); + assertEquals(g.inject(100, 200, 300).asAdmin().getGremlinLang(), + t.asAdmin().getGremlinLang()); } @Test(expected = GremlinParserException.class) diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/translator/GremlinTranslatorTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/translator/GremlinTranslatorTest.java index 7ddc147fe3c..49b8f4352f1 100644 --- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/translator/GremlinTranslatorTest.java +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/translator/GremlinTranslatorTest.java @@ -803,6 +803,15 @@ public static Collection data() { "g.inject(null, null).inject(null, null)", "g.inject(null, null).inject(null, null)", "g.inject(None, None).inject(None, None)"}, + {"g.inject(1, 2)", + null, + "g.inject(number0, number1)", + "g.Inject(1, 2)", + "g.Inject(1, 2)", + null, + null, + null, + null}, {"g.V().hasLabel(null)", null, "g.V().hasLabel(string0)", diff --git a/gremlin-js/gremlin-javascript/lib/language/translator/GroovyTranslateVisitor.ts b/gremlin-js/gremlin-javascript/lib/language/translator/GroovyTranslateVisitor.ts index f061975e2bd..9396a9ab948 100644 --- a/gremlin-js/gremlin-javascript/lib/language/translator/GroovyTranslateVisitor.ts +++ b/gremlin-js/gremlin-javascript/lib/language/translator/GroovyTranslateVisitor.ts @@ -290,29 +290,23 @@ export default class GroovyTranslateVisitor extends TranslateVisitor { private handleInject(ctx: any): void { if (ctx.getChildCount() > 3) { const child2 = ctx.getChild(2); - if (child2?.constructor?.name === 'GenericLiteralVarargsContext') { - const varArgs = child2; - if (varArgs.getChildCount() === 1) { - const child0 = varArgs.getChild(0); - if (child0?.constructor?.name === 'GenericLiteralExprContext') { - const injectArgs = child0; - if (injectArgs.getChildCount() > 2 && injectArgs.getChild(2).getText() === 'null') { - this.sb.push(ctx.getChild(0).getText()); - this.sb.push('('); - for (let i = 0; i < injectArgs.getChildCount(); i += 2) { - if (i === 2) { - this.sb.push('(Object) null'); - } else { - this.visit(injectArgs.getChild(i)); - } - if (i < injectArgs.getChildCount() - 1) { - this.sb.push(', '); - } - } - this.sb.push(')'); - return; + if (child2?.constructor?.name === 'GenericArgumentVarargsContext') { + const injectArgs = child2; + if (injectArgs.getChildCount() > 2 && injectArgs.getChild(2).getText() === 'null') { + this.sb.push(ctx.getChild(0).getText()); + this.sb.push('('); + for (let i = 0; i < injectArgs.getChildCount(); i += 2) { + if (i === 2) { + this.sb.push('(Object) null'); + } else { + this.visit(injectArgs.getChild(i)); + } + if (i < injectArgs.getChildCount() - 1) { + this.sb.push(', '); } } + this.sb.push(')'); + return; } } } diff --git a/gremlin-language/src/main/antlr4/Gremlin.g4 b/gremlin-language/src/main/antlr4/Gremlin.g4 index d15a2982df8..db253728d93 100644 --- a/gremlin-language/src/main/antlr4/Gremlin.g4 +++ b/gremlin-language/src/main/antlr4/Gremlin.g4 @@ -133,7 +133,7 @@ traversalSourceSpawnMethod_V ; traversalSourceSpawnMethod_inject - : K_INJECT LPAREN genericLiteralVarargs RPAREN + : K_INJECT LPAREN genericArgumentVarargs RPAREN ; traversalSourceSpawnMethod_io @@ -626,7 +626,7 @@ traversalMethod_index ; traversalMethod_inject - : K_INJECT LPAREN genericLiteralVarargs RPAREN + : K_INJECT LPAREN genericArgumentVarargs RPAREN ; traversalMethod_is diff --git a/gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/BasicGrammarTest.java b/gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/BasicGrammarTest.java index 14ac12108e8..da46d1e153f 100644 --- a/gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/BasicGrammarTest.java +++ b/gremlin-language/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/BasicGrammarTest.java @@ -28,4 +28,14 @@ public class BasicGrammarTest extends AbstractGrammarTest { public void shouldParseV() { parse("g.V()", ParserRule.QUERY_LIST); } + + @Test + public void shouldParseInjectWithVariable() { + parse("g.inject(x)", ParserRule.QUERY_LIST); + } + + @Test + public void shouldParseInjectStepWithVariable() { + parse("g.V().inject(x)", ParserRule.QUERY_LIST); + } }