| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import { ErrorNode, TerminalNode } from 'antlr4ng'; |
| import { GremlinLexer } from '../grammar/GremlinLexer.js'; |
| |
| /** |
| * Base visitor for translating Gremlin queries. Mirrors the Java TranslateVisitor. |
| * Makes no changes to input except: |
| * - Normalizes whitespace |
| * - Normalizes numeric suffixes to lower case |
| * - Makes anonymous traversals explicit with double underscore |
| * - Makes enums explicit with their proper name |
| */ |
| export default class TranslateVisitor { |
| |
| protected readonly graphTraversalSourceName: string; |
| protected readonly sb: string[] = []; |
| protected readonly parameters: Set<string> = new Set(); |
| |
| constructor(graphTraversalSourceName: string = 'g') { |
| this.graphTraversalSourceName = graphTraversalSourceName; |
| } |
| |
| getTranslated(): string { |
| return this.sb.join(''); |
| } |
| |
| getParameters(): ReadonlySet<string> { |
| return this.parameters; |
| } |
| |
| protected processGremlinSymbol(step: string): string { |
| return step; |
| } |
| |
| protected appendArgumentSeparator(): void { |
| this.sb.push(', '); |
| } |
| |
| protected appendStepSeparator(): void { |
| this.sb.push('.'); |
| } |
| |
| protected appendStepOpen(): void { |
| this.sb.push('('); |
| } |
| |
| protected appendStepClose(): void { |
| this.sb.push(')'); |
| } |
| |
| protected appendAnonymousSpawn(): void { |
| this.sb.push('__.'); |
| } |
| |
| visit(tree: any): void { |
| if (tree == null) return; |
| |
| // antlr-ng does not generate accept() overrides per context class — |
| // ParserRuleContext.accept() always calls visitChildren, never dispatching to |
| // the specific visitXxx method. We perform the dispatch ourselves here by |
| // mapping the context class name (e.g. StringLiteralContext) to the |
| // corresponding visitor method (e.g. visitStringLiteral). |
| const className: string = tree?.constructor?.name ?? ''; |
| if (className.endsWith('Context')) { |
| const methodName = 'visit' + className.slice(0, -'Context'.length); |
| const method = (this as any)[methodName]; |
| if (typeof method === 'function') { |
| method.call(this, tree); |
| return; |
| } |
| } |
| |
| // No specific visitor method found — fall through to accept() which calls |
| // visitChildren for rule contexts, visitTerminal for terminals. |
| (tree as any).accept(this); |
| } |
| |
| visitChildren(node: any): void { |
| const n = node.getChildCount(); |
| for (let i = 0; i < n; i++) { |
| this.visit(node.getChild(i)); |
| } |
| } |
| |
| protected getCardinalityFunctionClass(): string { |
| return 'Cardinality'; |
| } |
| |
| protected static removeFirstAndLastCharacters(text: string): string { |
| return text != null && text.length > 0 ? text.substring(1, text.length - 1) : ''; |
| } |
| |
| protected handleStringLiteralText(text: string): void { |
| this.sb.push('"'); |
| this.sb.push(text); |
| this.sb.push('"'); |
| } |
| |
| protected appendExplicitNaming(txt: string, prefix: string): void { |
| if (!txt.startsWith(prefix + '.')) { |
| this.sb.push(this.processGremlinSymbol(prefix)); |
| this.sb.push('.'); |
| this.sb.push(this.processGremlinSymbol(txt)); |
| } else { |
| const parts = txt.split('.'); |
| this.sb.push(this.processGremlinSymbol(parts[0])); |
| this.sb.push('.'); |
| this.sb.push(this.processGremlinSymbol(parts[1])); |
| } |
| } |
| |
| visitTerminal(node: TerminalNode): void { |
| // skip EOF node |
| if (node == null || node.getSymbol().type === -1) return; |
| |
| // TRAVERSAL_ROOT is the traversal source token (e.g. 'g') — always output as-is, |
| // never subject to processGremlinSymbol (which may capitalize it in some dialects). |
| if (node.getSymbol().type === GremlinLexer.TRAVERSAL_ROOT) { |
| this.sb.push(this.graphTraversalSourceName); |
| return; |
| } |
| |
| const terminal = node.getSymbol().text ?? ''; |
| switch (terminal) { |
| case '(': |
| this.appendStepOpen(); |
| break; |
| case ')': |
| this.appendStepClose(); |
| break; |
| case ',': |
| this.appendArgumentSeparator(); |
| break; |
| case '.': |
| this.appendStepSeparator(); |
| break; |
| case 'new': |
| this.sb.push('new'); |
| if (!(node.parent?.constructor?.name === 'MapKeyContext')) { |
| this.sb.push(' '); |
| } |
| break; |
| default: |
| this.sb.push(this.processGremlinSymbol(terminal)); |
| } |
| } |
| |
| visitErrorNode(_node: ErrorNode): void { |
| // no-op |
| } |
| |
| visitTraversalSource(ctx: any): void { |
| this.sb.push(this.graphTraversalSourceName); |
| // child counts more than 1 means there is a step separator and a traversal source method |
| if (ctx.getChildCount() > 1) { |
| if (ctx.getChild(0).getChildCount() > 1) { |
| this.appendStepSeparator(); |
| this.visitTraversalSourceSelfMethod(ctx.getChild(0).getChild(2)); |
| } |
| this.appendStepSeparator(); |
| this.visitTraversalSourceSelfMethod(ctx.getChild(2)); |
| } |
| } |
| |
| visitTraversalSourceSelfMethod(ctx: any): void { |
| this.visitChildren(ctx); |
| } |
| |
| visitNestedTraversal(ctx: any): void { |
| if (ctx.ANON_TRAVERSAL_ROOT() == null) { |
| this.appendAnonymousSpawn(); |
| } |
| this.visitChildren(ctx); |
| } |
| |
| visitTraversalScope(ctx: any): void { |
| this.appendExplicitNaming(ctx.getText(), 'Scope'); |
| } |
| |
| visitTraversalT(ctx: any): void { |
| this.appendExplicitNaming(ctx.getText(), 'T'); |
| } |
| |
| visitTraversalTShort(ctx: any): void { |
| this.appendExplicitNaming(ctx.getText(), 'T'); |
| } |
| |
| visitTraversalTLong(ctx: any): void { |
| this.appendExplicitNaming(ctx.getText(), 'T'); |
| } |
| |
| visitTraversalMerge(ctx: any): void { |
| this.appendExplicitNaming(ctx.getText(), 'Merge'); |
| } |
| |
| visitTraversalOrder(ctx: any): void { |
| this.appendExplicitNaming(ctx.getText(), 'Order'); |
| } |
| |
| visitTraversalBarrier(ctx: any): void { |
| this.appendExplicitNaming(ctx.getText(), 'Barrier'); |
| } |
| |
| visitTraversalDirection(ctx: any): void { |
| this.appendExplicitNaming(ctx.getText(), 'Direction'); |
| } |
| |
| visitTraversalDirectionShort(ctx: any): void { |
| this.appendExplicitNaming(ctx.getText(), 'Direction'); |
| } |
| |
| visitTraversalDirectionLong(ctx: any): void { |
| this.appendExplicitNaming(ctx.getText(), 'Direction'); |
| } |
| |
| visitTraversalCardinality(ctx: any): void { |
| if (ctx.LPAREN() != null && ctx.RPAREN() != null) { |
| const idx = ctx.K_CARDINALITY() != null ? 2 : 0; |
| const txt = ctx.getChild(idx).getText(); |
| this.appendExplicitNaming(txt, this.getCardinalityFunctionClass()); |
| this.appendStepOpen(); |
| this.visit(ctx.genericLiteral()); |
| this.appendStepClose(); |
| } else { |
| this.appendExplicitNaming(ctx.getText(), 'Cardinality'); |
| } |
| } |
| |
| visitTraversalColumn(ctx: any): void { |
| this.appendExplicitNaming(ctx.getText(), 'Column'); |
| } |
| |
| visitTraversalPop(ctx: any): void { |
| this.appendExplicitNaming(ctx.getText(), 'Pop'); |
| } |
| |
| visitTraversalOperator(ctx: any): void { |
| this.appendExplicitNaming(ctx.getText(), 'Operator'); |
| } |
| |
| visitTraversalPick(ctx: any): void { |
| this.appendExplicitNaming(ctx.getText(), 'Pick'); |
| } |
| |
| visitTraversalDT(ctx: any): void { |
| this.appendExplicitNaming(ctx.getText(), 'DT'); |
| } |
| |
| visitTraversalGType(ctx: any): void { |
| this.appendExplicitNaming(ctx.getText(), 'GType'); |
| } |
| |
| visitTraversalPredicate(ctx: any): void { |
| switch (ctx.getChildCount()) { |
| case 1: |
| this.visit(ctx.getChild(0)); |
| break; |
| case 5: |
| // handle negate of P |
| this.visit(ctx.getChild(0)); |
| this.sb.push('.'); |
| this.sb.push(this.processGremlinSymbol('negate')); |
| this.sb.push('()'); |
| break; |
| case 6: |
| // handle and/or predicates |
| this.visit(ctx.getChild(0)); |
| this.sb.push('.'); |
| this.sb.push(this.processGremlinSymbol(ctx.getChild(2).getText())); |
| this.sb.push('('); |
| this.visit(ctx.getChild(4)); |
| this.sb.push(')'); |
| break; |
| } |
| } |
| |
| protected visitP(ctx: any, pClass: string, methodName: string): void { |
| this.sb.push(pClass); |
| this.appendStepSeparator(); |
| this.sb.push(this.processGremlinSymbol(methodName)); |
| this.appendStepOpen(); |
| |
| const children: any[] = []; |
| for (let i = 0; i < ctx.getChildCount(); i++) { |
| const child = ctx.getChild(i); |
| const name = child.constructor?.name ?? ''; |
| if (name === 'GenericArgumentContext' || |
| name === 'GenericArgumentVarargsContext' || |
| name === 'StringArgumentContext' || |
| name === 'StringLiteralContext' || |
| name === 'TraversalGTypeContext' || |
| name === 'TraversalPredicateContext') { |
| children.push(child); |
| } |
| } |
| |
| for (let ix = 0; ix < children.length; ix++) { |
| this.visit(children[ix]); |
| if (ix < children.length - 1) { |
| this.appendArgumentSeparator(); |
| } |
| } |
| |
| this.appendStepClose(); |
| } |
| |
| visitTraversalPredicate_eq(ctx: any): void { this.visitP(ctx, 'P', 'eq'); } |
| visitTraversalPredicate_neq(ctx: any): void { this.visitP(ctx, 'P', 'neq'); } |
| visitTraversalPredicate_lt(ctx: any): void { this.visitP(ctx, 'P', 'lt'); } |
| visitTraversalPredicate_lte(ctx: any): void { this.visitP(ctx, 'P', 'lte'); } |
| visitTraversalPredicate_gt(ctx: any): void { this.visitP(ctx, 'P', 'gt'); } |
| visitTraversalPredicate_gte(ctx: any): void { this.visitP(ctx, 'P', 'gte'); } |
| visitTraversalPredicate_inside(ctx: any): void { this.visitP(ctx, 'P', 'inside'); } |
| visitTraversalPredicate_outside(ctx: any): void { this.visitP(ctx, 'P', 'outside'); } |
| visitTraversalPredicate_between(ctx: any): void { this.visitP(ctx, 'P', 'between'); } |
| visitTraversalPredicate_within(ctx: any): void { this.visitP(ctx, 'P', 'within'); } |
| visitTraversalPredicate_without(ctx: any): void { this.visitP(ctx, 'P', 'without'); } |
| visitTraversalPredicate_typeOf(ctx: any): void { this.visitP(ctx, 'P', 'typeOf'); } |
| visitTraversalPredicate_not(ctx: any): void { this.visitP(ctx, 'P', 'not'); } |
| visitTraversalPredicate_containing(ctx: any): void { this.visitP(ctx, 'TextP', 'containing'); } |
| visitTraversalPredicate_notContaining(ctx: any): void { this.visitP(ctx, 'TextP', 'notContaining'); } |
| visitTraversalPredicate_startingWith(ctx: any): void { this.visitP(ctx, 'TextP', 'startingWith'); } |
| visitTraversalPredicate_notStartingWith(ctx: any): void { this.visitP(ctx, 'TextP', 'notStartingWith'); } |
| visitTraversalPredicate_endingWith(ctx: any): void { this.visitP(ctx, 'TextP', 'endingWith'); } |
| visitTraversalPredicate_notEndingWith(ctx: any): void { this.visitP(ctx, 'TextP', 'notEndingWith'); } |
| visitTraversalPredicate_regex(ctx: any): void { this.visitP(ctx, 'TextP', 'regex'); } |
| visitTraversalPredicate_notRegex(ctx: any): void { this.visitP(ctx, 'TextP', 'notRegex'); } |
| |
| visitBooleanArgument(ctx: any): void { |
| if (ctx.booleanLiteral() != null) { |
| this.visitBooleanLiteral(ctx.booleanLiteral()); |
| } else { |
| this.visitVariable(ctx.variable()); |
| } |
| } |
| |
| visitGenericArgument(ctx: any): void { |
| if (ctx.genericLiteral() != null) { |
| this.visitGenericLiteral(ctx.genericLiteral()); |
| } else { |
| this.visitVariable(ctx.variable()); |
| } |
| } |
| |
| visitGenericLiteral(ctx: any): void { |
| this.visitChildren(ctx); |
| } |
| |
| visitIntegerLiteral(ctx: any): void { |
| this.sb.push(ctx.getText().toLowerCase()); |
| } |
| |
| visitFloatLiteral(ctx: any): void { |
| if (ctx.infLiteral() != null) { this.visit(ctx.infLiteral()); return; } |
| if (ctx.nanLiteral() != null) { this.visit(ctx.nanLiteral()); return; } |
| this.sb.push(ctx.getText().toLowerCase()); |
| } |
| |
| visitBooleanLiteral(ctx: any): void { |
| this.sb.push(ctx.getText()); |
| } |
| |
| visitNullLiteral(ctx: any): void { |
| this.sb.push(ctx.getText()); |
| } |
| |
| visitNanLiteral(ctx: any): void { |
| this.sb.push(ctx.getText()); |
| } |
| |
| visitInfLiteral(ctx: any): void { |
| this.sb.push(ctx.getText()); |
| } |
| |
| visitUuidLiteral(ctx: any): void { |
| this.sb.push(ctx.getText()); |
| } |
| |
| visitCharacterLiteral(ctx: any): void { |
| this.sb.push(ctx.getText()); |
| } |
| |
| visitDurationLiteral(ctx: any): void { |
| this.sb.push(ctx.getText()); |
| } |
| |
| visitBinaryLiteral(ctx: any): void { |
| this.sb.push(ctx.getText()); |
| } |
| |
| visitStringLiteral(ctx: any): void { |
| const text = TranslateVisitor.removeFirstAndLastCharacters(ctx.getText()); |
| this.handleStringLiteralText(text); |
| } |
| |
| visitStringNullableLiteral(ctx: any): void { |
| if (ctx.getText() === 'null') { |
| this.sb.push('null'); |
| } else { |
| const text = TranslateVisitor.removeFirstAndLastCharacters(ctx.getText()); |
| this.handleStringLiteralText(text); |
| } |
| } |
| |
| visitNakedKey(ctx: any): void { |
| this.handleStringLiteralText(ctx.getText()); |
| } |
| |
| visitVariable(ctx: any): void { |
| const varName: string = ctx.getText(); |
| this.sb.push(varName); |
| this.parameters.add(varName); |
| } |
| |
| visitKeyword(ctx: any): void { |
| const keyword: string = ctx.getText(); |
| if (ctx.parent?.constructor?.name === 'MapKeyContext' || |
| ctx.parent?.constructor?.name === 'ConfigurationContext') { |
| this.handleStringLiteralText(keyword); |
| } else { |
| this.sb.push(keyword); |
| this.sb.push(' '); |
| } |
| } |
| |
| visitMapKey(ctx: any): void { |
| const keyIndex = (ctx.LPAREN() != null && ctx.RPAREN() != null) ? 1 : 0; |
| this.visit(ctx.getChild(keyIndex)); |
| } |
| |
| visitMapEntry(ctx: any): void { |
| this.visitChildren(ctx); |
| } |
| } |