diff options
Diffstat (limited to 'src/main/java')
11 files changed, 556 insertions, 94 deletions
diff --git a/src/main/java/com/squareup/javapoet/AnnotationSpec.java b/src/main/java/com/squareup/javapoet/AnnotationSpec.java index d1c5e53..5525d7b 100644 --- a/src/main/java/com/squareup/javapoet/AnnotationSpec.java +++ b/src/main/java/com/squareup/javapoet/AnnotationSpec.java @@ -192,7 +192,8 @@ public final class AnnotationSpec { public static final class Builder { private final TypeName type; - private final Map<String, List<CodeBlock>> members = new LinkedHashMap<>(); + + public final Map<String, List<CodeBlock>> members = new LinkedHashMap<>(); private Builder(TypeName type) { this.type = type; @@ -203,8 +204,6 @@ public final class AnnotationSpec { } public Builder addMember(String name, CodeBlock codeBlock) { - checkNotNull(name, "name == null"); - checkArgument(SourceVersion.isName(name), "not a valid name: %s", name); List<CodeBlock> values = members.computeIfAbsent(name, k -> new ArrayList<>()); values.add(codeBlock); return this; @@ -238,6 +237,10 @@ public final class AnnotationSpec { } public AnnotationSpec build() { + for (String name : members.keySet()) { + checkNotNull(name, "name == null"); + checkArgument(SourceVersion.isName(name), "not a valid name: %s", name); + } return new AnnotationSpec(this); } } diff --git a/src/main/java/com/squareup/javapoet/ClassName.java b/src/main/java/com/squareup/javapoet/ClassName.java index 99c4ed2..4bef49d 100644 --- a/src/main/java/com/squareup/javapoet/ClassName.java +++ b/src/main/java/com/squareup/javapoet/ClassName.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import javax.lang.model.element.Element; import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; @@ -32,6 +33,9 @@ import static com.squareup.javapoet.Util.checkNotNull; public final class ClassName extends TypeName implements Comparable<ClassName> { public static final ClassName OBJECT = ClassName.get(Object.class); + /** The name representing the default Java package. */ + private static final String NO_PACKAGE = ""; + /** The package name of this class, or "" if this is in the default package. */ final String packageName; @@ -41,6 +45,8 @@ public final class ClassName extends TypeName implements Comparable<ClassName> { /** This class name, like "Entry" for java.util.Map.Entry. */ final String simpleName; + private List<String> simpleNames; + /** The full class name like "java.util.Map.Entry". */ final String canonicalName; @@ -51,7 +57,7 @@ public final class ClassName extends TypeName implements Comparable<ClassName> { private ClassName(String packageName, ClassName enclosingClassName, String simpleName, List<AnnotationSpec> annotations) { super(annotations); - this.packageName = packageName; + this.packageName = Objects.requireNonNull(packageName, "packageName == null"); this.enclosingClassName = enclosingClassName; this.simpleName = simpleName; this.canonicalName = enclosingClassName != null @@ -108,11 +114,18 @@ public final class ClassName extends TypeName implements Comparable<ClassName> { } public List<String> simpleNames() { - List<String> simpleNames = new ArrayList<>(); - if (enclosingClassName != null) { - simpleNames.addAll(enclosingClassName().simpleNames()); + if (simpleNames != null) { + return simpleNames; + } + + if (enclosingClassName == null) { + simpleNames = Collections.singletonList(simpleName); + } else { + List<String> mutableNames = new ArrayList<>(); + mutableNames.addAll(enclosingClassName().simpleNames()); + mutableNames.add(simpleName); + simpleNames = Collections.unmodifiableList(mutableNames); } - simpleNames.add(simpleName); return simpleNames; } @@ -138,6 +151,14 @@ public final class ClassName extends TypeName implements Comparable<ClassName> { return simpleName; } + /** + * Returns the full class name of this class. + * Like {@code "java.util.Map.Entry"} for {@link Map.Entry}. + * */ + public String canonicalName() { + return canonicalName; + } + public static ClassName get(Class<?> clazz) { checkNotNull(clazz, "clazz == null"); checkArgument(!clazz.isPrimitive(), "primitive types cannot be represented as a ClassName"); @@ -155,7 +176,7 @@ public final class ClassName extends TypeName implements Comparable<ClassName> { 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; + String packageName = (lastDot != -1) ? clazz.getName().substring(0, lastDot) : NO_PACKAGE; return new ClassName(packageName, null, name); } @@ -177,7 +198,7 @@ public final class ClassName extends TypeName implements Comparable<ClassName> { p = classNameString.indexOf('.', p) + 1; checkArgument(p != 0, "couldn't make a guess for %s", classNameString); } - String packageName = p == 0 ? "" : classNameString.substring(0, p - 1); + String packageName = p == 0 ? NO_PACKAGE : classNameString.substring(0, p - 1); // Add class names like "Map" and "Entry". ClassName className = null; diff --git a/src/main/java/com/squareup/javapoet/CodeBlock.java b/src/main/java/com/squareup/javapoet/CodeBlock.java index 33e3846..02542f5 100644 --- a/src/main/java/com/squareup/javapoet/CodeBlock.java +++ b/src/main/java/com/squareup/javapoet/CodeBlock.java @@ -189,7 +189,7 @@ public final class CodeBlock { while (p < format.length()) { int nextP = format.indexOf("$", p); if (nextP == -1) { - formatParts.add(format.substring(p, format.length())); + formatParts.add(format.substring(p)); break; } @@ -424,6 +424,12 @@ public final class CodeBlock { return this; } + public Builder clear() { + formatParts.clear(); + args.clear(); + return this; + } + public CodeBlock build() { return new CodeBlock(this); } diff --git a/src/main/java/com/squareup/javapoet/CodeWriter.java b/src/main/java/com/squareup/javapoet/CodeWriter.java index 542f434..3b2f188 100644 --- a/src/main/java/com/squareup/javapoet/CodeWriter.java +++ b/src/main/java/com/squareup/javapoet/CodeWriter.java @@ -54,9 +54,11 @@ final class CodeWriter { private final List<TypeSpec> typeSpecStack = new ArrayList<>(); private final Set<String> staticImportClassNames; private final Set<String> staticImports; + private final Set<String> alwaysQualify; private final Map<String, ClassName> importedTypes; private final Map<String, ClassName> importableTypes = new LinkedHashMap<>(); private final Set<String> referencedNames = new LinkedHashSet<>(); + private final Multiset<String> currentTypeVariables = new Multiset<>(); private boolean trailingNewline; /** @@ -67,19 +69,23 @@ final class CodeWriter { int statementLine = -1; CodeWriter(Appendable out) { - this(out, " ", Collections.emptySet()); + this(out, " ", Collections.emptySet(), Collections.emptySet()); } - CodeWriter(Appendable out, String indent, Set<String> staticImports) { - this(out, indent, Collections.emptyMap(), staticImports); + CodeWriter(Appendable out, String indent, Set<String> staticImports, Set<String> alwaysQualify) { + this(out, indent, Collections.emptyMap(), staticImports, alwaysQualify); } - CodeWriter(Appendable out, String indent, Map<String, ClassName> importedTypes, - Set<String> staticImports) { + CodeWriter(Appendable out, + String indent, + Map<String, ClassName> importedTypes, + Set<String> staticImports, + Set<String> alwaysQualify) { this.out = new LineWrapper(out, indent, 100); this.indent = checkNotNull(indent, "indent == null"); this.importedTypes = checkNotNull(importedTypes, "importedTypes == null"); this.staticImports = checkNotNull(staticImports, "staticImports == null"); + this.alwaysQualify = checkNotNull(alwaysQualify, "alwaysQualify == null"); this.staticImportClassNames = new LinkedHashSet<>(); for (String signature : staticImports) { staticImportClassNames.add(signature.substring(0, signature.lastIndexOf('.'))); @@ -148,7 +154,7 @@ final class CodeWriter { emit("/**\n"); javadoc = true; try { - emit(javadocCodeBlock); + emit(javadocCodeBlock, true); } finally { javadoc = false; } @@ -187,6 +193,8 @@ final class CodeWriter { public void emitTypeVariables(List<TypeVariableName> typeVariables) throws IOException { if (typeVariables.isEmpty()) return; + typeVariables.forEach(typeVariable -> currentTypeVariables.add(typeVariable.name)); + emit("<"); boolean firstTypeVariable = true; for (TypeVariableName typeVariable : typeVariables) { @@ -203,6 +211,10 @@ final class CodeWriter { emit(">"); } + public void popTypeVariables(List<TypeVariableName> typeVariables) throws IOException { + typeVariables.forEach(typeVariable -> currentTypeVariables.remove(typeVariable.name)); + } + public CodeWriter emit(String s) throws IOException { return emitAndIndent(s); } @@ -212,6 +224,10 @@ final class CodeWriter { } public CodeWriter emit(CodeBlock codeBlock) throws IOException { + return emit(codeBlock, false); + } + + public CodeWriter emit(CodeBlock codeBlock, boolean ensureTrailingNewline) throws IOException { int a = 0; ClassName deferredTypeName = null; // used by "import static" logic ListIterator<String> partIterator = codeBlock.formatParts.listIterator(); @@ -300,6 +316,9 @@ final class CodeWriter { break; } } + if (ensureTrailingNewline && out.lastChar() != '\n') { + emit("\n"); + } return this; } @@ -353,6 +372,12 @@ final class CodeWriter { * names visible due to inheritance. */ String lookupName(ClassName className) { + // If the top level simple name is masked by a current type variable, use the canonical name. + String topLevelSimpleName = className.topLevelClassName().simpleName(); + if (currentTypeVariables.contains(topLevelSimpleName)) { + return className.canonicalName; + } + // Find the shortest suffix of className that resolves to className. This uses both local type // names (so `Entry` in `Map` refers to `Map.Entry`). Also uses imports. boolean nameResolved = false; @@ -374,7 +399,7 @@ final class CodeWriter { // If the class is in the same package, we're done. if (Objects.equals(packageName, className.packageName())) { - referencedNames.add(className.topLevelClassName().simpleName()); + referencedNames.add(topLevelSimpleName); return join(".", className.simpleNames()); } @@ -389,6 +414,9 @@ final class CodeWriter { private void importableType(ClassName className) { if (className.packageName().isEmpty()) { return; + } else if (alwaysQualify.contains(className.simpleName)) { + // TODO what about nested types like java.util.Map.Entry? + return; } ClassName topLevelClassName = className.topLevelClassName(); String simpleName = topLevelClassName.simpleName(); @@ -407,10 +435,8 @@ final class CodeWriter { // Match a child of the current (potentially nested) class. for (int i = typeSpecStack.size() - 1; i >= 0; i--) { TypeSpec typeSpec = typeSpecStack.get(i); - for (TypeSpec visibleChild : typeSpec.typeSpecs) { - if (Objects.equals(visibleChild.name, simpleName)) { - return stackClassName(i, simpleName); - } + if (typeSpec.nestedTypesSimpleNames.contains(simpleName)) { + return stackClassName(i, simpleName); } } @@ -443,7 +469,7 @@ final class CodeWriter { */ CodeWriter emitAndIndent(String s) throws IOException { boolean first = true; - for (String line : s.split("\n", -1)) { + for (String line : s.split("\\R", -1)) { // Emit a newline character. Make sure blank lines in Javadoc & comments look good. if (!first) { if ((javadoc || comment) && trailingNewline) { @@ -494,4 +520,26 @@ final class CodeWriter { result.keySet().removeAll(referencedNames); return result; } + + // A makeshift multi-set implementation + private static final class Multiset<T> { + private final Map<T, Integer> map = new LinkedHashMap<>(); + + void add(T t) { + int count = map.getOrDefault(t, 0); + map.put(t, count + 1); + } + + void remove(T t) { + int count = map.getOrDefault(t, 0); + if (count == 0) { + throw new IllegalStateException(t + " is not in the multiset"); + } + map.put(t, count - 1); + } + + boolean contains(T t) { + return map.getOrDefault(t, 0) > 0; + } + } } diff --git a/src/main/java/com/squareup/javapoet/FieldSpec.java b/src/main/java/com/squareup/javapoet/FieldSpec.java index 851b36d..f530d6e 100644 --- a/src/main/java/com/squareup/javapoet/FieldSpec.java +++ b/src/main/java/com/squareup/javapoet/FieldSpec.java @@ -111,10 +111,11 @@ public final class FieldSpec { private final String name; private final CodeBlock.Builder javadoc = CodeBlock.builder(); - private final List<AnnotationSpec> annotations = new ArrayList<>(); - private final List<Modifier> modifiers = new ArrayList<>(); private CodeBlock initializer = null; + public final List<AnnotationSpec> annotations = new ArrayList<>(); + public final List<Modifier> modifiers = new ArrayList<>(); + private Builder(TypeName type, String name) { this.type = type; this.name = name; diff --git a/src/main/java/com/squareup/javapoet/JavaFile.java b/src/main/java/com/squareup/javapoet/JavaFile.java index e7662dd..da3dd86 100644 --- a/src/main/java/com/squareup/javapoet/JavaFile.java +++ b/src/main/java/com/squareup/javapoet/JavaFile.java @@ -22,10 +22,12 @@ import java.io.InputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.net.URI; +import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -59,6 +61,7 @@ public final class JavaFile { public final TypeSpec typeSpec; public final boolean skipJavaLangImports; private final Set<String> staticImports; + private final Set<String> alwaysQualify; private final String indent; private JavaFile(Builder builder) { @@ -68,21 +71,63 @@ public final class JavaFile { this.skipJavaLangImports = builder.skipJavaLangImports; this.staticImports = Util.immutableSet(builder.staticImports); this.indent = builder.indent; + + Set<String> alwaysQualifiedNames = new LinkedHashSet<>(); + fillAlwaysQualifiedNames(builder.typeSpec, alwaysQualifiedNames); + this.alwaysQualify = Util.immutableSet(alwaysQualifiedNames); + } + + private void fillAlwaysQualifiedNames(TypeSpec spec, Set<String> alwaysQualifiedNames) { + alwaysQualifiedNames.addAll(spec.alwaysQualifiedNames); + for (TypeSpec nested : spec.typeSpecs) { + fillAlwaysQualifiedNames(nested, alwaysQualifiedNames); + } } public void writeTo(Appendable out) throws IOException { // First pass: emit the entire class, just to collect the types we'll need to import. - CodeWriter importsCollector = new CodeWriter(NULL_APPENDABLE, indent, staticImports); + CodeWriter importsCollector = new CodeWriter( + NULL_APPENDABLE, + indent, + staticImports, + alwaysQualify + ); emit(importsCollector); Map<String, ClassName> suggestedImports = importsCollector.suggestedImports(); // Second pass: write the code, taking advantage of the imports. - CodeWriter codeWriter = new CodeWriter(out, indent, suggestedImports, staticImports); + CodeWriter codeWriter + = new CodeWriter(out, indent, suggestedImports, staticImports, alwaysQualify); emit(codeWriter); } /** Writes this to {@code directory} as UTF-8 using the standard directory structure. */ public void writeTo(Path directory) throws IOException { + writeToPath(directory); + } + + /** + * Writes this to {@code directory} with the provided {@code charset} using the standard directory + * structure. + */ + public void writeTo(Path directory, Charset charset) throws IOException { + writeToPath(directory, charset); + } + + /** + * Writes this to {@code directory} as UTF-8 using the standard directory structure. + * Returns the {@link Path} instance to which source is actually written. + */ + public Path writeToPath(Path directory) throws IOException { + return writeToPath(directory, UTF_8); + } + + /** + * Writes this to {@code directory} with the provided {@code charset} using the standard directory + * structure. + * Returns the {@link Path} instance to which source is actually written. + */ + public Path writeToPath(Path directory, Charset charset) throws IOException { checkArgument(Files.notExists(directory) || Files.isDirectory(directory), "path %s exists but is not a directory.", directory); Path outputDirectory = directory; @@ -94,9 +139,11 @@ public final class JavaFile { } Path outputPath = outputDirectory.resolve(typeSpec.name + ".java"); - try (Writer writer = new OutputStreamWriter(Files.newOutputStream(outputPath), UTF_8)) { + try (Writer writer = new OutputStreamWriter(Files.newOutputStream(outputPath), charset)) { writeTo(writer); } + + return outputPath; } /** Writes this to {@code directory} as UTF-8 using the standard directory structure. */ @@ -104,6 +151,15 @@ public final class JavaFile { writeTo(directory.toPath()); } + /** + * Writes this to {@code directory} as UTF-8 using the standard directory structure. + * Returns the {@link File} instance to which source is actually written. + */ + public File writeToFile(File directory) throws IOException { + final Path outputPath = writeToPath(directory.toPath()); + return outputPath.toFile(); + } + /** Writes this to {@code filer}. */ public void writeTo(Filer filer) throws IOException { String fileName = packageName.isEmpty() @@ -144,7 +200,12 @@ public final class JavaFile { int importedTypesCount = 0; for (ClassName className : new TreeSet<>(codeWriter.importedTypes().values())) { - if (skipJavaLangImports && className.packageName().equals("java.lang")) continue; + // TODO what about nested types like java.util.Map.Entry? + if (skipJavaLangImports + && className.packageName().equals("java.lang") + && !alwaysQualify.contains(className.simpleName)) { + continue; + } codeWriter.emit("import $L;\n", className.withoutAnnotations()); importedTypesCount++; } @@ -216,10 +277,11 @@ public final class JavaFile { private final String packageName; private final TypeSpec typeSpec; private final CodeBlock.Builder fileComment = CodeBlock.builder(); - private final Set<String> staticImports = new TreeSet<>(); private boolean skipJavaLangImports; private String indent = " "; + public final Set<String> staticImports = new TreeSet<>(); + private Builder(String packageName, TypeSpec typeSpec) { this.packageName = packageName; this.typeSpec = typeSpec; diff --git a/src/main/java/com/squareup/javapoet/LineWrapper.java b/src/main/java/com/squareup/javapoet/LineWrapper.java index 6aa3131..928d9f4 100644 --- a/src/main/java/com/squareup/javapoet/LineWrapper.java +++ b/src/main/java/com/squareup/javapoet/LineWrapper.java @@ -24,7 +24,7 @@ import static com.squareup.javapoet.Util.checkNotNull; * or soft-wrapping spaces using {@link #wrappingSpace}. */ final class LineWrapper { - private final Appendable out; + private final RecordingAppendable out; private final String indent; private final int columnLimit; private boolean closed; @@ -47,11 +47,16 @@ final class LineWrapper { LineWrapper(Appendable out, String indent, int columnLimit) { checkNotNull(out, "out == null"); - this.out = out; + this.out = new RecordingAppendable(out); this.indent = indent; this.columnLimit = columnLimit; } + /** @return the last emitted char or {@link Character#MIN_VALUE} if nothing emitted yet. */ + char lastChar() { + return out.lastChar; + } + /** Emit {@code s}. This may be buffered to permit line wraps to be inserted. */ void append(String s) throws IOException { if (closed) throw new IllegalStateException("closed"); @@ -134,4 +139,33 @@ final class LineWrapper { private enum FlushType { WRAP, SPACE, EMPTY; } + + /** A delegating {@link Appendable} that records info about the chars passing through it. */ + static final class RecordingAppendable implements Appendable { + private final Appendable delegate; + + char lastChar = Character.MIN_VALUE; + + RecordingAppendable(Appendable delegate) { + this.delegate = delegate; + } + + @Override public Appendable append(CharSequence csq) throws IOException { + int length = csq.length(); + if (length != 0) { + lastChar = csq.charAt(length - 1); + } + return delegate.append(csq); + } + + @Override public Appendable append(CharSequence csq, int start, int end) throws IOException { + CharSequence sub = csq.subSequence(start, end); + return append(sub); + } + + @Override public Appendable append(char c) throws IOException { + lastChar = c; + return delegate.append(c); + } + } } diff --git a/src/main/java/com/squareup/javapoet/MethodSpec.java b/src/main/java/com/squareup/javapoet/MethodSpec.java index a2c7c43..2284ef5 100644 --- a/src/main/java/com/squareup/javapoet/MethodSpec.java +++ b/src/main/java/com/squareup/javapoet/MethodSpec.java @@ -24,6 +24,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; @@ -82,7 +83,7 @@ public final class MethodSpec { void emit(CodeWriter codeWriter, String enclosingName, Set<Modifier> implicitModifiers) throws IOException { - codeWriter.emitJavadoc(javadoc); + codeWriter.emitJavadoc(javadocWithParameters()); codeWriter.emitAnnotations(annotations, false); codeWriter.emitModifiers(modifiers, implicitModifiers); @@ -132,11 +133,26 @@ public final class MethodSpec { codeWriter.emit(" {\n"); codeWriter.indent(); - codeWriter.emit(code); + codeWriter.emit(code, true); codeWriter.unindent(); codeWriter.emit("}\n"); } + codeWriter.popTypeVariables(typeVariables); + } + + private CodeBlock javadocWithParameters() { + CodeBlock.Builder builder = javadoc.toBuilder(); + boolean emitTagNewline = true; + for (ParameterSpec parameterSpec : parameters) { + if (!parameterSpec.javadoc.isEmpty()) { + // Emit a new line before @param section only if the method javadoc is present. + if (emitTagNewline && !javadoc.isEmpty()) builder.add("\n"); + emitTagNewline = false; + builder.add("@param $L $L", parameterSpec.name, parameterSpec.javadoc); + } + } + return builder.build(); } public boolean hasModifier(Modifier modifier) { @@ -217,7 +233,16 @@ public final class MethodSpec { } methodBuilder.returns(TypeName.get(method.getReturnType())); - methodBuilder.addParameters(ParameterSpec.parametersOf(method)); + // Copying parameter annotations from the overridden method can be incorrect so we're + // deliberately dropping them. See https://github.com/square/javapoet/issues/482. + methodBuilder.addParameters(ParameterSpec.parametersOf(method) + .stream() + .map(parameterSpec -> { + ParameterSpec.Builder builder = parameterSpec.toBuilder(); + builder.annotations.clear(); + return builder.build(); + }) + .collect(Collectors.toList())); methodBuilder.varargs(method.isVarArgs()); for (TypeMirror thrownType : method.getThrownTypes()) { @@ -277,25 +302,31 @@ public final class MethodSpec { } public static final class Builder { - private final String name; + private String name; private final CodeBlock.Builder javadoc = CodeBlock.builder(); - private final List<AnnotationSpec> annotations = new ArrayList<>(); - private final List<Modifier> modifiers = new ArrayList<>(); - private List<TypeVariableName> typeVariables = new ArrayList<>(); private TypeName returnType; - private final List<ParameterSpec> parameters = new ArrayList<>(); private final Set<TypeName> exceptions = new LinkedHashSet<>(); private final CodeBlock.Builder code = CodeBlock.builder(); private boolean varargs; private CodeBlock defaultValue; + public final List<TypeVariableName> typeVariables = new ArrayList<>(); + public final List<AnnotationSpec> annotations = new ArrayList<>(); + public final List<Modifier> modifiers = new ArrayList<>(); + public final List<ParameterSpec> parameters = new ArrayList<>(); + private Builder(String name) { + setName(name); + } + + public Builder setName(String name) { checkNotNull(name, "name == null"); checkArgument(name.equals(CONSTRUCTOR) || SourceVersion.isName(name), "not a valid name: %s", name); this.name = name; this.returnType = name.equals(CONSTRUCTOR) ? null : TypeName.VOID; + return this; } public Builder addJavadoc(String format, Object... args) { @@ -454,6 +485,14 @@ public final class MethodSpec { } /** + * @param codeBlock the control flow construct and its code, such as "if (foo == 5)". + * Shouldn't contain braces or newline characters. + */ + public Builder beginControlFlow(CodeBlock codeBlock) { + return beginControlFlow("$L", codeBlock); + } + + /** * @param controlFlow the control flow construct and its code, such as "else if (foo == 10)". * Shouldn't contain braces or newline characters. */ @@ -462,6 +501,14 @@ public final class MethodSpec { return this; } + /** + * @param codeBlock the control flow construct and its code, such as "else if (foo == 10)". + * Shouldn't contain braces or newline characters. + */ + public Builder nextControlFlow(CodeBlock codeBlock) { + return nextControlFlow("$L", codeBlock); + } + public Builder endControlFlow() { code.endControlFlow(); return this; @@ -476,6 +523,14 @@ public final class MethodSpec { return this; } + /** + * @param codeBlock the optional control flow construct and its code, such as + * "while(foo == 20)". Only used for "do/while" control flows. + */ + public Builder endControlFlow(CodeBlock codeBlock) { + return endControlFlow("$L", codeBlock); + } + public Builder addStatement(String format, Object... args) { code.addStatement(format, args); return this; diff --git a/src/main/java/com/squareup/javapoet/ParameterSpec.java b/src/main/java/com/squareup/javapoet/ParameterSpec.java index 63da3f2..b8f3129 100644 --- a/src/main/java/com/squareup/javapoet/ParameterSpec.java +++ b/src/main/java/com/squareup/javapoet/ParameterSpec.java @@ -21,7 +21,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import javax.lang.model.SourceVersion; +import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.VariableElement; @@ -35,12 +37,14 @@ public final class ParameterSpec { public final List<AnnotationSpec> annotations; public final Set<Modifier> modifiers; public final TypeName type; + public final CodeBlock javadoc; private ParameterSpec(Builder builder) { this.name = checkNotNull(builder.name, "name == null"); this.annotations = Util.immutableList(builder.annotations); this.modifiers = Util.immutableSet(builder.modifiers); this.type = checkNotNull(builder.type, "type == null"); + this.javadoc = builder.javadoc.build(); } public boolean hasModifier(Modifier modifier) { @@ -81,10 +85,19 @@ public final class ParameterSpec { } public static ParameterSpec get(VariableElement element) { + checkArgument(element.getKind().equals(ElementKind.PARAMETER), "element is not a parameter"); + + // Copy over any annotations from element. + List<AnnotationSpec> annotations = element.getAnnotationMirrors() + .stream() + .map((mirror) -> AnnotationSpec.get(mirror)) + .collect(Collectors.toList()); + TypeName type = TypeName.get(element.asType()); String name = element.getSimpleName().toString(); return ParameterSpec.builder(type, name) .addModifiers(element.getModifiers()) + .addAnnotations(annotations) .build(); } @@ -121,15 +134,26 @@ public final class ParameterSpec { public static final class Builder { private final TypeName type; private final String name; + private final CodeBlock.Builder javadoc = CodeBlock.builder(); - private final List<AnnotationSpec> annotations = new ArrayList<>(); - private final List<Modifier> modifiers = new ArrayList<>(); + public final List<AnnotationSpec> annotations = new ArrayList<>(); + public final List<Modifier> modifiers = new ArrayList<>(); private Builder(TypeName type, String name) { this.type = type; this.name = name; } + public Builder addJavadoc(String format, Object... args) { + javadoc.add(format, args); + return this; + } + + public Builder addJavadoc(CodeBlock block) { + javadoc.add(block); + return this; + } + public Builder addAnnotations(Iterable<AnnotationSpec> annotationSpecs) { checkArgument(annotationSpecs != null, "annotationSpecs == null"); for (AnnotationSpec annotationSpec : annotationSpecs) { @@ -160,6 +184,9 @@ public final class ParameterSpec { public Builder addModifiers(Iterable<Modifier> modifiers) { checkNotNull(modifiers, "modifiers == null"); for (Modifier modifier : modifiers) { + if (!modifier.equals(Modifier.FINAL)) { + throw new IllegalStateException("unexpected parameter modifier: " + modifier); + } this.modifiers.add(modifier); } return this; diff --git a/src/main/java/com/squareup/javapoet/TypeName.java b/src/main/java/com/squareup/javapoet/TypeName.java index 38877f7..c0986bb 100644 --- a/src/main/java/com/squareup/javapoet/TypeName.java +++ b/src/main/java/com/squareup/javapoet/TypeName.java @@ -44,7 +44,7 @@ import javax.lang.model.util.SimpleTypeVisitor8; * identifies composite types like {@code char[]} and {@code Set<Long>}. * * <p>Type names are dumb identifiers only and do not model the values they name. For example, the - * type name for {@code java.lang.List} doesn't know about the {@code size()} method, the fact that + * type name for {@code java.util.List} doesn't know about the {@code size()} method, the fact that * lists are collections, or even that it accepts a single type parameter. * * <p>Instances of this class are immutable value objects that implement {@code equals()} and {@code diff --git a/src/main/java/com/squareup/javapoet/TypeSpec.java b/src/main/java/com/squareup/javapoet/TypeSpec.java index 46de3a5..5fb2bb3 100644 --- a/src/main/java/com/squareup/javapoet/TypeSpec.java +++ b/src/main/java/com/squareup/javapoet/TypeSpec.java @@ -16,13 +16,16 @@ package com.squareup.javapoet; import java.io.IOException; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -30,6 +33,11 @@ import java.util.Set; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; import static com.squareup.javapoet.Util.checkArgument; import static com.squareup.javapoet.Util.checkNotNull; @@ -53,7 +61,9 @@ public final class TypeSpec { public final CodeBlock initializerBlock; public final List<MethodSpec> methodSpecs; public final List<TypeSpec> typeSpecs; + final Set<String> nestedTypesSimpleNames; public final List<Element> originatingElements; + public final Set<String> alwaysQualifiedNames; private TypeSpec(Builder builder) { this.kind = builder.kind; @@ -71,12 +81,16 @@ public final class TypeSpec { this.initializerBlock = builder.initializerBlock.build(); this.methodSpecs = Util.immutableList(builder.methodSpecs); this.typeSpecs = Util.immutableList(builder.typeSpecs); + this.alwaysQualifiedNames = Util.immutableSet(builder.alwaysQualifiedNames); + nestedTypesSimpleNames = new HashSet<>(builder.typeSpecs.size()); List<Element> originatingElementsMutable = new ArrayList<>(); originatingElementsMutable.addAll(builder.originatingElements); for (TypeSpec typeSpec : builder.typeSpecs) { + nestedTypesSimpleNames.add(typeSpec.name); originatingElementsMutable.addAll(typeSpec.originatingElements); } + this.originatingElements = Util.immutableList(originatingElementsMutable); } @@ -102,6 +116,8 @@ public final class TypeSpec { this.methodSpecs = Collections.emptyList(); this.typeSpecs = Collections.emptyList(); this.originatingElements = Collections.emptyList(); + this.nestedTypesSimpleNames = Collections.emptySet(); + this.alwaysQualifiedNames = Collections.emptySet(); } public boolean hasModifier(Modifier modifier) { @@ -133,9 +149,7 @@ public final class TypeSpec { } public static Builder anonymousClassBuilder(String typeArgumentsFormat, Object... args) { - return anonymousClassBuilder(CodeBlock.builder() - .add(typeArgumentsFormat, args) - .build()); + return anonymousClassBuilder(CodeBlock.of(typeArgumentsFormat, args)); } public static Builder anonymousClassBuilder(CodeBlock typeArguments) { @@ -164,6 +178,8 @@ public final class TypeSpec { builder.typeSpecs.addAll(typeSpecs); builder.initializerBlock.add(initializerBlock); builder.staticBlock.add(staticBlock); + builder.originatingElements.addAll(originatingElements); + builder.alwaysQualifiedNames.addAll(alwaysQualifiedNames); return builder; } @@ -316,6 +332,7 @@ public final class TypeSpec { codeWriter.unindent(); codeWriter.popType(); + codeWriter.popTypeVariables(typeVariables); codeWriter.emit("}"); if (enumName == null && anonymousTypeArguments == null) { @@ -395,18 +412,20 @@ public final class TypeSpec { private final CodeBlock anonymousTypeArguments; private final CodeBlock.Builder javadoc = CodeBlock.builder(); - private final List<AnnotationSpec> annotations = new ArrayList<>(); - private final List<Modifier> modifiers = new ArrayList<>(); - private final List<TypeVariableName> typeVariables = new ArrayList<>(); private TypeName superclass = ClassName.OBJECT; - private final List<TypeName> superinterfaces = new ArrayList<>(); - private final Map<String, TypeSpec> enumConstants = new LinkedHashMap<>(); - private final List<FieldSpec> fieldSpecs = new ArrayList<>(); private final CodeBlock.Builder staticBlock = CodeBlock.builder(); private final CodeBlock.Builder initializerBlock = CodeBlock.builder(); - private final List<MethodSpec> methodSpecs = new ArrayList<>(); - private final List<TypeSpec> typeSpecs = new ArrayList<>(); - private final List<Element> originatingElements = new ArrayList<>(); + + public final Map<String, TypeSpec> enumConstants = new LinkedHashMap<>(); + public final List<AnnotationSpec> annotations = new ArrayList<>(); + public final List<Modifier> modifiers = new ArrayList<>(); + public final List<TypeVariableName> typeVariables = new ArrayList<>(); + public final List<TypeName> superinterfaces = new ArrayList<>(); + public final List<FieldSpec> fieldSpecs = new ArrayList<>(); + public final List<MethodSpec> methodSpecs = new ArrayList<>(); + public final List<TypeSpec> typeSpecs = new ArrayList<>(); + public final List<Element> originatingElements = new ArrayList<>(); + public final Set<String> alwaysQualifiedNames = new LinkedHashSet<>(); private Builder(Kind kind, String name, CodeBlock anonymousTypeArguments) { @@ -449,16 +468,11 @@ public final class TypeSpec { } public Builder addModifiers(Modifier... modifiers) { - checkState(anonymousTypeArguments == null, "forbidden on anonymous types."); - for (Modifier modifier : modifiers) { - checkArgument(modifier != null, "modifiers contain null"); - this.modifiers.add(modifier); - } + Collections.addAll(this.modifiers, modifiers); return this; } public Builder addTypeVariables(Iterable<TypeVariableName> typeVariables) { - checkState(anonymousTypeArguments == null, "forbidden on anonymous types."); checkArgument(typeVariables != null, "typeVariables == null"); for (TypeVariableName typeVariable : typeVariables) { this.typeVariables.add(typeVariable); @@ -467,7 +481,6 @@ public final class TypeSpec { } public Builder addTypeVariable(TypeVariableName typeVariable) { - checkState(anonymousTypeArguments == null, "forbidden on anonymous types."); typeVariables.add(typeVariable); return this; } @@ -482,7 +495,32 @@ public final class TypeSpec { } public Builder superclass(Type superclass) { - return superclass(TypeName.get(superclass)); + return superclass(superclass, true); + } + + public Builder superclass(Type superclass, boolean avoidNestedTypeNameClashes) { + superclass(TypeName.get(superclass)); + if (avoidNestedTypeNameClashes) { + Class<?> clazz = getRawType(superclass); + if (clazz != null) { + avoidClashesWithNestedClasses(clazz); + } + } + return this; + } + + public Builder superclass(TypeMirror superclass) { + return superclass(superclass, true); + } + + public Builder superclass(TypeMirror superclass, boolean avoidNestedTypeNameClashes) { + superclass(TypeName.get(superclass)); + if (avoidNestedTypeNameClashes && superclass instanceof DeclaredType) { + TypeElement superInterfaceElement = + (TypeElement) ((DeclaredType) superclass).asElement(); + avoidClashesWithNestedClasses(superInterfaceElement); + } + return this; } public Builder addSuperinterfaces(Iterable<? extends TypeName> superinterfaces) { @@ -500,7 +538,43 @@ public final class TypeSpec { } public Builder addSuperinterface(Type superinterface) { - return addSuperinterface(TypeName.get(superinterface)); + return addSuperinterface(superinterface, true); + } + + public Builder addSuperinterface(Type superinterface, boolean avoidNestedTypeNameClashes) { + addSuperinterface(TypeName.get(superinterface)); + if (avoidNestedTypeNameClashes) { + Class<?> clazz = getRawType(superinterface); + if (clazz != null) { + avoidClashesWithNestedClasses(clazz); + } + } + return this; + } + + private Class<?> getRawType(Type type) { + if (type instanceof Class<?>) { + return (Class<?>) type; + } else if (type instanceof ParameterizedType) { + return getRawType(((ParameterizedType) type).getRawType()); + } else { + return null; + } + } + + public Builder addSuperinterface(TypeMirror superinterface) { + return addSuperinterface(superinterface, true); + } + + public Builder addSuperinterface(TypeMirror superinterface, + boolean avoidNestedTypeNameClashes) { + addSuperinterface(TypeName.get(superinterface)); + if (avoidNestedTypeNameClashes && superinterface instanceof DeclaredType) { + TypeElement superInterfaceElement = + (TypeElement) ((DeclaredType) superinterface).asElement(); + avoidClashesWithNestedClasses(superInterfaceElement); + } + return this; } public Builder addEnumConstant(String name) { @@ -508,10 +582,6 @@ public final class TypeSpec { } public Builder addEnumConstant(String name, TypeSpec typeSpec) { - checkState(kind == Kind.ENUM, "%s is not enum", this.name); - checkArgument(typeSpec.anonymousTypeArguments != null, - "enum constants must have anonymous type arguments"); - checkArgument(SourceVersion.isName(name), "not a valid enum constant: %s", name); enumConstants.put(name, typeSpec); return this; } @@ -525,12 +595,6 @@ public final class TypeSpec { } public Builder addField(FieldSpec fieldSpec) { - if (kind == Kind.INTERFACE || kind == Kind.ANNOTATION) { - requireExactlyOneOf(fieldSpec.modifiers, Modifier.PUBLIC, Modifier.PRIVATE); - Set<Modifier> check = EnumSet.of(Modifier.STATIC, Modifier.FINAL); - checkState(fieldSpec.modifiers.containsAll(check), "%s %s.%s requires modifiers %s", - kind, name, fieldSpec.name, check); - } fieldSpecs.add(fieldSpec); return this; } @@ -569,23 +633,6 @@ public final class TypeSpec { } public Builder addMethod(MethodSpec methodSpec) { - if (kind == Kind.INTERFACE) { - requireExactlyOneOf(methodSpec.modifiers, Modifier.ABSTRACT, Modifier.STATIC, - Modifier.DEFAULT); - requireExactlyOneOf(methodSpec.modifiers, Modifier.PUBLIC, Modifier.PRIVATE); - } else if (kind == Kind.ANNOTATION) { - checkState(methodSpec.modifiers.equals(kind.implicitMethodModifiers), - "%s %s.%s requires modifiers %s", - kind, name, methodSpec.name, kind.implicitMethodModifiers); - } - if (kind != Kind.ANNOTATION) { - checkState(methodSpec.defaultValue == null, "%s %s.%s cannot have a default value", - kind, name, methodSpec.name); - } - if (kind != Kind.INTERFACE) { - checkState(!methodSpec.hasModifier(Modifier.DEFAULT), "%s %s.%s cannot be default", - kind, name, methodSpec.name); - } methodSpecs.add(methodSpec); return this; } @@ -599,9 +646,6 @@ public final class TypeSpec { } public Builder addType(TypeSpec typeSpec) { - checkArgument(typeSpec.modifiers.containsAll(kind.implicitTypeModifiers), - "%s %s.%s requires modifiers %s", kind, name, typeSpec.name, - kind.implicitTypeModifiers); typeSpecs.add(typeSpec); return this; } @@ -611,10 +655,171 @@ public final class TypeSpec { return this; } + public Builder alwaysQualify(String... simpleNames) { + checkArgument(simpleNames != null, "simpleNames == null"); + for (String name : simpleNames) { + checkArgument( + name != null, + "null entry in simpleNames array: %s", + Arrays.toString(simpleNames) + ); + alwaysQualifiedNames.add(name); + } + return this; + } + + /** + * Call this to always fully qualify any types that would conflict with possibly nested types of + * this {@code typeElement}. For example - if the following type was passed in as the + * typeElement: + * + * <pre><code> + * class Foo { + * class NestedTypeA { + * + * } + * class NestedTypeB { + * + * } + * } + * </code></pre> + * + * <p> + * Then this would add {@code "NestedTypeA"} and {@code "NestedTypeB"} as names that should + * always be qualified via {@link #alwaysQualify(String...)}. This way they would avoid + * possible import conflicts when this JavaFile is written. + * + * @param typeElement the {@link TypeElement} with nested types to avoid clashes with. + * @return this builder instance. + */ + public Builder avoidClashesWithNestedClasses(TypeElement typeElement) { + checkArgument(typeElement != null, "typeElement == null"); + for (TypeElement nestedType : ElementFilter.typesIn(typeElement.getEnclosedElements())) { + alwaysQualify(nestedType.getSimpleName().toString()); + } + TypeMirror superclass = typeElement.getSuperclass(); + if (!(superclass instanceof NoType) && superclass instanceof DeclaredType) { + TypeElement superclassElement = (TypeElement) ((DeclaredType) superclass).asElement(); + avoidClashesWithNestedClasses(superclassElement); + } + for (TypeMirror superinterface : typeElement.getInterfaces()) { + if (superinterface instanceof DeclaredType) { + TypeElement superinterfaceElement + = (TypeElement) ((DeclaredType) superinterface).asElement(); + avoidClashesWithNestedClasses(superinterfaceElement); + } + } + return this; + } + + /** + * Call this to always fully qualify any types that would conflict with possibly nested types of + * this {@code typeElement}. For example - if the following type was passed in as the + * typeElement: + * + * <pre><code> + * class Foo { + * class NestedTypeA { + * + * } + * class NestedTypeB { + * + * } + * } + * </code></pre> + * + * <p> + * Then this would add {@code "NestedTypeA"} and {@code "NestedTypeB"} as names that should + * always be qualified via {@link #alwaysQualify(String...)}. This way they would avoid + * possible import conflicts when this JavaFile is written. + * + * @param clazz the {@link Class} with nested types to avoid clashes with. + * @return this builder instance. + */ + public Builder avoidClashesWithNestedClasses(Class<?> clazz) { + checkArgument(clazz != null, "clazz == null"); + for (Class<?> nestedType : clazz.getDeclaredClasses()) { + alwaysQualify(nestedType.getSimpleName()); + } + Class<?> superclass = clazz.getSuperclass(); + if (superclass != null && !Object.class.equals(superclass)) { + avoidClashesWithNestedClasses(superclass); + } + for (Class<?> superinterface : clazz.getInterfaces()) { + avoidClashesWithNestedClasses(superinterface); + } + return this; + } + public TypeSpec build() { + for (AnnotationSpec annotationSpec : annotations) { + checkNotNull(annotationSpec, "annotationSpec == null"); + } + + if (!modifiers.isEmpty()) { + checkState(anonymousTypeArguments == null, "forbidden on anonymous types."); + for (Modifier modifier : modifiers) { + checkArgument(modifier != null, "modifiers contain null"); + } + } + checkArgument(kind != Kind.ENUM || !enumConstants.isEmpty(), "at least one enum constant is required for %s", name); + for (TypeName superinterface : superinterfaces) { + checkArgument(superinterface != null, "superinterfaces contains null"); + } + + if (!typeVariables.isEmpty()) { + checkState(anonymousTypeArguments == null, + "typevariables are forbidden on anonymous types."); + for (TypeVariableName typeVariableName : typeVariables) { + checkArgument(typeVariableName != null, "typeVariables contain null"); + } + } + + for (Map.Entry<String, TypeSpec> enumConstant : enumConstants.entrySet()) { + checkState(kind == Kind.ENUM, "%s is not enum", this.name); + checkArgument(enumConstant.getValue().anonymousTypeArguments != null, + "enum constants must have anonymous type arguments"); + checkArgument(SourceVersion.isName(name), "not a valid enum constant: %s", name); + } + + for (FieldSpec fieldSpec : fieldSpecs) { + if (kind == Kind.INTERFACE || kind == Kind.ANNOTATION) { + requireExactlyOneOf(fieldSpec.modifiers, Modifier.PUBLIC, Modifier.PRIVATE); + Set<Modifier> check = EnumSet.of(Modifier.STATIC, Modifier.FINAL); + checkState(fieldSpec.modifiers.containsAll(check), "%s %s.%s requires modifiers %s", + kind, name, fieldSpec.name, check); + } + } + + for (MethodSpec methodSpec : methodSpecs) { + if (kind == Kind.INTERFACE) { + requireExactlyOneOf(methodSpec.modifiers, Modifier.ABSTRACT, Modifier.STATIC, + Modifier.DEFAULT); + requireExactlyOneOf(methodSpec.modifiers, Modifier.PUBLIC, Modifier.PRIVATE); + } else if (kind == Kind.ANNOTATION) { + checkState(methodSpec.modifiers.equals(kind.implicitMethodModifiers), + "%s %s.%s requires modifiers %s", + kind, name, methodSpec.name, kind.implicitMethodModifiers); + } + if (kind != Kind.ANNOTATION) { + checkState(methodSpec.defaultValue == null, "%s %s.%s cannot have a default value", + kind, name, methodSpec.name); + } + if (kind != Kind.INTERFACE) { + checkState(!methodSpec.hasModifier(Modifier.DEFAULT), "%s %s.%s cannot be default", + kind, name, methodSpec.name); + } + } + + for (TypeSpec typeSpec : typeSpecs) { + checkArgument(typeSpec.modifiers.containsAll(kind.implicitTypeModifiers), + "%s %s.%s requires modifiers %s", kind, name, typeSpec.name, + kind.implicitTypeModifiers); + } + boolean isAbstract = modifiers.contains(Modifier.ABSTRACT) || kind != Kind.CLASS; for (MethodSpec methodSpec : methodSpecs) { checkArgument(isAbstract || !methodSpec.hasModifier(Modifier.ABSTRACT), |