/*
* Copyright 2021 the original author or authors.
*
* Licensed 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
*
* https://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.
*/
package org.openrewrite.groovy;
import groovy.lang.GroovySystem;
import groovyjarjarasm.asm.Opcodes;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import org.codehaus.groovy.ast.*;
import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.stmt.*;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.stc.StaticTypesMarker;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.openrewrite.Cursor;
import org.openrewrite.ExecutionContext;
import org.openrewrite.FileAttributes;
import org.openrewrite.groovy.marker.*;
import org.openrewrite.groovy.tree.G;
import org.openrewrite.internal.EncodingDetectingInputStream;
import org.openrewrite.internal.ListUtils;
import org.openrewrite.java.internal.JavaTypeCache;
import org.openrewrite.java.marker.ImplicitReturn;
import org.openrewrite.java.marker.Semicolon;
import org.openrewrite.java.tree.*;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.Statement;
import org.openrewrite.marker.Markers;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.openrewrite.Tree.randomId;
import static org.openrewrite.internal.StringUtils.indexOfNextNonWhitespace;
import static org.openrewrite.java.tree.Space.EMPTY;
import static org.openrewrite.java.tree.Space.format;
/**
* See the language syntax reference .
*/
public class GroovyParserVisitor {
private final Path sourcePath;
@Nullable
private final FileAttributes fileAttributes;
private final String source;
private final Charset charset;
private final boolean charsetBomMarked;
private final GroovyTypeMapping typeMapping;
private int cursor = 0;
private static final Pattern whitespacePrefixPattern = Pattern.compile("^\\s*");
@SuppressWarnings("RegExpSimplifiable")
private static final Pattern whitespaceSuffixPattern = Pattern.compile("\\s*[^\\s]+(\\s*)");
/**
* Elements within GString expressions which omit curly braces have column positions which are incorrect.
* The column positions act like there *is* a curly brace.
*/
private int columnOffset;
@Nullable
private static Boolean olderThanGroovy3;
@SuppressWarnings("unused")
public GroovyParserVisitor(Path sourcePath, @Nullable FileAttributes fileAttributes, EncodingDetectingInputStream source, JavaTypeCache typeCache, ExecutionContext ctx) {
this.sourcePath = sourcePath;
this.fileAttributes = fileAttributes;
this.source = source.readFully();
this.charset = source.getCharset();
this.charsetBomMarked = source.isCharsetBomMarked();
this.typeMapping = new GroovyTypeMapping(typeCache);
}
private static boolean isOlderThanGroovy3() {
if (olderThanGroovy3 == null) {
String groovyVersionText = GroovySystem.getVersion();
int majorVersion = Integer.parseInt(groovyVersionText.substring(0, groovyVersionText.indexOf('.')));
olderThanGroovy3 = majorVersion < 3;
}
return olderThanGroovy3;
}
public G.CompilationUnit visit(SourceUnit unit, ModuleNode ast) throws GroovyParsingException {
NavigableMap> sortedByPosition = new TreeMap<>();
for (org.codehaus.groovy.ast.stmt.Statement s : ast.getStatementBlock().getStatements()) {
if (!isSynthetic(s)) {
sortedByPosition.computeIfAbsent(pos(s), i -> new ArrayList<>()).add(s);
}
}
String shebang = null;
if (source.startsWith("#!")) {
int i = 0;
while (i < source.length() && source.charAt(i) != '\n' && source.charAt(i) != '\r') {
i++;
}
shebang = source.substring(0, i);
cursor += i;
}
Space prefix = EMPTY;
JRightPadded pkg = null;
if (ast.getPackage() != null) {
prefix = whitespace();
cursor += "package".length();
pkg = JRightPadded.build(new J.Package(randomId(), EMPTY, Markers.EMPTY,
typeTree(null), emptyList()));
}
for (ImportNode anImport : ast.getImports()) {
sortedByPosition.computeIfAbsent(pos(anImport), i -> new ArrayList<>()).add(anImport);
}
for (ImportNode anImport : ast.getStarImports()) {
sortedByPosition.computeIfAbsent(pos(anImport), i -> new ArrayList<>()).add(anImport);
}
for (ImportNode anImport : ast.getStaticImports().values()) {
sortedByPosition.computeIfAbsent(pos(anImport), i -> new ArrayList<>()).add(anImport);
}
for (ImportNode anImport : ast.getStaticStarImports().values()) {
sortedByPosition.computeIfAbsent(pos(anImport), i -> new ArrayList<>()).add(anImport);
}
for (ClassNode aClass : ast.getClasses()) {
if (aClass.getSuperClass() == null
|| !("groovy.lang.Script".equals(aClass.getSuperClass().getName())
|| "RewriteGradleProject".equals(aClass.getSuperClass().getName())
|| "RewriteSettings".equals(aClass.getSuperClass().getName()))) {
sortedByPosition.computeIfAbsent(pos(aClass), i -> new ArrayList<>()).add(aClass);
}
}
for (MethodNode method : ast.getMethods()) {
sortedByPosition.computeIfAbsent(pos(method), i -> new ArrayList<>()).add(method);
}
List> statements = new ArrayList<>(sortedByPosition.size());
for (Map.Entry> entry : sortedByPosition.entrySet()) {
if (entry.getKey().getLine() == -1) {
// default import
continue;
}
try {
for (ASTNode value : entry.getValue()) {
if (value instanceof InnerClassNode) {
// Inner classes will be visited as part of visiting their containing class
continue;
}
JRightPadded statement = convertTopLevelStatement(unit, value);
if (statements.isEmpty() && pkg == null && statement.getElement() instanceof J.Import) {
prefix = statement.getElement().getPrefix();
statement = statement.withElement(statement.getElement().withPrefix(EMPTY));
}
statements.add(statement);
}
} catch (Throwable t) {
if (t instanceof StringIndexOutOfBoundsException) {
throw new GroovyParsingException("Failed to parse " + sourcePath + ", cursor position likely inaccurate.", t);
}
throw new GroovyParsingException(
"Failed to parse " + sourcePath + " at cursor position " + cursor +
". The next 10 characters in the original source are `" +
source.substring(cursor, Math.min(source.length(), cursor + 10)) + "`", t);
}
}
return new G.CompilationUnit(
randomId(),
shebang,
prefix,
Markers.EMPTY,
sourcePath,
fileAttributes,
charset.name(),
charsetBomMarked,
null,
pkg,
statements,
format(source.substring(cursor))
);
}
@RequiredArgsConstructor
private class RewriteGroovyClassVisitor extends ClassCodeVisitorSupport {
@Getter
private final SourceUnit sourceUnit;
private final Queue queue = new LinkedList<>();
@Override
public void visitClass(ClassNode clazz) {
Space fmt = whitespace();
List leadingAnnotations;
if (clazz.getAnnotations().isEmpty()) {
leadingAnnotations = emptyList();
} else {
leadingAnnotations = new ArrayList<>(clazz.getAnnotations().size());
for (AnnotationNode annotation : clazz.getAnnotations()) {
visitAnnotation(annotation);
leadingAnnotations.add(pollQueue());
}
}
List modifiers = visitModifiers(clazz.getModifiers());
Space kindPrefix = whitespace();
J.ClassDeclaration.Kind.Type kindType = null;
if (source.startsWith("class", cursor)) {
kindType = J.ClassDeclaration.Kind.Type.Class;
cursor += "class".length();
} else if (source.startsWith("interface", cursor)) {
kindType = J.ClassDeclaration.Kind.Type.Interface;
cursor += "interface".length();
} else if (source.startsWith("@interface", cursor)) {
kindType = J.ClassDeclaration.Kind.Type.Annotation;
cursor += "@interface".length();
} else if (source.startsWith("enum", cursor)) {
kindType = J.ClassDeclaration.Kind.Type.Enum;
cursor += "enum".length();
}
assert kindType != null;
J.ClassDeclaration.Kind kind = new J.ClassDeclaration.Kind(randomId(), kindPrefix, Markers.EMPTY, emptyList(), kindType);
Space namePrefix = whitespace();
String simpleName = name();
J.Identifier name = new J.Identifier(randomId(), namePrefix, Markers.EMPTY, emptyList(), simpleName, typeMapping.type(clazz), null);
JContainer typeParameterContainer = null;
if (clazz.isUsingGenerics() && clazz.getGenericsTypes() != null) {
typeParameterContainer = visitTypeParameters(clazz.getGenericsTypes());
}
JLeftPadded extendings = null;
if (clazz.getSuperClass().getLineNumber() >= 0) {
extendings = padLeft(sourceBefore("extends"), visitTypeTree(clazz.getSuperClass()));
}
JContainer implementings = null;
if (clazz.getInterfaces().length > 0) {
Space implPrefix;
if (kindType == J.ClassDeclaration.Kind.Type.Interface || kindType == J.ClassDeclaration.Kind.Type.Annotation) {
implPrefix = sourceBefore("extends");
} else {
implPrefix = sourceBefore("implements");
}
List> implTypes = new ArrayList<>(clazz.getInterfaces().length);
ClassNode[] interfaces = clazz.getInterfaces();
for (int i = 0; i < interfaces.length; i++) {
ClassNode anInterface = interfaces[i];
// Any annotation @interface is listed as extending java.lang.annotation.Annotation, although it doesn't appear in source
if (kindType == J.ClassDeclaration.Kind.Type.Annotation && "java.lang.annotation.Annotation".equals(anInterface.getName())) {
continue;
}
implTypes.add(JRightPadded.build(visitTypeTree(anInterface))
.withAfter(i == interfaces.length - 1 ? EMPTY : sourceBefore(",")));
}
// Can be empty for an annotation @interface which only implements Annotation
if (!implTypes.isEmpty()) {
implementings = JContainer.build(implPrefix, implTypes, Markers.EMPTY);
}
}
queue.add(new J.ClassDeclaration(randomId(), fmt, Markers.EMPTY,
leadingAnnotations,
modifiers,
kind,
name,
typeParameterContainer,
null,
extendings,
implementings,
null,
visitClassBlock(clazz),
TypeUtils.asFullyQualified(typeMapping.type(clazz))));
}
J.Block visitClassBlock(ClassNode clazz) {
NavigableMap> sortedByPosition = new TreeMap<>();
for (MethodNode method : clazz.getMethods()) {
if (method.isSynthetic()) {
continue;
}
sortedByPosition.computeIfAbsent(pos(method), i -> new ArrayList<>()).add(method);
}
/*
In certain circumstances the same AST node may appear in multiple places.
class A {
def a = new Object() {
// this anonymous class is both part of the initializing expression for the variable "a"
// And appears in the list of inner classes of "A"
}
}
So keep track of inner classes that are part of field initializers so that they don't get parsed twice
*/
Set fieldInitializers = new HashSet<>();
for (FieldNode field : clazz.getFields()) {
if (!appearsInSource(field)) {
continue;
}
if (field.hasInitialExpression() && field.getInitialExpression() instanceof ConstructorCallExpression) {
ConstructorCallExpression cce = (ConstructorCallExpression) field.getInitialExpression();
if (cce.isUsingAnonymousInnerClass() && cce.getType() instanceof InnerClassNode) {
fieldInitializers.add((InnerClassNode) cce.getType());
}
}
sortedByPosition.computeIfAbsent(pos(field), i -> new ArrayList<>()).add(field);
}
Iterator innerClassIterator = clazz.getInnerClasses();
while (innerClassIterator.hasNext()) {
InnerClassNode icn = innerClassIterator.next();
if (icn.isSynthetic() || fieldInitializers.contains(icn)) {
continue;
}
sortedByPosition.computeIfAbsent(pos(icn), i -> new ArrayList<>()).add(icn);
}
return new J.Block(randomId(), sourceBefore("{"), Markers.EMPTY,
JRightPadded.build(false),
sortedByPosition.values().stream()
.flatMap(asts -> asts.stream()
.map(ast -> {
if (ast instanceof FieldNode) {
visitField((FieldNode) ast);
} else if (ast instanceof MethodNode) {
visitMethod((MethodNode) ast);
} else if (ast instanceof ClassNode) {
visitClass((ClassNode) ast);
}
Statement stat = pollQueue();
return maybeSemicolon(stat);
}))
.collect(Collectors.toList()),
sourceBefore("}"));
}
@Override
public void visitField(FieldNode field) {
if ((field.getModifiers() & Opcodes.ACC_ENUM) != 0) {
visitEnumField(field);
} else {
visitVariableField(field);
}
}
private void visitEnumField(@SuppressWarnings("unused") FieldNode fieldNode) {
// Requires refactoring visitClass to use a similar pattern as Java11ParserVisitor.
// Currently, each field is visited one at a time, so we cannot construct the EnumValueSet.
throw new UnsupportedOperationException("enum fields are not implemented.");
}
private void visitVariableField(FieldNode field) {
RewriteGroovyVisitor visitor = new RewriteGroovyVisitor(field, this);
List annotations = field.getAnnotations().stream()
.map(a -> {
visitAnnotation(a);
return (J.Annotation) pollQueue();
})
.collect(Collectors.toList());
List modifiers = visitModifiers(field.getModifiers());
TypeTree typeExpr = visitTypeTree(field.getOriginType());
J.Identifier name = new J.Identifier(randomId(), sourceBefore(field.getName()), Markers.EMPTY,
emptyList(), field.getName(), typeMapping.type(field.getOriginType()), typeMapping.variableType(field));
J.VariableDeclarations.NamedVariable namedVariable = new J.VariableDeclarations.NamedVariable(
randomId(),
name.getPrefix(),
Markers.EMPTY,
name.withPrefix(EMPTY),
emptyList(),
null,
typeMapping.variableType(field)
);
if (field.getInitialExpression() != null) {
Space beforeAssign = sourceBefore("=");
Expression initializer = visitor.visit(field.getInitialExpression());
namedVariable = namedVariable.getPadding().withInitializer(padLeft(beforeAssign, initializer));
}
J.VariableDeclarations variableDeclarations = new J.VariableDeclarations(
randomId(),
EMPTY,
Markers.EMPTY,
annotations,
modifiers,
typeExpr,
null,
emptyList(),
singletonList(JRightPadded.build(namedVariable))
);
queue.add(variableDeclarations);
}
@Override
protected void visitAnnotation(AnnotationNode annotation) {
RewriteGroovyVisitor bodyVisitor = new RewriteGroovyVisitor(annotation, this);
String lastArgKey = annotation.getMembers().keySet().stream().reduce("", (k1, k2) -> k2);
Space prefix = sourceBefore("@");
NameTree annotationType = visitTypeTree(annotation.getClassNode());
JContainer arguments = null;
if (!annotation.getMembers().isEmpty()) {
// This doesn't handle the case where an annotation has empty arguments like @Foo(), but that is rare
arguments = JContainer.build(
sourceBefore("("),
annotation.getMembers().entrySet().stream()
.map(arg -> {
Space argPrefix;
if ("value".equals(arg.getKey())) {
// Determine whether the value is implicit or explicit
int saveCursor = cursor;
argPrefix = whitespace();
if (!source.startsWith("value", cursor)) {
return new JRightPadded(
((Expression) bodyVisitor.visit(arg.getValue())).withPrefix(argPrefix),
arg.getKey().equals(lastArgKey) ? sourceBefore(")") : sourceBefore(","),
Markers.EMPTY);
}
cursor = saveCursor;
}
argPrefix = sourceBefore(arg.getKey());
J.Identifier argName = new J.Identifier(randomId(), EMPTY, Markers.EMPTY, emptyList(), arg.getKey(), null, null);
J.Assignment assign = new J.Assignment(randomId(), argPrefix, Markers.EMPTY,
argName, padLeft(sourceBefore("="), bodyVisitor.visit(arg.getValue())),
null);
return JRightPadded.build((Expression) assign)
.withAfter(arg.getKey().equals(lastArgKey) ? sourceBefore(")") : sourceBefore(","));
})
.collect(Collectors.toList()),
Markers.EMPTY
);
}
queue.add(new J.Annotation(randomId(), prefix, Markers.EMPTY, annotationType, arguments));
}
@Override
public void visitMethod(MethodNode method) {
Space fmt = whitespace();
List annotations = method.getAnnotations().stream()
.map(a -> {
visitAnnotation(a);
return (J.Annotation) pollQueue();
})
.collect(Collectors.toList());
List modifiers = visitModifiers(method.getModifiers());
TypeTree returnType = visitTypeTree(method.getReturnType());
// Method name might be in quotes
Space namePrefix = whitespace();
String methodName;
if (source.startsWith(method.getName(), cursor)) {
methodName = method.getName();
} else {
char openingQuote = source.charAt(cursor);
methodName = openingQuote + method.getName() + openingQuote;
}
cursor += methodName.length();
J.Identifier name = new J.Identifier(randomId(),
namePrefix,
Markers.EMPTY,
emptyList(),
methodName,
null, null);
RewriteGroovyVisitor bodyVisitor = new RewriteGroovyVisitor(method, this);
// Parameter has no visit implementation, so we've got to do this by hand
Space beforeParen = sourceBefore("(");
List> params = new ArrayList<>(method.getParameters().length);
Parameter[] unparsedParams = method.getParameters();
for (int i = 0; i < unparsedParams.length; i++) {
Parameter param = unparsedParams[i];
List paramAnnotations = param.getAnnotations().stream()
.map(a -> {
visitAnnotation(a);
return (J.Annotation) pollQueue();
})
.collect(Collectors.toList());
TypeTree paramType;
if (param.isDynamicTyped()) {
paramType = new J.Identifier(randomId(), EMPTY, Markers.EMPTY, emptyList(), "", JavaType.ShallowClass.build("java.lang.Object"), null);
} else {
paramType = visitTypeTree(param.getOriginType());
}
JRightPadded paramName = JRightPadded.build(
new J.VariableDeclarations.NamedVariable(randomId(), EMPTY, Markers.EMPTY,
new J.Identifier(randomId(), whitespace(), Markers.EMPTY, emptyList(), param.getName(), null, null),
emptyList(), null, null)
);
cursor += param.getName().length();
org.codehaus.groovy.ast.expr.Expression defaultValue = param.getInitialExpression();
if (defaultValue != null) {
paramName = paramName.withElement(paramName.getElement().getPadding()
.withInitializer(new JLeftPadded<>(
sourceBefore("="),
new RewriteGroovyVisitor(defaultValue, this).visit(defaultValue),
Markers.EMPTY)));
}
Space rightPad = sourceBefore(i == unparsedParams.length - 1 ? ")" : ",");
params.add(JRightPadded.build((Statement) new J.VariableDeclarations(randomId(), EMPTY,
Markers.EMPTY, paramAnnotations, emptyList(), paramType,
null, emptyList(),
singletonList(paramName))).withAfter(rightPad));
}
if (unparsedParams.length == 0) {
params.add(JRightPadded.build(new J.Empty(randomId(), sourceBefore(")"), Markers.EMPTY)));
}
JContainer throws_ = method.getExceptions().length == 0 ? null : JContainer.build(
sourceBefore("throws"),
bodyVisitor.visitRightPadded(method.getExceptions(), null),
Markers.EMPTY
);
J.Block body = method.getCode() == null ? null :
bodyVisitor.visit(method.getCode());
queue.add(new J.MethodDeclaration(
randomId(), fmt, Markers.EMPTY,
annotations,
modifiers,
null,
returnType,
new J.MethodDeclaration.IdentifierWithAnnotations(name, emptyList()),
JContainer.build(beforeParen, params, Markers.EMPTY),
throws_,
body,
null,
typeMapping.methodType(method)
));
}
@SuppressWarnings({"ConstantConditions", "unchecked"})
private T pollQueue() {
return (T) queue.poll();
}
}
private class RewriteGroovyVisitor extends CodeVisitorSupport {
private Cursor nodeCursor;
private final Queue queue = new LinkedList<>();
private final RewriteGroovyClassVisitor classVisitor;
public RewriteGroovyVisitor(ASTNode root, RewriteGroovyClassVisitor classVisitor) {
this.nodeCursor = new Cursor(null, root);
this.classVisitor = classVisitor;
}
private T visit(ASTNode node) {
nodeCursor = new Cursor(nodeCursor, node);
node.visit(this);
nodeCursor = nodeCursor.getParentOrThrow();
return pollQueue();
}
private List> visitRightPadded(ASTNode[] nodes, @Nullable String afterLast) {
List> ts = new ArrayList<>(nodes.length);
for (int i = 0; i < nodes.length; i++) {
ASTNode node = nodes[i];
@SuppressWarnings("unchecked") JRightPadded converted = JRightPadded.build(
node instanceof ClassNode ? (T) visitTypeTree((ClassNode) node) : visit(node));
if (i == nodes.length - 1) {
converted = converted.withAfter(whitespace());
if (',' == source.charAt(cursor)) {
// In Groovy trailing "," are allowed
cursor += 1;
converted = converted.withMarkers(Markers.EMPTY.add(new org.openrewrite.java.marker.TrailingComma(randomId(), whitespace())));
}
ts.add(converted);
if (afterLast != null && source.startsWith(afterLast, cursor)) {
cursor += afterLast.length();
}
} else {
ts.add(converted.withAfter(sourceBefore(",")));
}
}
return ts;
}
private Expression insideParentheses(ASTNode node, Function parenthesizedTree) {
Integer insideParenthesesLevel = getInsideParenthesesLevel(node);
if (insideParenthesesLevel != null) {
Stack openingParens = new Stack<>();
for (int i = 0; i < insideParenthesesLevel; i++) {
openingParens.push(sourceBefore("("));
}
Expression parenthesized = parenthesizedTree.apply(whitespace());
for (int i = 0; i < insideParenthesesLevel; i++) {
parenthesized = new J.Parentheses<>(randomId(), openingParens.pop(), Markers.EMPTY,
padRight(parenthesized, sourceBefore(")")));
}
return parenthesized;
}
return parenthesizedTree.apply(whitespace());
}
private Statement labeled(org.codehaus.groovy.ast.stmt.Statement statement, Supplier labeledTree) {
List labels = null;
if (statement.getStatementLabels() != null && !statement.getStatementLabels().isEmpty()) {
labels = new ArrayList<>(statement.getStatementLabels().size());
// Labels appear in statement.getStatementLabels() in reverse order of their appearance in source code
// Could iterate over those in reverse order, but feels safer to just take the count and go off source code alone
for (int i = 0; i < statement.getStatementLabels().size(); i++) {
labels.add(new J.Label(randomId(), whitespace(), Markers.EMPTY, JRightPadded.build(
new J.Identifier(randomId(), EMPTY, Markers.EMPTY, emptyList(), name(), null, null)).withAfter(sourceBefore(":")),
new J.Empty(randomId(), EMPTY, Markers.EMPTY)));
}
}
Statement s = labeledTree.get();
if (labels != null) {
//noinspection ConstantConditions
return condenseLabels(labels, s);
}
return s;
}
@Override
public void visitArgumentlistExpression(ArgumentListExpression expression) {
List> args = new ArrayList<>(expression.getExpressions().size());
int saveCursor = cursor;
Space beforeOpenParen = whitespace();
org.openrewrite.java.marker.OmitParentheses omitParentheses = null;
if (source.charAt(cursor) == '(') {
cursor++;
} else {
omitParentheses = new org.openrewrite.java.marker.OmitParentheses(randomId());
beforeOpenParen = EMPTY;
cursor = saveCursor;
}
List unparsedArgs = expression.getExpressions().stream()
.filter(GroovyParserVisitor::appearsInSource)
.collect(Collectors.toList());
// If the first parameter to a function is a Map, then groovy allows "named parameters" style invocations, see:
// https://docs.groovy-lang.org/latest/html/documentation/#_named_parameters_2
// When named parameters are in use they may appear before, after, or intermixed with any positional arguments
if (unparsedArgs.size() > 1 && unparsedArgs.get(0) instanceof MapExpression
&& (unparsedArgs.get(0).getLastLineNumber() > unparsedArgs.get(1).getLastLineNumber()
|| (unparsedArgs.get(0).getLastLineNumber() == unparsedArgs.get(1).getLastLineNumber()
&& unparsedArgs.get(0).getLastColumnNumber() > unparsedArgs.get(1).getLastColumnNumber()))) {
// Figure out the source-code ordering of the expressions
MapExpression namedArgExpressions = (MapExpression) unparsedArgs.get(0);
unparsedArgs =
Stream.concat(
namedArgExpressions.getMapEntryExpressions().stream(),
unparsedArgs.subList(1, unparsedArgs.size()).stream())
.sorted(Comparator.comparing(ASTNode::getLastLineNumber)
.thenComparing(ASTNode::getLastColumnNumber))
.collect(Collectors.toList());
} else if (!unparsedArgs.isEmpty() && unparsedArgs.get(0) instanceof MapExpression) {
// The map literal may or may not be wrapped in "[]"
// If it is wrapped in "[]" then this isn't a named arguments situation, and we should not lift the parameters out of the enclosing MapExpression
saveCursor = cursor;
whitespace();
boolean isOpeningBracketPresent = '[' == source.charAt(cursor);
cursor = saveCursor;
if (!isOpeningBracketPresent) {
// Bring named parameters out of their containing MapExpression so that they can be parsed correctly
MapExpression namedArgExpressions = (MapExpression) unparsedArgs.get(0);
unparsedArgs =
Stream.concat(
namedArgExpressions.getMapEntryExpressions().stream(),
unparsedArgs.subList(1, unparsedArgs.size()).stream())
.collect(Collectors.toList());
}
}
if (unparsedArgs.isEmpty()) {
args.add(JRightPadded.build((Expression) new J.Empty(randomId(), whitespace(), Markers.EMPTY))
.withAfter(omitParentheses == null ? sourceBefore(")") : EMPTY));
} else {
for (int i = 0; i < unparsedArgs.size(); i++) {
org.codehaus.groovy.ast.expr.Expression rawArg = unparsedArgs.get(i);
Expression arg = visit(rawArg);
if (omitParentheses != null) {
arg = arg.withMarkers(arg.getMarkers().add(omitParentheses));
}
Space after = EMPTY;
if (i == unparsedArgs.size() - 1) {
if (omitParentheses == null) {
after = sourceBefore(")");
}
} else {
after = whitespace();
if (source.charAt(cursor) == ')') {
// the next argument will have an OmitParentheses marker
omitParentheses = new org.openrewrite.java.marker.OmitParentheses(randomId());
}
cursor++;
}
args.add(JRightPadded.build(arg).withAfter(after));
}
}
queue.add(JContainer.build(beforeOpenParen, args, Markers.EMPTY));
}
@Override
public void visitClassExpression(ClassExpression clazz) {
String unresolvedName = clazz.getType().getUnresolvedName().replace('$', '.');
queue.add(TypeTree.build(unresolvedName)
.withType(typeMapping.type(clazz.getType()))
.withPrefix(sourceBefore(unresolvedName)));
}
@Override
public void visitAssertStatement(AssertStatement statement) {
Space prefix = whitespace();
skip("assert");
Expression condition = visit(statement.getBooleanExpression());
JLeftPadded message = null;
if (!(statement.getMessageExpression() instanceof ConstantExpression) || !((ConstantExpression) statement.getMessageExpression()).isNullExpression()) {
Space messagePrefix = whitespace();
skip(":");
message = padLeft(messagePrefix, visit(statement.getMessageExpression()));
}
queue.add(new J.Assert(randomId(), prefix, Markers.EMPTY, condition, message));
}
@Override
public void visitBinaryExpression(BinaryExpression binary) {
queue.add(insideParentheses(binary, fmt -> {
Expression left = visit(binary.getLeftExpression());
Space opPrefix = whitespace();
boolean assignment = false;
boolean instanceOf = false;
J.AssignmentOperation.Type assignOp = null;
J.Binary.Type binaryOp = null;
G.Binary.Type gBinaryOp = null;
switch (binary.getOperation().getText()) {
case "+":
binaryOp = J.Binary.Type.Addition;
break;
case "&&":
binaryOp = J.Binary.Type.And;
break;
case "&":
binaryOp = J.Binary.Type.BitAnd;
break;
case "|":
binaryOp = J.Binary.Type.BitOr;
break;
case "^":
binaryOp = J.Binary.Type.BitXor;
break;
case "/":
binaryOp = J.Binary.Type.Division;
break;
case "==":
binaryOp = J.Binary.Type.Equal;
break;
case ">":
binaryOp = J.Binary.Type.GreaterThan;
break;
case ">=":
binaryOp = J.Binary.Type.GreaterThanOrEqual;
break;
case "<<":
binaryOp = J.Binary.Type.LeftShift;
break;
case "<":
binaryOp = J.Binary.Type.LessThan;
break;
case "<=":
binaryOp = J.Binary.Type.LessThanOrEqual;
break;
case "%":
binaryOp = J.Binary.Type.Modulo;
break;
case "*":
binaryOp = J.Binary.Type.Multiplication;
break;
case "!=":
binaryOp = J.Binary.Type.NotEqual;
break;
case "||":
binaryOp = J.Binary.Type.Or;
break;
case ">>":
binaryOp = J.Binary.Type.RightShift;
break;
case "-":
binaryOp = J.Binary.Type.Subtraction;
break;
case ">>>":
binaryOp = J.Binary.Type.UnsignedRightShift;
break;
case "instanceof":
instanceOf = true;
break;
case "=":
assignment = true;
break;
case "+=":
assignOp = J.AssignmentOperation.Type.Addition;
break;
case "-=":
assignOp = J.AssignmentOperation.Type.Subtraction;
break;
case "&=":
assignOp = J.AssignmentOperation.Type.BitAnd;
break;
case "|=":
assignOp = J.AssignmentOperation.Type.BitOr;
break;
case "^=":
assignOp = J.AssignmentOperation.Type.BitXor;
break;
case "/=":
assignOp = J.AssignmentOperation.Type.Division;
break;
case "<<=":
assignOp = J.AssignmentOperation.Type.LeftShift;
break;
case "%=":
assignOp = J.AssignmentOperation.Type.Modulo;
break;
case "*=":
assignOp = J.AssignmentOperation.Type.Multiplication;
break;
case ">>=":
assignOp = J.AssignmentOperation.Type.RightShift;
break;
case ">>>=":
assignOp = J.AssignmentOperation.Type.UnsignedRightShift;
break;
case "=~":
gBinaryOp = G.Binary.Type.Find;
break;
case "==~":
gBinaryOp = G.Binary.Type.Match;
break;
case "[":
gBinaryOp = G.Binary.Type.Access;
break;
case "in":
gBinaryOp = G.Binary.Type.In;
break;
}
cursor += binary.getOperation().getText().length();
Expression right = visit(binary.getRightExpression());
if (assignment) {
return new J.Assignment(randomId(), fmt, Markers.EMPTY,
left, JLeftPadded.build(right).withBefore(opPrefix),
typeMapping.type(binary.getType()));
} else if (instanceOf) {
return new J.InstanceOf(randomId(), fmt, Markers.EMPTY,
JRightPadded.build(left).withAfter(opPrefix), right, null,
typeMapping.type(binary.getType()));
} else if (assignOp != null) {
return new J.AssignmentOperation(randomId(), fmt, Markers.EMPTY,
left, JLeftPadded.build(assignOp).withBefore(opPrefix),
right, typeMapping.type(binary.getType()));
} else if (binaryOp != null) {
return new J.Binary(randomId(), fmt, Markers.EMPTY,
left, JLeftPadded.build(binaryOp).withBefore(opPrefix),
right, typeMapping.type(binary.getType()));
} else if (gBinaryOp != null) {
Space after = EMPTY;
if (gBinaryOp == G.Binary.Type.Access) {
after = sourceBefore("]");
}
return new G.Binary(randomId(), fmt, Markers.EMPTY,
left, JLeftPadded.build(gBinaryOp).withBefore(opPrefix),
right, after, typeMapping.type(binary.getType()));
}
throw new IllegalStateException("Unknown binary expression " + binary.getClass().getSimpleName());
}));
}
@Override
public void visitBlockStatement(BlockStatement block) {
Space fmt = EMPTY;
Object parent = nodeCursor.getParentOrThrow().getValue();
if (!(parent instanceof ClosureExpression)) {
fmt = sourceBefore("{");
}
List> statements = new ArrayList<>(block.getStatements().size());
List blockStatements = block.getStatements();
for (int i = 0; i < blockStatements.size(); i++) {
ASTNode statement = blockStatements.get(i);
J expr = visit(statement);
if (i == blockStatements.size() - 1 && (expr instanceof Expression)) {
if (parent instanceof ClosureExpression || (parent instanceof MethodNode &&
!JavaType.Primitive.Void.equals(typeMapping.type(((MethodNode) parent).getReturnType())))) {
expr = new J.Return(randomId(), expr.getPrefix(), Markers.EMPTY,
expr.withPrefix(EMPTY));
expr = expr.withMarkers(expr.getMarkers().add(new ImplicitReturn(randomId())));
}
}
JRightPadded stat = JRightPadded.build((Statement) expr);
int saveCursor = cursor;
Space beforeSemicolon = whitespace();
if (cursor < source.length() && source.charAt(cursor) == ';') {
stat = stat
.withMarkers(stat.getMarkers().add(new Semicolon(randomId())))
.withAfter(beforeSemicolon);
cursor++;
} else {
cursor = saveCursor;
}
statements.add(stat);
}
Space beforeBrace = whitespace();
queue.add(new J.Block(randomId(), fmt, Markers.EMPTY, JRightPadded.build(false), statements, beforeBrace));
if (!(parent instanceof ClosureExpression)) {
sourceBefore("}");
}
}
@Override
public void visitCatchStatement(CatchStatement node) {
Space prefix = sourceBefore("catch");
Space parenPrefix = sourceBefore("(");
// This does not handle multi-catch statements like catch(ExceptionTypeA | ExceptionTypeB e)
// The Groovy AST seems to only record the first type in the list, so some extra hacking is required to get the others
Parameter param = node.getVariable();
TypeTree paramType;
Space paramPrefix = whitespace();
// Groovy allows catch variables to omit their type, shorthand for being of type java.lang.Exception
// Can't use isSynthetic() here because groovy doesn't record the line number on the Parameter
if ("java.lang.Exception".equals(param.getType().getName())
&& !source.startsWith("Exception", cursor)
&& !source.startsWith("java.lang.Exception", cursor)) {
paramType = new J.Identifier(randomId(), paramPrefix, Markers.EMPTY, emptyList(), "",
JavaType.ShallowClass.build("java.lang.Exception"), null);
} else {
paramType = visitTypeTree(param.getOriginType()).withPrefix(paramPrefix);
}
JRightPadded paramName = JRightPadded.build(
new J.VariableDeclarations.NamedVariable(randomId(), whitespace(), Markers.EMPTY,
new J.Identifier(randomId(), EMPTY, Markers.EMPTY, emptyList(), param.getName(), null, null),
emptyList(), null, null)
);
cursor += param.getName().length();
Space rightPad = whitespace();
cursor += 1; // skip )
JRightPadded variable = JRightPadded.build(new J.VariableDeclarations(randomId(), paramType.getPrefix(),
Markers.EMPTY, emptyList(), emptyList(), paramType.withPrefix(EMPTY),
null, emptyList(),
singletonList(paramName))
).withAfter(rightPad);
J.ControlParentheses catchControl = new J.ControlParentheses<>(randomId(), parenPrefix, Markers.EMPTY, variable);
queue.add(new J.Try.Catch(randomId(), prefix, Markers.EMPTY, catchControl, visit(node.getCode())));
}
@Override
public void visitBreakStatement(BreakStatement statement) {
queue.add(new J.Break(randomId(),
sourceBefore("break"),
Markers.EMPTY,
(statement.getLabel() == null) ?
null :
new J.Identifier(randomId(),
sourceBefore(statement.getLabel()),
Markers.EMPTY, emptyList(), statement.getLabel(), null, null))
);
}
@Override
public void visitCaseStatement(CaseStatement statement) {
queue.add(new J.Case(randomId(),
sourceBefore("case"),
Markers.EMPTY,
J.Case.Type.Statement,
null,
JContainer.build(singletonList(JRightPadded.build(visit(statement.getExpression())))),
statement.getCode() instanceof EmptyStatement
? JContainer.build(sourceBefore(":"), convertStatements(emptyList()), Markers.EMPTY)
: JContainer.build(sourceBefore(":"), convertStatements(((BlockStatement) statement.getCode()).getStatements()), Markers.EMPTY)
, null)
);
}
private J.Case visitDefaultCaseStatement(BlockStatement statement) {
return new J.Case(randomId(),
sourceBefore("default"),
Markers.EMPTY,
J.Case.Type.Statement,
null,
JContainer.build(singletonList(JRightPadded.build(new J.Identifier(randomId(), Space.EMPTY, Markers.EMPTY, emptyList(), skip("default"), null, null)))),
JContainer.build(sourceBefore(":"),
convertStatements(statement.getStatements()), Markers.EMPTY),
null
);
}
@Override
public void visitCastExpression(CastExpression cast) {
queue.add(insideParentheses(cast, prefix -> {
// Might be looking at a Java-style cast "(type)object" or a groovy-style cast "object as type"
if (source.charAt(cursor) == '(') {
cursor++; // skip '('
return new J.TypeCast(randomId(), prefix, Markers.EMPTY,
new J.ControlParentheses<>(randomId(), EMPTY, Markers.EMPTY,
new JRightPadded<>(visitTypeTree(cast.getType()), sourceBefore(")"), Markers.EMPTY)
),
visit(cast.getExpression()));
} else {
Expression expr = visit(cast.getExpression());
Space asPrefix = sourceBefore("as");
return new J.TypeCast(randomId(), prefix, new Markers(randomId(), singletonList(new AsStyleTypeCast(randomId()))),
new J.ControlParentheses<>(randomId(), EMPTY, Markers.EMPTY,
new JRightPadded<>(visitTypeTree(cast.getType()), asPrefix, Markers.EMPTY)),
expr);
}
}));
}
@Override
public void visitClosureExpression(ClosureExpression expression) {
Space prefix = whitespace();
LambdaStyle ls = new LambdaStyle(randomId(), expression instanceof LambdaExpression, true);
boolean parenthesized = false;
if (source.charAt(cursor) == '(') {
parenthesized = true;
cursor += 1; // skip '('
} else if (source.charAt(cursor) == '{') {
cursor += 1; // skip '{'
}
JavaType closureType = typeMapping.type(staticType(expression));
List> paramExprs;
if (expression.getParameters() != null && expression.getParameters().length > 0) {
paramExprs = new ArrayList<>(expression.getParameters().length);
Parameter[] parameters = expression.getParameters();
for (int i = 0; i < parameters.length; i++) {
Parameter p = parameters[i];
JavaType type = typeMapping.type(staticType(p));
J expr = new J.VariableDeclarations(randomId(), whitespace(), Markers.EMPTY,
emptyList(), emptyList(), p.isDynamicTyped() ? null : visitTypeTree(p.getType()),
null, emptyList(),
singletonList(
JRightPadded.build(
new J.VariableDeclarations.NamedVariable(randomId(), sourceBefore(p.getName()), Markers.EMPTY,
new J.Identifier(randomId(), EMPTY, Markers.EMPTY, emptyList(), p.getName(), type, null),
emptyList(), null,
typeMapping.variableType(p.getName(), staticType(p)))
)
));
JRightPadded param = JRightPadded.build(expr);
if (i != parameters.length - 1) {
param = param.withAfter(sourceBefore(","));
} else {
param = param.withAfter(whitespace());
}
paramExprs.add(param);
}
} else {
Space argPrefix = EMPTY;
if (parenthesized) {
argPrefix = whitespace();
}
paramExprs = singletonList(JRightPadded.build(new J.Empty(randomId(), argPrefix, Markers.EMPTY)));
}
if (parenthesized) {
cursor += 1;
}
J.Lambda.Parameters params = new J.Lambda.Parameters(randomId(), EMPTY, Markers.EMPTY, parenthesized, paramExprs);
int saveCursor = cursor;
Space arrowPrefix = whitespace();
if (source.startsWith("->", cursor)) {
cursor += "->".length();
} else {
ls = ls.withArrow(false);
cursor = saveCursor;
arrowPrefix = EMPTY;
}
J body = visit(expression.getCode());
queue.add(new J.Lambda(randomId(), prefix, Markers.build(singletonList(ls)), params,
arrowPrefix,
body,
closureType));
if (cursor < source.length() && source.charAt(cursor) == '}') {
cursor++;
}
}
@Override
public void visitClosureListExpression(ClosureListExpression closureListExpression) {
List expressions = closureListExpression.getExpressions();
List> results = new ArrayList<>(closureListExpression.getExpressions().size());
for (int i = 0, expressionsSize = expressions.size(); i < expressionsSize; i++) {
results.add(JRightPadded.build(visit(expressions.get(i))).withAfter(whitespace()));
if (i < expressionsSize - 1) {
cursor++; // ","
}
}
queue.add(results);
}
@Override
public void visitConstantExpression(ConstantExpression expression) {
queue.add(insideParentheses(expression, fmt -> {
JavaType.Primitive jType;
// The unaryPlus is not included in the expression and must be handled through the source.
String text = expression.getText();
Object value = expression.getValue();
ClassNode type = expression.getType();
if (type == ClassHelper.BigDecimal_TYPE) {
// TODO: Proper support for BigDecimal literals
jType = JavaType.Primitive.Double;
value = ((BigDecimal) value).doubleValue();
} else if (type == ClassHelper.boolean_TYPE) {
jType = JavaType.Primitive.Boolean;
} else if (type == ClassHelper.byte_TYPE) {
jType = JavaType.Primitive.Byte;
} else if (type == ClassHelper.char_TYPE) {
jType = JavaType.Primitive.Char;
} else if (type == ClassHelper.double_TYPE || "java.lang.Double".equals(type.getName())) {
jType = JavaType.Primitive.Double;
if (expression.getNodeMetaData().get("_FLOATING_POINT_LITERAL_TEXT") instanceof String) {
text = (String) expression.getNodeMetaData().get("_FLOATING_POINT_LITERAL_TEXT");
}
} else if (type == ClassHelper.float_TYPE || "java.lang.Float".equals(type.getName())) {
jType = JavaType.Primitive.Float;
if (expression.getNodeMetaData().get("_FLOATING_POINT_LITERAL_TEXT") instanceof String) {
text = (String) expression.getNodeMetaData().get("_FLOATING_POINT_LITERAL_TEXT");
}
} else if (type == ClassHelper.int_TYPE || "java.lang.Integer".equals(type.getName())) {
jType = JavaType.Primitive.Int;
if (expression.getNodeMetaData().get("_INTEGER_LITERAL_TEXT") instanceof String) {
text = (String) expression.getNodeMetaData().get("_INTEGER_LITERAL_TEXT");
}
} else if (type == ClassHelper.long_TYPE || "java.lang.Long".equals(type.getName())) {
if (expression.getNodeMetaData().get("_INTEGER_LITERAL_TEXT") instanceof String) {
text = (String) expression.getNodeMetaData().get("_INTEGER_LITERAL_TEXT");
}
jType = JavaType.Primitive.Long;
} else if (type == ClassHelper.short_TYPE || "java.lang.Short".equals(type.getName())) {
jType = JavaType.Primitive.Short;
} else if (type == ClassHelper.STRING_TYPE) {
jType = JavaType.Primitive.String;
// String literals value returned by getValue()/getText() has already processed sequences like "\\" -> "\"
int length = sourceLengthOfString(expression);
// this is an attribute selector
if (source.startsWith("@" + value, cursor)) {
length += 1;
}
text = source.substring(cursor, cursor + length);
int delimiterLength = 0;
if (text.startsWith("$/")) {
delimiterLength = 2;
} else if (text.startsWith("\"\"\"") || text.startsWith("'''")) {
delimiterLength = 3;
} else if (text.startsWith("/") || text.startsWith("\"") || text.startsWith("'")) {
delimiterLength = 1;
}
value = text.substring(delimiterLength, text.length() - delimiterLength);
} else if (expression.isNullExpression()) {
if (source.startsWith("null", cursor)) {
text = "null";
} else {
text = "";
}
jType = JavaType.Primitive.Null;
} else {
throw new IllegalStateException("Unexpected constant type " + type);
}
if (cursor < source.length() && source.charAt(cursor) == '+' && !text.startsWith("+")) {
// A unaryPlus operator is implied on numerics and needs to be manually detected / added via the source.
text = "+" + text;
}
cursor += text.length();
// Numeric literals may be followed by "L", "f", or "d" to indicate Long, float, or double respectively
if (jType == JavaType.Primitive.Long || jType == JavaType.Primitive.Float || jType == JavaType.Primitive.Double) {
if (source.startsWith("L", cursor) || source.startsWith("f", cursor) || source.startsWith("d", cursor)) {
text += source.charAt(cursor);
cursor++;
}
}
return new J.Literal(randomId(), fmt, Markers.EMPTY, value, text,
null, jType);
}));
}
@Override
public void visitConstructorCallExpression(ConstructorCallExpression ctor) {
queue.add(insideParentheses(ctor, fmt -> {
cursor += 3; // skip "new"
TypeTree clazz = visitTypeTree(ctor.getType(), ctor.getMetaDataMap().containsKey(StaticTypesMarker.INFERRED_TYPE));
JContainer args = visit(ctor.getArguments());
J.Block body = null;
if (ctor.isUsingAnonymousInnerClass() && ctor.getType() instanceof InnerClassNode) {
body = classVisitor.visitClassBlock(ctor.getType());
}
MethodNode methodNode = (MethodNode) ctor.getNodeMetaData().get(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET);
return new J.NewClass(randomId(), fmt, Markers.EMPTY, null, EMPTY,
clazz, args, body, typeMapping.methodType(methodNode));
}));
}
@Override
public void visitContinueStatement(ContinueStatement statement) {
queue.add(new J.Continue(randomId(),
sourceBefore("continue"),
Markers.EMPTY,
(statement.getLabel() == null) ?
null :
new J.Identifier(randomId(),
sourceBefore(statement.getLabel()),
Markers.EMPTY, emptyList(), statement.getLabel(), null, null))
);
}
@Override
public void visitNotExpression(NotExpression expression) {
queue.add(insideParentheses(expression, fmt -> {
skip("!");
JLeftPadded op = padLeft(EMPTY, J.Unary.Type.Not);
Expression expr = visit(expression.getExpression());
return new J.Unary(randomId(), fmt, Markers.EMPTY, op, expr, typeMapping.type(expression.getType()));
}));
}
@Override
public void visitDeclarationExpression(DeclarationExpression expression) {
TypeTree typeExpr = visitVariableExpressionType(expression.getVariableExpression());
J.VariableDeclarations.NamedVariable namedVariable;
if (expression.isMultipleAssignmentDeclaration()) {
// def (a, b) = [1, 2]
throw new UnsupportedOperationException("FIXME");
} else {
J.Identifier name = visit(expression.getVariableExpression());
namedVariable = new J.VariableDeclarations.NamedVariable(
randomId(),
name.getPrefix(),
Markers.EMPTY,
name.withPrefix(EMPTY),
emptyList(),
null,
typeMapping.variableType(name.getSimpleName(), typeExpr.getType())
);
}
if (!(expression.getRightExpression() instanceof EmptyExpression)) {
Space beforeAssign = sourceBefore("=");
Expression initializer = visit(expression.getRightExpression());
namedVariable = namedVariable.getPadding().withInitializer(padLeft(beforeAssign, initializer));
}
J.VariableDeclarations variableDeclarations = new J.VariableDeclarations(
randomId(),
EMPTY,
Markers.EMPTY,
emptyList(),
emptyList(),
typeExpr,
null,
emptyList(),
singletonList(JRightPadded.build(namedVariable))
);
queue.add(variableDeclarations);
}
@Override
public void visitEmptyExpression(EmptyExpression expression) {
queue.add(new J.Empty(randomId(), EMPTY, Markers.EMPTY));
}
@Override
public void visitExpressionStatement(ExpressionStatement statement) {
queue.add(labeled(statement, () -> {
super.visitExpressionStatement(statement);
Object e = queue.poll();
if (e instanceof Statement) {
return (Statement) e;
}
return new G.ExpressionStatement(randomId(), (Expression) e);
}));
}
Statement condenseLabels(List labels, Statement s) {
if (labels.isEmpty()) {
return s;
}
return labels.get(0).withStatement(condenseLabels(labels.subList(1, labels.size()), s));
}
@SuppressWarnings("unchecked")
@Override
public void visitForLoop(ForStatement forLoop) {
queue.add(labeled(forLoop, () -> {
Space prefix = sourceBefore("for");
Space controlFmt = sourceBefore("(");
if (forLoop.getCollectionExpression() instanceof ClosureListExpression) {
List> controls = visit(forLoop.getCollectionExpression());
// There will always be exactly three elements in a for loop's ClosureListExpression
List> init = controls.get(0).getElement() instanceof List ?
(List>) controls.get(0).getElement() :
singletonList((JRightPadded) controls.get(0));
JRightPadded condition = (JRightPadded) controls.get(1);
List> update = controls.get(2).getElement() instanceof List ?
(List>) controls.get(2).getElement() :
singletonList((JRightPadded) controls.get(2));
cursor++; // skip ')'
return new J.ForLoop(randomId(), prefix, Markers.EMPTY,
new J.ForLoop.Control(randomId(), controlFmt,
Markers.EMPTY, init, condition, update),
JRightPadded.build(visit(forLoop.getLoopBlock())));
} else {
Parameter param = forLoop.getVariable();
Space paramFmt = whitespace();
TypeTree paramType = param.getOriginType().getColumnNumber() >= 0 ?
visitTypeTree(param.getOriginType()) : null;
JRightPadded paramName = JRightPadded.build(
new J.VariableDeclarations.NamedVariable(randomId(), whitespace(), Markers.EMPTY,
new J.Identifier(randomId(), EMPTY, Markers.EMPTY, emptyList(), param.getName(), null, null),
emptyList(), null, null)
);
cursor += param.getName().length();
Space rightPad = whitespace();
Markers forEachMarkers = Markers.EMPTY;
if (source.charAt(cursor) == ':') {
cursor++; // Skip ":"
} else {
cursor += 2; // Skip "in"
forEachMarkers = forEachMarkers.add(new InStyleForEachLoop(randomId()));
}
JRightPadded variable = JRightPadded.build(new J.VariableDeclarations(randomId(), paramFmt,
Markers.EMPTY, emptyList(), emptyList(), paramType, null, emptyList(),
singletonList(paramName))
).withAfter(rightPad);
JRightPadded iterable = JRightPadded.build((Expression) visit(forLoop.getCollectionExpression()))
.withAfter(sourceBefore(")"));
return new J.ForEachLoop(randomId(), prefix, forEachMarkers,
new J.ForEachLoop.Control(randomId(), controlFmt, Markers.EMPTY, variable, iterable),
JRightPadded.build(visit(forLoop.getLoopBlock())));
}
}));
}
@Override
public void visitIfElse(IfStatement ifElse) {
Space fmt = sourceBefore("if");
J.ControlParentheses ifCondition = new J.ControlParentheses<>(randomId(), sourceBefore("("), Markers.EMPTY,
JRightPadded.build((Expression) visit(ifElse.getBooleanExpression().getExpression())).withAfter(sourceBefore(")")));
JRightPadded then = maybeSemicolon(visit(ifElse.getIfBlock()));
J.If.Else else_ = ifElse.getElseBlock() instanceof EmptyStatement ? null :
new J.If.Else(randomId(), sourceBefore("else"), Markers.EMPTY,
maybeSemicolon(visit(ifElse.getElseBlock())));
queue.add(new J.If(randomId(), fmt, Markers.EMPTY, ifCondition, then, else_));
}
@Override
public void visitGStringExpression(GStringExpression gstring) {
Space fmt = whitespace();
String delimiter;
if (source.startsWith("\"\"\"", cursor)) {
delimiter = "\"\"\"";
} else if (source.startsWith("/", cursor)) {
delimiter = "/";
} else if (source.startsWith("$/", cursor)) {
delimiter = "$/";
} else {
delimiter = "\"";
}
cursor += delimiter.length();
NavigableMap sortedByPosition = new TreeMap<>();
for (org.codehaus.groovy.ast.expr.ConstantExpression e : gstring.getStrings()) {
// There will always be constant expressions before and after any values
// No need to represent these empty strings
if (!e.getText().isEmpty()) {
sortedByPosition.put(pos(e), e);
}
}
for (org.codehaus.groovy.ast.expr.Expression e : gstring.getValues()) {
sortedByPosition.put(pos(e), e);
}
List rawExprs = new ArrayList<>(sortedByPosition.values());
List strings = new ArrayList<>(rawExprs.size());
for (int i = 0; i < rawExprs.size(); i++) {
org.codehaus.groovy.ast.expr.Expression e = rawExprs.get(i);
if (source.charAt(cursor) == '$') {
cursor++;
boolean inCurlies = source.charAt(cursor) == '{';
if (inCurlies) {
cursor++;
} else {
columnOffset--;
}
strings.add(new G.GString.Value(randomId(), Markers.EMPTY, visit(e), inCurlies ? sourceBefore("}") : Space.EMPTY, inCurlies));
if (!inCurlies) {
columnOffset++;
}
} else if (e instanceof ConstantExpression) {
ConstantExpression cs = (ConstantExpression) e;
// The sub-strings within a GString have no delimiters of their own, confusing visitConstantExpression()
// ConstantExpression.getValue() cannot be trusted for strings as its values don't match source code because sequences like "\\" have already been replaced with a single "\"
// Use the AST element's line/column positions to figure out its extent, but those numbers need tweaks to be correct
int length = lengthAccordingToAst(cs);
if (i == 0 || i == rawExprs.size() - 1) {
// The first and last constants within a GString have line/column position which incorrectly include the GString's delimiters
length -= delimiter.length();
}
// The line/column numbers incorrectly indicate that the following expression's opening "$" is part of this expression
if (i < rawExprs.size() - 1) {
length--;
}
String value = source.substring(cursor, cursor + length);
strings.add(new J.Literal(randomId(), EMPTY, Markers.EMPTY, value, value, null, JavaType.Primitive.String));
cursor += value.length();
} else {
// Everything should be handled already by the other two code paths, but just in case
strings.add(visit(e));
}
}
queue.add(new G.GString(randomId(), fmt, Markers.EMPTY, delimiter, strings,
typeMapping.type(gstring.getType())));
cursor += delimiter.length();
}
@Override
public void visitListExpression(ListExpression list) {
if (list.getExpressions().isEmpty()) {
queue.add(new G.ListLiteral(randomId(), sourceBefore("["), Markers.EMPTY,
JContainer.build(singletonList(new JRightPadded<>(new J.Empty(randomId(), EMPTY, Markers.EMPTY), sourceBefore("]"), Markers.EMPTY))),
typeMapping.type(list.getType())));
} else {
queue.add(new G.ListLiteral(randomId(), sourceBefore("["), Markers.EMPTY,
JContainer.build(visitRightPadded(list.getExpressions().toArray(new ASTNode[0]), "]")),
typeMapping.type(list.getType())));
}
}
@Override
public void visitMapEntryExpression(MapEntryExpression expression) {
G.MapEntry mapEntry = new G.MapEntry(randomId(), whitespace(), Markers.EMPTY,
JRightPadded.build((Expression) visit(expression.getKeyExpression())).withAfter(sourceBefore(":")),
visit(expression.getValueExpression()),
null
);
queue.add(mapEntry);
}
@Override
public void visitMapExpression(MapExpression map) {
Space prefix = sourceBefore("[");
JContainer entries;
if (map.getMapEntryExpressions().isEmpty()) {
entries = JContainer.build(Collections.singletonList(JRightPadded.build(
new G.MapEntry(randomId(), whitespace(), Markers.EMPTY,
JRightPadded.build(new J.Empty(randomId(), sourceBefore(":"), Markers.EMPTY)),
new J.Empty(randomId(), sourceBefore("]"), Markers.EMPTY), null))));
} else {
entries = JContainer.build(visitRightPadded(map.getMapEntryExpressions().toArray(new ASTNode[0]), "]"));
}
queue.add(new G.MapLiteral(randomId(), prefix, Markers.EMPTY, entries, typeMapping.type(map.getType())));
}
@Override
public void visitMethodCallExpression(MethodCallExpression call) {
queue.add(insideParentheses(call, fmt -> {
ImplicitDot implicitDot = null;
JRightPadded select = null;
if (!call.isImplicitThis()) {
Expression selectExpr = visit(call.getObjectExpression());
int saveCursor = cursor;
Space afterSelect = whitespace();
if (source.charAt(cursor) == '.' || source.charAt(cursor) == '?' || source.charAt(cursor) == '*') {
cursor = saveCursor;
afterSelect = sourceBefore(call.isSpreadSafe() ? "*." : call.isSafe() ? "?." : ".");
} else {
implicitDot = new ImplicitDot(randomId());
}
select = JRightPadded.build(selectExpr).withAfter(afterSelect);
}
// Closure invocations that are written as closure.call() and closure() are parsed into identical MethodCallExpression
// closure() has implicitThis set to false
// So the "select" that was just parsed _may_ have actually been the method name
J.Identifier name;
String methodNameExpression = call.getMethodAsString();
if (source.charAt(cursor) == '"' || source.charAt(cursor) == '\'') {
// we have an escaped groovy method name, commonly used for test `def 'some scenario description'() {}`
// or to workaround names that are also keywords in groovy
methodNameExpression = source.charAt(cursor) + methodNameExpression + source.charAt(cursor);
}
Space prefix = whitespace();
if (methodNameExpression.equals(source.substring(cursor, cursor + methodNameExpression.length()))) {
cursor += methodNameExpression.length();
name = new J.Identifier(randomId(), prefix, Markers.EMPTY,
emptyList(), methodNameExpression, null, null);
} else if (select != null && select.getElement() instanceof J.Identifier) {
name = (J.Identifier) select.getElement();
select = null;
} else {
throw new IllegalArgumentException("Unable to parse method call");
}
if (call.isSpreadSafe()) {
name = name.withMarkers(name.getMarkers().add(new StarDot(randomId())));
}
if (call.isSafe()) {
name = name.withMarkers(name.getMarkers().add(new NullSafe(randomId())));
}
if (implicitDot != null) {
name = name.withMarkers(name.getMarkers().add(implicitDot));
}
// Method invocations may have type information that can enrich the type information of its parameters
Markers markers = Markers.EMPTY;
if (call.getArguments() instanceof ArgumentListExpression) {
ArgumentListExpression args = (ArgumentListExpression) call.getArguments();
if (call.getNodeMetaData(StaticTypesMarker.INFERRED_TYPE) != null) {
for (org.codehaus.groovy.ast.expr.Expression arg : args.getExpressions()) {
if (!(arg instanceof ClosureExpression)) {
continue;
}
ClosureExpression cl = (ClosureExpression) arg;
ClassNode actualParamTypeRaw = call.getNodeMetaData(StaticTypesMarker.INFERRED_TYPE);
for (Parameter p : cl.getParameters()) {
if (p.isDynamicTyped()) {
p.setType(actualParamTypeRaw);
p.removeNodeMetaData(StaticTypesMarker.INFERRED_TYPE);
}
}
for (Map.Entry declaredVariable : cl.getVariableScope().getDeclaredVariables().entrySet()) {
if (declaredVariable.getValue() instanceof Parameter && declaredVariable.getValue().isDynamicTyped()) {
Parameter p = (Parameter) declaredVariable.getValue();
p.setType(actualParamTypeRaw);
p.removeNodeMetaData(StaticTypesMarker.INFERRED_TYPE);
}
}
}
}
// handle the obscure case where there are empty parens ahead of a closure
if (args.getExpressions().size() == 1 && args.getExpressions().get(0) instanceof ClosureExpression) {
int saveCursor = cursor;
Space argPrefix = whitespace();
if (source.charAt(cursor) == '(') {
cursor += 1;
Space infix = whitespace();
if (source.charAt(cursor) == ')') {
cursor += 1;
markers = markers.add(new EmptyArgumentListPrecedesArgument(randomId(), argPrefix, infix));
} else {
cursor = saveCursor;
}
} else {
cursor = saveCursor;
}
}
}
JContainer args = visit(call.getArguments());
MethodNode methodNode = (MethodNode) call.getNodeMetaData().get(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET);
JavaType.Method methodType = null;
if (methodNode == null && call.getObjectExpression() instanceof VariableExpression
&& ((VariableExpression) call.getObjectExpression()).getAccessedVariable() != null) {
// Groovy doesn't know what kind of object this method is being invoked on
// But if this invocation is inside a Closure we may have already enriched its parameters with types from the static type checker
// Use any such type information to attempt to find a matching method
ClassNode parameterType = staticType(((VariableExpression) call.getObjectExpression()).getAccessedVariable());
if (args.getElements().size() == 1 && args.getElements().get(0) instanceof J.Empty) {
methodType = typeMapping.methodType(parameterType.getMethod(name.getSimpleName(), new Parameter[]{}));
} else if (call.getArguments() instanceof ArgumentListExpression) {
List rawArgs = ((ArgumentListExpression) call.getArguments()).getExpressions();
/*
Look through the methods returning the closest match on a best-effort basis
Factors which can result in a less accurate match, or no match, include:
* The type of each parameter may or may not be known to us
* Usage of Groovy's "named parameters" syntactic sugaring throwing off argument count and order
*/
methodLoop:
for (MethodNode candidateMethod : parameterType.getAllDeclaredMethods()) {
if (!name.getSimpleName().equals(candidateMethod.getName())) {
continue;
}
if (rawArgs.size() != candidateMethod.getParameters().length) {
continue;
}
// Better than nothing
methodType = typeMapping.methodType(candidateMethod);
// If all parameter types agree then we have found an exact match
for (int i = 0; i < candidateMethod.getParameters().length; i++) {
JavaType param = typeMapping.type(staticType(candidateMethod.getParameters()[i]));
JavaType arg = typeMapping.type(staticType(rawArgs.get(i)));
if (!TypeUtils.isAssignableTo(param, arg)) {
continue methodLoop;
}
}
break;
}
}
} else {
methodType = typeMapping.methodType(methodNode);
}
return new J.MethodInvocation(randomId(), fmt, markers,
select, null, name, args, methodType);
}));
}
@Override
public void visitStaticMethodCallExpression(StaticMethodCallExpression call) {
Space fmt = whitespace();
MethodNode methodNode = (MethodNode) call.getNodeMetaData().get(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET);
JavaType.Method methodType = typeMapping.methodType(methodNode);
J.Identifier name = new J.Identifier(randomId(), sourceBefore(call.getMethodAsString()), Markers.EMPTY,
emptyList(), call.getMethodAsString(), methodType, null);
// Method invocations may have type information that can enrich the type information of its parameters
Markers markers = Markers.EMPTY;
if (call.getArguments() instanceof ArgumentListExpression) {
ArgumentListExpression args = (ArgumentListExpression) call.getArguments();
if (call.getNodeMetaData(StaticTypesMarker.INFERRED_TYPE) != null) {
for (org.codehaus.groovy.ast.expr.Expression arg : args.getExpressions()) {
if (!(arg instanceof ClosureExpression)) {
continue;
}
ClosureExpression cl = (ClosureExpression) arg;
ClassNode actualParamTypeRaw = call.getNodeMetaData(StaticTypesMarker.INFERRED_TYPE);
for (Parameter p : cl.getParameters()) {
if (p.isDynamicTyped()) {
p.setType(actualParamTypeRaw);
p.removeNodeMetaData(StaticTypesMarker.INFERRED_TYPE);
}
}
for (Map.Entry declaredVariable : cl.getVariableScope().getDeclaredVariables().entrySet()) {
if (declaredVariable.getValue() instanceof Parameter && declaredVariable.getValue().isDynamicTyped()) {
Parameter p = (Parameter) declaredVariable.getValue();
p.setType(actualParamTypeRaw);
p.removeNodeMetaData(StaticTypesMarker.INFERRED_TYPE);
}
}
}
}
// handle the obscure case where there are empty parens ahead of a closure
if (args.getExpressions().size() == 1 && args.getExpressions().get(0) instanceof ClosureExpression) {
int saveCursor = cursor;
Space prefix = whitespace();
if (source.charAt(cursor) == '(') {
cursor += 1;
Space infix = whitespace();
if (source.charAt(cursor) == ')') {
cursor += 1;
markers = markers.add(new EmptyArgumentListPrecedesArgument(randomId(), prefix, infix));
} else {
cursor = saveCursor;
}
} else {
cursor = saveCursor;
}
}
}
JContainer args = visit(call.getArguments());
queue.add(new J.MethodInvocation(randomId(), fmt, markers,
null, null, name, args, methodType));
}
@Override
public void visitAttributeExpression(AttributeExpression attr) {
Space fmt = whitespace();
Expression target = visit(attr.getObjectExpression());
Space beforeDot = attr.isSafe() ? sourceBefore("?.") :
sourceBefore(attr.isSpreadSafe() ? "*." : ".");
J name = visit(attr.getProperty());
if (name instanceof J.Literal) {
String nameStr = ((J.Literal) name).getValueSource();
assert nameStr != null;
name = new J.Identifier(randomId(), name.getPrefix(), Markers.EMPTY, emptyList(), nameStr, null, null);
}
if (attr.isSpreadSafe()) {
name = name.withMarkers(name.getMarkers().add(new StarDot(randomId())));
}
if (attr.isSafe()) {
name = name.withMarkers(name.getMarkers().add(new NullSafe(randomId())));
}
queue.add(new J.FieldAccess(randomId(), fmt, Markers.EMPTY, target, padLeft(beforeDot, (J.Identifier) name), null));
}
@Override
public void visitPropertyExpression(PropertyExpression prop) {
queue.add(insideParentheses(prop, fmt -> {
Expression target = visit(prop.getObjectExpression());
Space beforeDot = prop.isSpreadSafe() ? sourceBefore("*.") :
sourceBefore(prop.isSafe() ? "?." : ".");
J name = visit(prop.getProperty());
if (name instanceof J.Literal) {
J.Literal nameLiteral = ((J.Literal) name);
name = new J.Identifier(randomId(), name.getPrefix(), Markers.EMPTY, emptyList(), nameLiteral.getValueSource(), nameLiteral.getType(), null);
}
if (prop.isSpreadSafe()) {
name = name.withMarkers(name.getMarkers().add(new StarDot(randomId())));
} else if (prop.isSafe()) {
name = name.withMarkers(name.getMarkers().add(new NullSafe(randomId())));
}
return new J.FieldAccess(randomId(), fmt, Markers.EMPTY, target, padLeft(beforeDot, (J.Identifier) name), null);
}));
}
@Override
public void visitRangeExpression(RangeExpression range) {
queue.add(insideParentheses(range, fmt -> new G.Range(randomId(), fmt, Markers.EMPTY,
visit(range.getFrom()),
JLeftPadded.build(range.isInclusive()).withBefore(sourceBefore(range.isInclusive() ? ".." : "..>")),
visit(range.getTo()))));
}
@Override
public void visitReturnStatement(ReturnStatement return_) {
Space fmt = sourceBefore("return");
if (return_.getExpression() instanceof ConstantExpression && isSynthetic(return_.getExpression()) &&
(((ConstantExpression) return_.getExpression()).getValue() == null)) {
queue.add(new J.Return(randomId(), fmt, Markers.EMPTY, null));
} else {
queue.add(new J.Return(randomId(), fmt, Markers.EMPTY, visit(return_.getExpression())));
}
}
@Override
public void visitShortTernaryExpression(ElvisOperatorExpression ternary) {
queue.add(insideParentheses(ternary, fmt -> {
Expression trueExpr = visit(ternary.getBooleanExpression());
J.Ternary elvis = new J.Ternary(randomId(), fmt, Markers.EMPTY,
trueExpr,
padLeft(sourceBefore("?"), trueExpr),
padLeft(sourceBefore(":"), visit(ternary.getFalseExpression())),
typeMapping.type(staticType(ternary)));
return elvis.withMarkers(elvis.getMarkers().add(new Elvis(randomId())));
}));
}
@Override
public void visitSwitch(SwitchStatement statement) {
queue.add(new J.Switch(
randomId(),
sourceBefore("switch"),
Markers.EMPTY,
new J.ControlParentheses<>(randomId(), sourceBefore("("), Markers.EMPTY,
JRightPadded.build((Expression) visit(statement.getExpression())).withAfter(sourceBefore(")"))),
new J.Block(
randomId(), sourceBefore("{"), Markers.EMPTY,
JRightPadded.build(false),
ListUtils.concat(
convertAll(statement.getCaseStatements(), t -> Space.EMPTY, t -> Space.EMPTY),
statement.getDefaultStatement().isEmpty() ? null : JRightPadded.build(visitDefaultCaseStatement((BlockStatement) statement.getDefaultStatement()))
),
sourceBefore("}"))));
}
@Override
public void visitSynchronizedStatement(SynchronizedStatement statement) {
Space fmt = sourceBefore("synchronized");
queue.add(new J.Synchronized(randomId(), fmt, Markers.EMPTY,
new J.ControlParentheses<>(randomId(), sourceBefore("("), Markers.EMPTY,
JRightPadded.build((Expression) visit(statement.getExpression())).withAfter(sourceBefore(")"))),
visit(statement.getCode())));
}
@Override
public void visitTernaryExpression(TernaryExpression ternary) {
queue.add(insideParentheses(ternary, fmt -> new J.Ternary(randomId(), fmt, Markers.EMPTY,
visit(ternary.getBooleanExpression()),
padLeft(sourceBefore("?"), visit(ternary.getTrueExpression())),
padLeft(sourceBefore(":"), visit(ternary.getFalseExpression())),
typeMapping.type(ternary.getType()))));
}
@Override
public void visitThrowStatement(ThrowStatement statement) {
Space fmt = sourceBefore("throw");
queue.add(new J.Throw(randomId(), fmt, Markers.EMPTY, visit(statement.getExpression())));
}
// the current understanding is that TupleExpression only exist as method invocation arguments.
// this is the reason behind the simplifying assumption that there is one expression, and it is
// a NamedArgumentListExpression.
@Override
public void visitTupleExpression(TupleExpression tuple) {
int saveCursor = cursor;
Space beforeOpenParen = whitespace();
org.openrewrite.java.marker.OmitParentheses omitParentheses = null;
if (source.charAt(cursor) == '(') {
cursor++;
} else {
omitParentheses = new org.openrewrite.java.marker.OmitParentheses(randomId());
beforeOpenParen = EMPTY;
cursor = saveCursor;
}
List> args = new ArrayList<>(tuple.getExpressions().size());
for (org.codehaus.groovy.ast.expr.Expression expression : tuple.getExpressions()) {
NamedArgumentListExpression namedArgList = (NamedArgumentListExpression) expression;
List mapEntryExpressions = namedArgList.getMapEntryExpressions();
for (int i = 0; i < mapEntryExpressions.size(); i++) {
Expression arg = visit(mapEntryExpressions.get(i));
if (omitParentheses != null) {
arg = arg.withMarkers(arg.getMarkers().add(omitParentheses));
}
Space after = EMPTY;
if (i == mapEntryExpressions.size() - 1) {
if (omitParentheses == null) {
after = sourceBefore(")");
}
} else {
after = whitespace();
if (source.charAt(cursor) == ')') {
// the next argument will have an OmitParentheses marker
omitParentheses = new org.openrewrite.java.marker.OmitParentheses(randomId());
}
cursor++;
}
args.add(JRightPadded.build(arg).withAfter(after));
}
}
queue.add(JContainer.build(beforeOpenParen, args, Markers.EMPTY));
}
@Override
public void visitTryCatchFinally(TryCatchStatement node) {
Space prefix = sourceBefore("try");
// Recent versions of groovy support try-with-resources, usage of this pattern in groovy is uncommon
JContainer resources = null;
J.Block body = visit(node.getTryStatement());
List catches;
if (node.getCatchStatements().isEmpty()) {
catches = emptyList();
} else {
catches = new ArrayList<>(node.getCatchStatements().size());
for (CatchStatement catchStatement : node.getCatchStatements()) {
visitCatchStatement(catchStatement);
catches.add((J.Try.Catch) queue.poll());
}
}
// Strangely, groovy parses the finally's block as a BlockStatement which contains another BlockStatement
// The true contents of the block are within the first statement of this apparently pointless enclosing BlockStatement
JLeftPadded finally_ = !(node.getFinallyStatement() instanceof BlockStatement) ? null :
padLeft(sourceBefore("finally"), visit(((BlockStatement) node.getFinallyStatement()).getStatements().get(0)));
//noinspection ConstantConditions
queue.add(new J.Try(randomId(), prefix, Markers.EMPTY, resources, body, catches, finally_));
}
@Override
public void visitPostfixExpression(PostfixExpression unary) {
Space fmt = whitespace();
Expression expression = visit(unary.getExpression());
Space operatorPrefix = whitespace();
String typeToken = unary.getOperation().getText();
cursor += typeToken.length();
J.Unary.Type operator = null;
switch (typeToken) {
case "++":
operator = J.Unary.Type.PostIncrement;
break;
case "--":
operator = J.Unary.Type.PostDecrement;
break;
}
assert operator != null;
queue.add(new J.Unary(randomId(), fmt, Markers.EMPTY,
JLeftPadded.build(operator).withBefore(operatorPrefix),
expression, null));
}
@Override
public void visitPrefixExpression(PrefixExpression unary) {
Space fmt = whitespace();
String typeToken = unary.getOperation().getText();
cursor += typeToken.length();
J.Unary.Type operator = null;
switch (typeToken) {
case "++":
operator = J.Unary.Type.PreIncrement;
break;
case "--":
operator = J.Unary.Type.PreDecrement;
break;
case "~":
operator = J.Unary.Type.Complement;
break;
}
assert operator != null;
queue.add(new J.Unary(randomId(), fmt, Markers.EMPTY,
JLeftPadded.build(operator),
visit(unary.getExpression()),
null));
}
public TypeTree visitVariableExpressionType(VariableExpression expression) {
JavaType type = typeMapping.type(staticType(((org.codehaus.groovy.ast.expr.Expression) expression)));
if (expression.isDynamicTyped()) {
Space prefix = whitespace();
String keyword;
if (source.substring(cursor).startsWith("final")) {
keyword = "final";
} else {
keyword = source.substring(cursor, cursor + 3);
}
cursor += keyword.length();
return new J.Identifier(randomId(),
prefix,
Markers.EMPTY,
emptyList(),
keyword,
type, null);
}
Space prefix = sourceBefore(expression.getOriginType().getUnresolvedName());
J.Identifier ident = new J.Identifier(randomId(),
EMPTY,
Markers.EMPTY,
emptyList(),
expression.getOriginType().getUnresolvedName(),
type, null);
if (expression.getOriginType().getGenericsTypes() != null) {
return new J.ParameterizedType(randomId(), prefix, Markers.EMPTY, ident, visitTypeParameterizations(
staticType((org.codehaus.groovy.ast.expr.Expression) expression).getGenericsTypes()), type);
}
return ident.withPrefix(prefix);
}
@Override
public void visitVariableExpression(VariableExpression expression) {
JavaType type;
if (expression.isDynamicTyped() && expression.getAccessedVariable() != null && expression.getAccessedVariable().getType() != expression.getOriginType()) {
type = typeMapping.type(staticType(expression.getAccessedVariable()));
} else {
type = typeMapping.type(staticType((org.codehaus.groovy.ast.expr.Expression) expression));
}
queue.add(new J.Identifier(randomId(),
sourceBefore(expression.getName()),
Markers.EMPTY,
emptyList(),
expression.getName(),
type, null)
);
}
@Override
public void visitWhileLoop(WhileStatement loop) {
Space fmt = sourceBefore("while");
queue.add(new J.WhileLoop(randomId(), fmt, Markers.EMPTY,
new J.ControlParentheses<>(randomId(), sourceBefore("("), Markers.EMPTY,
JRightPadded.build((Expression) visit(loop.getBooleanExpression().getExpression()))
.withAfter(sourceBefore(")"))),
JRightPadded.build(visit(loop.getLoopBlock()))
));
}
private List> convertAll(List extends ASTNode> nodes,
Function innerSuffix,
Function suffix) {
if (nodes.isEmpty()) {
return emptyList();
}
List> converted = new ArrayList<>(nodes.size());
for (int i = 0; i < nodes.size(); i++) {
converted.add(convert(nodes.get(i), i == nodes.size() - 1 ? suffix : innerSuffix));
}
return converted;
}
private JRightPadded convert(ASTNode node, Function suffix) {
J2 j = visit(node);
return padRight(j, suffix.apply(node));
}
private List> convertStatements(List extends ASTNode> nodes) {
if (nodes.isEmpty()) {
return emptyList();
}
List> converted = new ArrayList<>(nodes.size());
for (ASTNode node : nodes) {
Statement statement = visit(node);
converted.add(maybeSemicolon(statement));
}
return converted;
}
@SuppressWarnings({"unchecked", "ConstantConditions"})
private T pollQueue() {
return (T) queue.poll();
}
}
private JRightPadded convertTopLevelStatement(SourceUnit unit, ASTNode node) {
if (node instanceof ClassNode) {
ClassNode classNode = (ClassNode) node;
RewriteGroovyClassVisitor classVisitor = new RewriteGroovyClassVisitor(unit);
classVisitor.visitClass(classNode);
return JRightPadded.build(classVisitor.pollQueue());
} else if (node instanceof MethodNode) {
MethodNode methodNode = (MethodNode) node;
RewriteGroovyClassVisitor classVisitor = new RewriteGroovyClassVisitor(unit);
classVisitor.visitMethod(methodNode);
return JRightPadded.build(classVisitor.pollQueue());
} else if (node instanceof ImportNode) {
ImportNode importNode = (ImportNode) node;
Space prefix = sourceBefore("import");
JLeftPadded statik;
if (importNode.isStatic()) {
statik = padLeft(sourceBefore("static"), true);
} else {
statik = padLeft(EMPTY, false);
}
String packageName = importNode.getPackageName();
J.FieldAccess qualid;
if (packageName == null) {
String type = importNode.getType().getName().replace('$', '.');
if (importNode.isStar()) {
type += ".*";
} else if (importNode.getFieldName() != null) {
type += "." + importNode.getFieldName();
}
Space space = sourceBefore(type);
qualid = TypeTree.build(type).withPrefix(space);
} else {
if (importNode.isStar()) {
packageName += "*";
}
qualid = TypeTree.build(packageName).withPrefix(sourceBefore(packageName));
}
JLeftPadded alias = null;
int endOfWhitespace = indexOfNextNonWhitespace(cursor, source);
if (endOfWhitespace + 2 <= source.length() && "as".equals(source.substring(endOfWhitespace, endOfWhitespace + 2))) {
String simpleName = importNode.getAlias();
alias = padLeft(sourceBefore("as"), new J.Identifier(randomId(), sourceBefore(simpleName), Markers.EMPTY, emptyList(), simpleName, null, null));
}
J.Import anImport = new J.Import(randomId(), prefix, Markers.EMPTY, statik, qualid, alias);
return maybeSemicolon(anImport);
}
RewriteGroovyVisitor groovyVisitor = new RewriteGroovyVisitor(node, new RewriteGroovyClassVisitor(unit));
node.visit(groovyVisitor);
return maybeSemicolon(groovyVisitor.pollQueue());
}
private static LineColumn pos(ASTNode node) {
return new LineColumn(node.getLineNumber(), node.getColumnNumber());
}
private static boolean isSynthetic(ASTNode node) {
return node.getLineNumber() == -1;
}
@Value
private static class LineColumn implements Comparable {
int line;
int column;
@Override
public int compareTo(GroovyParserVisitor.@NonNull LineColumn lc) {
return line != lc.line ? line - lc.line : column - lc.column;
}
}
private JRightPadded padRight(T tree, Space right) {
return new JRightPadded<>(tree, right, Markers.EMPTY);
}
private JLeftPadded padLeft(Space left, T tree) {
return new JLeftPadded<>(left, tree, Markers.EMPTY);
}
private int positionOfNext(String untilDelim) {
boolean inMultiLineComment = false;
boolean inSingleLineComment = false;
int delimIndex = cursor;
for (; delimIndex < source.length() - untilDelim.length() + 1; delimIndex++) {
if (inSingleLineComment) {
if (source.charAt(delimIndex) == '\n') {
inSingleLineComment = false;
}
} else {
if (source.length() - untilDelim.length() > delimIndex + 1) {
switch (source.substring(delimIndex, delimIndex + 2)) {
case "//":
inSingleLineComment = true;
delimIndex++;
break;
case "/*":
inMultiLineComment = true;
delimIndex++;
break;
case "*/":
inMultiLineComment = false;
delimIndex = delimIndex + 2;
break;
}
}
if (!inMultiLineComment && !inSingleLineComment) {
if (source.startsWith(untilDelim, delimIndex)) {
break; // found it!
}
}
}
}
return delimIndex > source.length() - untilDelim.length() ? -1 : delimIndex;
}
private Space whitespace() {
String prefix = source.substring(cursor, indexOfNextNonWhitespace(cursor, source));
cursor += prefix.length();
return format(prefix);
}
private String skip(@Nullable String token) {
if (token == null) {
//noinspection ConstantConditions
return null;
}
if (source.startsWith(token, cursor)) {
cursor += token.length();
}
return token;
}
@SuppressWarnings("SameParameterValue")
private T typeTree(@Nullable ClassNode classNode) {
return typeTree(classNode, false);
}
private T typeTree(@Nullable ClassNode classNode, boolean inferredType) {
if (classNode != null && classNode.isArray()) {
//noinspection unchecked
return (T) arrayType(classNode);
}
Space prefix = whitespace();
String maybeFullyQualified = name();
String[] parts = maybeFullyQualified.split("\\.");
String fullName = "";
Expression expr = null;
for (int i = 0; i < parts.length; i++) {
String part = parts[i];
if (i == 0) {
fullName = part;
expr = new J.Identifier(randomId(), EMPTY, Markers.EMPTY, emptyList(), part, typeMapping.type(classNode), null);
} else {
fullName += "." + part;
Matcher whitespacePrefix = whitespacePrefixPattern.matcher(part);
Space identFmt = whitespacePrefix.matches() ? format(whitespacePrefix.group(0)) : Space.EMPTY;
Matcher whitespaceSuffix = whitespaceSuffixPattern.matcher(part);
//noinspection ResultOfMethodCallIgnored
whitespaceSuffix.matches();
Space namePrefix = i == parts.length - 1 ? Space.EMPTY : format(whitespaceSuffix.group(1));
expr = new J.FieldAccess(
randomId(),
EMPTY,
Markers.EMPTY,
expr,
padLeft(namePrefix, new J.Identifier(randomId(), identFmt, Markers.EMPTY, emptyList(), part.trim(), null, null)),
(Character.isUpperCase(part.charAt(0)) || i == parts.length - 1) ?
JavaType.ShallowClass.build(fullName) :
null
);
}
}
assert expr != null;
if (classNode != null) {
if (classNode.isUsingGenerics() && !classNode.isGenericsPlaceHolder()) {
JContainer typeParameters = inferredType ?
JContainer.build(sourceBefore("<"), singletonList(padRight(new J.Empty(randomId(), EMPTY, Markers.EMPTY), sourceBefore(">"))), Markers.EMPTY) :
visitTypeParameterizations(classNode.getGenericsTypes());
expr = new J.ParameterizedType(randomId(), EMPTY, Markers.EMPTY, (NameTree) expr, typeParameters, typeMapping.type(classNode));
}
}
return expr.withPrefix(prefix);
}
private TypeTree arrayType(ClassNode classNode) {
ClassNode typeTree = classNode.getComponentType();
int count = 1;
while (typeTree.isArray()) {
count++;
typeTree = typeTree.getComponentType();
}
Space prefix = whitespace();
TypeTree elemType = typeTree(typeTree);
JLeftPadded dimension = padLeft(sourceBefore("["), sourceBefore("]"));
return new J.ArrayType(randomId(), prefix, Markers.EMPTY,
count == 1 ? elemType : mapDimensions(elemType, classNode.getComponentType()),
null,
dimension,
typeMapping.type(classNode));
}
private TypeTree mapDimensions(TypeTree baseType, ClassNode classNode) {
if (classNode.isArray()) {
Space prefix = whitespace();
JLeftPadded dimension = padLeft(sourceBefore("["), sourceBefore("]"));
return new J.ArrayType(
randomId(),
prefix,
Markers.EMPTY,
mapDimensions(baseType, classNode.getComponentType()),
null,
dimension,
typeMapping.type(classNode)
);
}
return baseType;
}
private Space sourceBefore(String untilDelim) {
int delimIndex = positionOfNext(untilDelim);
if (delimIndex < 0) {
return EMPTY; // unable to find this delimiter
}
String prefix = source.substring(cursor, delimIndex);
cursor += prefix.length() + untilDelim.length(); // advance past the delimiter
return Space.format(prefix);
}
/**
* Gets the length in characters of a String literal.
* Attempts to account for a number of different strange compiler behaviors, old bugs, and edge cases.
* cursor is presumed to point at the beginning of the node.
*/
private int sourceLengthOfString(ConstantExpression expr) {
// ConstantExpression.getValue() already has resolved escaped characters. So "\t" and a literal tab will look the same.
// Since we cannot differentiate between the two, use this alternate method only when an old version of groovy indicates risk
// and the literal doesn't contain any characters which might be from an escape sequence. e.g.: tabs, newlines, carriage returns
String value = (String) expr.getValue();
if (isOlderThanGroovy3() && value.matches("[^\\t\\r\\n\\\\]*")) {
int delimiterLength = getDelimiterLength();
return delimiterLength + value.length() + delimiterLength;
}
int lengthAccordingToAst = lengthAccordingToAst(expr);
// subtract any parentheses that were included with lengthAccordingToAst
Integer insideParenthesesLevel = getInsideParenthesesLevel(expr);
if (insideParenthesesLevel != null) {
return lengthAccordingToAst - insideParenthesesLevel * 2;
}
return lengthAccordingToAst;
}
private static @Nullable Integer getInsideParenthesesLevel(ASTNode node) {
Object rawIpl = node.getNodeMetaData("_INSIDE_PARENTHESES_LEVEL");
if (rawIpl instanceof AtomicInteger) {
// On Java 11 and newer _INSIDE_PARENTHESES_LEVEL is an AtomicInteger
return ((AtomicInteger) rawIpl).get();
} else {
// On Java 8 _INSIDE_PARENTHESES_LEVEL is a regular Integer
return (Integer) rawIpl;
}
}
private int getDelimiterLength() {
String maybeDelimiter = source.substring(cursor, Math.min(cursor + 3, source.length()));
int delimiterLength = 0;
if (maybeDelimiter.startsWith("$/")) {
delimiterLength = 2;
} else if (maybeDelimiter.startsWith("\"\"\"") || maybeDelimiter.startsWith("'''")) {
delimiterLength = 3;
} else if (maybeDelimiter.startsWith("/") || maybeDelimiter.startsWith("\"") || maybeDelimiter.startsWith("'")) {
delimiterLength = 1;
}
return delimiterLength;
}
/**
* Gets the length according to the Groovy compiler's attestation of starting/ending line and column numbers.
* On older versions of the JDK/Groovy compiler string literals with following whitespace sometimes erroneously include
* the length of the whitespace in the length of the AST node.
* So in this method invocation:
* foo( 'a' )
* the correct source length for the AST node representing 'a' is 3, but in affected groovy versions the length
* on the node is '4' because it is also counting the trailing whitespace.
*/
private int lengthAccordingToAst(ConstantExpression node) {
if (!appearsInSource(node)) {
return 0;
}
int lineCount = node.getLastLineNumber() - node.getLineNumber();
if (lineCount == 0) {
return node.getLastColumnNumber() - node.getColumnNumber() + columnOffset;
}
int linesSoFar = 0;
int length = 0;
int finalLineChars = 0;
while (true) {
char c = source.charAt(cursor + length);
if (c == '\n') {
linesSoFar++;
}
if (linesSoFar == lineCount) {
finalLineChars++;
}
length++;
if (finalLineChars == node.getLastColumnNumber() + columnOffset) {
return length;
}
}
}
private TypeTree visitTypeTree(ClassNode classNode) {
return visitTypeTree(classNode, false);
}
private TypeTree visitTypeTree(ClassNode classNode, boolean inferredType) {
JavaType.Primitive primitiveType = JavaType.Primitive.fromKeyword(classNode.getUnresolvedName());
if (primitiveType != null) {
return new J.Primitive(randomId(), sourceBefore(classNode.getUnresolvedName()), Markers.EMPTY, primitiveType);
}
int saveCursor = cursor;
Space fmt = whitespace();
if (cursor < source.length() && source.startsWith("def", cursor)) {
cursor += 3;
return new J.Identifier(randomId(), fmt, Markers.EMPTY, emptyList(), "def",
JavaType.ShallowClass.build("java.lang.Object"), null);
} else {
cursor = saveCursor;
}
return typeTree(classNode, inferredType);
}
private List visitModifiers(int modifiers) {
List unorderedModifiers = new ArrayList<>();
if ((modifiers & Opcodes.ACC_ABSTRACT) != 0) {
unorderedModifiers.add(new J.Modifier(randomId(), EMPTY, Markers.EMPTY, null, J.Modifier.Type.Abstract, emptyList()));
}
if ((modifiers & Opcodes.ACC_FINAL) != 0) {
unorderedModifiers.add(new J.Modifier(randomId(), EMPTY, Markers.EMPTY, null, J.Modifier.Type.Final, emptyList()));
}
if ((modifiers & Opcodes.ACC_PRIVATE) != 0) {
unorderedModifiers.add(new J.Modifier(randomId(), EMPTY, Markers.EMPTY, null, J.Modifier.Type.Private, emptyList()));
}
if ((modifiers & Opcodes.ACC_PROTECTED) != 0) {
unorderedModifiers.add(new J.Modifier(randomId(), EMPTY, Markers.EMPTY, null, J.Modifier.Type.Protected, emptyList()));
}
if ((modifiers & Opcodes.ACC_PUBLIC) != 0) {
unorderedModifiers.add(new J.Modifier(randomId(), EMPTY, Markers.EMPTY, null, J.Modifier.Type.Public, emptyList()));
}
if ((modifiers & Opcodes.ACC_STATIC) != 0) {
unorderedModifiers.add(new J.Modifier(randomId(), EMPTY, Markers.EMPTY, null, J.Modifier.Type.Static, emptyList()));
}
if ((modifiers & Opcodes.ACC_SYNCHRONIZED) != 0) {
unorderedModifiers.add(new J.Modifier(randomId(), EMPTY, Markers.EMPTY, null, J.Modifier.Type.Synchronized, emptyList()));
}
if ((modifiers & Opcodes.ACC_TRANSIENT) != 0) {
unorderedModifiers.add(new J.Modifier(randomId(), EMPTY, Markers.EMPTY, null, J.Modifier.Type.Transient, emptyList()));
}
if ((modifiers & Opcodes.ACC_VOLATILE) != 0) {
unorderedModifiers.add(new J.Modifier(randomId(), EMPTY, Markers.EMPTY, null, J.Modifier.Type.Volatile, emptyList()));
}
List orderedModifiers = new ArrayList<>(unorderedModifiers.size());
boolean foundModifier = true;
nextModifier:
while (foundModifier) {
int saveCursor = cursor;
Space fmt = whitespace();
for (J.Modifier mod : unorderedModifiers) {
String modName = mod.getType().name().toLowerCase();
if (source.startsWith(modName, cursor)) {
orderedModifiers.add(mod.withPrefix(fmt));
unorderedModifiers.remove(mod);
cursor += modName.length();
continue nextModifier;
}
}
foundModifier = false;
cursor = saveCursor;
}
return orderedModifiers;
}
private JRightPadded maybeSemicolon(G2 g) {
int saveCursor = cursor;
Space beforeSemi = whitespace();
Semicolon semicolon = null;
if (cursor < source.length() && source.charAt(cursor) == ';') {
semicolon = new Semicolon(randomId());
cursor++;
} else {
beforeSemi = EMPTY;
cursor = saveCursor;
}
JRightPadded paddedG = JRightPadded.build(g).withAfter(beforeSemi);
if (semicolon != null) {
paddedG = paddedG.withMarkers(paddedG.getMarkers().add(semicolon));
}
return paddedG;
}
private String name() {
int i = cursor;
for (; i < source.length(); i++) {
char c = source.charAt(i);
if (!(Character.isJavaIdentifierPart(c) || c == '.' || c == '*')) {
break;
}
}
String result = source.substring(cursor, i);
cursor += i - cursor;
return result;
}
/*
Visit the value filled into a parameterized type, such as in a variable declaration.
Not to be confused with the declaration of a type parameter on a class or method.
Can contain a J.Identifier, as is the case in a typical variable declaration:
List
Map
Can contain a J.Blank, as in a diamond operator:
List s = new ArrayList< >()
Can contain a J.FieldAccess, as in a variable declaration with fully qualified type parameterization:
List
*/
private JContainer visitTypeParameterizations(@Nullable GenericsType[] genericsTypes) {
Space prefix = sourceBefore("<");
List> parameters;
//noinspection ConstantConditions
if (genericsTypes == null) {
// Groovy compiler does not always bother to record type parameter info in places it does not care about
Space paramPrefix = whitespace();
if (source.charAt(cursor) == '>') {
parameters = singletonList(JRightPadded.build(new J.Empty(randomId(), paramPrefix, Markers.EMPTY)));
} else {
parameters = new ArrayList<>();
while (true) {
Expression param = typeTree(null).withPrefix(paramPrefix);
Space suffix = whitespace();
parameters.add(JRightPadded.build(param).withAfter(suffix));
if (source.charAt(cursor) == '>') {
cursor++;
break;
}
cursor++;
paramPrefix = whitespace();
}
}
} else {
parameters = new ArrayList<>(genericsTypes.length);
for (int i = 0; i < genericsTypes.length; i++) {
parameters.add(JRightPadded.build(visitTypeParameterization(genericsTypes[i]))
.withAfter(
i < genericsTypes.length - 1 ?
sourceBefore(",") :
sourceBefore(">")
));
}
}
return JContainer.build(prefix, parameters, Markers.EMPTY);
}
private Expression visitTypeParameterization(GenericsType genericsType) {
int saveCursor = cursor;
Space prefix = whitespace();
if (source.charAt(cursor) == '?') {
cursor = saveCursor;
return visitWildcard(genericsType);
} else if (source.charAt(cursor) == '>') {
cursor++;
return new J.Empty(randomId(), prefix, Markers.EMPTY);
}
cursor = saveCursor;
return typeTree(null)
.withType(typeMapping.type(genericsType));
}
/*
Visit the declaration of a type parameter as part of a class declaration or method declaration
*/
private JContainer visitTypeParameters(GenericsType[] genericsTypes) {
Space prefix = sourceBefore("<");
List> typeParameters = new ArrayList<>(genericsTypes.length);
for (int i = 0; i < genericsTypes.length; i++) {
typeParameters.add(JRightPadded.build(visitTypeParameter(genericsTypes[i]))
.withAfter(
i < genericsTypes.length - 1 ?
sourceBefore(",") :
sourceBefore(">")
));
}
return JContainer.build(prefix, typeParameters, Markers.EMPTY);
}
private J.TypeParameter visitTypeParameter(GenericsType genericType) {
Space prefix = whitespace();
Expression name = typeTree(null)
.withType(typeMapping.type(genericType));
JContainer bounds = null;
if (genericType.getUpperBounds() != null) {
Space boundsPrefix = sourceBefore("extends");
ClassNode[] upperBounds = genericType.getUpperBounds();
List> convertedBounds = new ArrayList<>(upperBounds.length);
for (int i = 0; i < upperBounds.length; i++) {
convertedBounds.add(JRightPadded.build(visitTypeTree(upperBounds[i]))
.withAfter(
i < upperBounds.length - 1 ?
sourceBefore("&") :
EMPTY
));
}
bounds = JContainer.build(boundsPrefix, convertedBounds, Markers.EMPTY);
} else if (genericType.getLowerBound() != null) {
Space boundsPrefix = sourceBefore("super");
ClassNode lowerBound = genericType.getLowerBound();
List> convertedBounds = new ArrayList<>(1);
convertedBounds.add(JRightPadded.build(visitTypeTree(lowerBound))
.withAfter(EMPTY));
bounds = JContainer.build(boundsPrefix, convertedBounds, Markers.EMPTY);
}
return new J.TypeParameter(randomId(), prefix, Markers.EMPTY, emptyList(), emptyList(), name, bounds);
}
private J.Wildcard visitWildcard(GenericsType genericType) {
Space namePrefix = sourceBefore("?");
JLeftPadded bound;
NameTree boundedType;
if (genericType.getUpperBounds() != null) {
bound = padLeft(sourceBefore("extends"), J.Wildcard.Bound.Extends);
boundedType = visitTypeTree(genericType.getUpperBounds()[0]);
} else if (genericType.getLowerBound() != null) {
bound = padLeft(sourceBefore("super"), J.Wildcard.Bound.Super);
boundedType = visitTypeTree(genericType.getLowerBound());
} else {
bound = null;
boundedType = null;
}
return new J.Wildcard(randomId(), namePrefix, Markers.EMPTY, bound, boundedType);
}
/**
* Sometimes the groovy compiler inserts phantom elements into argument lists and class bodies,
* presumably to pass type information around. These elements do not appear in source code and should not
* be represented in our AST.
*
* @param node possible phantom node
* @return true if the node reports that it does have a position within the source code
*/
private static boolean appearsInSource(ASTNode node) {
return node.getColumnNumber() >= 0 && node.getLineNumber() >= 0 && node.getLastColumnNumber() >= 0 && node.getLastLineNumber() >= 0;
}
/**
* Static type checking for groovy is an add-on that places the type information it discovers into expression metadata.
*
* @param expression which may have more accurate type information in its metadata
* @return most accurate available type of the supplied node
*/
private static ClassNode staticType(org.codehaus.groovy.ast.expr.Expression expression) {
ClassNode inferred = (ClassNode) expression.getNodeMetaData().get(StaticTypesMarker.INFERRED_TYPE);
if (inferred == null) {
return expression.getType();
} else {
return inferred;
}
}
private static ClassNode staticType(Variable variable) {
if (variable instanceof Parameter) {
return staticType((Parameter) variable);
} else if (variable instanceof org.codehaus.groovy.ast.expr.Expression) {
return staticType((org.codehaus.groovy.ast.expr.Expression) variable);
}
return variable.getType();
}
private static ClassNode staticType(Parameter parameter) {
ClassNode inferred = (ClassNode) parameter.getNodeMetaData().get(StaticTypesMarker.INFERRED_TYPE);
if (inferred == null) {
return parameter.getType();
} else {
return inferred;
}
}
}