blob: 56eaad25f3bbb6be5ba2f284d736201d152ed7f0 [file] [log] [blame]
/*
* Copyright 2015 Google LLC
*
* 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 com.google.auto.value.processor;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.testing.compile.CompilationSubject.assertThat;
import static com.google.testing.compile.Compiler.javac;
import com.google.auto.common.MoreTypes;
import com.google.auto.value.extension.AutoValueExtension;
import com.google.auto.value.extension.AutoValueExtension.BuilderContext;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.truth.Truth;
import com.google.testing.compile.Compilation;
import com.google.testing.compile.JavaFileObjects;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Writer;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceConfigurationError;
import java.util.Set;
import java.util.function.Consumer;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
import javax.annotation.processing.Filer;
import javax.annotation.processing.SupportedOptions;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class ExtensionTest {
@Test
public void testExtensionCompilation() {
JavaFileObject javaFileObject =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"",
"import com.google.auto.value.AutoValue;",
"",
"@AutoValue",
"public abstract class Baz {",
" abstract String foo();",
"}");
JavaFileObject expectedExtensionOutput =
JavaFileObjects.forSourceLines(
"foo.bar.AutoValue_Baz",
"package foo.bar;",
"",
"final class AutoValue_Baz extends $AutoValue_Baz {",
" public AutoValue_Baz(String foo) {",
" super(foo);",
" }",
" @Override public String foo() {",
" return \"foo\";",
" }",
" public String dizzle() {\n",
" return \"dizzle\";\n",
" }",
"}");
Compilation compilation =
javac()
.withProcessors(new AutoValueProcessor(ImmutableList.of(new FooExtension())))
.compile(javaFileObject);
assertThat(compilation).succeededWithoutWarnings();
assertThat(compilation)
.generatedSourceFile("foo.bar.AutoValue_Baz")
.hasSourceEquivalentTo(expectedExtensionOutput);
}
@Test
public void testExtensionConsumesProperties() {
JavaFileObject javaFileObject =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"",
"import com.google.auto.value.AutoValue;",
"",
"@AutoValue",
"public abstract class Baz {",
" abstract String foo();",
" abstract String dizzle();",
"}");
JavaFileObject expectedExtensionOutput =
JavaFileObjects.forSourceLines(
"foo.bar.$AutoValue_Baz",
"package foo.bar;",
"",
GeneratedImport.importGeneratedAnnotationType(),
"",
"@Generated(\"com.google.auto.value.processor.AutoValueProcessor\")",
" abstract class $AutoValue_Baz extends Baz {",
"",
" private final String foo;",
"",
" $AutoValue_Baz(",
" String foo) {",
" if (foo == null) {",
" throw new NullPointerException(\"Null foo\");",
" }",
" this.foo = foo;",
" }",
"",
" @Override",
" String foo() {",
" return foo;",
" }",
"",
" @Override",
" public String toString() {",
" return \"Baz{\"",
" + \"foo=\" + foo",
" + \"}\";",
" }",
"",
" @Override",
" public boolean equals(Object o) {",
" if (o == this) {",
" return true;",
" }",
" if (o instanceof Baz) {",
" Baz that = (Baz) o;",
" return this.foo.equals(that.foo());",
" }",
" return false;",
" }",
"",
" @Override",
" public int hashCode() {",
" int h$ = 1;",
" h$ *= 1000003;",
" h$ ^= foo.hashCode();",
" return h$;",
" }",
"}");
Compilation compilation =
javac()
.withProcessors(new AutoValueProcessor(ImmutableList.of(new FooExtension())))
.withOptions("-A" + Nullables.NULLABLE_OPTION + "=")
.compile(javaFileObject);
assertThat(compilation).succeededWithoutWarnings();
assertThat(compilation)
.generatedSourceFile("foo.bar.$AutoValue_Baz")
.hasSourceEquivalentTo(expectedExtensionOutput);
}
@Test
public void testDoesntRaiseWarningForConsumedProperties() {
JavaFileObject impl =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"import com.google.auto.value.AutoValue;",
"@AutoValue public abstract class Baz {",
" abstract String foo();",
" abstract String dizzle();",
"",
" @AutoValue.Builder",
" public abstract static class Builder {",
" public abstract Builder foo(String s);",
" public abstract Baz build();",
" }",
"}");
Compilation compilation =
javac()
.withProcessors(new AutoValueProcessor(ImmutableList.of(new FooExtension())))
.compile(impl);
assertThat(compilation).succeededWithoutWarnings();
}
@Test
public void testDoesntRaiseWarningForToBuilder() {
JavaFileObject impl =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"import com.google.auto.value.AutoValue;",
"@AutoValue public abstract class Baz {",
" abstract String foo();",
" abstract String dizzle();",
" abstract Builder toBuilder();",
"",
" @AutoValue.Builder",
" public abstract static class Builder {",
" public abstract Builder foo(String s);",
" public abstract Baz build();",
" }",
"}");
Compilation compilation =
javac()
.withProcessors(new AutoValueProcessor(ImmutableList.of(new FooExtension())))
.compile(impl);
assertThat(compilation).succeededWithoutWarnings();
}
@Test
public void testCantConsumeTwice() {
class ConsumeDizzle extends NonFinalExtension {
@Override
public Set<String> consumeProperties(Context context) {
return ImmutableSet.of("dizzle");
}
}
class AlsoConsumeDizzle extends ConsumeDizzle {}
AutoValueExtension ext1 = new ConsumeDizzle();
AutoValueExtension ext2 = new AlsoConsumeDizzle();
Truth.assertThat(ext1).isNotEqualTo(ext2);
JavaFileObject impl =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"import com.google.auto.value.AutoValue;",
"@AutoValue public abstract class Baz {",
" abstract String foo();",
" abstract String dizzle();",
"}");
Compilation compilation =
javac().withProcessors(new AutoValueProcessor(ImmutableList.of(ext1, ext2))).compile(impl);
assertThat(compilation)
.hadErrorContaining("wants to consume a method that was already consumed")
.inFile(impl)
.onLineContaining("String dizzle()");
}
@Test
public void testCantConsumeNonExistentProperty() {
class ConsumeDizzle extends NonFinalExtension {
@Override
public Set<String> consumeProperties(Context context) {
return ImmutableSet.of("dizzle");
}
}
JavaFileObject impl =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"import com.google.auto.value.AutoValue;",
"@AutoValue public abstract class Baz {",
" abstract String foo();",
"}");
Compilation compilation =
javac()
.withProcessors(new AutoValueProcessor(ImmutableList.of(new ConsumeDizzle())))
.compile(impl);
assertThat(compilation)
.hadErrorContaining("wants to consume a property that does not exist: dizzle")
.inFile(impl)
.onLineContaining("@AutoValue public abstract class Baz");
}
@Test
public void testCantConsumeConcreteMethod() {
class ConsumeConcreteMethod extends NonFinalExtension {
@Override
public Set<ExecutableElement> consumeMethods(Context context) {
TypeElement autoValueClass = context.autoValueClass();
for (ExecutableElement method :
ElementFilter.methodsIn(autoValueClass.getEnclosedElements())) {
if (method.getSimpleName().contentEquals("frob")) {
return ImmutableSet.of(method);
}
}
throw new AssertionError("Could not find frob method");
}
}
JavaFileObject impl =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"import com.google.auto.value.AutoValue;",
"@AutoValue public abstract class Baz {",
" abstract String foo();",
" void frob(int x) {}",
"}");
Compilation compilation =
javac()
.withProcessors(new AutoValueProcessor(ImmutableList.of(new ConsumeConcreteMethod())))
.compile(impl);
assertThat(compilation)
.hadErrorContainingMatch(
"wants to consume a method that is not one of the abstract methods in this class"
+ ".*frob\\(int\\)")
.inFile(impl)
.onLineContaining("@AutoValue public abstract class Baz");
}
@Test
public void testCantConsumeNonExistentMethod() {
class ConsumeBogusMethod extends NonFinalExtension {
@Override
public Set<ExecutableElement> consumeMethods(Context context) {
// Find Integer.intValue() and try to consume that.
Elements elementUtils = context.processingEnvironment().getElementUtils();
TypeElement javaLangInteger = elementUtils.getTypeElement(Integer.class.getName());
for (ExecutableElement method :
ElementFilter.methodsIn(javaLangInteger.getEnclosedElements())) {
if (method.getSimpleName().contentEquals("intValue")) {
return ImmutableSet.of(method);
}
}
throw new AssertionError("Could not find Integer.intValue()");
}
}
JavaFileObject impl =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"import com.google.auto.value.AutoValue;",
"@AutoValue public abstract class Baz {",
" abstract String foo();",
"}");
Compilation compilation =
javac()
.withProcessors(new AutoValueProcessor(ImmutableList.of(new ConsumeBogusMethod())))
.compile(impl);
assertThat(compilation)
.hadErrorContainingMatch(
"wants to consume a method that is not one of the abstract methods in this class"
+ ".*intValue\\(\\)")
.inFile(impl)
.onLineContaining("@AutoValue public abstract class Baz");
}
@Test
public void testExtensionWithoutConsumedPropertiesFails() {
JavaFileObject javaFileObject =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"",
"import com.google.auto.value.AutoValue;",
"",
"@AutoValue",
"public abstract class Baz {",
" abstract String foo();",
" abstract String dizzle();",
" abstract Double[] bad();",
"}");
Compilation compilation =
javac()
.withProcessors(new AutoValueProcessor(ImmutableList.of(new FooExtension())))
.compile(javaFileObject);
assertThat(compilation)
.hadErrorContaining(
"An @AutoValue class cannot define an array-valued property unless "
+ "it is a primitive array")
.inFile(javaFileObject)
.onLineContaining("abstract Double[] bad()");
}
@Test
public void testConsumeMethodWithArguments() {
JavaFileObject javaFileObject =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"",
"import com.google.auto.value.AutoValue;",
"",
"@AutoValue",
"public abstract class Baz {",
" abstract String foo();",
" abstract void writeToParcel(Object parcel, int flags);",
"}");
Compilation compilation =
javac()
.withProcessors(
new AutoValueProcessor(ImmutableList.of(new FakeWriteToParcelExtension())))
.compile(javaFileObject);
assertThat(compilation).succeededWithoutWarnings();
}
@Test
public void testExtensionWithBuilderCompilation() {
JavaFileObject javaFileObject =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"",
"import com.google.auto.value.AutoValue;",
"",
"@AutoValue",
"public abstract class Baz {",
" abstract String foo();",
" abstract String bar();",
"",
" @AutoValue.Builder public static abstract class Builder {",
" public abstract Builder foo(String foo);",
" public abstract Builder bar(String bar);",
" public abstract Baz build();",
" }",
"}");
JavaFileObject expectedExtensionOutput =
JavaFileObjects.forSourceLines(
"foo.bar.AutoValue_Baz",
"package foo.bar;",
"",
"final class AutoValue_Baz extends $AutoValue_Baz {",
" public AutoValue_Baz(String foo, String bar) {",
" super(foo, bar);",
" }",
" @Override public String foo() {",
" return \"foo\";",
" }",
" public String dizzle() {\n",
" return \"dizzle\";\n",
" }",
"}");
Compilation compilation =
javac()
.withProcessors(new AutoValueProcessor(ImmutableList.of(new FooExtension())))
.compile(javaFileObject);
assertThat(compilation).succeededWithoutWarnings();
assertThat(compilation)
.generatedSourceFile("foo.bar.AutoValue_Baz")
.hasSourceEquivalentTo(expectedExtensionOutput);
}
@Test
public void testLastExtensionGeneratesNoCode() {
doTestNoCode(new FooExtension(), new NonFinalExtension(), new SideFileExtension());
}
@Test
public void testFirstExtensionGeneratesNoCode() {
doTestNoCode(new SideFileExtension(), new FooExtension(), new NonFinalExtension());
}
@Test
public void testMiddleExtensionGeneratesNoCode() {
doTestNoCode(new FooExtension(), new SideFileExtension(), new NonFinalExtension());
}
@Test
public void testLoneExtensionGeneratesNoCode() {
doTestNoCode(new SideFileExtension());
}
private void doTestNoCode(AutoValueExtension... extensions) {
JavaFileObject javaFileObject =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"",
"import com.google.auto.value.AutoValue;",
"",
"@AutoValue",
"public abstract class Baz {",
" public abstract String foo();",
"",
" public static Baz create(String foo) {",
" return new AutoValue_Baz(foo);",
" }",
"}");
Compilation compilation =
javac()
.withProcessors(new AutoValueProcessor(ImmutableList.copyOf(extensions)))
.compile(javaFileObject);
assertThat(compilation).succeededWithoutWarnings();
assertThat(compilation)
.generatedFile(StandardLocation.SOURCE_OUTPUT, "foo.bar", "Side_Baz.java");
}
@Test
public void testTwoExtensionsBothWantToBeFinal() {
JavaFileObject javaFileObject =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"",
"import com.google.auto.value.AutoValue;",
"",
"@AutoValue",
"public abstract class Baz {",
" abstract String foo();",
"}");
Compilation compilation =
javac()
.withProcessors(
new AutoValueProcessor(ImmutableList.of(new FooExtension(), new FinalExtension())))
.compile(javaFileObject);
assertThat(compilation)
.hadErrorContaining(
"More than one extension wants to generate the final class: "
+ FooExtension.class.getName()
+ ", "
+ FinalExtension.class.getName())
.inFile(javaFileObject)
.onLineContaining("public abstract class Baz");
}
@Test
public void testNonFinalThenFinal() {
JavaFileObject javaFileObject =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"",
"import com.google.auto.value.AutoValue;",
"",
"@AutoValue",
"public abstract class Baz {",
" abstract String foo();",
"}");
FinalExtension finalExtension = new FinalExtension();
NonFinalExtension nonFinalExtension = new NonFinalExtension();
assertThat(finalExtension.generated).isFalse();
assertThat(nonFinalExtension.generated).isFalse();
Compilation compilation =
javac()
.withProcessors(
new AutoValueProcessor(ImmutableList.of(finalExtension, nonFinalExtension)))
.compile(javaFileObject);
assertThat(compilation).succeededWithoutWarnings();
assertThat(finalExtension.generated).isTrue();
assertThat(nonFinalExtension.generated).isTrue();
}
@Test
public void testFinalThenNonFinal() {
JavaFileObject javaFileObject =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"",
"import com.google.auto.value.AutoValue;",
"",
"@AutoValue",
"public abstract class Baz {",
" abstract String foo();",
"}");
FinalExtension finalExtension = new FinalExtension();
NonFinalExtension nonFinalExtension = new NonFinalExtension();
assertThat(finalExtension.generated).isFalse();
assertThat(nonFinalExtension.generated).isFalse();
Compilation compilation =
javac()
.withProcessors(
new AutoValueProcessor(ImmutableList.of(nonFinalExtension, finalExtension)))
.compile(javaFileObject);
assertThat(compilation).succeededWithoutWarnings();
assertThat(finalExtension.generated).isTrue();
assertThat(nonFinalExtension.generated).isTrue();
}
@Test
public void testUnconsumedMethod() {
JavaFileObject javaFileObject =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"",
"import com.google.auto.value.AutoValue;",
"",
"@AutoValue",
"public abstract class Baz {",
" abstract String foo();",
" abstract void writeToParcel(Object parcel, int flags);",
"}");
Compilation compilation =
javac()
.withProcessors(new AutoValueProcessor(ImmutableList.of(new FooExtension())))
.compile(javaFileObject);
assertThat(compilation).hadErrorContaining("writeToParcel");
assertThat(compilation)
.hadWarningContaining(
"Abstract method is neither a property getter nor a Builder converter, "
+ "and no extension consumed it")
.inFile(javaFileObject)
.onLineContaining("abstract void writeToParcel");
// The error here comes from the Java compiler rather than AutoValue, so we don't assume
// much about what it looks like. On the other hand, the warning does come from AutoValue
// so we know what to expect.
}
/**
* Tests that the search for extensions doesn't completely blow AutoValue up if there is a corrupt
* jar in the {@code processorpath}. If we're not careful, that can lead to a
* ServiceConfigurationError.
*/
@Test
public void testBadJarDoesntBlowUp() throws IOException {
File badJar = File.createTempFile("bogus", ".jar");
try {
doTestBadJarDoesntBlowUp(badJar);
} finally {
badJar.delete();
}
}
private void doTestBadJarDoesntBlowUp(File badJar) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(badJar);
JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream);
byte[] bogusLine = "bogus line\n".getBytes("UTF-8");
ZipEntry zipEntry = new ZipEntry("META-INF/services/" + AutoValueExtension.class.getName());
zipEntry.setSize(bogusLine.length);
jarOutputStream.putNextEntry(zipEntry);
jarOutputStream.write(bogusLine);
jarOutputStream.close();
ClassLoader badJarLoader = new URLClassLoader(new URL[] {badJar.toURI().toURL()});
JavaFileObject javaFileObject =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"",
"import com.google.auto.value.AutoValue;",
"",
"@AutoValue",
"public abstract class Baz {",
"}");
Compilation compilation =
javac().withProcessors(new AutoValueProcessor(badJarLoader)).compile(javaFileObject);
assertThat(compilation).succeeded();
assertThat(compilation)
.hadWarningContaining(
"This may be due to a corrupt jar file in the compiler's classpath.\n "
+ ServiceConfigurationError.class.getName());
assertThat(compilation).generatedSourceFile("foo.bar.AutoValue_Baz");
}
private static final String CUSTOM_OPTION = "customAnnotation.customOption";
/**
* Tests that extensions providing their own (annotated) annotation types or options get picked
* up.
*/
@Test
public void extensionsWithAnnotatedOptions() {
ExtensionWithAnnotatedOptions extension = new ExtensionWithAnnotatedOptions();
// Ensure default annotation support works
assertThat(extension.getSupportedOptions()).contains(CUSTOM_OPTION);
// Ensure it's carried over to the AutoValue processor
assertThat(new AutoValueProcessor(ImmutableList.of(extension)).getSupportedOptions())
.contains(CUSTOM_OPTION);
}
/**
* Tests that extensions providing their own implemented annotation types or options get picked
* up.
*/
@Test
public void extensionsWithImplementedOptions() {
ExtensionWithImplementedOptions extension = new ExtensionWithImplementedOptions();
// Ensure it's carried over to the AutoValue processor
assertThat(new AutoValueProcessor(ImmutableList.of(extension)).getSupportedOptions())
.contains(CUSTOM_OPTION);
}
@SupportedOptions(CUSTOM_OPTION)
static class ExtensionWithAnnotatedOptions extends AutoValueExtension {
@Override
public String generateClass(
Context context, String className, String classToExtend, boolean isFinal) {
return null;
}
}
static class ExtensionWithImplementedOptions extends AutoValueExtension {
@Override
public Set<String> getSupportedOptions() {
return ImmutableSet.of(CUSTOM_OPTION);
}
@Override
public String generateClass(
Context context, String className, String classToExtend, boolean isFinal) {
return null;
}
}
private static class FooExtension extends AutoValueExtension {
@Override
public boolean applicable(Context context) {
return true;
}
@Override
public boolean mustBeFinal(Context context) {
return true;
}
@Override
public Set<String> consumeProperties(Context context) {
if (context.properties().containsKey("dizzle")) {
return ImmutableSet.of("dizzle");
} else {
return Collections.emptySet();
}
}
@Override
public String generateClass(
Context context, String className, String classToExtend, boolean isFinal) {
StringBuilder constructor =
new StringBuilder().append(" public ").append(className).append("(");
boolean first = true;
for (Map.Entry<String, ExecutableElement> el : context.properties().entrySet()) {
if (first) {
first = false;
} else {
constructor.append(", ");
}
constructor.append("String ").append(el.getKey());
}
constructor.append(") {\n");
constructor.append(" super(");
first = true;
for (Map.Entry<String, ExecutableElement> el : context.properties().entrySet()) {
if (first) {
first = false;
} else {
constructor.append(", ");
}
constructor.append(el.getKey());
}
constructor.append(");\n");
constructor.append(" }\n");
return String.format(
"package %s;\n"
+ "\n"
+ "%s class %s extends %s {\n"
+ constructor
+ " @Override public String foo() {\n"
+ " return \"foo\";\n"
+ " }\n"
+ " public String dizzle() {\n"
+ " return \"dizzle\";\n"
+ " }\n"
+ "}",
context.packageName(),
isFinal ? "final" : "abstract",
className,
classToExtend);
}
}
// Extension that generates a class that just forwards to the parent constructor.
// We will make subclasses that are respectively final and non-final.
private abstract static class EmptyExtension extends AutoValueExtension {
@Override
public boolean applicable(Context context) {
return true;
}
@Override
public abstract boolean mustBeFinal(Context context);
String extraText(Context context) {
return "";
}
boolean generated = false;
@Override
public String generateClass(
Context context, String className, String classToExtend, boolean isFinal) {
generated = true;
ImmutableList.Builder<String> typesAndNamesBuilder = ImmutableList.builder();
context.propertyTypes().forEach((name, type) -> typesAndNamesBuilder.add(type + " " + name));
String typesAndNames = Joiner.on(", ").join(typesAndNamesBuilder.build());
String template =
"package {pkg};\n"
+ "\n"
+ "{finalOrAbstract} class {className} extends {classToExtend} {\n"
+ " {className}({propertyTypesAndNames}) {\n"
+ " super({propertyNames});\n"
+ " }\n"
+ " {extraText}\n"
+ "}\n";
return template
.replace("{pkg}", context.packageName())
.replace("{finalOrAbstract}", isFinal ? "final" : "abstract")
.replace("{className}", className)
.replace("{classToExtend}", classToExtend)
.replace("{propertyTypesAndNames}", typesAndNames)
.replace("{propertyNames}", Joiner.on(", ").join(context.properties().keySet()))
.replace("{extraText}", extraText(context));
}
}
private static class NonFinalExtension extends EmptyExtension {
@Override
public boolean mustBeFinal(Context context) {
return false;
}
}
private static class FinalExtension extends EmptyExtension {
@Override
public boolean mustBeFinal(Context context) {
return true;
}
}
private static class SideFileExtension extends AutoValueExtension {
@Override
public boolean applicable(Context context) {
return true;
}
@Override
public boolean mustBeFinal(Context context) {
return false;
}
@Override
public String generateClass(
Context context, String className, String classToExtend, boolean isFinal) {
String sideClassName = "Side_" + context.autoValueClass().getSimpleName();
String sideClass =
"" //
+ "package "
+ context.packageName()
+ ";\n"
+ "class "
+ sideClassName
+ " {}\n";
Filer filer = context.processingEnvironment().getFiler();
try {
String sideClassFqName = context.packageName() + "." + sideClassName;
JavaFileObject sourceFile =
filer.createSourceFile(sideClassFqName, context.autoValueClass());
try (Writer sourceWriter = sourceFile.openWriter()) {
sourceWriter.write(sideClass);
}
} catch (IOException e) {
context
.processingEnvironment()
.getMessager()
.printMessage(Diagnostic.Kind.ERROR, e.toString());
}
return null;
}
}
private static class FakeWriteToParcelExtension extends NonFinalExtension {
private ExecutableElement writeToParcelMethod(Context context) {
for (ExecutableElement method : context.abstractMethods()) {
if (method.getSimpleName().contentEquals("writeToParcel")) {
return method;
}
}
throw new AssertionError("Did not see abstract method writeToParcel");
}
@Override
public Set<ExecutableElement> consumeMethods(Context context) {
return ImmutableSet.of(writeToParcelMethod(context));
}
@Override
String extraText(Context context) {
// This is perhaps overgeneral. It is simply going to generate this:
// @Override void writeToParcel(Object parcel, int flags) {}
ExecutableElement methodToImplement = writeToParcelMethod(context);
assertThat(methodToImplement.getReturnType().getKind()).isEqualTo(TypeKind.VOID);
ImmutableList.Builder<String> typesAndNamesBuilder = ImmutableList.builder();
for (VariableElement p : methodToImplement.getParameters()) {
typesAndNamesBuilder.add(p.asType() + " " + p.getSimpleName());
}
return "@Override void "
+ methodToImplement.getSimpleName()
+ "("
+ Joiner.on(", ").join(typesAndNamesBuilder.build())
+ ") {}";
}
}
@Test
public void propertyTypes() {
JavaFileObject parent =
JavaFileObjects.forSourceLines(
"foo.bar.Parent",
"package foo.bar;",
"",
"import java.util.List;",
"",
"interface Parent<T> {",
" T thing();",
" List<T> list();",
"}");
JavaFileObject autoValueClass =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"",
"import com.google.auto.value.AutoValue;",
"",
"@AutoValue",
"abstract class Baz implements Parent<String> {",
"}");
ContextChecker checker =
context -> {
assertThat(context.builder()).isEmpty();
Map<String, TypeMirror> propertyTypes = context.propertyTypes();
assertThat(propertyTypes.keySet()).containsExactly("thing", "list");
TypeMirror thingType = propertyTypes.get("thing");
assertThat(thingType).isNotNull();
assertThat(thingType.getKind()).isEqualTo(TypeKind.DECLARED);
assertThat(MoreTypes.asTypeElement(thingType).getQualifiedName().toString())
.isEqualTo("java.lang.String");
TypeMirror listType = propertyTypes.get("list");
assertThat(listType).isNotNull();
assertThat(listType.toString()).isEqualTo("java.util.List<java.lang.String>");
};
ContextCheckingExtension extension = new ContextCheckingExtension(checker);
Compilation compilation =
javac()
.withProcessors(new AutoValueProcessor(ImmutableList.of(extension)))
.compile(autoValueClass, parent);
assertThat(compilation).succeededWithoutWarnings();
}
@Test
public void finalAutoValueClassName() {
JavaFileObject autoValueClass =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"",
"import com.google.auto.value.AutoValue;",
"",
"@AutoValue",
"abstract class Baz {",
"}");
ContextChecker checker =
context -> {
assertThat(context.finalAutoValueClassName()).isEqualTo("foo.bar.AutoValue_Baz");
};
ContextCheckingExtension extension = new ContextCheckingExtension(checker);
Compilation compilation =
javac()
.withProcessors(
new AutoValueProcessor(ImmutableList.of(extension, new FinalExtension())))
.compile(autoValueClass);
assertThat(compilation).succeededWithoutWarnings();
assertThat(compilation).generatedSourceFile("foo.bar.AutoValue_Baz");
// ContextCheckingExtension doesn't generate any code, so that name must be the class generated
// by FinalExtension.
}
@Test
public void builderContext() {
JavaFileObject parent =
JavaFileObjects.forSourceLines(
"foo.bar.Parent",
"package foo.bar;",
"",
"import com.google.common.collect.ImmutableList;",
"",
"interface Parent<T> {",
" T thing();",
" ImmutableList<T> list();",
"}");
JavaFileObject autoValueClass =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"",
"import com.google.auto.value.AutoValue;",
"import com.google.common.collect.ImmutableList;",
"",
"@AutoValue",
"abstract class Baz implements Parent<String> {",
" static Builder builder() {",
" return new AutoValue_Baz.Builder();",
" }",
"",
" abstract Builder toBuilder();",
"",
" @AutoValue.Builder",
" abstract static class Builder {",
" abstract Builder setThing(String x);",
" abstract Builder setList(Iterable<String> x);",
" abstract Builder setList(ImmutableList<String> x);",
" abstract ImmutableList.Builder<String> listBuilder();",
" abstract Baz autoBuild();",
" Baz build() {",
" return autoBuild();",
" }",
" }",
"}");
ContextChecker checker =
context -> {
assertThat(context.builder()).isPresent();
BuilderContext builderContext = context.builder().get();
assertThat(builderContext.builderType().getQualifiedName().toString())
.isEqualTo("foo.bar.Baz.Builder");
Set<ExecutableElement> builderMethods = builderContext.builderMethods();
assertThat(builderMethods).hasSize(1);
ExecutableElement builderMethod = Iterables.getOnlyElement(builderMethods);
assertThat(builderMethod.getSimpleName().toString()).isEqualTo("builder");
Set<ExecutableElement> toBuilderMethods = builderContext.toBuilderMethods();
assertThat(toBuilderMethods).hasSize(1);
ExecutableElement toBuilderMethod = Iterables.getOnlyElement(toBuilderMethods);
assertThat(toBuilderMethod.getSimpleName().toString()).isEqualTo("toBuilder");
Optional<ExecutableElement> buildMethod = builderContext.buildMethod();
assertThat(buildMethod).isPresent();
assertThat(buildMethod.get().getSimpleName().toString()).isEqualTo("build");
assertThat(buildMethod.get().getParameters()).isEmpty();
assertThat(buildMethod.get().getReturnType().toString()).isEqualTo("foo.bar.Baz");
ExecutableElement autoBuildMethod = builderContext.autoBuildMethod();
assertThat(autoBuildMethod.getSimpleName().toString()).isEqualTo("autoBuild");
assertThat(autoBuildMethod.getModifiers()).contains(Modifier.ABSTRACT);
assertThat(autoBuildMethod.getParameters()).isEmpty();
assertThat(autoBuildMethod.getReturnType().toString()).isEqualTo("foo.bar.Baz");
Map<String, Set<ExecutableElement>> setters = builderContext.setters();
assertThat(setters.keySet()).containsExactly("thing", "list");
Set<ExecutableElement> thingSetters = setters.get("thing");
assertThat(thingSetters).hasSize(1);
ExecutableElement thingSetter = Iterables.getOnlyElement(thingSetters);
assertThat(thingSetter.getSimpleName().toString()).isEqualTo("setThing");
Set<ExecutableElement> listSetters = setters.get("list");
assertThat(listSetters).hasSize(2);
for (ExecutableElement listSetter : listSetters) {
assertThat(listSetter.getSimpleName().toString()).isEqualTo("setList");
}
Map<String, ExecutableElement> propertyBuilders = builderContext.propertyBuilders();
assertThat(propertyBuilders.keySet()).containsExactly("list");
assertThat(propertyBuilders.get("list").getSimpleName().toString())
.isEqualTo("listBuilder");
};
ContextCheckingExtension extension = new ContextCheckingExtension(checker);
Compilation compilation =
javac()
.withProcessors(new AutoValueProcessor(ImmutableList.of(extension)))
.compile(autoValueClass, parent);
assertThat(compilation).succeededWithoutWarnings();
}
@Test
public void builderContextWithInheritance() {
JavaFileObject parent =
JavaFileObjects.forSourceLines(
"foo.bar.Parent",
"package foo.bar;",
"",
"interface Parent<BuilderT> {",
" BuilderT toBuilder();",
" interface Builder<T, BuilderT, BuiltT> {",
" BuilderT setThing(T x);",
" BuiltT build();",
" }",
"}");
JavaFileObject autoValueClass =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"",
"import com.google.auto.value.AutoValue;",
"",
"@AutoValue",
"abstract class Baz<T> implements Parent<Baz.Builder<T>> {",
" abstract T thing();",
" static <T> Builder<T> builder() {",
" return new AutoValue_Baz.Builder<>();",
" }",
"",
" @AutoValue.Builder",
" abstract static class Builder<T> implements Parent.Builder<T, Builder<T>, Baz<T>> {",
" }",
"}");
ContextChecker checker =
context -> {
assertThat(context.builder()).isPresent();
BuilderContext builderContext = context.builder().get();
assertThat(builderContext.builderType().getQualifiedName().toString())
.isEqualTo("foo.bar.Baz.Builder");
Set<ExecutableElement> builderMethods = builderContext.builderMethods();
assertThat(builderMethods).hasSize(1);
ExecutableElement builderMethod = Iterables.getOnlyElement(builderMethods);
assertThat(builderMethod.getSimpleName().toString()).isEqualTo("builder");
Set<ExecutableElement> toBuilderMethods = builderContext.toBuilderMethods();
assertThat(toBuilderMethods).hasSize(1);
ExecutableElement toBuilderMethod = Iterables.getOnlyElement(toBuilderMethods);
assertThat(toBuilderMethod.getSimpleName().toString()).isEqualTo("toBuilder");
Optional<ExecutableElement> buildMethod = builderContext.buildMethod();
assertThat(buildMethod).isPresent();
assertThat(buildMethod.get().getSimpleName().toString()).isEqualTo("build");
assertThat(buildMethod.get().getParameters()).isEmpty();
assertThat(buildMethod.get().getReturnType().toString()).isEqualTo("BuiltT");
ExecutableElement autoBuildMethod = builderContext.autoBuildMethod();
assertThat(autoBuildMethod).isEqualTo(buildMethod.get());
Map<String, Set<ExecutableElement>> setters = builderContext.setters();
assertThat(setters.keySet()).containsExactly("thing");
Set<ExecutableElement> thingSetters = setters.get("thing");
assertThat(thingSetters).hasSize(1);
ExecutableElement thingSetter = Iterables.getOnlyElement(thingSetters);
assertThat(thingSetter.getSimpleName().toString()).isEqualTo("setThing");
};
ContextCheckingExtension extension = new ContextCheckingExtension(checker);
Compilation compilation =
javac()
.withProcessors(new AutoValueProcessor(ImmutableList.of(extension)))
.compile(autoValueClass, parent);
assertThat(compilation).succeededWithoutWarnings();
}
@Test
public void oddBuilderContext() {
JavaFileObject autoValueClass =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"",
"import com.google.auto.value.AutoValue;",
"import com.google.common.collect.ImmutableList;",
"",
"@AutoValue",
"abstract class Baz {",
" abstract String string();",
"",
" @AutoValue.Builder",
" abstract static class Builder {",
" abstract Builder setString(String x);",
" abstract Baz oddBuild();",
" Baz build(int butNotReallyBecauseOfThisParameter) {",
" return null;",
" }",
" }",
"}");
ContextChecker checker =
context -> {
assertThat(context.builder()).isPresent();
BuilderContext builderContext = context.builder().get();
assertThat(builderContext.builderMethods()).isEmpty();
assertThat(builderContext.toBuilderMethods()).isEmpty();
assertThat(builderContext.buildMethod()).isEmpty();
assertThat(builderContext.autoBuildMethod().getSimpleName().toString())
.isEqualTo("oddBuild");
Map<String, Set<ExecutableElement>> setters = builderContext.setters();
assertThat(setters.keySet()).containsExactly("string");
Set<ExecutableElement> thingSetters = setters.get("string");
assertThat(thingSetters).hasSize(1);
ExecutableElement thingSetter = Iterables.getOnlyElement(thingSetters);
assertThat(thingSetter.getSimpleName().toString()).isEqualTo("setString");
assertThat(builderContext.propertyBuilders()).isEmpty();
};
ContextCheckingExtension extension = new ContextCheckingExtension(checker);
Compilation compilation =
javac()
.withProcessors(new AutoValueProcessor(ImmutableList.of(extension)))
.compile(autoValueClass);
assertThat(compilation).succeededWithoutWarnings();
}
// https://github.com/google/auto/issues/809
@Test
public void propertyErrorShouldNotCrash() {
JavaFileObject autoValueClass =
JavaFileObjects.forSourceLines(
"test.Test",
"package test;",
"import com.google.auto.value.AutoValue;",
"import java.util.List;",
"",
"@AutoValue",
"public abstract class Test {",
" abstract Integer property();",
" abstract List<String> listProperty();",
"",
" @AutoValue.Builder",
" public interface Builder {",
" Builder property(Integer property);",
" Builder listProperty(List<String> listProperty);",
" Builder listProperty(Integer listPropertyValues);",
" Test build();",
" }",
"}");
// We don't actually expect the extension to be invoked. Previously it was, and that led to a
// NullPointerException when calling .setters() in the checker.
ContextChecker checker =
context -> {
assertThat(context.builder()).isPresent();
assertThat(context.builder().get().setters()).isEmpty();
};
ContextCheckingExtension extension = new ContextCheckingExtension(checker);
Compilation compilation =
javac()
.withProcessors(new AutoValueProcessor(ImmutableList.of(extension)))
.compile(autoValueClass);
assertThat(compilation)
.hadErrorContaining("Parameter type java.lang.Integer of setter method")
.inFile(autoValueClass)
.onLineContaining("Builder listProperty(Integer listPropertyValues)");
}
private interface ContextChecker extends Consumer<AutoValueExtension.Context> {}
private static class ContextCheckingExtension extends AutoValueExtension {
private final Consumer<Context> checker;
ContextCheckingExtension(Consumer<Context> checker) {
this.checker = checker;
}
@Override
public boolean applicable(Context context) {
return true;
}
@Override
public String generateClass(
Context context, String className, String classToExtend, boolean isFinal) {
checker.accept(context);
return null;
}
}
}