blob: f061975e2bd7d42496ddd73a3e008736b52fb78f [file]
/*
* 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.
*/
// Context parameters are typed as `any` because the generated grammar files
// require accepting arbitrary context types at runtime.
import TranslateVisitor from './TranslateVisitor.js';
/**
* Converts a Gremlin traversal string into a Groovy source code representation.
* Mirrors the Java GroovyTranslateVisitor.
*/
export default class GroovyTranslateVisitor extends TranslateVisitor {
constructor(graphTraversalSourceName: string = 'g') {
super(graphTraversalSourceName);
}
visitIntegerLiteral(ctx: any): void {
const integerLiteral: string = ctx.getText().toLowerCase();
const lastChar = integerLiteral[integerLiteral.length - 1];
if (!/[a-z]/.test(lastChar)) {
this.sb.push(integerLiteral);
return;
}
const num = integerLiteral.slice(0, -1);
switch (lastChar) {
case 'b':
this.sb.push('(byte)');
this.sb.push(num);
break;
case 's':
this.sb.push('(short)');
this.sb.push(num);
break;
case 'n':
// Groovy BigInteger uses the 'g' suffix
this.sb.push(num);
this.sb.push('g');
break;
default:
// i and l suffixes and anything else kept as-is
this.sb.push(integerLiteral);
}
}
visitFloatLiteral(ctx: any): void {
if (ctx.infLiteral() != null) { this.visit(ctx.infLiteral()); return; }
if (ctx.nanLiteral() != null) { this.visit(ctx.nanLiteral()); return; }
const floatLiteral: string = ctx.getText().toLowerCase();
const lastChar = floatLiteral[floatLiteral.length - 1];
if (!/[a-z]/.test(lastChar)) {
this.sb.push(floatLiteral);
return;
}
const num = floatLiteral.slice(0, -1);
switch (lastChar) {
case 'f':
case 'd':
// keep f/d suffix as-is
this.sb.push(floatLiteral);
break;
case 'm':
// Groovy BigDecimal: if no decimal point use new BigDecimal(NL), else just the number
if (!floatLiteral.includes('.')) {
this.sb.push('new BigDecimal(');
this.sb.push(num);
this.sb.push('L)');
} else {
this.sb.push(num);
}
break;
default:
this.sb.push(floatLiteral);
}
}
visitInfLiteral(ctx: any): void {
if (ctx.SignedInfLiteral() != null && ctx.SignedInfLiteral().getText() === '-Infinity') {
this.sb.push('Double.NEGATIVE_INFINITY');
} else {
this.sb.push('Double.POSITIVE_INFINITY');
}
}
visitUuidLiteral(ctx: any): void {
if (ctx.stringLiteral() == null) {
this.sb.push('UUID.randomUUID()');
return;
}
this.sb.push('UUID.fromString(');
this.sb.push(ctx.stringLiteral().getText());
this.sb.push(')');
}
visitCharacterLiteral(ctx: any): void {
const text: string = ctx.getText();
const withoutSuffix = text.substring(0, text.length - 1);
const inner = TranslateVisitor.removeFirstAndLastCharacters(withoutSuffix);
this.sb.push("'");
this.sb.push(inner);
this.sb.push("' as char");
}
visitDurationLiteral(ctx: any): void {
const seconds = parseInt(ctx.integerLiteral(0).getText(), 10);
const nanos = parseInt(ctx.integerLiteral(1).getText(), 10);
const isPositive = ctx.booleanLiteral() === null ||
ctx.booleanLiteral().getText() === 'true';
this.sb.push(`Duration.ofSeconds(${seconds}, ${nanos})`);
if (!isPositive) {
this.sb.push('.negated()');
}
}
visitBinaryLiteral(ctx: any): void {
this.sb.push('ByteBuffer.wrap(Base64.getDecoder().decode(');
this.sb.push(ctx.stringLiteral().getText());
this.sb.push('))');
}
visitNullLiteral(ctx: any): void {
if (ctx.parent?.constructor?.name === 'GenericMapNullableArgumentContext') {
this.sb.push('null as Map');
} else {
this.sb.push(ctx.getText());
}
}
visitGenericSetLiteral(ctx: any): void {
this.sb.push('[');
const literals: any[] = ctx.genericLiteral();
for (let i = 0; i < literals.length; i++) {
this.visit(literals[i]);
if (i < literals.length - 1) this.sb.push(', ');
}
this.sb.push('] as Set');
}
visitStringLiteral(ctx: any): void {
// Preserve original quote style and escape Groovy GString interpolation
this.sb.push(ctx.getText().replace(/\$/g, () => '\\$'));
}
visitStringNullableLiteral(ctx: any): void {
if (ctx.getText() === 'null') {
this.sb.push('null');
} else {
// Preserve original quote style and escape Groovy GString interpolation
this.sb.push(ctx.getText().replace(/\$/g, () => '\\$'));
}
}
visitMapKey(ctx: any): void {
if (ctx.LPAREN() != null && ctx.RPAREN() != null) {
// Groovy map syntax: expression keys must be parenthesized — preserve the parens
this.sb.push('(');
this.visit(ctx.getChild(1));
this.sb.push(')');
} else {
this.visit(ctx.getChild(0));
}
}
visitNakedKey(ctx: any): void {
// Groovy map/config syntax uses unquoted identifier keys
this.sb.push(ctx.getText());
}
visitKeyword(ctx: any): void {
const keyword: string = ctx.getText();
const parentName = ctx.parent?.constructor?.name ?? '';
if (parentName === 'MapKeyContext' || parentName === 'ConfigurationContext') {
// Groovy uses unquoted keys
this.sb.push(keyword);
} else {
this.sb.push(keyword);
this.sb.push(' ');
}
}
visitTraversalStrategy(ctx: any): void {
if (ctx.getChildCount() === 1) {
// No-arg strategy: output name as-is (no .instance())
this.sb.push(ctx.getText());
} else {
// Ensure 'new' keyword is present
if (ctx.getChild(0).getText() !== 'new') {
this.sb.push('new ');
}
this.visitChildren(ctx);
}
}
visitTraversalSourceSpawnMethod_inject(ctx: any): void {
this.handleInject(ctx);
}
visitTraversalMethod_inject(ctx: any): void {
this.handleInject(ctx);
}
visitTraversalMethod_hasLabel_String_String(ctx: any): void {
if (ctx.getChildCount() > 4 && ctx.getChild(2).getText() === 'null') {
const varArgs = ctx.getChild(4);
this.sb.push(ctx.getChild(0).getText());
this.sb.push('((String) null, ');
for (let i = 0; i < varArgs.getChildCount(); i += 2) {
if (varArgs.getChild(i).getText() === 'null') {
this.sb.push('(String) null');
} else {
this.visit(varArgs.getChild(i));
}
if (i < varArgs.getChildCount() - 1) {
this.sb.push(', ');
}
}
this.sb.push(')');
return;
}
this.visitChildren(ctx);
}
visitTraversalMethod_option_Merge_Map(ctx: any): void {
this.sb.push('option(');
this.visit(ctx.traversalMerge());
this.sb.push(', ');
this.tryCastMapNullableArgument(ctx.genericMapNullableArgument());
this.sb.push(')');
}
visitTraversalMethod_mergeV_Map(ctx: any): void {
this.sb.push('mergeV(');
this.tryCastMapNullableArgument(ctx.genericMapNullableArgument());
this.sb.push(')');
}
visitTraversalSourceSpawnMethod_mergeV_Map(ctx: any): void {
this.sb.push('mergeV(');
this.tryCastMapNullableArgument(ctx.genericMapNullableArgument());
this.sb.push(')');
}
visitTraversalMethod_mergeE_Map(ctx: any): void {
this.sb.push('mergeE(');
this.tryCastMapNullableArgument(ctx.genericMapNullableArgument());
this.sb.push(')');
}
visitTraversalSourceSpawnMethod_mergeE_Map(ctx: any): void {
this.sb.push('mergeE(');
this.tryCastMapNullableArgument(ctx.genericMapNullableArgument());
this.sb.push(')');
}
private tryCastMapNullableArgument(ctx: any): void {
const nullableLit = ctx?.genericMapNullableLiteral?.();
if (nullableLit != null && nullableLit?.nullLiteral?.() != null) {
this.sb.push('(Map) null');
} else {
this.visit(ctx);
}
}
/**
* Special handling for inject with second `null` argument like g.inject(1, null).
* inject() is ambiguous in Groovy with jdk extension inject(Object, Closure).
*/
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;
}
}
}
}
}
this.visitChildren(ctx);
}
}