diff options
author | Michael Wright <michaelwr@google.com> | 2016-02-03 13:09:22 -0800 |
---|---|---|
committer | Michael Wright <michaelwr@google.com> | 2016-09-12 18:13:12 +0100 |
commit | 697a33db1bb0df263769d4a32f6a72ecf64d49db (patch) | |
tree | 16754aabd470857ed6fe271a4740ef65acfb510a | |
parent | 3b8749dedda660a84f4720208e4b9e94b99cf14e (diff) | |
download | platform_external_doclava-697a33db1bb0df263769d4a32f6a72ecf64d49db.tar.gz platform_external_doclava-697a33db1bb0df263769d4a32f6a72ecf64d49db.tar.bz2 platform_external_doclava-697a33db1bb0df263769d4a32f6a72ecf64d49db.zip |
DO NOT MERGE Keep track of method and class type variables.
Also, add consistency checks for parameterized classes and return
types.
Bug: 24265043
Change-Id: Iafe9762f15f6ea439e28cd35bc73277cf1f6e405
-rw-r--r-- | src/com/google/doclava/ClassInfo.java | 38 | ||||
-rw-r--r-- | src/com/google/doclava/MethodInfo.java | 86 | ||||
-rw-r--r-- | src/com/google/doclava/Stubs.java | 7 | ||||
-rw-r--r-- | src/com/google/doclava/TypeInfo.java | 73 | ||||
-rw-r--r-- | src/com/google/doclava/apicheck/ApiFile.java | 94 |
5 files changed, 247 insertions, 51 deletions
diff --git a/src/com/google/doclava/ClassInfo.java b/src/com/google/doclava/ClassInfo.java index 8f0199e..7b50938 100644 --- a/src/com/google/doclava/ClassInfo.java +++ b/src/com/google/doclava/ClassInfo.java @@ -312,6 +312,19 @@ public class ClassInfo extends DocInfo implements ContainerInfo, Comparable, Sco return result; } + public TypeInfo getTypeParameter(String qualifiedTypeName) { + List<TypeInfo> parameters = mTypeInfo.typeArguments(); + if (parameters == null) { + return null; + } + for (TypeInfo parameter : parameters) { + if (parameter.qualifiedTypeName().equals(qualifiedTypeName)) { + return parameter; + } + } + return null; + } + /** * List of only direct interface's classes, without worrying about type param mapping. * This can't be lazy loaded, because its overloads depend on changing type parameters @@ -2232,6 +2245,17 @@ public class ClassInfo extends DocInfo implements ContainerInfo, Comparable, Sco } } + if (hasTypeParameters() && cl.hasTypeParameters()) { + ArrayList<TypeInfo> oldParams = typeParameters(); + ArrayList<TypeInfo> newParams = cl.typeParameters(); + if (oldParams.size() != newParams.size()) { + consistent = false; + Errors.error(Errors.CHANGED_TYPE, cl.position(), "Class " + qualifiedName() + + " changed number of type parameters from " + oldParams.size() + + " to " + newParams.size()); + } + } + return consistent; } @@ -2284,6 +2308,20 @@ public class ClassInfo extends DocInfo implements ContainerInfo, Comparable, Sco return mTypeInfo; } + public boolean hasTypeParameters() { + if (mTypeInfo != null && mTypeInfo.typeArguments() != null) { + return !mTypeInfo.typeArguments().isEmpty(); + } + return false; + } + + public ArrayList<TypeInfo> typeParameters() { + if (hasTypeParameters()) { + return mTypeInfo.typeArguments(); + } + return null; + } + public void addInnerClass(ClassInfo innerClass) { if (mInnerClasses == null) { mInnerClasses = new ArrayList<ClassInfo>(); diff --git a/src/com/google/doclava/MethodInfo.java b/src/com/google/doclava/MethodInfo.java index 299043d..28f1899 100644 --- a/src/com/google/doclava/MethodInfo.java +++ b/src/com/google/doclava/MethodInfo.java @@ -237,6 +237,10 @@ public class MethodInfo extends MemberInfo implements AbstractMethodInfo, Resolv return mTypeParameters; } + public boolean hasTypeParameters() { + return mTypeParameters != null && !mTypeParameters.isEmpty(); + } + /** * Clone this MethodInfo as if it belonged to the specified ClassInfo and apply the * typeArgumentMapping to the parameters and return types. @@ -668,7 +672,7 @@ public class MethodInfo extends MemberInfo implements AbstractMethodInfo, Resolv } public String typeArgumentsName(HashSet<String> typeVars) { - if (mTypeParameters == null || mTypeParameters.isEmpty()) { + if (!hasTypeParameters()) { return ""; } else { return TypeInfo.typeArgumentsName(mTypeParameters, typeVars); @@ -792,26 +796,39 @@ public class MethodInfo extends MemberInfo implements AbstractMethodInfo, Resolv public boolean isConsistent(MethodInfo mInfo) { boolean consistent = true; - if (this.mReturnType != mInfo.mReturnType && !this.mReturnType.equals(mInfo.mReturnType)) { - if (!mReturnType.isPrimitive() && !mInfo.mReturnType.isPrimitive() - && Objects.equals(mInfo.mReturnType.dimension(), mReturnType.dimension())) { - // Check to see if our class extends the old class. - ApiInfo infoApi = mInfo.containingClass().containingPackage().containingApi(); - ClassInfo infoReturnClass = infoApi.findClass(mInfo.mReturnType.qualifiedTypeName()); - // Find the classes. - consistent = infoReturnClass != null && - infoReturnClass.isAssignableTo(mReturnType.qualifiedTypeName()); - } else { + if (!mReturnType.isTypeVariable() && !mInfo.mReturnType.isTypeVariable()) { + if (!mReturnType.equals(mInfo.mReturnType) || + mReturnType.dimension() != mInfo.mReturnType.dimension()) { consistent = false; } - - if (!consistent) { - Errors.error(Errors.CHANGED_TYPE, mInfo.position(), "Method " - + mInfo.prettyQualifiedSignature() + " has changed return type from " + mReturnType - + " to " + mInfo.mReturnType); + } else if (!mReturnType.isTypeVariable() && mInfo.mReturnType.isTypeVariable()) { + List<ClassInfo> constraints = mInfo.resolveConstraints(mInfo.mReturnType); + for (ClassInfo constraint : constraints) { + if (!constraint.isAssignableTo(mReturnType.qualifiedTypeName())) { + consistent = false; + } + } + } else if (mReturnType.isTypeVariable() && !mInfo.mReturnType.isTypeVariable()) { + // It's never valid to go from being a parameterized type to not being one. + // This would drop the implicit cast breaking backwards compatibility. + consistent = false; + } else { + // If both return types are parameterized then the constraints must be + // exactly the same. + List<ClassInfo> currentConstraints = mInfo.resolveConstraints(mReturnType); + List<ClassInfo> newConstraints = mInfo.resolveConstraints(mInfo.mReturnType); + if (currentConstraints.size() != newConstraints.size() || + currentConstraints.retainAll(newConstraints)) { + consistent = false; } } + if (!consistent) { + Errors.error(Errors.CHANGED_TYPE, mInfo.position(), "Method " + + mInfo.prettyQualifiedSignature() + " has changed return type from " + mReturnType + + " to " + mInfo.mReturnType); + } + if (mIsAbstract != mInfo.mIsAbstract) { consistent = false; Errors.error(Errors.CHANGED_ABSTRACT, mInfo.position(), "Method " @@ -898,6 +915,43 @@ public class MethodInfo extends MemberInfo implements AbstractMethodInfo, Resolv return consistent; } + private TypeInfo getTypeParameter(String qualifiedTypeName) { + if (hasTypeParameters()) { + for (TypeInfo parameter : mTypeParameters) { + if (parameter.qualifiedTypeName().equals(qualifiedTypeName)) { + return parameter; + } + } + } + return containingClass().getTypeParameter(qualifiedTypeName); + } + + // Given a type parameter it returns a list of all of the classes and interfaces it must extend + // and implement. + private List<ClassInfo> resolveConstraints(TypeInfo type) { + ApiInfo api = containingClass().containingPackage().containingApi(); + List<ClassInfo> classes = new ArrayList<>(); + Queue<TypeInfo> types = new LinkedList<>(); + types.add(type); + while (!types.isEmpty()) { + type = types.poll(); + if (!type.isTypeVariable()) { + ClassInfo cl = api.findClass(type.qualifiedTypeName()); + if (cl != null) { + classes.add(cl); + } + } else { + TypeInfo parameter = getTypeParameter(type.qualifiedTypeName()); + if (parameter.extendsBounds() != null) { + for (TypeInfo bound : parameter.extendsBounds()) { + types.add(bound); + } + } + } + } + return classes; + } + public void printResolutions() { if (mResolutions == null || mResolutions.isEmpty()) { return; diff --git a/src/com/google/doclava/Stubs.java b/src/com/google/doclava/Stubs.java index dbec6f7..e1ada09 100644 --- a/src/com/google/doclava/Stubs.java +++ b/src/com/google/doclava/Stubs.java @@ -1343,6 +1343,10 @@ public class Stubs { apiWriter.print(cl.isInterface() ? "interface" : "class"); apiWriter.print(" "); apiWriter.print(cl.name()); + if (cl.hasTypeParameters()) { + apiWriter.print(TypeInfo.typeArgumentsName(cl.asTypeInfo().typeArguments(), + new HashSet<String>())); + } if (!cl.isInterface() && !"java.lang.Object".equals(cl.qualifiedName()) @@ -1432,6 +1436,9 @@ public class Stubs { if (mi.isSynchronized()) { apiWriter.print(" synchronized"); } + if (mi.hasTypeParameters()) { + apiWriter.print(" " + mi.typeArgumentsName(new HashSet<String>())); + } apiWriter.print(" "); if (mi.returnType() == null) { apiWriter.print("void"); diff --git a/src/com/google/doclava/TypeInfo.java b/src/com/google/doclava/TypeInfo.java index 567415b..ad26def 100644 --- a/src/com/google/doclava/TypeInfo.java +++ b/src/com/google/doclava/TypeInfo.java @@ -39,42 +39,75 @@ public class TypeInfo implements Resolvable { if (typeString.endsWith("...")) { typeString = typeString.substring(0, typeString.length() - 3); } - + // Generic parameters + int extendsPos = typeString.indexOf(" extends "); int paramStartPos = typeString.indexOf('<'); - if (paramStartPos > -1) { + if (paramStartPos > -1 && (extendsPos == -1 || paramStartPos < extendsPos)) { ArrayList<TypeInfo> generics = new ArrayList<TypeInfo>(); - int paramEndPos = typeString.lastIndexOf('>'); - + int paramEndPos = 0; + int entryStartPos = paramStartPos + 1; int bracketNesting = 0; - for (int i = entryStartPos; i < paramEndPos; i++) { + for (int i = entryStartPos; i < typeString.length(); i++) { char c = typeString.charAt(i); if (c == ',' && bracketNesting == 0) { String entry = typeString.substring(entryStartPos, i).trim(); TypeInfo info = new TypeInfo(entry); + info.setIsTypeVariable(true); generics.add(info); entryStartPos = i + 1; } else if (c == '<') { bracketNesting++; } else if (c == '>') { bracketNesting--; + // Once bracketNesting goes negative, we've found the closing angle bracket + if (bracketNesting < 0) { + paramEndPos = i; + break; + } } } - + TypeInfo info = new TypeInfo(typeString.substring(entryStartPos, paramEndPos).trim()); + info.setIsTypeVariable(true); generics.add(info); - + mTypeArguments = generics; - + if (paramEndPos < typeString.length() - 1) { typeString = typeString.substring(0,paramStartPos) + typeString.substring(paramEndPos + 1); } else { typeString = typeString.substring(0,paramStartPos); } } - - // Dimensions + + // The previous extends may have been within the generic type parameters which we don't + // actually care about and were removed from the type string above + extendsPos = typeString.indexOf(" extends "); + if (extendsPos > -1) { + ArrayList<TypeInfo> extendsBounds = new ArrayList<>(); + int entryStartPos = extendsPos + 9; + int bracketNesting = 0; + for (int i = entryStartPos; i < typeString.length(); i++) { + char c = typeString.charAt(i); + if (c == '&' && bracketNesting == 0) { + String entry = typeString.substring(entryStartPos, i).trim(); + TypeInfo info = new TypeInfo(entry); + extendsBounds.add(info); + entryStartPos = i + 1; + } else if (c == '<') { + bracketNesting++; + } else if (c == '>') { + bracketNesting--; + } + } + TypeInfo info = new TypeInfo(typeString.substring(entryStartPos, typeString.length()).trim()); + extendsBounds.add(info); + mExtendsBounds = extendsBounds; + typeString = typeString.substring(0, extendsPos); + } + int pos = typeString.indexOf('['); if (pos > -1) { mDimension = typeString.substring(pos); @@ -82,7 +115,7 @@ public class TypeInfo implements Resolvable { } else { mDimension = ""; } - + if (PRIMITIVE_TYPES.contains(typeString)) { mIsPrimitive = true; mSimpleTypeName = typeString; @@ -317,7 +350,7 @@ public class TypeInfo implements Resolvable { mTypeArguments.add(arg); } - void setBounds(ArrayList<TypeInfo> superBounds, ArrayList<TypeInfo> extendsBounds) { + public void setBounds(ArrayList<TypeInfo> superBounds, ArrayList<TypeInfo> extendsBounds) { mSuperBounds = superBounds; mExtendsBounds = extendsBounds; } @@ -330,7 +363,7 @@ public class TypeInfo implements Resolvable { return mExtendsBounds; } - void setIsTypeVariable(boolean b) { + public void setIsTypeVariable(boolean b) { mIsTypeVariable = b; } @@ -342,7 +375,7 @@ public class TypeInfo implements Resolvable { return mIsWildcard; } - static HashSet<String> typeVariables(ArrayList<TypeInfo> params) { + public static HashSet<String> typeVariables(ArrayList<TypeInfo> params) { return typeVariables(params, new HashSet<String>()); } @@ -362,6 +395,16 @@ public class TypeInfo implements Resolvable { return mIsTypeVariable; } + public void resolveTypeVariables(HashSet<String> variables) { + if (mExtendsBounds != null) { + for (TypeInfo bound : mExtendsBounds) { + if (variables.contains(bound.qualifiedTypeName())) { + bound.setIsTypeVariable(true); + } + } + } + } + public String defaultValue() { if (mIsPrimitive) { if ("boolean".equals(mSimpleTypeName)) { @@ -477,7 +520,7 @@ public class TypeInfo implements Resolvable { Map<String, TypeInfo> map = new HashMap<String, TypeInfo>(); for (int i = 0; i < generic.typeArguments().size(); i++) { if (typed.typeArguments() != null && typed.typeArguments().size() > i) { - map.put(generic.typeArguments().get(i).fullName(), typed.typeArguments().get(i)); + map.put(generic.typeArguments().get(i).simpleTypeName(), typed.typeArguments().get(i)); } } return map; diff --git a/src/com/google/doclava/apicheck/ApiFile.java b/src/com/google/doclava/apicheck/ApiFile.java index 2d958d4..d865e20 100644 --- a/src/com/google/doclava/apicheck/ApiFile.java +++ b/src/com/google/doclava/apicheck/ApiFile.java @@ -30,6 +30,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -163,12 +164,15 @@ class ApiFile { } assertIdent(tokenizer, token); name = token; - token = tokenizer.requireToken(); qname = qualifiedName(pkg.name(), name, null); + final TypeInfo typeInfo = Converter.obtainTypeFromString(qname); + token = tokenizer.requireToken(); cl = new ClassInfo(null/*classDoc*/, ""/*rawCommentText*/, tokenizer.pos(), pub, prot, pkgpriv, false/*isPrivate*/, stat, iface, abs, true/*isOrdinaryClass*/, false/*isException*/, false/*isError*/, false/*isEnum*/, false/*isAnnotation*/, - fin, false/*isIncluded*/, name, qname, null/*qualifiedTypeName*/, false/*isPrimitive*/); + fin, false/*isIncluded*/, typeInfo.simpleTypeName(), typeInfo.qualifiedTypeName(), + null/*qualifiedTypeName*/, false/*isPrimitive*/); + cl.setTypeInfo(typeInfo); cl.setDeprecated(dep); if ("extends".equals(token)) { token = tokenizer.requireToken(); @@ -178,8 +182,6 @@ class ApiFile { } // Resolve superclass after done parsing api.mapClassToSuper(cl, ext); - final TypeInfo typeInfo = Converter.obtainTypeFromString(qname) ; - cl.setTypeInfo(typeInfo); cl.setAnnotations(new ArrayList<AnnotationInstanceInfo>()); if ("implements".equals(token)) { while (true) { @@ -261,7 +263,7 @@ class ApiFile { new ArrayList<AnnotationInstanceInfo>()/*annotations*/); method.setDeprecated(dep); token = tokenizer.requireToken(); - parseParameterList(tokenizer, method, token); + parseParameterList(tokenizer, method, new HashSet<String>(), token); token = tokenizer.requireToken(); if ("throws".equals(token)) { token = parseThrows(tokenizer, method); @@ -283,7 +285,9 @@ class ApiFile { boolean dep = false; boolean syn = false; boolean def = false; - String type; + ArrayList<TypeInfo> typeParameters = new ArrayList<>(); + TypeInfo returnType; + HashSet<String> typeVariableNames; String name; String ext = null; MethodInfo method; @@ -321,25 +325,32 @@ class ApiFile { syn = true; token = tokenizer.requireToken(); } + if ("<".equals(token)) { + parseTypeParameterList(tokenizer, typeParameters, cl); + token = tokenizer.requireToken(); + } assertIdent(tokenizer, token); - type = token; + returnType = Converter.obtainTypeFromString(token); + typeVariableNames = TypeInfo.typeVariables(typeParameters); + if (typeVariableNames.contains(returnType.qualifiedTypeName())) { + returnType.setIsTypeVariable(true); + } token = tokenizer.requireToken(); assertIdent(tokenizer, token); name = token; - method = new MethodInfo(""/*rawCommentText*/, new ArrayList<TypeInfo>()/*typeParameters*/, - name, null/*signature*/, cl, cl, pub, prot, pkgpriv, false/*isPrivate*/, fin, - stat, false/*isSynthetic*/, abs/*isAbstract*/, syn, false/*isNative*/, def/*isDefault*/, - false /*isAnnotationElement*/, "method", null/*flatSignature*/, null/*overriddenMethod*/, - Converter.obtainTypeFromString(type), new ArrayList<ParameterInfo>(), - new ArrayList<ClassInfo>()/*thrownExceptions*/, tokenizer.pos(), - new ArrayList<AnnotationInstanceInfo>()/*annotations*/); + method = new MethodInfo(""/*rawCommentText*/, typeParameters, name, null/*signature*/, cl, cl, + pub, prot, pkgpriv, false/*isPrivate*/, fin, stat, false/*isSynthetic*/, abs/*isAbstract*/, + syn, false/*isNative*/, def/*isDefault*/, false /*isAnnotationElement*/, "method", + null/*flatSignature*/, null/*overriddenMethod*/, returnType, + new ArrayList<ParameterInfo>(), new ArrayList<ClassInfo>()/*thrownExceptions*/, + tokenizer.pos(), new ArrayList<AnnotationInstanceInfo>()/*annotations*/); method.setDeprecated(dep); token = tokenizer.requireToken(); if (!"(".equals(token)) { throw new ApiParseException("expected (", tokenizer.getLine()); } token = tokenizer.requireToken(); - parseParameterList(tokenizer, method, token); + parseParameterList(tokenizer, method, typeVariableNames, token); token = tokenizer.requireToken(); if ("throws".equals(token)) { token = parseThrows(tokenizer, method); @@ -475,8 +486,47 @@ class ApiFile { } } + private static void parseTypeParameterList(Tokenizer tokenizer, + List<TypeInfo> methodTypeParameters, ClassInfo cl) throws ApiParseException { + String token; + HashSet<String> variables = cl.typeVariables(); + do { + token = tokenizer.requireToken(); + assertIdent(tokenizer, token); + TypeInfo type = new TypeInfo(token); + type.setIsTypeVariable(true); + variables.add(type.qualifiedTypeName()); + ArrayList<TypeInfo> extendsBounds = new ArrayList<>(); + token = tokenizer.requireToken(); + if ("extends".equals(token)) { + do { + token = tokenizer.requireToken(); + assertIdent(tokenizer, token); + extendsBounds.add(new TypeInfo(token)); + token = tokenizer.requireToken(); + } while ("&".equals(token)); + } + if (!extendsBounds.isEmpty()) { + type.setBounds(null, extendsBounds); + } + methodTypeParameters.add(type); + } while (",".equals(token)); + + // Type variables aren't guaranteed to be declared before they're referenced so we need to wait + // until after we've processed them all to figure out which ones are type variables and which + // ones are classes (which we may not have processed yet either). + for (TypeInfo type : methodTypeParameters) { + type.resolveTypeVariables(variables); + } + + if (!">".equals(token)) { + throw new ApiParseException("Expected '>' to end type parameter list, found " + + token, tokenizer.getLine()); + } + } + private static void parseParameterList(Tokenizer tokenizer, AbstractMethodInfo method, - String token) throws ApiParseException { + HashSet<String> typeParameters, String token) throws ApiParseException { while (true) { if (")".equals(token)) { return; @@ -497,8 +547,12 @@ class ApiFile { } // api file does not preserve annotations. List<AnnotationInstanceInfo> annotations = Collections.emptyList(); + TypeInfo typeInfo = Converter.obtainTypeFromString(type); + if (typeParameters.contains(typeInfo.qualifiedTypeName())) { + typeInfo.setIsTypeVariable(true); + } method.addParameter(new ParameterInfo(name, type, - Converter.obtainTypeFromString(type), + typeInfo, type.endsWith("..."), tokenizer.pos(), annotations)); @@ -660,10 +714,10 @@ class ApiFile { if (mBuf[mPos] == '<') { genericDepth++; mPos++; - } else if (mBuf[mPos] == '>') { - genericDepth--; - mPos++; } else if (genericDepth != 0) { + if (mBuf[mPos] == '>') { + genericDepth--; + } mPos++; } } |