blob: 7f2dfe5acbb697e43327899a0b005248b5d20d8a [file] [log] [blame]
/*
* 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.
*/
package org.codehaus.groovy.ast.tools;
import groovy.lang.Tuple2;
import groovy.transform.stc.IncorrectTypeHintException;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.GenericsType;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.decompiled.DecompiledClassNode;
import org.codehaus.groovy.ast.expr.DeclarationExpression;
import org.codehaus.groovy.ast.stmt.EmptyStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.ParserPlugin;
import org.codehaus.groovy.control.ResolveVisitor;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.runtime.memoize.ConcurrentSoftCache;
import org.codehaus.groovy.runtime.memoize.EvictableCache;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.Function;
import java.util.function.Predicate;
import static org.apache.groovy.util.SystemUtil.getSystemPropertySafe;
import static org.codehaus.groovy.runtime.DefaultGroovyMethods.plus;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.implementsInterfaceOrIsSubclassOf;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isUnboundedWildcard;
/**
* Utility methods to deal with parameterized types.
*/
public class GenericsUtils {
/**
* @since 2.0.0
*/
public static final GenericsType[] EMPTY_GENERICS_ARRAY = GenericsType.EMPTY_ARRAY;
/**
* @since 3.0.0
*/
public static final String JAVA_LANG_OBJECT = ClassHelper.OBJECT;
/**
* Given a parameterized type and a generic type information, aligns actual type parameters. For example, if a
* class uses generic type <pre>&lt;T,U,V&gt;</pre> (redirectGenericTypes), is used with actual type parameters
* <pre>&lt;java.lang.String, U,V&gt;</pre>, then a class or interface using generic types <pre>&lt;T,V&gt;</pre>
* will be aligned to <pre>&lt;java.lang.String,V&gt;</pre>
*
* @param redirectGenericTypes the type arguments or the redirect class node
* @param parameterizedTypes the actual type arguments used on this class node
* @param alignmentTarget the generic type arguments to which we want to align to
* @return aligned type arguments
*
* @since 2.0.0
* @deprecated You shouldn't call this method because it is inherently unreliable.
*/
@Deprecated(forRemoval = true, since = "2.3.0")
public static GenericsType[] alignGenericTypes(final GenericsType[] redirectGenericTypes, final GenericsType[] parameterizedTypes, final GenericsType[] alignmentTarget) {
if (alignmentTarget == null) return EMPTY_GENERICS_ARRAY;
if (parameterizedTypes == null || parameterizedTypes.length == 0) return alignmentTarget;
GenericsType[] generics = new GenericsType[alignmentTarget.length];
for (int i = 0, scgtLength = alignmentTarget.length; i < scgtLength; i++) {
final GenericsType currentTarget = alignmentTarget[i];
GenericsType match = null;
if (redirectGenericTypes != null) {
for (int j = 0; j < redirectGenericTypes.length && match == null; j++) {
GenericsType redirectGenericType = redirectGenericTypes[j];
if (redirectGenericType.isCompatibleWith(currentTarget.getType())) {
if (currentTarget.isPlaceholder() && redirectGenericType.isPlaceholder() && !currentTarget.getName().equals(redirectGenericType.getName())) {
// check if there's a potential better match
boolean skip = false;
for (int k = j + 1; k < redirectGenericTypes.length && !skip; k++) {
GenericsType ogt = redirectGenericTypes[k];
if (ogt.isPlaceholder() && ogt.isCompatibleWith(currentTarget.getType()) && ogt.getName().equals(currentTarget.getName())) {
skip = true;
}
}
if (skip) continue;
}
match = parameterizedTypes[j];
if (currentTarget.isWildcard()) {
// if alignment target is a wildcard type
// then we must make best effort to return a parameterized
// wildcard
ClassNode lower = currentTarget.getLowerBound() != null ? match.getType() : null;
ClassNode[] currentUpper = currentTarget.getUpperBounds();
ClassNode[] upper = currentUpper != null ? new ClassNode[currentUpper.length] : null;
if (upper != null) {
for (int k = 0; k < upper.length; k++) {
upper[k] = currentUpper[k].isGenericsPlaceHolder() ? match.getType() : currentUpper[k];
}
}
match = new GenericsType(ClassHelper.makeWithoutCaching("?"), upper, lower);
match.setWildcard(true);
}
}
}
}
if (match == null) {
match = currentTarget;
}
generics[i] = match;
}
return generics;
}
/**
* Generates a wildcard generic type in order to be used for checks against
* class nodes. See {@link GenericsType#isCompatibleWith(ClassNode)}.
*
* @since 2.0.0
*/
public static GenericsType buildWildcardType(final ClassNode... upperBounds) {
GenericsType gt = new GenericsType(ClassHelper.makeWithoutCaching("?"), upperBounds, null);
gt.setWildcard(true);
return gt;
}
/**
* Returns the type parameter/argument relationships of the specified type.
*
* @since 2.0.0
*/
public static Map<GenericsType.GenericsTypeName, GenericsType> extractPlaceholders(final ClassNode type) {
Map<GenericsType.GenericsTypeName, GenericsType> placeholders = new HashMap<>();
extractPlaceholders(type, placeholders);
return placeholders;
}
/**
* Populates the supplied map with the type parameter/argument relationships
* of the specified type.
*
* @since 2.0.0
*/
public static void extractPlaceholders(final ClassNode type, final Map<GenericsType.GenericsTypeName, GenericsType> placeholders) {
if (type == null) return;
if (type.isArray()) {
extractPlaceholders(type.getComponentType(), placeholders);
return;
}
if (!type.isUsingGenerics() || !type.isRedirectNode()) return;
GenericsType[] genericsTypes = type.getGenericsTypes(); int n;
if (genericsTypes == null || (n = genericsTypes.length) == 0) return;
// GROOVY-8609, GROOVY-10067, etc.
if (type.isGenericsPlaceHolder()) {
GenericsType gt = genericsTypes[0];
placeholders.putIfAbsent(new GenericsType.GenericsTypeName(gt.getName()), gt);
return;
}
GenericsType[] redirectGenericsTypes = type.redirect().getGenericsTypes();
if (redirectGenericsTypes == null) {
redirectGenericsTypes = genericsTypes;
} else if (redirectGenericsTypes.length != n) {
throw new GroovyBugError("Expected earlier checking to detect generics parameter arity mismatch" +
"\nExpected: " + type.getName() + toGenericTypesString(redirectGenericsTypes) +
"\nSupplied: " + type.getName() + toGenericTypesString(genericsTypes));
}
List<GenericsType> typeArguments = new ArrayList<>(n);
for (int i = 0; i < n; i += 1) {
GenericsType rgt = redirectGenericsTypes[i];
if (rgt.isPlaceholder()) { // type parameter
GenericsType typeArgument = genericsTypes[i];
placeholders.computeIfAbsent(new GenericsType.GenericsTypeName(rgt.getName()), x -> {
typeArguments.add(typeArgument);
return typeArgument;
});
}
}
// examine non-placeholder type args
for (GenericsType gt : typeArguments) {
if (gt.isWildcard()) {
ClassNode lowerBound = gt.getLowerBound();
if (lowerBound != null) {
extractPlaceholders(lowerBound, placeholders);
} else {
ClassNode[] upperBounds = gt.getUpperBounds();
if (upperBounds != null) {
for (ClassNode upperBound : upperBounds) {
extractPlaceholders(upperBound, placeholders);
}
}
}
} else if (!gt.isPlaceholder()) {
extractPlaceholders(gt.getType(), placeholders);
}
}
}
/**
* @since 3.0.0
*/
public static String toGenericTypesString(final GenericsType[] genericsTypes) {
if (genericsTypes == null) return "";
StringJoiner sj = new StringJoiner(",","<","> ");
for (GenericsType genericsType : genericsTypes) {
sj.add(genericsType.toString());
}
return sj.toString();
}
/**
* Interface class nodes retrieved from {@link org.codehaus.groovy.ast.ClassNode#getInterfaces()}
* or {@link org.codehaus.groovy.ast.ClassNode#getAllInterfaces()} are returned with generic type
* arguments. This method allows returning a parameterized interface given the parameterized class
* node which implements this interface.
*
* @param hint the class node where generics types are parameterized
* @param target the interface we want to parameterize generics types
* @return a parameterized interface class node
*
* @since 2.0.0
* @deprecated Use #parameterizeType instead
*/
@Deprecated(forRemoval = true, since = "2.5.0")
public static ClassNode parameterizeInterfaceGenerics(final ClassNode hint, final ClassNode target) {
return parameterizeType(hint, target);
}
/**
* Interface class nodes retrieved from {@link org.codehaus.groovy.ast.ClassNode#getInterfaces()}
* or {@link org.codehaus.groovy.ast.ClassNode#getAllInterfaces()} are returned with generic type
* arguments. This method allows returning a parameterized interface given the parameterized class
* node which implements this interface.
*
* @param hint the ClassNode where generics types are parameterized
* @param target the interface we want to parameterize generics types
* @return a parameterized interface ClassNode
*
* @since 2.1.0
*/
public static ClassNode parameterizeType(ClassNode hint, final ClassNode target) {
if (hint.isArray()) {
if (target.isArray()) {
return parameterizeType(hint.getComponentType(), target.getComponentType()).makeArray();
}
return target;
}
if (hint.isGenericsPlaceHolder()) {
ClassNode bound = hint.redirect();
return parameterizeType(bound, target);
}
if (target.redirect().getGenericsTypes() == null) {
return target;
}
ClassNode cn = target;
Map<String, ClassNode> gt;
// relationship may be reversed for cases like "Iterable<String> x = []"
if (!cn.equals(hint) && implementsInterfaceOrIsSubclassOf(target, hint)) {
do { // walk target type hierarchy towards hint
cn = ClassHelper.getNextSuperClass(cn, hint);
if (hasUnresolvedGenerics(cn)) {
gt = createGenericsSpec(hint);
extractSuperClassGenerics(hint, cn, gt);
cn = correctToGenericsSpecRecurse(gt, cn);
}
} while (!cn.equals(hint));
hint = cn;
}
cn = target.redirect();
gt = createGenericsSpec(hint);
gt = createGenericsSpec(cn, gt);
extractSuperClassGenerics(hint, cn, gt);
return correctToGenericsSpecRecurse(gt, cn);
}
/**
* @since 2.3.0
*/
public static ClassNode nonGeneric(final ClassNode type) {
int dims = 0;
ClassNode temp = type;
while (temp.isArray()) { dims += 1;
temp = temp.getComponentType();
}
if (temp instanceof DecompiledClassNode // GROOVY-10461: check without resolving supers
? ((DecompiledClassNode) temp).isParameterized() : temp.isUsingGenerics()) {
ClassNode result = ClassHelper.makeWithoutCaching(temp.getName());
result.setRedirect(temp);
result.setGenericsTypes(null);
result.setUsingGenerics(false);
while (dims > 0) { dims -= 1;
result = result.makeArray();
}
return result;
}
return type;
}
/**
* @since 2.3.0
*/
public static ClassNode newClass(ClassNode type) {
return type.getPlainNodeReference();
}
/**
* @since 2.3.0
*/
public static ClassNode makeClassSafe(Class klass) {
return makeClassSafeWithGenerics(ClassHelper.make(klass));
}
/**
* @since 2.4.0
*/
public static ClassNode makeClassSafeWithGenerics(Class klass, ClassNode genericsType) {
GenericsType[] genericsTypes = new GenericsType[1];
genericsTypes[0] = new GenericsType(genericsType);
return makeClassSafeWithGenerics(ClassHelper.make(klass), genericsTypes);
}
/**
* @since 2.3.0
*/
public static ClassNode makeClassSafe0(ClassNode type, GenericsType... genericTypes) {
ClassNode plainNodeReference = newClass(type);
if (genericTypes != null && genericTypes.length > 0) {
plainNodeReference.setGenericsTypes(genericTypes);
if (type.isGenericsPlaceHolder()) plainNodeReference.setGenericsPlaceHolder(true);
}
return plainNodeReference;
}
/**
* @since 2.3.0
*/
public static ClassNode makeClassSafeWithGenerics(ClassNode type, GenericsType... genericTypes) {
if (type.isArray()) {
return makeClassSafeWithGenerics(type.getComponentType(), genericTypes).makeArray();
}
int nTypes = (genericTypes == null ? 0 : genericTypes.length);
GenericsType[] gTypes;
if (nTypes == 0) {
gTypes = EMPTY_GENERICS_ARRAY;
} else {
gTypes = new GenericsType[nTypes];
System.arraycopy(genericTypes, 0, gTypes, 0, nTypes);
}
return makeClassSafe0(type, gTypes);
}
/**
* @since 2.3.0
*/
public static MethodNode correctToGenericsSpec(Map<String, ClassNode> genericsSpec, MethodNode mn) {
if (genericsSpec == null) return mn;
if (mn.getGenericsTypes() != null) genericsSpec = addMethodGenerics(mn, genericsSpec);
ClassNode returnType = correctToGenericsSpecRecurse(genericsSpec, mn.getReturnType());
Parameter[] oldParameters = mn.getParameters(); int nParameters= oldParameters.length;
Parameter[] newParameters = new Parameter[nParameters];
for (int i = 0; i < nParameters; i += 1) {
Parameter oldParameter = oldParameters[i];
newParameters[i] = new Parameter(correctToGenericsSpecRecurse(genericsSpec, oldParameter.getType()), oldParameter.getName(), oldParameter.getInitialExpression());
}
MethodNode newMethod = new MethodNode(mn.getName(), mn.getModifiers(), returnType, newParameters, mn.getExceptions(), mn.getCode());
newMethod.setGenericsTypes(mn.getGenericsTypes());
return newMethod;
}
/**
* @since 2.3.0
*/
public static ClassNode correctToGenericsSpecRecurse(Map<String, ClassNode> genericsSpec, ClassNode type) {
return correctToGenericsSpecRecurse(genericsSpec, type, Collections.emptyList());
}
/**
* @since 2.4.1
*/
public static ClassNode[] correctToGenericsSpecRecurse(Map<String, ClassNode> genericsSpec, ClassNode[] types) {
if (types == null || types.length == 1) return types;
ClassNode[] newTypes = new ClassNode[types.length];
boolean modified = false;
for (int i = 0; i < types.length; i++) {
newTypes[i] = correctToGenericsSpecRecurse(genericsSpec, types[i], Collections.emptyList());
modified = modified || (types[i] != newTypes[i]);
}
if (!modified) return types;
return newTypes;
}
/**
* @since 2.4.1
*/
public static ClassNode correctToGenericsSpecRecurse(Map<String, ClassNode> genericsSpec, ClassNode type, List<String> exclusions) {
if (type.isArray()) {
return correctToGenericsSpecRecurse(genericsSpec, type.getComponentType(), exclusions).makeArray();
}
String name = type.getUnresolvedName();
if (type.isGenericsPlaceHolder() && !exclusions.contains(name)) {
exclusions = plus(exclusions, name); // GROOVY-7722
type = genericsSpec.get(name);
if (type != null && type.isGenericsPlaceHolder()) {
if (type.getGenericsTypes() == null) {
ClassNode placeholder = ClassHelper.makeWithoutCaching(type.getUnresolvedName());
placeholder.setGenericsPlaceHolder(true);
return makeClassSafeWithGenerics(type, new GenericsType(placeholder));
} else if (!name.equals(type.getUnresolvedName())) {
return correctToGenericsSpecRecurse(genericsSpec, type, exclusions);
}
}
}
if (type == null) type = ClassHelper.OBJECT_TYPE.getPlainNodeReference();
GenericsType[] oldgTypes = type.getGenericsTypes();
GenericsType[] newgTypes = EMPTY_GENERICS_ARRAY;
if (oldgTypes != null) {
newgTypes = new GenericsType[oldgTypes.length];
for (int i = 0; i < newgTypes.length; i++) {
GenericsType oldgType = oldgTypes[i];
if (oldgType.isWildcard()) {
ClassNode[] oldUpper = oldgType.getUpperBounds();
ClassNode[] upper = null;
if (oldUpper != null) {
// correct "? extends T" or "? extends T & I"
upper = new ClassNode[oldUpper.length];
for (int j = 0; j < oldUpper.length; j++) {
upper[j] = correctToGenericsSpecRecurse(genericsSpec, oldUpper[j], exclusions);
}
}
ClassNode oldLower = oldgType.getLowerBound();
ClassNode lower = null;
if (oldLower != null) {
// correct "? super T"
lower = correctToGenericsSpecRecurse(genericsSpec, oldLower, exclusions);
}
GenericsType fixed = new GenericsType(oldgType.getType(), upper, lower);
fixed.setWildcard(true);
newgTypes[i] = fixed;
} else if (oldgType.isPlaceholder()) {
// correct "T"
newgTypes[i] = new GenericsType(genericsSpec.getOrDefault(oldgType.getName(), ClassHelper.OBJECT_TYPE));
} else {
// correct "List<T>", etc.
newgTypes[i] = new GenericsType(correctToGenericsSpecRecurse(genericsSpec, correctToGenericsSpec(genericsSpec, oldgType), exclusions));
}
}
}
return makeClassSafeWithGenerics(type, newgTypes);
}
/**
* @since 2.3.0
*/
public static ClassNode correctToGenericsSpec(final Map<String, ClassNode> genericsSpec, final GenericsType type) {
ClassNode cn = null;
if (type.isPlaceholder()) {
String name = type.getName();
if (name.charAt(0) != '#') //
cn = genericsSpec.get(name);
}
else if (type.isWildcard()) {
if (type.getUpperBounds() != null)
cn = type.getUpperBounds()[0]; // GROOVY-9891
}
if (cn == null) {
cn = type.getType();
}
return cn;
}
/**
* @since 2.3.0
*/
public static ClassNode correctToGenericsSpec(final Map<String, ClassNode> genericsSpec, ClassNode type) {
if (type.isArray()) {
return correctToGenericsSpec(genericsSpec, type.getComponentType()).makeArray();
}
if (type.isGenericsPlaceHolder() && type.getGenericsTypes() != null) {
String name = type.getGenericsTypes()[0].getName();
type = genericsSpec.get(name);
if (type != null && type.isGenericsPlaceHolder()
&& !name.equals(type.getUnresolvedName())) {
return correctToGenericsSpec(genericsSpec, type);
}
}
return type != null ? type : ClassHelper.OBJECT_TYPE.getPlainNodeReference();
}
/**
* @since 2.4.0
*/
public static Map<String, ClassNode> createGenericsSpec(final ClassNode type) {
return createGenericsSpec(type, Collections.emptyMap());
}
/**
* @since 2.3.0
*/
public static Map<String, ClassNode> createGenericsSpec(final ClassNode type, final Map<String, ClassNode> oldSpec) {
// Example:
// abstract class A<X,Y,Z> { ... }
// class C<T extends Number> extends A<T,Object,String> { }
// the type "A<T,Object,String> -> A<X,Y,Z>" will produce [X:Number,Y:Object,Z:String]
ClassNode oc = type.getNodeMetaData("outer.class"); // GROOVY-10646: outer class type parameters
Map<String, ClassNode> newSpec = oc != null ? createGenericsSpec(oc, oldSpec) : new HashMap<>();
GenericsType[] gt = type.getGenericsTypes(), rgt = type.redirect().getGenericsTypes();
if (gt != null && rgt != null) {
for (int i = 0, n = gt.length; i < n; i += 1) {
newSpec.put(rgt[i].getName(), correctToGenericsSpec(oldSpec, gt[i]));
}
}
return newSpec;
}
/**
* @since 2.4.1
*/
public static Map<String, ClassNode> addMethodGenerics(final MethodNode node, final Map<String, ClassNode> oldSpec) {
Map<String, ClassNode> newSpec = new HashMap<>(oldSpec);
GenericsType[] tps = node.getGenericsTypes();
if (tps != null) {
for (GenericsType tp : tps) {
String name = tp.getName();
ClassNode type = tp.getType();
ClassNode redirect;
if (tp.getUpperBounds() != null) {
redirect = tp.getUpperBounds()[0];
} else {
redirect = ClassHelper.OBJECT_TYPE;
}
if (redirect.isGenericsPlaceHolder()) {
// "T extends U (& Face)*"
type = redirect;
} else {
// "T" or "T extends Type (& Face)*"
type = ClassHelper.makeWithoutCaching(name);
type.setGenericsPlaceHolder(true);
type.setRedirect(redirect);
}
newSpec.put(name, type);
}
}
return newSpec;
}
/**
* @since 2.3.1
*/
public static void extractSuperClassGenerics(final ClassNode type, final ClassNode target, final Map<String, ClassNode> spec) {
// TODO: this is very similar to StaticTypesCheckingSupport#extractGenericsConnections, using ClassNode instead of GenericsType
if (target == null || target == type) return;
if (target.isGenericsPlaceHolder()) {
spec.put(target.getUnresolvedName(), type);
} else if (type.isArray() && target.isArray()) {
extractSuperClassGenerics(type.getComponentType(), target.getComponentType(), spec);
} else if (type.isArray() && target.getName().equals(JAVA_LANG_OBJECT)) {
// Object is the superclass of an array, but no generics are involved
} else if (type.equals(target) || !implementsInterfaceOrIsSubclassOf(type, target)) {
extractSuperClassGenerics(type.getGenericsTypes(), target.getGenericsTypes(), spec);
} else {
ClassNode superClass = getSuperClass(type, target);
if (superClass != null) {
if (hasUnresolvedGenerics(superClass)) {
GenericsType[] tp = type.redirect().getGenericsTypes();
if (tp != null) {
GenericsType[] ta = type.getGenericsTypes();
boolean noTypeArguments = ta == null || ta.length == 0 || !type.isRedirectNode();
Map<String, ClassNode> genericsSpec = new HashMap<>();
for (int i = 0, n = tp.length; i < n; i += 1) {
ClassNode cn;
if (noTypeArguments || isUnboundedWildcard(ta[i])) { // GROOVY-10651
GenericsType gt = tp[i];
cn = gt.getUpperBounds() != null ? gt.getUpperBounds()[0] : gt.getType().redirect();
} else {
GenericsType gt = ta[i];
cn = gt.isWildcard() && gt.getUpperBounds() != null ? gt.getUpperBounds()[0] : gt.getType();
}
genericsSpec.put(tp[i].getName(), cn);
}
superClass = correctToGenericsSpecRecurse(genericsSpec, superClass);
}
}
extractSuperClassGenerics(superClass, target, spec);
} else {
// if we reach here, we have an unhandled case
throw new GroovyBugError("The type " + type + " seems not to normally extend " + target + ". Sorry, I cannot handle this.");
}
}
}
/**
* @since 3.0.0
*/
public static ClassNode getSuperClass(final ClassNode type, final ClassNode target) {
return ClassHelper.getNextSuperClass(ClassHelper.isPrimitiveType(type) ? ClassHelper.getWrapper(type) : type, target);
}
private static void extractSuperClassGenerics(final GenericsType[] usage, final GenericsType[] declaration, final Map<String, ClassNode> spec) {
// if declaration does not provide generics, there is no connection to make
if (declaration == null || declaration.length == 0) return;
// if usage is a raw type, remove type parameters from spec
if (usage == null) {
for (GenericsType dt : declaration) {
String name = dt.getName();
ClassNode type = spec.get(name);
if (type != null && type.isGenericsPlaceHolder()
&& type.getUnresolvedName().equals(name)) {
type = type.asGenericsType().getUpperBounds()[0];
spec.put(name, type);
}
}
return;
}
if (usage.length != declaration.length) return;
for (int i = 0, n = usage.length; i < n; i += 1) {
GenericsType ui = usage[i];
GenericsType di = declaration[i];
if (di.isPlaceholder()) {
spec.put(di.getName(), ui.getType());
} else if (di.isWildcard()) {
if (ui.isWildcard()) {
extractSuperClassGenerics(ui.getLowerBound(), di.getLowerBound(), spec);
extractSuperClassGenerics(ui.getUpperBounds(), di.getUpperBounds(), spec);
} else {
ClassNode cu = ui.getType();
extractSuperClassGenerics(cu, di.getLowerBound(), spec);
ClassNode[] upperBounds = di.getUpperBounds();
if (upperBounds != null) {
for (ClassNode cn : upperBounds) {
extractSuperClassGenerics(cu, cn, spec);
}
}
}
} else {
extractSuperClassGenerics(ui.getType(), di.getType(), spec);
}
}
}
private static void extractSuperClassGenerics(final ClassNode[] usage, final ClassNode[] declaration, final Map<String, ClassNode> spec) {
if (usage == null || declaration == null || declaration.length == 0) return;
// both have generics
for (int i = 0; i < usage.length; i++) {
ClassNode ui = usage[i];
ClassNode di = declaration[i];
if (di.isGenericsPlaceHolder()) {
spec.put(di.getGenericsTypes()[0].getName(), di);
} else if (di.isUsingGenerics()) {
extractSuperClassGenerics(ui.getGenericsTypes(), di.getGenericsTypes(), spec);
}
}
}
/**
* @since 2.4.0
*/
public static ClassNode[] parseClassNodesFromString(final String option, final SourceUnit sourceUnit, final CompilationUnit compilationUnit, final MethodNode mn, final ASTNode usage) {
try {
ModuleNode moduleNode = ParserPlugin.buildAST("Dummy<" + option + "> dummy;", compilationUnit.getConfiguration(), compilationUnit.getClassLoader(), null);
DeclarationExpression dummyDeclaration = (DeclarationExpression) ((ExpressionStatement) moduleNode.getStatementBlock().getStatements().get(0)).getExpression();
// the returned node is DummyNode<Param1, Param2, Param3, ...)
ClassNode dummyNode = dummyDeclaration.getLeftExpression().getType();
GenericsType[] dummyNodeGenericsTypes = dummyNode.getGenericsTypes();
if (dummyNodeGenericsTypes != null) {
int n = dummyNodeGenericsTypes.length;
ClassNode[] signature = new ClassNode[n];
for (int i = 0; i < n; i += 1) {
GenericsType genericsType = dummyNodeGenericsTypes[i];
signature[i] = genericsType.isWildcard() ? ClassHelper.dynamicType()
: resolveClassNode(sourceUnit, compilationUnit, mn, usage, genericsType.getType());
}
return signature;
}
} catch (Exception | LinkageError e) {
sourceUnit.addError(new IncorrectTypeHintException(mn, e, usage.getLineNumber(), usage.getColumnNumber()));
}
return null;
}
private static ClassNode resolveClassNode(final SourceUnit sourceUnit, final CompilationUnit compilationUnit, final MethodNode mn, final ASTNode usage, final ClassNode parsedNode) {
ClassNode dummyClass = new ClassNode("dummy", 0, ClassHelper.OBJECT_TYPE);
dummyClass.setModule(new ModuleNode(sourceUnit));
dummyClass.setGenericsTypes(mn.getDeclaringClass().getGenericsTypes());
MethodNode dummyMN = new MethodNode(
"dummy",
0,
parsedNode,
Parameter.EMPTY_ARRAY,
ClassNode.EMPTY_ARRAY,
EmptyStatement.INSTANCE
);
dummyMN.setGenericsTypes(mn.getGenericsTypes());
dummyClass.addMethod(dummyMN);
ResolveVisitor visitor = new ResolveVisitor(compilationUnit) {
@Override
public void addError(final String msg, final ASTNode expr) {
sourceUnit.addError(new IncorrectTypeHintException(mn, msg, usage.getLineNumber(), usage.getColumnNumber()));
}
};
visitor.startResolving(dummyClass, sourceUnit);
return dummyMN.getReturnType();
}
/**
* Transforms generics types from an old context to a new context using the
* given spec. This method assumes all generics types will be placeholders.
* WARNING: The resulting generics types may or may not be placeholders
* after the transformation.
*
* @param genericsSpec the generics context information spec
* @param oldPlaceHolders the old placeholders
* @return the new generics types
*
* @since 2.5.0
*/
public static GenericsType[] applyGenericsContextToPlaceHolders(Map<String, ClassNode> genericsSpec, GenericsType[] oldPlaceHolders) {
if (oldPlaceHolders == null || oldPlaceHolders.length == 0) return oldPlaceHolders;
if (genericsSpec.isEmpty()) return oldPlaceHolders;
GenericsType[] newTypes = new GenericsType[oldPlaceHolders.length];
for (int i = 0; i < oldPlaceHolders.length; i++) {
GenericsType old = oldPlaceHolders[i];
if (!old.isPlaceholder())
throw new GroovyBugError("Given generics type " + old + " must be a placeholder!");
ClassNode fromSpec = genericsSpec.get(old.getName());
if (fromSpec != null) {
newTypes[i] = fromSpec.asGenericsType();
} else {
ClassNode[] upper = old.getUpperBounds();
ClassNode[] newUpper = upper;
if (upper != null && upper.length > 0) {
ClassNode[] upperCorrected = new ClassNode[upper.length];
for (ClassNode classNode : upper) {
upperCorrected[i] = correctToGenericsSpecRecurse(genericsSpec, classNode);
}
upper = upperCorrected;
}
ClassNode lower = old.getLowerBound();
ClassNode newLower = correctToGenericsSpecRecurse(genericsSpec, lower);
if (lower == newLower && upper == newUpper) {
newTypes[i] = oldPlaceHolders[i];
} else {
ClassNode newPlaceHolder = ClassHelper.make(old.getName());
GenericsType gt = new GenericsType(newPlaceHolder, newUpper, newLower);
gt.setPlaceholder(true);
newTypes[i] = gt;
}
}
}
return newTypes;
}
private static final boolean PARAMETERIZED_TYPE_CACHE_ENABLED = Boolean.parseBoolean(getSystemPropertySafe("groovy.enable.parameterized.type.cache", "true"));
private static final EvictableCache<ParameterizedTypeCacheKey, SoftReference<ClassNode>> PARAMETERIZED_TYPE_CACHE = new ConcurrentSoftCache<>(64);
/**
* Clears the parameterized type cache.
* <p>
* It is useful to IDE as the type being compiled are continuously being edited/altered; see GROOVY-8675
*
* @since 3.0.0
*/
public static void clearParameterizedTypeCache() {
PARAMETERIZED_TYPE_CACHE.clearAll();
}
/**
* Convenience method for {@link #findParameterizedTypeFromCache(ClassNode,ClassNode,boolean)}
* with {@code tryToFindExactType} set to {@code false}.
*
* @since 3.0.0
*/
public static ClassNode findParameterizedTypeFromCache(final ClassNode genericsClass, final ClassNode actualType) {
return findParameterizedTypeFromCache(genericsClass, actualType, false);
}
/**
* Try to get the parameterized type from the cache.
* <p>
* If no cached item found, cache and return the result of {@link #findParameterizedType(ClassNode,ClassNode,boolean)}.
*
* @since 3.0.0
*/
public static ClassNode findParameterizedTypeFromCache(final ClassNode genericsClass, final ClassNode actualType, boolean tryToFindExactType) {
if (!PARAMETERIZED_TYPE_CACHE_ENABLED) {
return findParameterizedType(genericsClass, actualType, tryToFindExactType);
}
SoftReference<ClassNode> sr = PARAMETERIZED_TYPE_CACHE.getAndPut(
new ParameterizedTypeCacheKey(genericsClass, actualType),
key -> new SoftReference<>(findParameterizedType(key.getGenericsClass(), key.getActualType(), tryToFindExactType)));
return sr != null ? sr.get() : null;
}
/**
* Convenience method for {@link #findParameterizedType(ClassNode,ClassNode,boolean)} with {@code tryToFindExactType} set to {@code false}.
*
* @since 3.0.0
*/
public static ClassNode findParameterizedType(final ClassNode genericsClass, final ClassNode actualType) {
return findParameterizedType(genericsClass, actualType, false);
}
/**
* Gets the parameterized type by searching the whole class hierarchy according to generics class and actual receiver.
* <p>
* {@link #findParameterizedTypeFromCache(ClassNode,ClassNode,boolean)} is strongly recommended for better performance.
*
* @since 3.0.0
*/
public static ClassNode findParameterizedType(final ClassNode genericsClass, final ClassNode actualType, final boolean tryToFindExactType) {
final GenericsType[] genericsTypes = genericsClass.getGenericsTypes();
if (genericsTypes == null || genericsClass.isGenericsPlaceHolder()) {
return null;
}
if (actualType.equals(genericsClass)) {
return actualType;
}
LinkedList<ClassNode> todo = new LinkedList<>();
Set<ClassNode> done = new HashSet<>();
todo.add(actualType);
ClassNode type;
while ((type = todo.poll()) != null) {
if (done.add(type)) {
if (!type.isInterface()) {
ClassNode cn = type.getUnresolvedSuperClass();
if (cn != null && cn.redirect() != ClassHelper.OBJECT_TYPE) {
if (hasUnresolvedGenerics(cn)) {
cn = parameterizeType(type, cn);
}
if (cn.equals(genericsClass)) {
return cn;
}
todo.add(cn);
}
}
for (ClassNode cn : type.getInterfaces()) {
if (hasUnresolvedGenerics(cn)) {
cn = parameterizeType(type, cn);
}
if (cn.equals(genericsClass)) {
return cn;
}
todo.add(cn);
}
}
}
return null;
}
/**
* map declaring generics type to actual generics type, e.g. GROOVY-7204:
* declaring generics types: T, S extends Serializable
* actual generics types : String, Long
*
* the result map is [
* T: String,
* S: Long
* ]
*
* The resolved types can not help us to choose methods correctly if the argument is a string: T: Object, S: Serializable
* so we need actual types: T: String, S: Long
*
* @since 3.0.0
*/
public static Map<GenericsType, GenericsType> makeDeclaringAndActualGenericsTypeMap(final ClassNode declaringClass, final ClassNode actualReceiver) {
return correlateTypeParametersAndTypeArguments(declaringClass, actualReceiver, false);
}
/**
* The method is similar with {@link GenericsUtils#makeDeclaringAndActualGenericsTypeMap(ClassNode, ClassNode)},
* The main difference is that the method will try to map all placeholders found to the relevant exact types,
* but the other will not try even if the parameterized type has placeholders
*
* @param declaringClass the generics class node declaring the generics types
* @param actualReceiver the subclass class node
* @return the placeholder-to-actualtype mapping
*
* @since 3.0.0
*/
public static Map<GenericsType, GenericsType> makeDeclaringAndActualGenericsTypeMapOfExactType(final ClassNode declaringClass, final ClassNode actualReceiver) {
return correlateTypeParametersAndTypeArguments(declaringClass, actualReceiver, true);
}
private static Map<GenericsType, GenericsType> correlateTypeParametersAndTypeArguments(final ClassNode declaringClass, final ClassNode actualReceiver, final boolean tryToFindExactType) {
ClassNode parameterizedType = findParameterizedTypeFromCache(declaringClass, actualReceiver, tryToFindExactType);
if (parameterizedType != null && parameterizedType.isRedirectNode() && !parameterizedType.isGenericsPlaceHolder()) { // GROOVY-10166
// declaringClass may be "List<T> -> List<E>" and parameterizedType may be "List<String> -> List<E>" or "List<> -> List<E>"
final GenericsType[] typeParameters = parameterizedType.redirect().getGenericsTypes();
if (typeParameters != null) {
final GenericsType[] typeArguments = parameterizedType.getGenericsTypes();
final int m = typeArguments == null ? 0 : typeArguments.length;
final int n = typeParameters.length;
Map<GenericsType, GenericsType> map = new LinkedHashMap<>();
for (int i = 0; i < n; i += 1) {
map.put(typeParameters[i], i < m ? typeArguments[i] : erasure(typeParameters[i]));
}
return map;
}
}
return Collections.emptyMap();
}
/**
* @see org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport#extractType(GenericsType)
*/
private static GenericsType erasure(GenericsType gt) {
ClassNode cn = gt.getType().redirect(); // discard the placeholder
if (gt.getType().getGenericsTypes() != null)
gt = gt.getType().getGenericsTypes()[0];
if (gt.getUpperBounds() != null)
cn = gt.getUpperBounds()[0]; // TODO: if length > 1 then union type?
return cn.asGenericsType();
}
/**
* Checks if the type has any non-placeholder (aka resolved) generics.
*
* @since 3.0.0
*/
public static boolean hasNonPlaceHolders(final ClassNode type) {
return checkPlaceHolders(type, gt -> !gt.isPlaceholder());
}
/**
* Checks if the type has any placeholder (aka unresolved) generics.
*
* @since 3.0.0
*/
public static boolean hasPlaceHolders(final ClassNode type) {
return checkPlaceHolders(type, gt -> gt.isPlaceholder());
}
private static boolean checkPlaceHolders(final ClassNode type, final Predicate<GenericsType> p) {
if (type != null) {
GenericsType[] genericsTypes = type.getGenericsTypes();
if (genericsTypes != null) {
for (GenericsType genericsType : genericsTypes) {
if (p.test(genericsType)) {
return true;
}
}
}
}
return false;
}
/**
* Checks for any placeholder (aka unresolved) generics.
*
* @since 4.0.0
*/
public static boolean hasUnresolvedGenerics(final ClassNode type) {
if (type.isGenericsPlaceHolder()) return true;
if (type.isArray()) {
return hasUnresolvedGenerics(type.getComponentType());
}
GenericsType[] genericsTypes = type.getGenericsTypes();
if (genericsTypes != null) {
for (GenericsType genericsType : genericsTypes) {
if (genericsType.isPlaceholder()) return true;
ClassNode lowerBound = genericsType.getLowerBound();
ClassNode[] upperBounds = genericsType.getUpperBounds();
if (lowerBound != null) {
if (hasUnresolvedGenerics(lowerBound)) return true;
} else if (upperBounds != null) {
for (ClassNode upperBound : upperBounds) {
if (hasUnresolvedGenerics(upperBound)) return true;
}
} else {
if (hasUnresolvedGenerics(genericsType.getType())) return true;
}
}
}
return false;
}
/**
* Gets the parameter and return types of the abstract method of SAM.
*
* If the abstract method is not parameterized, we will get generics placeholders, e.g. T, U
* For example, the abstract method of {@link java.util.function.Function} is
* <pre>
* R apply(T t);
* </pre>
*
* We parameterize the above interface as {@code Function<String, Integer>}, then the abstract method will be
* <pre>
* Integer apply(String t);
* </pre>
*
* When we call {@code parameterizeSAM} on the ClassNode {@code Function<String, Integer>},
* we can get parameter types and return type of the above abstract method,
* i.e. ClassNode {@code ClassHelper.STRING_TYPE} and {@code ClassHelper.Integer_TYPE}
*
* @param samType the class node which contains only one abstract method
*
* @since 3.0.0
*/
public static Tuple2<ClassNode[], ClassNode> parameterizeSAM(final ClassNode samType) {
MethodNode abstractMethod = ClassHelper.findSAM(samType);
Map<GenericsType, GenericsType> generics = makeDeclaringAndActualGenericsTypeMapOfExactType(abstractMethod.getDeclaringClass(), samType);
Function<ClassNode , ClassNode> resolver = t -> {
return t.isGenericsPlaceHolder() ? findActualTypeByGenericsPlaceholderName(t.getUnresolvedName(), generics) : t;
};
ClassNode[] parameterTypes = Arrays.stream(abstractMethod.getParameters()).map(Parameter::getType).map(resolver).toArray(ClassNode[]::new);
ClassNode returnType = resolver.apply(abstractMethod.getReturnType());
return new Tuple2<>(parameterTypes, returnType);
}
/**
* Gets the actual type according to the placeholder name.
*
* @param placeholderName the placeholder name (i.e. "T", "E", etc.)
* @param genericsPlaceholderAndTypeMap the result of {@link #makeDeclaringAndActualGenericsTypeMap}
*
* @since 3.0.0
*/
public static ClassNode findActualTypeByGenericsPlaceholderName(final String placeholderName, final Map<GenericsType, GenericsType> genericsPlaceholderAndTypeMap) {
Function<GenericsType, ClassNode> resolver = gt -> {
if (gt.isWildcard()) {
if (gt.getLowerBound() != null) {
return gt.getLowerBound();
}
if (gt.getUpperBounds() != null) {
return gt.getUpperBounds()[0];
}
}
return gt.getType();
};
return genericsPlaceholderAndTypeMap.entrySet().stream()
.filter(e -> e.getKey().getName().equals(placeholderName))
.map(Map.Entry::getValue).map(resolver).findFirst().orElse(null);
}
private static class ParameterizedTypeCacheKey {
private ClassNode genericsClass;
private ClassNode actualType;
public ParameterizedTypeCacheKey(ClassNode genericsClass, ClassNode actualType) {
this.genericsClass = genericsClass;
this.actualType = actualType;
}
public ClassNode getGenericsClass() {
return genericsClass;
}
@SuppressWarnings("unused")
public void setGenericsClass(ClassNode genericsClass) {
this.genericsClass = genericsClass;
}
public ClassNode getActualType() {
return actualType;
}
@SuppressWarnings("unused")
public void setActualType(ClassNode actualType) {
this.actualType = actualType;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ParameterizedTypeCacheKey cacheKey = (ParameterizedTypeCacheKey) o;
return genericsClass == cacheKey.genericsClass &&
actualType == cacheKey.actualType;
}
@Override
public int hashCode() {
return Objects.hash(genericsClass, actualType);
}
}
}