From d91f8e1374f7ccdb68d92c206c84864cd09c02f6 Mon Sep 17 00:00:00 2001 From: Roberto Lublinerman Date: Sat, 10 Mar 2018 09:35:51 -0800 Subject: Add tests for Java 8 TYPE_USE annotations on arrays (#614). (#618) * Add tests for Java 8 TYPE_USE annotations on arrays (#614). * Implement TYPE_USE annotations semantics. * Cleanup in AnnotatedTypeTest. * Nits from review --- .../java/com/squareup/javapoet/ArrayTypeName.java | 29 +- src/main/java/com/squareup/javapoet/ClassName.java | 354 +++++++++++++++------ .../java/com/squareup/javapoet/CodeWriter.java | 4 - src/main/java/com/squareup/javapoet/JavaFile.java | 2 +- .../java/com/squareup/javapoet/MethodSpec.java | 2 +- .../java/com/squareup/javapoet/ParameterSpec.java | 5 +- .../squareup/javapoet/ParameterizedTypeName.java | 18 +- src/main/java/com/squareup/javapoet/TypeName.java | 15 +- .../com/squareup/javapoet/TypeVariableName.java | 1 + 9 files changed, 313 insertions(+), 117 deletions(-) (limited to 'src/main') diff --git a/src/main/java/com/squareup/javapoet/ArrayTypeName.java b/src/main/java/com/squareup/javapoet/ArrayTypeName.java index 7c54465..219c3f3 100644 --- a/src/main/java/com/squareup/javapoet/ArrayTypeName.java +++ b/src/main/java/com/squareup/javapoet/ArrayTypeName.java @@ -48,9 +48,36 @@ public final class ArrayTypeName extends TypeName { } @Override CodeWriter emit(CodeWriter out) throws IOException { - return out.emit("$T[]", componentType); + return emit(out, false); } + CodeWriter emit(CodeWriter out, boolean varargs) throws IOException { + emitLeafType(out); + return emitBrackets(out, varargs); + } + + private CodeWriter emitLeafType(CodeWriter out) throws IOException { + if (TypeName.asArray(componentType) != null) { + return TypeName.asArray(componentType).emitLeafType(out); + } + return componentType.emit(out); + } + + private CodeWriter emitBrackets(CodeWriter out, boolean varargs) throws IOException { + if (isAnnotated()) { + out.emit(" "); + emitAnnotations(out); + } + + if (TypeName.asArray(componentType) == null) { + // Last bracket. + return out.emit(varargs ? "..." : "[]"); + } + out.emit("[]"); + return TypeName.asArray(componentType) .emitBrackets(out, varargs); + } + + /** Returns an array type whose elements are all instances of {@code componentType}. */ public static ArrayTypeName of(TypeName componentType) { return new ArrayTypeName(componentType); diff --git a/src/main/java/com/squareup/javapoet/ClassName.java b/src/main/java/com/squareup/javapoet/ClassName.java index c5c065d..6c10024 100644 --- a/src/main/java/com/squareup/javapoet/ClassName.java +++ b/src/main/java/com/squareup/javapoet/ClassName.java @@ -17,7 +17,7 @@ package com.squareup.javapoet; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; +import java.util.Arrays; import java.util.List; import java.util.Map; import javax.lang.model.SourceVersion; @@ -32,106 +32,236 @@ import static javax.lang.model.element.NestingKind.MEMBER; import static javax.lang.model.element.NestingKind.TOP_LEVEL; /** A fully-qualified class name for top-level and member classes. */ -public final class ClassName extends TypeName implements Comparable { - public static final ClassName OBJECT = ClassName.get(Object.class); +public abstract class ClassName extends TypeName implements Comparable { + final String simpleName; + String canonicalName; - /** From top to bottom. This will be ["java.util", "Map", "Entry"] for {@link Map.Entry}. */ - final List names; - final String canonicalName; + /** A fully-qualified class name for top-level classes. */ + private static final class TopLevelClassName extends ClassName { + final String packageName; - private ClassName(List names) { - this(names, new ArrayList<>()); - } + private TopLevelClassName(String packageName, String simpleName) { + this(packageName, simpleName, new ArrayList<>()); + } - private ClassName(List names, List annotations) { - super(annotations); - for (int i = 1; i < names.size(); i++) { - checkArgument(SourceVersion.isName(names.get(i)), "part '%s' is keyword", names.get(i)); + private TopLevelClassName( + String packageName, String simpleName, List annotations) { + super(simpleName, annotations); + this.packageName = packageName == null ? "" : packageName; + this.canonicalName = isDefaultPackage(packageName) + ? simpleName : String.join(".", Arrays.asList(packageName, simpleName)); + checkArgument( + isDefaultPackage(simpleName) || SourceVersion.isName(simpleName), + "part '%s' is keyword", simpleName); + } + + @Override public TopLevelClassName annotated(List annotations) { + return new TopLevelClassName(packageName, simpleName, concatAnnotations(annotations)); + } + + @Override public TopLevelClassName withoutAnnotations() { + return new TopLevelClassName(packageName, simpleName); + } + + public String packageName() { + return packageName; + } + + @Override + public ClassName enclosingClassName() { + return null; + } + + @Override + public TopLevelClassName topLevelClassName() { + return this; + } + + @Override + public String reflectionName() { + return isDefaultPackage(packageName) + ? simpleName + : String.join(".", Arrays.asList(packageName, simpleName)); + } + + @Override + public List simpleNames() { + return Arrays.asList(simpleName); + } + + @Override + public ClassName peerClass(String name) { + return new TopLevelClassName(packageName, name); + } + + @Override + ClassName prefixWithAtMostOneAnnotatedClass() { + return this; + } + + @Override + boolean hasAnnotatedEnclosingClass() { + return false; + } + + @Override + CodeWriter emitWithoutPrefix(CodeWriter out, ClassName unannotatedPrefix) { + return out; } - this.names = Util.immutableList(names); - this.canonicalName = (names.get(0).isEmpty() - ? String.join(".", names.subList(1, names.size())) - : String.join(".", names)); } - @Override public ClassName annotated(List annotations) { - return new ClassName(names, concatAnnotations(annotations)); + /** A fully-qualified class name for nested classes. */ + private static final class NestedClassName extends ClassName { + /** From top to bottom. This will be ["java.util", "Map", "Entry"] for {@link Map.Entry}. */ + final ClassName enclosingClassName; + + private NestedClassName(ClassName enclosingClassName, String simpleName) { + this(enclosingClassName, simpleName, new ArrayList<>()); + } + + private NestedClassName( + ClassName enclosingClassName, String simpleName, List annotations) { + super(simpleName, annotations); + this.enclosingClassName = enclosingClassName; + this.canonicalName = + String.join(".", Arrays.asList(enclosingClassName.canonicalName, simpleName)); + } + + @Override public NestedClassName annotated(List annotations) { + return new NestedClassName(enclosingClassName, simpleName, concatAnnotations(annotations)); + } + + @Override public NestedClassName withoutAnnotations() { + return new NestedClassName(enclosingClassName.withoutAnnotations(), simpleName); + } + + /** Returns the package name, like {@code "java.util"} for {@code Map.Entry}. */ + public String packageName() { + return enclosingClassName.packageName(); + } + + @Override + public ClassName enclosingClassName() { + return enclosingClassName; + } + + @Override + public ClassName topLevelClassName() { + return enclosingClassName.topLevelClassName(); + } + + @Override + public String reflectionName() { + return enclosingClassName.reflectionName() + "$" + simpleName; + } + + @Override + public List simpleNames() { + List simpleNames = new ArrayList<>(enclosingClassName().simpleNames()); + simpleNames.add(simpleName); + return simpleNames; + } + + @Override + public ClassName peerClass(String name) { + return enclosingClassName.nestedClass(name); + } + + @Override + ClassName prefixWithAtMostOneAnnotatedClass() { + if (hasAnnotatedEnclosingClass()) { + enclosingClassName.prefixWithAtMostOneAnnotatedClass(); + } + + return this; + } + + @Override + CodeWriter emitWithoutPrefix( + CodeWriter out, ClassName unannotatedPrefix) throws IOException { + + if (unannotatedPrefix.equals(this)) { + return out; + } + + enclosingClassName.emitWithoutPrefix(out, unannotatedPrefix); + out.emit("."); + if (isAnnotated()) { + out.emit(" "); + emitAnnotations(out); + } + return out.emit(simpleName); + } + + @Override + boolean hasAnnotatedEnclosingClass() { + return enclosingClassName.isAnnotated() || enclosingClassName.hasAnnotatedEnclosingClass(); + } } - @Override public ClassName withoutAnnotations() { - return new ClassName(names); + public static final ClassName OBJECT = ClassName.get(Object.class); + + private ClassName(String simpleName, List annotations) { + super(annotations); + checkArgument(SourceVersion.isName(simpleName), "part '%s' is keyword", simpleName); + this.simpleName = simpleName; } + /** Returns the package name, like {@code "java.util"} for {@code Map.Entry}. */ - public String packageName() { - return names.get(0); - } + public abstract String packageName(); /** * Returns the enclosing class, like {@link Map} for {@code Map.Entry}. Returns null if this class * is not nested in another class. */ - public ClassName enclosingClassName() { - if (names.size() == 2) return null; - return new ClassName(names.subList(0, names.size() - 1)); - } + public abstract ClassName enclosingClassName(); /** * Returns the top class in this nesting group. Equivalent to chained calls to {@link * #enclosingClassName()} until the result's enclosing class is null. */ - public ClassName topLevelClassName() { - return new ClassName(names.subList(0, 2)); - } + public abstract ClassName topLevelClassName(); - public String reflectionName() { - // trivial case: no nested names - if (names.size() == 2) { - String packageName = packageName(); - if (packageName.isEmpty()) { - return names.get(1); - } - return packageName + "." + names.get(1); - } - // concat top level class name and nested names - StringBuilder builder = new StringBuilder(); - builder.append(topLevelClassName()); - for (String name : simpleNames().subList(1, simpleNames().size())) { - builder.append('$').append(name); - } - return builder.toString(); - } + /** + * Return the binary name of a class. + */ + public abstract String reflectionName(); + + public abstract ClassName withoutAnnotations(); /** * Returns a new {@link ClassName} instance for the specified {@code name} as nested inside this * class. */ public ClassName nestedClass(String name) { - checkNotNull(name, "name == null"); - List result = new ArrayList<>(names.size() + 1); - result.addAll(names); - result.add(name); - return new ClassName(result); + return new NestedClassName(this, name); } - public List simpleNames() { - return names.subList(1, names.size()); + @Override + public ClassName annotated(List annotations) { + return (ClassName) super.annotated(annotations); } + public abstract List simpleNames(); + /** * Returns a class that shares the same enclosing package or class. If this class is enclosed by * another class, this is equivalent to {@code enclosingClassName().nestedClass(name)}. Otherwise * it is equivalent to {@code get(packageName(), name)}. */ - public ClassName peerClass(String name) { - List result = new ArrayList<>(names); - result.set(result.size() - 1, name); - return new ClassName(result); - } + public abstract ClassName peerClass(String name); + + abstract ClassName prefixWithAtMostOneAnnotatedClass(); + + abstract boolean hasAnnotatedEnclosingClass(); + + abstract CodeWriter emitWithoutPrefix( + CodeWriter out, ClassName unannotatedPrefix) throws IOException; /** Returns the simple name of this class, like {@code "Entry"} for {@link Map.Entry}. */ public String simpleName() { - return names.get(names.size() - 1); + return simpleName; } public static ClassName get(Class clazz) { @@ -139,24 +269,23 @@ public final class ClassName extends TypeName implements Comparable { checkArgument(!clazz.isPrimitive(), "primitive types cannot be represented as a ClassName"); checkArgument(!void.class.equals(clazz), "'void' type cannot be represented as a ClassName"); checkArgument(!clazz.isArray(), "array types cannot be represented as a ClassName"); - List names = new ArrayList<>(); - while (true) { - String anonymousSuffix = ""; - while (clazz.isAnonymousClass()) { - int lastDollar = clazz.getName().lastIndexOf('$'); - anonymousSuffix = clazz.getName().substring(lastDollar) + anonymousSuffix; - clazz = clazz.getEnclosingClass(); - } - names.add(clazz.getSimpleName() + anonymousSuffix); - Class enclosing = clazz.getEnclosingClass(); - if (enclosing == null) break; - clazz = enclosing; - } - // Avoid unreliable Class.getPackage(). https://github.com/square/javapoet/issues/295 - int lastDot = clazz.getName().lastIndexOf('.'); - if (lastDot != -1) names.add(clazz.getName().substring(0, lastDot)); - Collections.reverse(names); - return new ClassName(names); + + String anonymousSuffix = ""; + while (clazz.isAnonymousClass()) { + int lastDollar = clazz.getName().lastIndexOf('$'); + anonymousSuffix = clazz.getName().substring(lastDollar) + anonymousSuffix; + clazz = clazz.getEnclosingClass(); + } + String name = clazz.getSimpleName() + anonymousSuffix; + + if (clazz.getEnclosingClass() == null) { + // Avoid unreliable Class.getPackage(). https://github.com/square/javapoet/issues/295 + int lastDot = clazz.getName().lastIndexOf('.'); + String packageName = (lastDot != -1) ? clazz.getName().substring(0, lastDot) : null; + return new TopLevelClassName(packageName, name); + } + + return ClassName.get(clazz.getEnclosingClass()).nestedClass(name); } /** @@ -176,17 +305,24 @@ public final class ClassName extends TypeName implements Comparable { p = classNameString.indexOf('.', p) + 1; checkArgument(p != 0, "couldn't make a guess for %s", classNameString); } - names.add(p != 0 ? classNameString.substring(0, p - 1) : ""); + String packageName = p == 0 ? null : classNameString.substring(0, p - 1); + String[] classNames = classNameString.substring(p).split("\\.", -1); + + checkArgument(classNames.length >= 1, "couldn't make a guess for %s", classNameString); + + String simpleName = classNames[0]; + checkArgument(!simpleName.isEmpty() && Character.isUpperCase(simpleName.codePointAt(0)), + "couldn't make a guess for %s", classNameString); + ClassName className = new TopLevelClassName(packageName, simpleName); // Add the class names, like "Map" and "Entry". - for (String part : classNameString.substring(p).split("\\.", -1)) { + for (String part : Arrays.asList(classNames).subList(1, classNames.length)) { checkArgument(!part.isEmpty() && Character.isUpperCase(part.codePointAt(0)), "couldn't make a guess for %s", classNameString); - names.add(part); + className = className.nestedClass(part); } - checkArgument(names.size() >= 2, "couldn't make a guess for %s", classNameString); - return new ClassName(names); + return className; } /** @@ -194,25 +330,26 @@ public final class ClassName extends TypeName implements Comparable { * {@code "java.util"} and simple names {@code "Map"}, {@code "Entry"} yields {@link Map.Entry}. */ public static ClassName get(String packageName, String simpleName, String... simpleNames) { - List result = new ArrayList<>(); - result.add(packageName); - result.add(simpleName); - Collections.addAll(result, simpleNames); - return new ClassName(result); + ClassName className = new TopLevelClassName(packageName, simpleName); + for (String name : simpleNames) { + className = className.nestedClass(name); + } + return className; } /** Returns the class name for {@code element}. */ public static ClassName get(TypeElement element) { checkNotNull(element, "element == null"); - List names = new ArrayList<>(); - for (Element e = element; isClassOrInterface(e); e = e.getEnclosingElement()) { - checkArgument(element.getNestingKind() == TOP_LEVEL || element.getNestingKind() == MEMBER, - "unexpected type testing"); - names.add(e.getSimpleName().toString()); - } - names.add(getPackage(element).getQualifiedName().toString()); - Collections.reverse(names); - return new ClassName(names); + checkArgument(element.getNestingKind() == TOP_LEVEL || element.getNestingKind() == MEMBER, + "unexpected type nesting"); + String simpleName = element.getSimpleName().toString(); + + if (isClassOrInterface(element.getEnclosingElement())) { + return ClassName.get((TypeElement) element.getEnclosingElement()).nestedClass(simpleName); + } + + String packageName = getPackage(element.getEnclosingElement()).getQualifiedName().toString(); + return new TopLevelClassName(packageName, simpleName); } private static boolean isClassOrInterface(Element e) { @@ -227,10 +364,27 @@ public final class ClassName extends TypeName implements Comparable { } @Override public int compareTo(ClassName o) { - return canonicalName.compareTo(o.canonicalName); + return reflectionName().compareTo(o.reflectionName()); } @Override CodeWriter emit(CodeWriter out) throws IOException { - return out.emitAndIndent(out.lookupName(this)); + ClassName prefix = prefixWithAtMostOneAnnotatedClass(); + String unqualifiedName = out.lookupName(prefix); + if (prefix.isAnnotated()) { + int dot = unqualifiedName.lastIndexOf("."); + out.emitAndIndent(unqualifiedName.substring(0, dot + 1)); + if (dot != -1) { + out.emit(" "); + } + prefix.emitAnnotations(out); + out.emit(unqualifiedName.substring(dot + 1)); + } else { + out.emitAndIndent(unqualifiedName); + } + return emitWithoutPrefix(out, prefix); + } + + private static boolean isDefaultPackage(String packageName) { + return packageName == null || packageName.isEmpty(); } } diff --git a/src/main/java/com/squareup/javapoet/CodeWriter.java b/src/main/java/com/squareup/javapoet/CodeWriter.java index 9468a08..542f434 100644 --- a/src/main/java/com/squareup/javapoet/CodeWriter.java +++ b/src/main/java/com/squareup/javapoet/CodeWriter.java @@ -236,10 +236,6 @@ final class CodeWriter { case "$T": TypeName typeName = (TypeName) codeBlock.args.get(a++); - if (typeName.isAnnotated()) { - typeName.emitAnnotations(this); - typeName = typeName.withoutAnnotations(); - } // defer "typeName.emit(this)" if next format part will be handled by the default case if (typeName instanceof ClassName && partIterator.hasNext()) { if (!codeBlock.formatParts.get(partIterator.nextIndex()).startsWith("$")) { diff --git a/src/main/java/com/squareup/javapoet/JavaFile.java b/src/main/java/com/squareup/javapoet/JavaFile.java index b6b5a75..e7662dd 100644 --- a/src/main/java/com/squareup/javapoet/JavaFile.java +++ b/src/main/java/com/squareup/javapoet/JavaFile.java @@ -145,7 +145,7 @@ public final class JavaFile { int importedTypesCount = 0; for (ClassName className : new TreeSet<>(codeWriter.importedTypes().values())) { if (skipJavaLangImports && className.packageName().equals("java.lang")) continue; - codeWriter.emit("import $L;\n", className); + codeWriter.emit("import $L;\n", className.withoutAnnotations()); importedTypesCount++; } diff --git a/src/main/java/com/squareup/javapoet/MethodSpec.java b/src/main/java/com/squareup/javapoet/MethodSpec.java index 453d64d..a2c7c43 100644 --- a/src/main/java/com/squareup/javapoet/MethodSpec.java +++ b/src/main/java/com/squareup/javapoet/MethodSpec.java @@ -77,7 +77,7 @@ public final class MethodSpec { private boolean lastParameterIsArray(List parameters) { return !parameters.isEmpty() - && TypeName.arrayComponent(parameters.get(parameters.size() - 1).type) != null; + && TypeName.asArray((parameters.get(parameters.size() - 1).type)) != null; } void emit(CodeWriter codeWriter, String enclosingName, Set implicitModifiers) diff --git a/src/main/java/com/squareup/javapoet/ParameterSpec.java b/src/main/java/com/squareup/javapoet/ParameterSpec.java index 4f52872..63da3f2 100644 --- a/src/main/java/com/squareup/javapoet/ParameterSpec.java +++ b/src/main/java/com/squareup/javapoet/ParameterSpec.java @@ -51,10 +51,11 @@ public final class ParameterSpec { codeWriter.emitAnnotations(annotations, true); codeWriter.emitModifiers(modifiers); if (varargs) { - codeWriter.emit("$T... $L", TypeName.arrayComponent(type), name); + TypeName.asArray(type).emit(codeWriter, true); } else { - codeWriter.emit("$T $L", type, name); + type.emit(codeWriter); } + codeWriter.emit(" $L", name); } @Override public boolean equals(Object o) { diff --git a/src/main/java/com/squareup/javapoet/ParameterizedTypeName.java b/src/main/java/com/squareup/javapoet/ParameterizedTypeName.java index d46aba8..3a8bf62 100644 --- a/src/main/java/com/squareup/javapoet/ParameterizedTypeName.java +++ b/src/main/java/com/squareup/javapoet/ParameterizedTypeName.java @@ -41,7 +41,7 @@ public final class ParameterizedTypeName extends TypeName { private ParameterizedTypeName(ParameterizedTypeName enclosingType, ClassName rawType, List typeArguments, List annotations) { super(annotations); - this.rawType = checkNotNull(rawType, "rawType == null"); + this.rawType = checkNotNull(rawType, "rawType == null").annotated(annotations); this.enclosingType = enclosingType; this.typeArguments = Util.immutableList(typeArguments); @@ -58,17 +58,22 @@ public final class ParameterizedTypeName extends TypeName { enclosingType, rawType, typeArguments, concatAnnotations(annotations)); } - @Override public TypeName withoutAnnotations() { - return new ParameterizedTypeName(enclosingType, rawType, typeArguments, new ArrayList<>()); + @Override + public TypeName withoutAnnotations() { + return new ParameterizedTypeName( + enclosingType, rawType.withoutAnnotations(), typeArguments, new ArrayList<>()); } @Override CodeWriter emit(CodeWriter out) throws IOException { if (enclosingType != null) { - enclosingType.emitAnnotations(out); enclosingType.emit(out); - out.emit("." + rawType.simpleName()); + out.emit("."); + if (isAnnotated()) { + out.emit(" "); + emitAnnotations(out); + } + out.emit(rawType.simpleName()); } else { - rawType.emitAnnotations(out); rawType.emit(out); } if (!typeArguments.isEmpty()) { @@ -76,7 +81,6 @@ public final class ParameterizedTypeName extends TypeName { boolean firstParameter = true; for (TypeName parameter : typeArguments) { if (!firstParameter) out.emitAndIndent(", "); - parameter.emitAnnotations(out); parameter.emit(out); firstParameter = false; } diff --git a/src/main/java/com/squareup/javapoet/TypeName.java b/src/main/java/com/squareup/javapoet/TypeName.java index e09d785..38877f7 100644 --- a/src/main/java/com/squareup/javapoet/TypeName.java +++ b/src/main/java/com/squareup/javapoet/TypeName.java @@ -209,7 +209,6 @@ public class TypeName { try { StringBuilder resultBuilder = new StringBuilder(); CodeWriter codeWriter = new CodeWriter(resultBuilder); - emitAnnotations(codeWriter); emit(codeWriter); result = resultBuilder.toString(); cachedString = result; @@ -222,6 +221,11 @@ public class TypeName { CodeWriter emit(CodeWriter out) throws IOException { if (keyword == null) throw new AssertionError(); + + if (isAnnotated()) { + out.emit(""); + emitAnnotations(out); + } return out.emitAndIndent(keyword); } @@ -233,6 +237,7 @@ public class TypeName { return out; } + /** Returns a type name equivalent to {@code mirror}. */ public static TypeName get(TypeMirror mirror) { return get(mirror, new LinkedHashMap<>()); @@ -369,4 +374,12 @@ public class TypeName { ? ((ArrayTypeName) type).componentType : null; } + + /** Returns {@code type} as an array, or null if {@code type} is not an array. */ + static ArrayTypeName asArray(TypeName type) { + return type instanceof ArrayTypeName + ? ((ArrayTypeName) type) + : null; + } + } diff --git a/src/main/java/com/squareup/javapoet/TypeVariableName.java b/src/main/java/com/squareup/javapoet/TypeVariableName.java index e0e54fb..54c2fa5 100644 --- a/src/main/java/com/squareup/javapoet/TypeVariableName.java +++ b/src/main/java/com/squareup/javapoet/TypeVariableName.java @@ -80,6 +80,7 @@ public final class TypeVariableName extends TypeName { } @Override CodeWriter emit(CodeWriter out) throws IOException { + emitAnnotations(out); return out.emitAndIndent(name); } -- cgit v1.2.3