/* * Copyright 2009-2017 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 * * 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.eclipse.jdt.groovy.search; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.TreeMap; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.GenericsType; import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.ast.Parameter; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jdt.groovy.core.util.GroovyUtils; import org.eclipse.jdt.internal.core.util.Util; /** * Maps type parameters to resolved types. */ public class GenericsMapper { /** * Creates a mapper for a particular resolved type tracing up the type hierarchy until the declaring type is reached. * This is the public entry point for this class. * * @param resolvedType unredirected type that has generic types already parameterized * @param declaringType a type that is somewhere in resolvedType's hierarchy used to find the target of the mapping */ public static GenericsMapper gatherGenerics(ClassNode resolvedType, ClassNode declaringType) { GenericsMapper mapper = new GenericsMapper(); ClassNode rCandidate = resolvedType; ClassNode uCandidate = resolvedType.redirect(); Iterator<ClassNode> rIterator = getTypeHierarchy(rCandidate, true); Iterator<ClassNode> uIterator = getTypeHierarchy(uCandidate, false); // travel up the hierarchy while (rIterator.hasNext() && uIterator.hasNext()) { rCandidate = rIterator.next(); uCandidate = uIterator.next(); GenericsType[] rgts = GroovyUtils.getGenericsTypes(rCandidate); GenericsType[] ugts = GroovyUtils.getGenericsTypes(uCandidate); int n = Math.min(rgts.length, ugts.length); Map<String, ClassNode> resolved = (n <= 0) ? Collections.EMPTY_MAP : new TreeMap<String, ClassNode>(); for (int i = 0; i < n; i += 1) { // now try to resolve the parameter in the context of the // most recently visited type. If it doesn't exist, then // default to the resovled type resolved.put(ugts[i].getName(), mapper.resolveParameter(rgts[i], 0)); } mapper.allGenerics.add(resolved); // don't need to travel up the whole hierarchy; stop at the declaring class if (rCandidate.getName().equals(declaringType.getName())) { break; } } return mapper; } public static GenericsMapper gatherGenerics(List<ClassNode> argumentTypes, ClassNode delegateOrThisType, MethodNode methodDeclaration, GenericsType... methodGenerics) { // GOAL: resolve return type of something like "static <T> Iterator<T> iterator(T[] array)" // inspect owner type for generics GenericsMapper mapper = gatherGenerics(delegateOrThisType, methodDeclaration.getDeclaringClass()); GenericsType[] ugts = GroovyUtils.getGenericsTypes(methodDeclaration); if (ugts.length > 0) { Map<String, ClassNode> resolved; // add method generics to the end of the chain if (mapper.allGenerics.isEmpty() || (resolved = mapper.allGenerics.removeLast()).isEmpty()) { resolved = new TreeMap<String, ClassNode>(); } mapper.allGenerics.add(resolved); if (methodGenerics != null && methodGenerics.length > 0) { assert methodGenerics.length == ugts.length; // method generics are explicitly defined for (int i = 0; i < ugts.length; i += 1) { resolved.put(ugts[i].getName(), methodGenerics[i].getType()); } } else if (argumentTypes != null) { // try to resolve each generics type by matching arguments to parameters for (GenericsType ugt : ugts) { Parameter[] methodParameters = methodDeclaration.getParameters(); for (int i = 0, n = isVargs(methodParameters) ? argumentTypes.size() : Math.min(argumentTypes.size(), methodParameters.length); i < n; i += 1) { ClassNode rbt = argumentTypes.get(i); ClassNode ubt = methodParameters[Math.min(i, methodParameters.length - 1)].getType(); while (rbt.isArray() && ubt.isArray()) { rbt = rbt.getComponentType(); ubt = ubt.getComponentType(); } // rbt could be "String" and ubt could be "T" or "T extends CharSequence" if (ubt.isGenericsPlaceHolder() && ubt.getUnresolvedName().equals(ugt.getName())) { if (GroovyUtils.isAssignable(rbt, ubt)) saveParameterType(resolved, ugt.getName(), rbt, true); } else { // rbt could be "Foo<String, Object> and ubt could be "Foo<K, V>" GenericsType[] ubt_gts = GroovyUtils.getGenericsTypes(ubt); for (int j = 0; j < ubt_gts.length; j += 1) { if (ubt_gts[j].getType().isGenericsPlaceHolder() && ubt_gts[j].getName().equals(ugt.getName())) { // to resolve "T" follow "List<T> -> List<E>" then walk resolved type hierarchy to find "List<E>" String key = GroovyUtils.getGenericsTypes(ubt.redirect())[j].getName(); GenericsMapper map = gatherGenerics(rbt, ubt.redirect()); ClassNode rt = map.findParameter(key, null); // rt could be "String" and ubt_gts[j] could be "T" or "T extends CharSequence" if (rt != null && GroovyUtils.isAssignable(rt, ubt_gts[j].getType())) { saveParameterType(resolved, ugt.getName(), rt, false); } break; } } // TODO: What about "Foo<Bar<T>>", "Foo<? extends T>", or "Foo<? super T>"? } } } } } return mapper; } /** * takes this type or type parameter and determines what its type should be based on the type parameter resolution in the top level of the mapper * * @param depth ensure that we don't recur forever, bottom out after a certain depth */ public ClassNode resolveParameter(GenericsType topGT, int depth) { if (allGenerics.isEmpty()) { return topGT.getType(); } if (depth > 10) { // don't recur forever // FIXADE This problem is believed fixed. If this conidtional is never reached, then we should be able to delete this section. Util.log(new Status(IStatus.WARNING, "org.eclipse.jdt.groovy.core", "GRECLIPSE-1040: prevent infinite recursion when resolving type parameters on generics type: " + topGT)); return topGT.getType(); } ClassNode origType = findParameter(topGT.getName(), topGT.getType()); // now recur down all type parameters inside of this type // class Enum<E extends Enum<E>> if (origType.getGenericsTypes() != null) { origType = VariableScope.clone(origType); GenericsType[] genericsTypes = origType.getGenericsTypes(); for (GenericsType genericsType : genericsTypes) { if (genericsType.getName().equals(topGT.getName())) { // avoid infinite loops // I still don't like this solution, but better than using a depth counter. continue; } genericsType.setType(findParameter(genericsType.getName(), resolveParameter(genericsType, depth + 1))); genericsType.setLowerBound(null); genericsType.setUpperBounds(null); genericsType.setName(genericsType.getType().getName()); } } return origType; } //-------------------------------------------------------------------------- /** Keeps track of all type parameterization up the type hierarchy. */ private final LinkedList<Map<String, ClassNode>> allGenerics = new LinkedList<Map<String, ClassNode>>(); protected boolean hasGenerics() { return !allGenerics.isEmpty() && !allGenerics.getLast().isEmpty(); } /** * Finds the type of a parameter name in the highest level of the type hierarchy currently analyzed. * * @param defaultType type to return if parameter name doesn't exist */ protected ClassNode findParameter(String parameterName, ClassNode defaultType) { if (allGenerics.isEmpty()) { return defaultType; } ClassNode type = allGenerics.getLast().get(parameterName); if (type == null) { return defaultType; } return type; } protected static Iterator<ClassNode> getTypeHierarchy(ClassNode type, boolean useResolved) { LinkedHashSet<ClassNode> hierarchy = new LinkedHashSet<ClassNode>(); VariableScope.createTypeHierarchy(type, hierarchy, useResolved); hierarchy.remove(VariableScope.GROOVY_OBJECT_CLASS_NODE); hierarchy.remove(VariableScope.OBJECT_CLASS_NODE); return hierarchy.iterator(); } /** * @see org.codehaus.groovy.classgen.AsmClassGenerator#isVargs(Parameter[]) * @see org.codehaus.jdt.groovy.internal.compiler.ast.GroovyCompilationUnitDeclaration.UnitPopulator#isVargs(Parameter[]) */ protected static boolean isVargs(Parameter[] parameters) { if (parameters.length > 0) { Parameter last = parameters[parameters.length - 1]; ClassNode type = last.getType(); if (type.isArray()) { return true; } } return false; } protected static void saveParameterType(Map<String, ClassNode> map, String key, ClassNode val, boolean weak) { // special case 1: Arrays.asList(T...): List<T> -- each param has a chance to influence the LUB // special case 2: Collections.replaceAll(List<T>, T, T) -- list should dictate type unless it's dynamic // special case 3: Collections.checkedSet(Set<E>, Class<E>): Set<E> -- set type and class type should agree ClassNode old = map.remove(key); // if mapped type is Object, consider it malleable if (old != null && !old.equals(val) && !old.equals(VariableScope.OBJECT_CLASS_NODE) && weak) { val = /*WideningCategories.lowestUpperBound(*/old/*, val)*/; } map.put(key, val); } }