diff options
26 files changed, 1095 insertions, 504 deletions
diff --git a/factory/pom.xml b/factory/pom.xml index 4726732b..021c5c3f 100644 --- a/factory/pom.xml +++ b/factory/pom.xml @@ -44,7 +44,7 @@ <dependency> <groupId>com.google.auto</groupId> <artifactId>auto-common</artifactId> - <version>0.3</version> + <version>0.4</version> </dependency> <dependency> <groupId>com.google.auto.service</groupId> @@ -62,17 +62,6 @@ <version>2.5.1</version> </dependency> <dependency> - <groupId>com.google.dagger</groupId> - <artifactId>dagger</artifactId> - <version>2.0-SNAPSHOT</version> - </dependency> - <dependency> - <groupId>com.google.dagger</groupId> - <artifactId>dagger-compiler</artifactId> - <version>2.0-SNAPSHOT</version> - <optional>true</optional> - </dependency> - <dependency> <groupId>javax.inject</groupId> <artifactId>javax.inject</artifactId> <version>1</version> diff --git a/factory/src/it/functional/pom.xml b/factory/src/it/functional/pom.xml index 59308e8d..7283a2ae 100644 --- a/factory/src/it/functional/pom.xml +++ b/factory/src/it/functional/pom.xml @@ -42,12 +42,12 @@ <dependency> <groupId>com.google.dagger</groupId> <artifactId>dagger</artifactId> - <version>2.0-SNAPSHOT</version> + <version>2.0</version> </dependency> <dependency> <groupId>com.google.dagger</groupId> <artifactId>dagger-compiler</artifactId> - <version>2.0-SNAPSHOT</version> + <version>2.0</version> <optional>true</optional> </dependency> <dependency> diff --git a/factory/src/it/functional/src/main/java/com/google/auto/factory/DaggerModule.java b/factory/src/it/functional/src/main/java/com/google/auto/factory/DaggerModule.java index 4d079f51..2080d904 100644 --- a/factory/src/it/functional/src/main/java/com/google/auto/factory/DaggerModule.java +++ b/factory/src/it/functional/src/main/java/com/google/auto/factory/DaggerModule.java @@ -18,7 +18,7 @@ package com.google.auto.factory; import dagger.Module; import dagger.Provides; -@Module(injects = FactoryGeneratedFactory.class) +@Module final class DaggerModule { @Provides Dependency provideDependency(DependencyImpl impl) { return impl; diff --git a/factory/src/main/java/com/google/auto/factory/processor/AutoFactoryProcessorComponent.java b/factory/src/it/functional/src/main/java/com/google/auto/factory/FactoryComponent.java index 66aaf03f..a14530c9 100644 --- a/factory/src/main/java/com/google/auto/factory/processor/AutoFactoryProcessorComponent.java +++ b/factory/src/it/functional/src/main/java/com/google/auto/factory/FactoryComponent.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Google, Inc. + * Copyright (C) 2015 Google, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,16 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.auto.factory.processor; +package com.google.auto.factory; import dagger.Component; -/** - * The Dagger component that declares all of the bindings for {@link AutoFactoryProcessor}. - * - * @author Gregory Kick - */ -@Component(modules = ProcessorModule.class) -interface AutoFactoryProcessorComponent { - void injectProcessor(AutoFactoryProcessor processor); +/** A component to materialize the factory using Dagger 2 */ +@Component(modules = DaggerModule.class) +interface FactoryComponent { + FactoryGeneratedFactory factory(); } diff --git a/factory/src/it/functional/src/test/java/com/google/auto/factory/DependencyInjectionIntegrationTest.java b/factory/src/it/functional/src/test/java/com/google/auto/factory/DependencyInjectionIntegrationTest.java index 3c4090fe..e5c7b1bb 100644 --- a/factory/src/it/functional/src/test/java/com/google/auto/factory/DependencyInjectionIntegrationTest.java +++ b/factory/src/it/functional/src/test/java/com/google/auto/factory/DependencyInjectionIntegrationTest.java @@ -4,8 +4,6 @@ import static com.google.common.truth.Truth.assertThat; import com.google.inject.Guice; -import dagger.ObjectGraph; - import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -13,8 +11,7 @@ import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class DependencyInjectionIntegrationTest { @Test public void daggerInjectedFactory() { - FactoryGeneratedFactory factoryGeneratedFactory = - ObjectGraph.create(DaggerModule.class).get(FactoryGeneratedFactory.class); + FactoryGeneratedFactory factoryGeneratedFactory = DaggerFactoryComponent.create().factory(); FactoryGenerated one = factoryGeneratedFactory.create("A"); FactoryGenerated two = factoryGeneratedFactory.create("B"); assertThat(one.name()).isEqualTo("A"); diff --git a/factory/src/main/java/com/google/auto/factory/processor/AutoFactoryDeclaration.java b/factory/src/main/java/com/google/auto/factory/processor/AutoFactoryDeclaration.java index 73fd12e4..19d98b2c 100644 --- a/factory/src/main/java/com/google/auto/factory/processor/AutoFactoryDeclaration.java +++ b/factory/src/main/java/com/google/auto/factory/processor/AutoFactoryDeclaration.java @@ -21,10 +21,18 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static javax.tools.Diagnostic.Kind.ERROR; +import com.google.auto.factory.AutoFactory; +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.base.Strings; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + import java.util.Map; import javax.annotation.processing.Messager; -import javax.inject.Inject; import javax.lang.model.SourceVersion; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; @@ -35,15 +43,6 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.util.ElementFilter; import javax.lang.model.util.Elements; -import com.google.auto.factory.AutoFactory; -import com.google.common.base.Optional; -import com.google.common.base.Predicate; -import com.google.common.base.Strings; -import com.google.common.collect.FluentIterable; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; - /** * This is a value object that mirrors the static declaration of an {@link AutoFactory} annotation. * @@ -116,7 +115,7 @@ final class AutoFactoryDeclaration { private final Elements elements; private final Messager messager; - @Inject Factory(Elements elements, Messager messager) { + Factory(Elements elements, Messager messager) { this.elements = elements; this.messager = messager; } diff --git a/factory/src/main/java/com/google/auto/factory/processor/AutoFactoryProcessor.java b/factory/src/main/java/com/google/auto/factory/processor/AutoFactoryProcessor.java index 2f1006aa..250b2070 100644 --- a/factory/src/main/java/com/google/auto/factory/processor/AutoFactoryProcessor.java +++ b/factory/src/main/java/com/google/auto/factory/processor/AutoFactoryProcessor.java @@ -15,6 +15,19 @@ */ package com.google.auto.factory.processor; +import com.google.auto.factory.AutoFactory; +import com.google.auto.factory.Provided; +import com.google.auto.service.AutoService; +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Multimaps; + import java.io.IOException; import java.util.Arrays; import java.util.Collection; @@ -27,7 +40,6 @@ import javax.annotation.processing.Messager; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; -import javax.inject.Inject; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; @@ -39,19 +51,6 @@ import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import javax.tools.Diagnostic.Kind; -import com.google.auto.factory.AutoFactory; -import com.google.auto.factory.Provided; -import com.google.auto.service.AutoService; -import com.google.common.base.Function; -import com.google.common.base.Optional; -import com.google.common.base.Throwables; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableListMultimap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.ImmutableSortedSet; -import com.google.common.collect.Iterables; -import com.google.common.collect.Multimaps; - /** * The annotation processor that generates factories for {@link AutoFactory} annotations. * @@ -59,27 +58,31 @@ import com.google.common.collect.Multimaps; */ @AutoService(Processor.class) public final class AutoFactoryProcessor extends AbstractProcessor { - @Inject FactoryDescriptorGenerator factoryDescriptorGenerator; - @Inject AutoFactoryDeclaration.Factory declarationFactory; - @Inject ProvidedChecker providedChecker; - @Inject Messager messager; - @Inject Elements elements; - @Inject Types types; - @Inject FactoryWriter factoryWriter; + private FactoryDescriptorGenerator factoryDescriptorGenerator; + private AutoFactoryDeclaration.Factory declarationFactory; + private ProvidedChecker providedChecker; + private Messager messager; + private Elements elements; + private Types types; + private FactoryWriter factoryWriter; @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); - Dagger_AutoFactoryProcessorComponent.builder() - .processorModule(new ProcessorModule(processingEnv)) - .build() - .injectProcessor(this); + elements = processingEnv.getElementUtils(); + types = processingEnv.getTypeUtils(); + messager = processingEnv.getMessager(); + factoryWriter = new FactoryWriter(processingEnv.getFiler()); + providedChecker = new ProvidedChecker(messager); + declarationFactory = new AutoFactoryDeclaration.Factory(elements, messager); + factoryDescriptorGenerator = + new FactoryDescriptorGenerator(messager, elements, declarationFactory); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { try { - doProcess(annotations, roundEnv); + doProcess(roundEnv); } catch (Throwable e) { messager.printMessage(Kind.ERROR, "Failed to process @AutoFactory annotations:\n" + Throwables.getStackTraceAsString(e)); @@ -87,7 +90,7 @@ public final class AutoFactoryProcessor extends AbstractProcessor { return false; } - private void doProcess(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { + private void doProcess(RoundEnvironment roundEnv) { for (Element element : roundEnv.getElementsAnnotatedWith(Provided.class)) { providedChecker.checkProvidedParameter(element); } diff --git a/factory/src/main/java/com/google/auto/factory/processor/FactoryDescriptorGenerator.java b/factory/src/main/java/com/google/auto/factory/processor/FactoryDescriptorGenerator.java index 51f61fa3..8ede5d25 100644 --- a/factory/src/main/java/com/google/auto/factory/processor/FactoryDescriptorGenerator.java +++ b/factory/src/main/java/com/google/auto/factory/processor/FactoryDescriptorGenerator.java @@ -21,18 +21,6 @@ import static javax.lang.model.element.Modifier.ABSTRACT; import static javax.lang.model.element.Modifier.PUBLIC; import static javax.tools.Diagnostic.Kind.ERROR; -import javax.annotation.processing.Messager; -import javax.inject.Inject; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.Name; -import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; -import javax.lang.model.util.ElementKindVisitor6; -import javax.lang.model.util.Elements; - import com.google.auto.factory.AutoFactory; import com.google.auto.factory.Provided; import com.google.common.base.Function; @@ -44,6 +32,17 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimaps; +import javax.annotation.processing.Messager; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Name; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.util.ElementKindVisitor6; +import javax.lang.model.util.Elements; + /** * A service that traverses an element and returns the set of factory methods defined therein. * @@ -54,7 +53,7 @@ final class FactoryDescriptorGenerator { private final Elements elements; private final AutoFactoryDeclaration.Factory declarationFactory; - @Inject FactoryDescriptorGenerator(Messager messager, Elements elements, + FactoryDescriptorGenerator(Messager messager, Elements elements, AutoFactoryDeclaration.Factory declarationFactory) { this.messager = messager; this.elements = elements; diff --git a/factory/src/main/java/com/google/auto/factory/processor/FactoryMethodDescriptor.java b/factory/src/main/java/com/google/auto/factory/processor/FactoryMethodDescriptor.java index 4b40ca9e..7fb96bcb 100644 --- a/factory/src/main/java/com/google/auto/factory/processor/FactoryMethodDescriptor.java +++ b/factory/src/main/java/com/google/auto/factory/processor/FactoryMethodDescriptor.java @@ -18,14 +18,14 @@ package com.google.auto.factory.processor; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; -import java.util.Set; - import com.google.common.base.Objects; import com.google.common.base.Optional; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; +import java.util.Set; + /** * A value object representing a factory method to be generated. * diff --git a/factory/src/main/java/com/google/auto/factory/processor/FactoryWriter.java b/factory/src/main/java/com/google/auto/factory/processor/FactoryWriter.java index 2cc782ba..ea4879eb 100644 --- a/factory/src/main/java/com/google/auto/factory/processor/FactoryWriter.java +++ b/factory/src/main/java/com/google/auto/factory/processor/FactoryWriter.java @@ -19,6 +19,17 @@ import static javax.lang.model.element.Modifier.FINAL; import static javax.lang.model.element.Modifier.PRIVATE; import static javax.lang.model.element.Modifier.PUBLIC; +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.base.Optional; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; + +import com.squareup.javawriter.JavaWriter; + import java.io.IOException; import java.util.Collection; import java.util.EnumSet; @@ -31,20 +42,10 @@ import javax.inject.Inject; import javax.lang.model.element.Modifier; import javax.tools.JavaFileObject; -import com.google.common.base.Function; -import com.google.common.base.Joiner; -import com.google.common.base.Optional; -import com.google.common.collect.FluentIterable; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; -import com.google.common.collect.Ordering; -import com.squareup.javawriter.JavaWriter; - final class FactoryWriter { private final Filer filer; - @Inject FactoryWriter(Filer filer) { + FactoryWriter(Filer filer) { this.filer = filer; } diff --git a/factory/src/main/java/com/google/auto/factory/processor/ProcessorModule.java b/factory/src/main/java/com/google/auto/factory/processor/ProcessorModule.java deleted file mode 100644 index 441aca8b..00000000 --- a/factory/src/main/java/com/google/auto/factory/processor/ProcessorModule.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2013 Google, Inc. - * - * 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.factory.processor; - -import static com.google.common.base.Preconditions.checkNotNull; - -import javax.annotation.processing.AbstractProcessor; -import javax.annotation.processing.Filer; -import javax.annotation.processing.Messager; -import javax.annotation.processing.ProcessingEnvironment; -import javax.annotation.processing.Processor; -import javax.lang.model.util.Elements; -import javax.lang.model.util.Types; - -import dagger.Module; -import dagger.Provides; - -/** - * A Dagger {@link Module} that binds objects related to {@link Processor} instances. Most callers - * will create an instance using the {@link ProcessingEnvironment} passed into - * {@link AbstractProcessor#init}. - * - * @author Gregory Kick - */ -// TODO(gak): move this some place more common so that it can be shared amongst processors -@Module(library = true) -final class ProcessorModule { - private final ProcessingEnvironment processingEnvironment; - - ProcessorModule(ProcessingEnvironment processingEnvironment) { - this.processingEnvironment = checkNotNull(processingEnvironment); - } - - @Provides ProcessingEnvironment provideProcessingEnvironment() { - return processingEnvironment; - } - - @Provides Elements provideElements(ProcessingEnvironment processingEnvironment) { - return processingEnvironment.getElementUtils(); - } - - @Provides Filer provideFiler(ProcessingEnvironment processingEnvironment) { - return processingEnvironment.getFiler(); - } - - @Provides Messager provideMessager(ProcessingEnvironment processingEnvironment) { - return processingEnvironment.getMessager(); - } - - @Provides Types provideTypes(ProcessingEnvironment processingEnvironment) { - return processingEnvironment.getTypeUtils(); - } -} diff --git a/factory/src/main/java/com/google/auto/factory/processor/ProvidedChecker.java b/factory/src/main/java/com/google/auto/factory/processor/ProvidedChecker.java index ae26cb6a..06ca8e13 100644 --- a/factory/src/main/java/com/google/auto/factory/processor/ProvidedChecker.java +++ b/factory/src/main/java/com/google/auto/factory/processor/ProvidedChecker.java @@ -18,20 +18,19 @@ package com.google.auto.factory.processor; import static com.google.common.base.Preconditions.checkArgument; import static javax.tools.Diagnostic.Kind.ERROR; +import com.google.auto.factory.AutoFactory; +import com.google.auto.factory.Provided; + import javax.annotation.processing.Messager; -import javax.inject.Inject; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; import javax.lang.model.util.ElementKindVisitor6; -import com.google.auto.factory.AutoFactory; -import com.google.auto.factory.Provided; - final class ProvidedChecker { private final Messager messager; - @Inject ProvidedChecker(Messager messager) { + ProvidedChecker(Messager messager) { this.messager = messager; } diff --git a/value/README.md b/value/README.md index 06326b9a..0641fe7b 100644 --- a/value/README.md +++ b/value/README.md @@ -209,14 +209,13 @@ written using builders. abstract int numberOfLegs(); @AutoValue.Builder - interface Builder { - Builder name(String s); - Builder numberOfLegs(int n); - Animal build(); + abstract static class Builder { + abstract Builder name(String s); + abstract Builder numberOfLegs(int n); + abstract Animal build(); } } } - ``` Now client code can look something like this: @@ -260,7 +259,6 @@ how to define that animals have 4 legs by default: // ...remainder as before... } } - ``` ### Nullability @@ -293,26 +291,69 @@ steps, insert the code to do so into your static factory method before invoking the generated constructor. Remember that null checks are already present in the generated constructor. -When using builders, you can validate using a method annotated with -`@AutoValue.Validate`. The method will be called immediately after a -new instance is constructed by the build method and before that -instance is returned to the client. It can throw an exception if -validation fails. Here's how our example might be validated: +When using builders, you can validate by implementing your own build() +method that calls the generated build method, conventionally called +autoBuild(). You can construct an object provisionally and inspect its +properties before returning it. Here's how our example might be validated: ```java class Example { @AutoValue abstract static class Animal { - @AutoValue.Validate - void validate() { - if (numberOfLegs() < 0) { - throw new IllegalStateException("Negative legs"); + ... + @AutoValue.Builder + abstract static class Builder { + abstract Builder name(String s); + abstract Builder numberOfLegs(int n); + + abstract Animal autoBuild(); + Animal build() { + Animal animal = autoBuild(); + if (animal.numberOfLegs() < 0) { + throw new IllegalStateException("Negative legs"); + } + return animal; } } - // ...remainder as before... } } +``` + +If the Builder class is public, typically `build()` will be too but +`autoBuild()` will be package-private. + +Sometimes it may be more convenient to consult the value in the builder +before calling `autoBuild()`. If the `Builder` class has an abstract method +with the same name and type as one of the methods in the `@AutoValue` class, +it will be implemented to return the value set on the builder, or throw an +exception if no value has been set. Like `autoBuild()`, these getter methods +will typically not be public. + +```java + class Example { + @AutoValue + abstract static class Animal { + abstract String name(); + abstract int numberOfLegs(); + ... + + @AutoValue.Builder + abstract static class Builder { + abstract Builder name(String s); + abstract Builder numberOfLegs(int n); + abstract int numberOfLegs(); + + abstract Animal autoBuild(); + Animal build() { + if (numberOfLegs() < 0) { + throw new IllegalStateException("Negative legs"); + } + return autoBuild(); + } + } + } + } ``` ### Custom implementations @@ -361,10 +402,10 @@ or return new AutoValue_MapEntry.Builder<K, V>(); } - interface Builder<K extends Comparable<K>, V> { - Builder setKey(K key); - Builder setValue(V value); - MapEntry<K, V> build(); + abstract static class Builder<K extends Comparable<K>, V> { + abstract Builder setKey(K key); + abstract Builder setValue(V value); + abstract MapEntry<K, V> build(); } ... } diff --git a/value/src/it/functional/src/test/java/com/google/auto/value/AutoValueTest.java b/value/src/it/functional/src/test/java/com/google/auto/value/AutoValueTest.java index 8e856268..e9e65813 100644 --- a/value/src/it/functional/src/test/java/com/google/auto/value/AutoValueTest.java +++ b/value/src/it/functional/src/test/java/com/google/auto/value/AutoValueTest.java @@ -21,6 +21,8 @@ import com.google.common.base.Objects; import com.google.common.collect.ComparisonChain; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableTable; import com.google.common.testing.EqualsTester; import com.google.common.testing.SerializableTester; @@ -1118,53 +1120,6 @@ public class AutoValueTest extends TestCase { } @AutoValue - public abstract static class ValidationWithBuilder { - public abstract String string(); - public abstract int integer(); - - public static Builder builder() { - return new AutoValue_AutoValueTest_ValidationWithBuilder.Builder(); - } - - @AutoValue.Validate - void validate() { - if (string().isEmpty()) { - throw new IllegalStateException("String is empty"); - } - if (integer() < 0) { - throw new IllegalStateException("Integer is negative"); - } - } - - @AutoValue.Builder - public interface Builder { - Builder string(String string); - Builder integer(int integer); - ValidationWithBuilder build(); - } - } - - public void testValidation() { - ValidationWithBuilder ok = ValidationWithBuilder.builder().string("foo").integer(17).build(); - assertEquals("foo", ok.string()); - assertEquals(17, ok.integer()); - - try { - ValidationWithBuilder.builder().string("").integer(17).build(); - fail("Expected IllegalStateException for empty string"); - } catch (IllegalStateException expected) { - assertThat(expected).hasMessage("String is empty"); - } - - try { - ValidationWithBuilder.builder().string("foo").integer(-17).build(); - fail("Expected IllegalStateException for negative integer"); - } catch (IllegalStateException expected) { - assertThat(expected).hasMessage("Integer is negative"); - } - } - - @AutoValue public abstract static class GenericsWithBuilder<T extends Number & Comparable<T>, U extends T> { public abstract List<T> list(); public abstract U u(); @@ -1271,6 +1226,256 @@ public class AutoValueTest extends TestCase { assertEquals(17, instance3.getAnInt()); } + @AutoValue + public abstract static class BuilderWithUnprefixedGetters<T extends Comparable<T>> { + public abstract ImmutableList<T> list(); + @Nullable public abstract T t(); + public abstract int[] ints(); + public abstract int noGetter(); + + public static <T extends Comparable<T>> Builder<T> builder() { + return new AutoValue_AutoValueTest_BuilderWithUnprefixedGetters.Builder<T>(); + } + + @AutoValue.Builder + public interface Builder<T extends Comparable<T>> { + Builder<T> setList(ImmutableList<T> list); + Builder<T> setT(T t); + Builder<T> setInts(int[] ints); + Builder<T> setNoGetter(int x); + + ImmutableList<T> list(); + T t(); + int[] ints(); + + BuilderWithUnprefixedGetters<T> build(); + } + } + + public void testBuilderWithUnprefixedGetter() { + ImmutableList<String> names = ImmutableList.of("fred", "jim"); + int[] ints = {6, 28, 496, 8128, 33550336}; + int noGetter = -1; + + BuilderWithUnprefixedGetters.Builder<String> builder = BuilderWithUnprefixedGetters.builder(); + assertNull(builder.t()); + try { + builder.list(); + fail("Attempt to retrieve unset list property should have failed"); + } catch (IllegalStateException e) { + assertThat(e).hasMessage("Property \"list\" has not been set"); + } + try { + builder.ints(); + fail("Attempt to retrieve unset ints property should have failed"); + } catch (IllegalStateException e) { + assertThat(e).hasMessage("Property \"ints\" has not been set"); + } + + builder.setList(names); + assertThat(builder.list()).isSameAs(names); + builder.setInts(ints); + assertThat(builder.ints()).isEqualTo(ints); + ints[0] = 0; + assertThat(builder.ints()[0]).isEqualTo(6); + ints[0] = 6; + + BuilderWithUnprefixedGetters<String> instance = builder.setNoGetter(noGetter).build(); + assertThat(instance.list()).isSameAs(names); + assertThat(instance.t()).isNull(); + assertThat(instance.ints()).isEqualTo(ints); + assertThat(instance.noGetter()).isEqualTo(noGetter); + } + + @AutoValue + public abstract static class BuilderWithPrefixedGetters<T extends Comparable<T>> { + public abstract ImmutableList<T> getList(); + public abstract T getT(); + @Nullable public abstract int[] getInts(); + public abstract int getNoGetter(); + + public static <T extends Comparable<T>> Builder<T> builder() { + return new AutoValue_AutoValueTest_BuilderWithPrefixedGetters.Builder<T>(); + } + + @AutoValue.Builder + public abstract static class Builder<T extends Comparable<T>> { + public abstract Builder<T> setList(ImmutableList<T> list); + public abstract Builder<T> setT(T t); + public abstract Builder<T> setInts(int[] ints); + public abstract Builder<T> setNoGetter(int x); + + abstract ImmutableList<T> getList(); + abstract T getT(); + abstract int[] getInts(); + + public abstract BuilderWithPrefixedGetters<T> build(); + } + } + + public void testBuilderWithPrefixedGetter() { + ImmutableList<String> names = ImmutableList.of("fred", "jim"); + String name = "sheila"; + int noGetter = -1; + + BuilderWithPrefixedGetters.Builder<String> builder = BuilderWithPrefixedGetters.builder(); + assertThat(builder.getInts()).isNull(); + try { + builder.getList(); + fail("Attempt to retrieve unset list property should have failed"); + } catch (IllegalStateException e) { + assertThat(e).hasMessage("Property \"list\" has not been set"); + } + + builder.setList(names); + assertThat(builder.getList()).isSameAs(names); + builder.setT(name); + assertThat(builder.getInts()).isNull(); + + BuilderWithPrefixedGetters<String> instance = builder.setNoGetter(noGetter).build(); + assertThat(instance.getList()).isSameAs(names); + assertThat(instance.getT()).isEqualTo(name); + assertThat(instance.getInts()).isNull(); + assertThat(instance.getNoGetter()).isEqualTo(noGetter); + } + + @AutoValue + public abstract static class BuilderWithPropertyBuilders<FooT extends Comparable<FooT>> { + public abstract ImmutableList<FooT> getFoos(); + public abstract ImmutableSet<String> getStrings(); + + public abstract BuilderWithPropertyBuilders.Builder<FooT> toBuilder(); + + public static <FooT extends Comparable<FooT>> Builder<FooT> builder() { + return new AutoValue_AutoValueTest_BuilderWithPropertyBuilders.Builder<FooT>(); + } + + @AutoValue.Builder + public abstract static class Builder<FooT extends Comparable<FooT>> { + public abstract ImmutableList<FooT> getFoos(); + + public Builder<FooT> addFoos(Iterable<FooT> foos) { + foosBuilder().addAll(foos); + return this; + } + + abstract ImmutableList.Builder<FooT> foosBuilder(); + + public Builder<FooT> addToTs(FooT element) { + foosBuilder().add(element); + return this; + } + + abstract ImmutableSet.Builder<String> stringsBuilder(); + + public Builder<FooT> addToStrings(String element) { + stringsBuilder().add(element); + return this; + } + + public abstract BuilderWithPropertyBuilders<FooT> build(); + } + } + + public void testBuilderWithPropertyBuilders() { + ImmutableList<Integer> numbers = ImmutableList.of(1, 1, 2, 6, 24); + ImmutableSet<String> names = ImmutableSet.of("one", "two", "six", "twenty-four"); + + BuilderWithPropertyBuilders<Integer> a = BuilderWithPropertyBuilders.<Integer>builder() + .addFoos(numbers) + .addToStrings("one") + .addToStrings("two") + .addToStrings("six") + .addToStrings("twenty-four") + .build(); + + assertEquals(numbers, a.getFoos()); + assertEquals(names, a.getStrings()); + + BuilderWithPropertyBuilders.Builder<Integer> bBuilder = BuilderWithPropertyBuilders.builder(); + bBuilder.stringsBuilder().addAll(names); + bBuilder.foosBuilder().addAll(numbers); + + assertEquals(numbers, bBuilder.getFoos()); + + BuilderWithPropertyBuilders<Integer> b = bBuilder.build(); + assertEquals(a, b); + + BuilderWithPropertyBuilders.Builder<Integer> cBuilder = a.toBuilder(); + cBuilder.addToStrings("one hundred and twenty"); + cBuilder.addToTs(120); + BuilderWithPropertyBuilders<Integer> c = cBuilder.build(); + assertEquals(ImmutableSet.of("one", "two", "six", "twenty-four", "one hundred and twenty"), + c.getStrings()); + assertEquals(ImmutableList.of(1, 1, 2, 6, 24, 120), c.getFoos()); + + BuilderWithPropertyBuilders.Builder<Integer> dBuilder = a.toBuilder(); + dBuilder.addFoos(ImmutableList.of(120, 720)); + BuilderWithPropertyBuilders<Integer> d = dBuilder.build(); + assertEquals(ImmutableList.of(1, 1, 2, 6, 24, 120, 720), d.getFoos()); + assertEquals(names, d.getStrings()); + + BuilderWithPropertyBuilders<Integer> empty = + BuilderWithPropertyBuilders.<Integer>builder().build(); + assertEquals(ImmutableList.of(), empty.getFoos()); + assertEquals(ImmutableSet.of(), empty.getStrings()); + } + + @AutoValue + public abstract static class + BuilderWithExoticPropertyBuilders<K extends Number, V extends Comparable<K>> { + public abstract ImmutableMap<String, V> map(); + public abstract ImmutableTable<String, K, V> table(); + + public static <K extends Number, V extends Comparable<K>> Builder<K, V> builder() { + return new AutoValue_AutoValueTest_BuilderWithExoticPropertyBuilders.Builder<K, V>(); + } + + @AutoValue.Builder + public abstract static class Builder<K extends Number, V extends Comparable<K>> { + public Builder<K, V> putAll(Map<String, V> map) { + mapBuilder().putAll(map); + return this; + } + + public abstract ImmutableMap.Builder<String, V> mapBuilder(); + + public Builder<K, V> putAll(ImmutableTable<String, K, V> table) { + tableBuilder().putAll(table); + return this; + } + + public abstract ImmutableTable.Builder<String, K, V> tableBuilder(); + + public abstract BuilderWithExoticPropertyBuilders<K, V> build(); + } + } + + public void testBuilderWithExoticPropertyBuilders() { + ImmutableMap<String, Integer> map = ImmutableMap.of("one", 1); + ImmutableTable<String, Integer, Integer> table = ImmutableTable.of("one", 1, -1); + + BuilderWithExoticPropertyBuilders<Integer, Integer> a = + BuilderWithExoticPropertyBuilders.<Integer, Integer>builder() + .putAll(map) + .putAll(table) + .build(); + assertEquals(map, a.map()); + assertEquals(table, a.table()); + + BuilderWithExoticPropertyBuilders.Builder<Integer, Integer> bBuilder = + BuilderWithExoticPropertyBuilders.builder(); + bBuilder.mapBuilder().putAll(map); + bBuilder.tableBuilder().putAll(table); + BuilderWithExoticPropertyBuilders<Integer, Integer> b = bBuilder.build(); + assertEquals(a, b); + + BuilderWithExoticPropertyBuilders<Integer, Integer> empty = + BuilderWithExoticPropertyBuilders.<Integer, Integer>builder().build(); + assertEquals(ImmutableMap.of(), empty.map()); + assertEquals(ImmutableTable.of(), empty.table()); + } + @Retention(RetentionPolicy.RUNTIME) @interface GwtCompatible { boolean funky() default false; diff --git a/value/src/main/java/com/google/auto/value/AutoValue.java b/value/src/main/java/com/google/auto/value/AutoValue.java index d05be395..5948c3bd 100644 --- a/value/src/main/java/com/google/auto/value/AutoValue.java +++ b/value/src/main/java/com/google/auto/value/AutoValue.java @@ -75,14 +75,4 @@ public @interface AutoValue { @Retention(RetentionPolicy.SOURCE) @Target(ElementType.TYPE) public @interface Builder {} - - /** - * Specifies that the annotated method is a validation method. The method should be a non-private - * no-argument method in an AutoValue class. It will be called by the {@code build()} method of - * the {@link Builder @AutoValue.Builder} implementation, immediately after constructing the new - * object. It can throw an exception if the new object fails validation checks. - */ - @Retention(RetentionPolicy.SOURCE) - @Target(ElementType.METHOD) - public @interface Validate {} } diff --git a/value/src/main/java/com/google/auto/value/processor/AutoValueBuilderProcessor.java b/value/src/main/java/com/google/auto/value/processor/AutoValueBuilderProcessor.java index fec3c577..e932470a 100644 --- a/value/src/main/java/com/google/auto/value/processor/AutoValueBuilderProcessor.java +++ b/value/src/main/java/com/google/auto/value/processor/AutoValueBuilderProcessor.java @@ -44,8 +44,7 @@ import javax.tools.Diagnostic; public class AutoValueBuilderProcessor extends AbstractProcessor { @Override public Set<String> getSupportedAnnotationTypes() { - return ImmutableSet.of( - AutoValue.Builder.class.getCanonicalName(), AutoValue.Validate.class.getCanonicalName()); + return ImmutableSet.of(AutoValue.Builder.class.getCanonicalName()); } @Override @@ -71,19 +70,6 @@ public class AutoValueBuilderProcessor extends AbstractProcessor { + " @AutoValue class"); } } - - Set<? extends Element> validateMethods = - roundEnv.getElementsAnnotatedWith(AutoValue.Validate.class); - if (!SuperficialValidation.validateElements(validateMethods)) { - return false; - } - for (Element annotatedMethod : validateMethods) { - if (isAnnotationPresent(annotatedMethod, AutoValue.Validate.class)) { - validate( - annotatedMethod, - "@AutoValue.Validate can only be applied to a method inside an @AutoValue class"); - } - } return false; } diff --git a/value/src/main/java/com/google/auto/value/processor/AutoValueProcessor.java b/value/src/main/java/com/google/auto/value/processor/AutoValueProcessor.java index b079b4f2..e8eaa311 100644 --- a/value/src/main/java/com/google/auto/value/processor/AutoValueProcessor.java +++ b/value/src/main/java/com/google/auto/value/processor/AutoValueProcessor.java @@ -287,6 +287,16 @@ public class AutoValueProcessor extends AbstractProcessor { return ""; } } + + @Override + public boolean equals(Object obj) { + return obj instanceof Property && ((Property) obj).method.equals(method); + } + + @Override + public int hashCode() { + return method.hashCode(); + } } private static boolean isJavaLangObject(TypeElement type) { @@ -434,7 +444,7 @@ public class AutoValueProcessor extends AbstractProcessor { } // If we are running from Eclipse, undo the work of its compiler which sorts methods. eclipseHack().reorderProperties(props); - vars.props = props; + vars.props = ImmutableSet.copyOf(props); vars.serialVersionUID = getSerialVersionUID(type); vars.formalTypes = typeSimplifier.formalTypeParametersString(type); vars.actualTypes = TypeSimplifier.actualTypeParametersString(type); diff --git a/value/src/main/java/com/google/auto/value/processor/AutoValueTemplateVars.java b/value/src/main/java/com/google/auto/value/processor/AutoValueTemplateVars.java index 1c68845e..1f9b280e 100644 --- a/value/src/main/java/com/google/auto/value/processor/AutoValueTemplateVars.java +++ b/value/src/main/java/com/google/auto/value/processor/AutoValueTemplateVars.java @@ -15,13 +15,15 @@ */ package com.google.auto.value.processor; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedSet; + import org.apache.velocity.runtime.parser.node.SimpleNode; import java.util.Collections; -import java.util.List; -import java.util.Map; import java.util.Set; -import java.util.SortedSet; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.util.Types; @@ -33,8 +35,11 @@ import javax.lang.model.util.Types; */ @SuppressWarnings("unused") // the fields in this class are only read via reflection class AutoValueTemplateVars extends TemplateVars { - /** The properties defined by the parent class's abstract methods. */ - List<AutoValueProcessor.Property> props; + /** + * The properties defined by the parent class's abstract methods. The elements of this set are + * in the same order as the original abstract method declarations in the AutoValue class. + */ + ImmutableSet<AutoValueProcessor.Property> props; /** Whether to generate an equals(Object) method. */ Boolean equals; @@ -47,7 +52,7 @@ class AutoValueTemplateVars extends TemplateVars { Types types; /** The fully-qualified names of the classes to be imported in the generated class. */ - SortedSet<String> imports; + ImmutableSortedSet<String> imports; /** * The spelling of the javax.annotation.Generated class: Generated or javax.annotation.Generated. @@ -131,18 +136,33 @@ class AutoValueTemplateVars extends TemplateVars { /** * A map from property names (like foo) to the corresponding setter method names (foo or setFoo). */ - Map<String, String> builderSetterNames = Collections.emptyMap(); + ImmutableMap<String, String> builderSetterNames = ImmutableMap.of(); /** - * The names of any {@code toBuilder()} methods, that is methods that return the builder type. + * A map from property names to information about the associated property builder. A property + * called foo (defined by a method foo() or getFoo()) can have a property builder called + * fooBuilder(). The type of foo must be an immutable Guava type, like ImmutableSet, and + * fooBuilder() must return the corresponding builder, like ImmutableSet.Builder. */ - List<String> toBuilderMethods; + ImmutableMap<String, BuilderSpec.PropertyBuilder> builderPropertyBuilders = + ImmutableMap.of(); /** - * The simple names of validation methods (marked {@code @AutoValue.Validate}) in the AutoValue - * class. (Currently, this set is either empty or a singleton.) + * Properties that are required to be set. A property must be set explicitly unless it is either + * {@code @Nullable} (in which case it defaults to null), or has a property-builder method + * (in which case it defaults to empty). + */ + ImmutableSet<AutoValueProcessor.Property> builderRequiredProperties = ImmutableSet.of(); + + /** + * Properties that have getters in the builder. + */ + ImmutableSet<String> propertiesWithBuilderGetters = ImmutableSet.of(); + + /** + * The names of any {@code toBuilder()} methods, that is methods that return the builder type. */ - Set<String> validators = Collections.emptySet(); + ImmutableList<String> toBuilderMethods; private static final SimpleNode TEMPLATE = parsedTemplateForResource("autovalue.vm"); diff --git a/value/src/main/java/com/google/auto/value/processor/BuilderMethodClassifier.java b/value/src/main/java/com/google/auto/value/processor/BuilderMethodClassifier.java index 105177e3..d52d2264 100644 --- a/value/src/main/java/com/google/auto/value/processor/BuilderMethodClassifier.java +++ b/value/src/main/java/com/google/auto/value/processor/BuilderMethodClassifier.java @@ -15,19 +15,26 @@ */ package com.google.auto.value.processor; +import com.google.auto.common.MoreElements; import com.google.auto.common.MoreTypes; import com.google.common.base.Equivalence; import com.google.common.base.Optional; import com.google.common.collect.ImmutableBiMap; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.collect.Sets; + import java.beans.Introspector; import java.util.Map; import java.util.Set; + +import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; /** * Classifies methods inside builder types, based on their names and parameter and return types. @@ -38,25 +45,39 @@ class BuilderMethodClassifier { private static final Equivalence<TypeMirror> TYPE_EQUIVALENCE = MoreTypes.equivalence(); private final ErrorReporter errorReporter; + private final Types typeUtils; private final TypeElement autoValueClass; private final TypeElement builderType; private final ImmutableBiMap<ExecutableElement, String> getterToPropertyName; + private final ImmutableMap<String, ExecutableElement> getterNameToGetter; + private final Set<ExecutableElement> buildMethods = Sets.newLinkedHashSet(); + private final Set<String> propertiesWithBuilderGetters = Sets.newLinkedHashSet(); private final Map<String, ExecutableElement> propertyNameToPrefixedSetter = Maps.newLinkedHashMap(); private final Map<String, ExecutableElement> propertyNameToUnprefixedSetter = Maps.newLinkedHashMap(); + private final Map<String, ExecutableElement> propertyNameToPropertyBuilder = + Maps.newLinkedHashMap(); private boolean settersPrefixed; private BuilderMethodClassifier( ErrorReporter errorReporter, + ProcessingEnvironment processingEnv, TypeElement autoValueClass, TypeElement builderType, ImmutableBiMap<ExecutableElement, String> getterToPropertyName) { this.errorReporter = errorReporter; + this.typeUtils = processingEnv.getTypeUtils(); this.autoValueClass = autoValueClass; this.builderType = builderType; this.getterToPropertyName = getterToPropertyName; + ImmutableMap.Builder<String, ExecutableElement> getterToPropertyNameBuilder = + ImmutableMap.builder(); + for (ExecutableElement getter : getterToPropertyName.keySet()) { + getterToPropertyNameBuilder.put(getter.getSimpleName().toString(), getter); + } + this.getterNameToGetter = getterToPropertyNameBuilder.build(); } /** @@ -74,11 +95,12 @@ class BuilderMethodClassifier { static Optional<BuilderMethodClassifier> classify( Iterable<ExecutableElement> methods, ErrorReporter errorReporter, + ProcessingEnvironment processingEnv, TypeElement autoValueClass, TypeElement builderType, ImmutableBiMap<ExecutableElement, String> getterToPropertyName) { BuilderMethodClassifier classifier = new BuilderMethodClassifier( - errorReporter, autoValueClass, builderType, getterToPropertyName); + errorReporter, processingEnv, autoValueClass, builderType, getterToPropertyName); if (classifier.classifyMethods(methods)) { return Optional.of(classifier); } else { @@ -94,7 +116,22 @@ class BuilderMethodClassifier { * {@code foo} or {@code setFoo}. */ Map<String, ExecutableElement> propertyNameToSetter() { - return settersPrefixed ? propertyNameToPrefixedSetter : propertyNameToUnprefixedSetter; + return ImmutableMap.copyOf( + settersPrefixed ? propertyNameToPrefixedSetter : propertyNameToUnprefixedSetter); + } + + Map<String, ExecutableElement> propertyNameToPropertyBuilder() { + return propertyNameToPropertyBuilder; + } + + /** + * Returns the set of properties that have getters in the builder. If a property is defined by + * an abstract method in the {@code @AutoValue} class called {@code foo()} or {@code getFoo()} + * then the name of the property is {@code foo}, If the builder also has a method of the same name + * ({@code foo()} or {@code getFoo()}) then the set returned here will contain {@code foo}. + */ + ImmutableSet<String> propertiesWithBuilderGetters() { + return ImmutableSet.copyOf(propertiesWithBuilderGetters); } /** @@ -132,7 +169,14 @@ class BuilderMethodClassifier { } for (Map.Entry<ExecutableElement, String> getterEntry : getterToPropertyName.entrySet()) { String property = getterEntry.getValue(); - if (!propertyNameToSetter.containsKey(property)) { + boolean hasSetter = propertyNameToSetter.containsKey(property); + boolean hasBuilder = propertyNameToPropertyBuilder.containsKey(property); + if (hasSetter && hasBuilder) { + String error = + String.format("Property %s cannot have both a setter and a builder", property); + errorReporter.reportError(error, builderType); + } else if (!hasSetter && !hasBuilder) { + // TODO(emcmanus): also mention the possible builder method if the property type allows one String setterName = settersPrefixed ? prefixWithSet(property) : property; String error = String.format("Expected a method with this signature: %s%s %s(%s)", builderType, @@ -171,19 +215,90 @@ class BuilderMethodClassifier { * @return true if the method was successfully classified, false if an error has been reported. */ private boolean classifyMethodNoArgs(ExecutableElement method) { + String methodName = method.getSimpleName().toString(); TypeMirror returnType = method.getReturnType(); + ExecutableElement getter = getterNameToGetter.get(methodName); + if (getter != null) { + return classifyGetter(method, getter); + } + + if (methodName.endsWith("Builder")) { + String property = methodName.substring(0, methodName.length() - "Builder".length()); + if (getterToPropertyName.containsValue(property)) { + return classifyPropertyBuilder(method, property); + } + } + if (TYPE_EQUIVALENCE.equivalent(returnType, autoValueClass.asType())) { buildMethods.add(method); return true; } - errorReporter.reportError( - "Method without arguments should be a build method returning " - + autoValueClass + typeParamsString(), method); + String error = String.format( + "Method without arguments should be a build method returning %1$s%2$s" + + " or a getter method with the same name and type as a getter method of %1$s", + autoValueClass, typeParamsString()); + errorReporter.reportError(error, method); return false; } + private boolean classifyGetter( + ExecutableElement builderGetter, ExecutableElement originalGetter) { + if (!TYPE_EQUIVALENCE.equivalent( + builderGetter.getReturnType(), originalGetter.getReturnType())) { + String error = String.format( + "Method matches a property of %s but has return type %s instead of %s", + autoValueClass, builderGetter.getReturnType(), originalGetter.getReturnType()); + errorReporter.reportError(error, builderGetter); + return false; + } + propertiesWithBuilderGetters.add(getterToPropertyName.get(originalGetter)); + return true; + } + + // Construct this string so it won't be found by Maven shading and renamed, which is not what + // we want. + private static final String COM_GOOGLE_COMMON_COLLECT_IMMUTABLE = + new StringBuilder("com.").append("google.common.collect.Immutable").toString(); + + private boolean classifyPropertyBuilder(ExecutableElement method, String property) { + TypeMirror builderTypeMirror = method.getReturnType(); + TypeElement builderTypeElement = MoreTypes.asTypeElement(builderTypeMirror); + String builderTypeString = builderTypeElement.getQualifiedName().toString(); + boolean isGuavaBuilder = (builderTypeString.startsWith(COM_GOOGLE_COMMON_COLLECT_IMMUTABLE) + && builderTypeString.endsWith(".Builder")); + if (!isGuavaBuilder) { + errorReporter.reportError("Method looks like a property builder, but its return type " + + "is not a builder for an immutable type in com.google.common.collect", method); + return false; + } + // Given, e.g. ImmutableSet.Builder<String>, construct ImmutableSet<String> and check that + // it is indeed the type of the property. + DeclaredType builderTypeDeclared = MoreTypes.asDeclared(builderTypeMirror); + TypeMirror[] builderTypeArgs = + builderTypeDeclared.getTypeArguments().toArray(new TypeMirror[0]); + if (builderTypeArgs.length == 0) { + errorReporter.reportError("Property builder type cannot be raw (missing <...>)", method); + return false; + } + TypeElement enclosingTypeElement = + MoreElements.asType(builderTypeElement.getEnclosingElement()); + TypeMirror expectedPropertyType = + typeUtils.getDeclaredType(enclosingTypeElement, builderTypeArgs); + TypeMirror actualPropertyType = getterToPropertyName.inverse().get(property).getReturnType(); + if (!TYPE_EQUIVALENCE.equivalent(expectedPropertyType, actualPropertyType)) { + String error = String.format( + "Return type of property-builder method implies a property of type %s, but property " + + "%s has type %s", + expectedPropertyType, property, actualPropertyType); + errorReporter.reportError(error, method); + return false; + } + propertyNameToPropertyBuilder.put(property, method); + return true; + } + /** * Classify a method given that it has one argument. Currently, a method with one argument can * only be a setter, meaning that it must look like {@code foo(T)} or {@code setFoo(T)}, where @@ -205,7 +320,9 @@ class BuilderMethodClassifier { propertyNameToSetter = propertyNameToPrefixedSetter; valueGetter = propertyNameToGetter.get(propertyName); } - if (valueGetter == null) { + if (valueGetter == null || propertyNameToSetter == null) { + // The second disjunct isn't needed but convinces control-flow checkers that + // propertyNameToSetter can't be null when we call put on it below. errorReporter.reportError( "Method does not correspond to a property of " + autoValueClass, method); return false; diff --git a/value/src/main/java/com/google/auto/value/processor/BuilderSpec.java b/value/src/main/java/com/google/auto/value/processor/BuilderSpec.java index 1cfb2975..be6aacfc 100644 --- a/value/src/main/java/com/google/auto/value/processor/BuilderSpec.java +++ b/value/src/main/java/com/google/auto/value/processor/BuilderSpec.java @@ -15,9 +15,12 @@ */ package com.google.auto.value.processor; +import static com.google.common.base.Preconditions.checkState; + import com.google.auto.common.MoreElements; import com.google.auto.common.MoreTypes; import com.google.auto.value.AutoValue; +import com.google.auto.value.processor.AutoValueProcessor.Property; import com.google.common.base.Optional; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableBiMap; @@ -44,7 +47,6 @@ import javax.lang.model.type.DeclaredType; 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.lang.model.util.Types; /** @@ -54,7 +56,7 @@ import javax.lang.model.util.Types; */ class BuilderSpec { private final TypeElement autoValueClass; - private final Elements elementUtils; + private final ProcessingEnvironment processingEnv; private final ErrorReporter errorReporter; BuilderSpec( @@ -62,7 +64,7 @@ class BuilderSpec { ProcessingEnvironment processingEnv, ErrorReporter errorReporter) { this.autoValueClass = autoValueClass; - this.elementUtils = processingEnv.getElementUtils(); + this.processingEnv = processingEnv; this.errorReporter = errorReporter; } @@ -91,36 +93,9 @@ class BuilderSpec { } } - Optional<ExecutableElement> validateMethod = Optional.absent(); - for (ExecutableElement containedMethod : - ElementFilter.methodsIn(autoValueClass.getEnclosedElements())) { - if (MoreElements.isAnnotationPresent(containedMethod, AutoValue.Validate.class)) { - if (containedMethod.getModifiers().contains(Modifier.STATIC)) { - errorReporter.reportError( - "@AutoValue.Validate cannot apply to a static method", containedMethod); - } else if (!containedMethod.getParameters().isEmpty()) { - errorReporter.reportError( - "@AutoValue.Validate method must not have parameters", containedMethod); - } else if (containedMethod.getReturnType().getKind() != TypeKind.VOID) { - errorReporter.reportError( - "Return type of @AutoValue.Validate method must be void", containedMethod); - } else if (validateMethod.isPresent()) { - errorReporter.reportError( - "There can only be one @AutoValue.Validate method", containedMethod); - } else { - validateMethod = Optional.of(containedMethod); - } - } - } - if (builderTypeElement.isPresent()) { - return builderFrom(builderTypeElement.get(), validateMethod); + return builderFrom(builderTypeElement.get()); } else { - if (validateMethod.isPresent()) { - errorReporter.reportError( - "@AutoValue.Validate is only meaningful if there is an @AutoValue.Builder", - validateMethod.get()); - } return Optional.absent(); } } @@ -130,13 +105,9 @@ class BuilderSpec { */ class Builder { private final TypeElement builderTypeElement; - private final Optional<ExecutableElement> validateMethod; - Builder( - TypeElement builderTypeElement, - Optional<ExecutableElement> validateMethod) { + Builder(TypeElement builderTypeElement) { this.builderTypeElement = builderTypeElement; - this.validateMethod = validateMethod; } /** @@ -198,7 +169,12 @@ class BuilderSpec { ImmutableBiMap<ExecutableElement, String> getterToPropertyName) { Iterable<ExecutableElement> builderMethods = abstractMethods(builderTypeElement); Optional<BuilderMethodClassifier> optionalClassifier = BuilderMethodClassifier.classify( - builderMethods, errorReporter, autoValueClass, builderTypeElement, getterToPropertyName); + builderMethods, + errorReporter, + processingEnv, + autoValueClass, + builderTypeElement, + getterToPropertyName); if (!optionalClassifier.isPresent()) { return; } @@ -222,17 +198,106 @@ class BuilderSpec { vars.builderFormalTypes = typeSimplifier.formalTypeParametersString(builderTypeElement); vars.builderActualTypes = TypeSimplifier.actualTypeParametersString(builderTypeElement); vars.buildMethodName = buildMethod.getSimpleName().toString(); - if (validateMethod.isPresent()) { - vars.validators = ImmutableSet.of(validateMethod.get().getSimpleName().toString()); - } else { - vars.validators = ImmutableSet.of(); - } + vars.propertiesWithBuilderGetters = classifier.propertiesWithBuilderGetters(); + ImmutableMap.Builder<String, String> setterNameBuilder = ImmutableMap.builder(); for (Map.Entry<String, ExecutableElement> entry : classifier.propertyNameToSetter().entrySet()) { setterNameBuilder.put(entry.getKey(), entry.getValue().getSimpleName().toString()); } vars.builderSetterNames = setterNameBuilder.build(); + + vars.builderPropertyBuilders = + makeBuilderPropertyBuilderMap(classifier, typeSimplifier, getterToPropertyName); + + Set<Property> required = Sets.newLinkedHashSet(vars.props); + for (Property property : vars.props) { + if (property.isNullable() || vars.builderPropertyBuilders.containsKey(property.getName())) { + required.remove(property); + } + } + vars.builderRequiredProperties = ImmutableSet.copyOf(required); + } + + private ImmutableMap<String, PropertyBuilder> makeBuilderPropertyBuilderMap( + BuilderMethodClassifier classifier, + TypeSimplifier typeSimplifier, + ImmutableBiMap<ExecutableElement, String> getterToPropertyName) { + ImmutableMap.Builder<String, PropertyBuilder> map = ImmutableMap.builder(); + for (Map.Entry<String, ExecutableElement> entry : + classifier.propertyNameToPropertyBuilder().entrySet()) { + String property = entry.getKey(); + ExecutableElement autoValuePropertyMethod = getterToPropertyName.inverse().get(property); + ExecutableElement propertyBuilderMethod = entry.getValue(); + PropertyBuilder propertyBuilder = new PropertyBuilder( + autoValuePropertyMethod, propertyBuilderMethod, typeSimplifier); + map.put(property, propertyBuilder); + } + return map.build(); + } + } + + /** + * Information about a property builder, referenced from the autovalue.vm template. A property + * called foo (defined by a method foo() or getFoo()) can have a property builder called + * fooBuilder(). The type of foo must be an immutable Guava type, like ImmutableSet, and + * fooBuilder() must return the corresponding builder, like ImmutableSet.Builder. + */ + public class PropertyBuilder { + private final String builderType; + private final String initializer; + private final String copyAll; + + PropertyBuilder( + ExecutableElement autoValuePropertyMethod, + ExecutableElement propertyBuilderMethod, + TypeSimplifier typeSimplifier) { + String immutableType = typeSimplifier.simplify(autoValuePropertyMethod.getReturnType()); + int typeParamIndex = immutableType.indexOf('<'); + checkState(typeParamIndex > 0, immutableType); + String rawImmutableType = immutableType.substring(0, typeParamIndex); + this.builderType = + rawImmutableType + ".Builder" + immutableType.substring(typeParamIndex); + this.initializer = rawImmutableType + ".builder()"; + // TODO(emcmanus): clean up TypeSimplifier and remove this hack. + // We want it to simplify com.google.common.collect.ImmutableSet.Builder<E> + // the same way it would simplify com.google.common.collect.ImmutableSet<E>. + // The issue is that getEnclosingElement tends to do the wrong thing in Eclipse + // so we end up with ImmutableSet<E>.Builder<E>. + TypeElement builderTypeElement = MoreElements.asType( + processingEnv.getTypeUtils().asElement(propertyBuilderMethod.getReturnType())); + Set<String> methodNames = Sets.newHashSet(); + for (ExecutableElement builderMethod : + ElementFilter.methodsIn(builderTypeElement.getEnclosedElements())) { + methodNames.add(builderMethod.getSimpleName().toString()); + } + if (methodNames.contains("addAll")) { + this.copyAll = "addAll"; + } else if (methodNames.contains("putAll")) { + this.copyAll = "putAll"; + } else { + throw new AssertionError("Builder contains neither addAll nor putAll: " + methodNames); + } + } + + /** The type of the builder, for example {@code ImmutableSet.Builder<String>}. */ + public String getBuilderType() { + return builderType; + } + + /** An initializer for the builder field, for example {@code ImmutableSet.builder()}. */ + public String getInitializer() { + return initializer; + } + + /** + * The method to copy another immutable collection into this one. It is {@code copyAll} for + * one-dimensional collections like {@code ImmutableList} and {@code ImmutableSet}, and it is + * {@code putAll} for two-dimensional collections like {@code ImmutableMap} and + * {@code ImmutableTable}. + */ + public String getCopyAll() { + return copyAll; } } @@ -241,8 +306,7 @@ class BuilderSpec { * class or interface has abstract methods that could not be part of any builder, emits error * messages and returns null. */ - private Optional<Builder> builderFrom( - TypeElement builderTypeElement, Optional<ExecutableElement> validateMethod) { + private Optional<Builder> builderFrom(TypeElement builderTypeElement) { // We require the builder to have the same type parameters as the @AutoValue class, meaning the // same names and bounds. In principle the type parameters could have different names, but that @@ -276,7 +340,7 @@ class BuilderSpec { + "type parameters of " + autoValueClass, builderTypeElement); return Optional.absent(); } - return Optional.of(new Builder(builderTypeElement, validateMethod)); + return Optional.of(new Builder(builderTypeElement)); } // Return a list of all abstract methods in the given TypeElement or inherited from ancestors. @@ -300,7 +364,7 @@ class BuilderSpec { for (ExecutableElement method : ElementFilter.methodsIn(typeElement.getEnclosedElements())) { for (Iterator<ExecutableElement> it = abstractMethods.iterator(); it.hasNext(); ) { ExecutableElement maybeOverridden = it.next(); - if (elementUtils.overrides(method, maybeOverridden, typeElement)) { + if (processingEnv.getElementUtils().overrides(method, maybeOverridden, typeElement)) { it.remove(); } } diff --git a/value/src/main/java/com/google/auto/value/processor/GwtSerialization.java b/value/src/main/java/com/google/auto/value/processor/GwtSerialization.java index 0615cc1d..2b6bb013 100644 --- a/value/src/main/java/com/google/auto/value/processor/GwtSerialization.java +++ b/value/src/main/java/com/google/auto/value/processor/GwtSerialization.java @@ -244,7 +244,7 @@ class GwtSerialization { private static final Charset UTF8 = Charset.forName("UTF-8"); - private String computeClassHash(List<AutoValueProcessor.Property> props) { + private String computeClassHash(Iterable<AutoValueProcessor.Property> props) { TypeSimplifier typeSimplifier = new TypeSimplifier( processingEnv.getTypeUtils(), "", new TypeMirrorSet(), null); CRC32 crc = new CRC32(); diff --git a/value/src/main/java/com/google/auto/value/processor/Reformatter.java b/value/src/main/java/com/google/auto/value/processor/Reformatter.java index 4d2fc325..6f875845 100644 --- a/value/src/main/java/com/google/auto/value/processor/Reformatter.java +++ b/value/src/main/java/com/google/auto/value/processor/Reformatter.java @@ -78,7 +78,7 @@ class Reformatter { private static String compressSpace(String s) { // Remove extra spaces. An "extra" space is one that is not part of the indentation at the start // of a line, and where the next character is also a space or a right paren or a semicolon - // or a comma, or the preceding character is a left paren. + // or a dot or a comma, or the preceding character is a left paren. // TODO(emcmanus): consider merging all three passes using this tokenization approach. StringBuilder sb = new StringBuilder(s.length()); Tokenizer tokenizer = new Tokenizer(s); @@ -95,7 +95,7 @@ class Reformatter { // Since we ensure that the tokenized string ends with \n, and a whitespace token stops // at \n, it is safe to look at end. char nextC = s.charAt(end); - if (",;)".indexOf(nextC) >= 0) { + if (".,;)".indexOf(nextC) >= 0) { continue; } sb.append(' '); diff --git a/value/src/main/java/com/google/auto/value/processor/TypeSimplifier.java b/value/src/main/java/com/google/auto/value/processor/TypeSimplifier.java index 1e0f02f1..b2b2c4b9 100644 --- a/value/src/main/java/com/google/auto/value/processor/TypeSimplifier.java +++ b/value/src/main/java/com/google/auto/value/processor/TypeSimplifier.java @@ -20,14 +20,13 @@ import static javax.lang.model.element.Modifier.PRIVATE; import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableSortedSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.SortedSet; -import java.util.TreeSet; import javax.lang.model.element.Element; import javax.lang.model.element.PackageElement; @@ -103,14 +102,14 @@ final class TypeSimplifier { * import java.util.Map.Entry and refer to the property as Entry. We could also import just * java.util.Map in this case and refer to Map.Entry, but currently we never do that. */ - SortedSet<String> typesToImport() { - SortedSet<String> typesToImport = new TreeSet<String>(); + ImmutableSortedSet<String> typesToImport() { + ImmutableSortedSet.Builder<String> typesToImport = ImmutableSortedSet.naturalOrder(); for (Map.Entry<String, Spelling> entry : imports.entrySet()) { if (entry.getValue().importIt) { typesToImport.add(entry.getKey()); } } - return typesToImport; + return typesToImport.build(); } /** diff --git a/value/src/main/java/com/google/auto/value/processor/autovalue.vm b/value/src/main/java/com/google/auto/value/processor/autovalue.vm index 51e8d225..ba34854e 100644 --- a/value/src/main/java/com/google/auto/value/processor/autovalue.vm +++ b/value/src/main/java/com/google/auto/value/processor/autovalue.vm @@ -38,7 +38,7 @@ final class $subclass$formalTypes extends $origClass$actualTypes { #if ($p.nullable) ${p.nullableAnnotation} #end $p.type $p #if ($foreach.hasNext) , #end #end ) { #foreach ($p in $props) - #if (!$p.kind.primitive && !$p.nullable) + #if (!$p.kind.primitive && !$p.nullable && !$builderPropertyBuilders[$p.name]) if ($p == null) { throw new NullPointerException("Null $p.name"); @@ -210,6 +210,11 @@ final class $subclass$formalTypes extends $origClass$actualTypes { private $types.boxedClass($p.typeMirror).simpleName $p; + #elseif ($builderPropertyBuilders[$p.name]) + + private ${builderPropertyBuilders[$p.name].builderType} $p = ## + ${builderPropertyBuilders[$p.name].initializer}; + #else private $p.type $p; @@ -225,61 +230,117 @@ final class $subclass$formalTypes extends $origClass$actualTypes { #foreach ($p in $props) + #if ($builderPropertyBuilders[$p.name]) + + ${p}.${builderPropertyBuilders[$p.name].copyAll}(source.${p.getter}()); + + #else + $builderSetterNames[$p.name](source.${p.getter}()); ## We use the setter methods rather than assigning the fields directly so we don't have ## to duplicate the logic for cloning arrays. Not cloning arrays would conceivably be ## a security problem. + #end + #end } #foreach ($p in $props) + ## Setter and/or property builder + + #if ($builderSetterNames[$p.name]) + @Override public ${builderTypeName}${builderActualTypes} $builderSetterNames[$p.name]($p.type $p) { - #if ($p.kind == "ARRAY") - #if ($p.nullable) + #if ($builderPropertyBuilders[$p.name]) + + this.$p = ${builderPropertyBuilders[$p.name].initializer}; + this.$p.${builderPropertyBuilders[$p.name].copyAll}($p); + + #elseif ($p.kind == "ARRAY") + #if ($p.nullable) this.$p = ($p == null) ? null : ${p}.clone(); - #else + #else this.$p = ${p}.clone(); - #end - #else + #end + #else this.$p = $p; - #end + #end return this; } - #end + #end -#set ($requiredCount = 0) -#foreach ($p in $props) - #if (!$p.nullable) - #set ($requiredCount = $requiredCount + 1) + #if ($builderPropertyBuilders[$p.name]) + + @Override + public ${builderPropertyBuilders[$p.name].builderType} ${p.name}Builder() { + return $p; + } + + #end + + ## Getter + + #if ($propertiesWithBuilderGetters.contains($p.name)) + + @Override + public $p.type ${p.getter}() { + #if ($builderRequiredProperties.contains($p)) + + if ($p == null) { + throw new IllegalStateException("Property \"$p.name\" has not been set"); + } + + #end + + #if ($p.kind == "ARRAY") + #if ($p.nullable) + + return ($p == null) ? null : ${p}.clone(); + + #else + + return ${p}.clone(); + + #end + #elseif ($builderPropertyBuilders[$p.name]) + + return ${p}.build(); + + #else + + return $p; + + #end + + } + + #end #end -#end @Override public ${origClass}${actualTypes} ${buildMethodName}() { - #if ($requiredCount > 0) + #if (!$builderRequiredProperties.empty) String missing = ""; - #foreach ($p in $props) - #if (!$p.nullable) + #foreach ($p in $builderRequiredProperties) if ($p == null) { missing += " $p.name"; } - #end #end if (!missing.isEmpty()) { @@ -287,19 +348,13 @@ final class $subclass$formalTypes extends $origClass$actualTypes { } #end - ${origClass}${actualTypes} result = new ${subclass}${actualTypes}( + return new ${subclass}${actualTypes}( #foreach ($p in $props) - this.$p #if ($foreach.hasNext) , #end + this.$p ## + #if (${builderPropertyBuilders[$p.name]}) .build() #end + #if ($foreach.hasNext) , #end #end ); - - #foreach ($v in $validators) - - result.${v}(); - - #end - - return result; } } #end diff --git a/value/src/test/java/com/google/auto/value/processor/CompilationTest.java b/value/src/test/java/com/google/auto/value/processor/CompilationTest.java index d9122c48..40955873 100644 --- a/value/src/test/java/com/google/auto/value/processor/CompilationTest.java +++ b/value/src/test/java/com/google/auto/value/processor/CompilationTest.java @@ -496,6 +496,7 @@ public class CompilationTest extends TestCase { "package foo.bar;", "", "import com.google.auto.value.AutoValue;", + "import com.google.common.collect.ImmutableList;", "", "import java.util.List;", "import javax.annotation.Nullable;", @@ -506,22 +507,21 @@ public class CompilationTest extends TestCase { " public abstract byte[] aByteArray();", " @Nullable public abstract int[] aNullableIntArray();", " public abstract List<T> aList();", + " public abstract ImmutableList<T> anImmutableList();", "", " public abstract Builder<T> toBuilder();", "", - " @AutoValue.Validate", - " void validate() {", - " if (anInt() < 0) {", - " throw new IllegalStateException(\"Negative integer\");", - " }", - " }", - "", " @AutoValue.Builder", " public interface Builder<T extends Number> {", " Builder<T> anInt(int x);", " Builder<T> aByteArray(byte[] x);", " Builder<T> aNullableIntArray(@Nullable int[] x);", " Builder<T> aList(List<T> x);", + " ImmutableList.Builder<T> anImmutableListBuilder();", + "", + " List<T> aList();", + " ImmutableList<T> anImmutableList();", + "", " Baz<T> build();", " }", "", @@ -533,6 +533,7 @@ public class CompilationTest extends TestCase { "foo.bar.AutoValue_Baz", "package foo.bar;", "", + "import com.google.common.collect.ImmutableList", "import java.util.Arrays;", "import java.util.List;", "import javax.annotation.Generated;", @@ -543,9 +544,14 @@ public class CompilationTest extends TestCase { " private final byte[] aByteArray;", " private final int[] aNullableIntArray;", " private final List<T> aList;", - "", - " private AutoValue_Baz(" - + "int anInt, byte[] aByteArray, @javax.annotation.Nullable int[] aNullableIntArray, List<T> aList) {", + " private final ImmutableList<T> anImmutableList;", + "", + " private AutoValue_Baz(", + " int anInt,", + " byte[] aByteArray,", + " @javax.annotation.Nullable int[] aNullableIntArray,", + " List<T> aList,", + " ImmutableList<T> anImmutableList) {", " this.anInt = anInt;", " if (aByteArray == null) {", " throw new NullPointerException(\"Null aByteArray\");", @@ -556,6 +562,7 @@ public class CompilationTest extends TestCase { " throw new NullPointerException(\"Null aList\");", " }", " this.aList = aList;", + " this.anImmutableList = anImmutableList;", " }", "", " @Override public int anInt() {", @@ -575,12 +582,17 @@ public class CompilationTest extends TestCase { " return aList;", " }", "", + " @Override public ImmutableList<T> anImmutableList() {", + " return anImmutableList;", + " }", + "", " @Override public String toString() {", " return \"Baz{\"", " + \"anInt=\" + anInt + \", \"", " + \"aByteArray=\" + Arrays.toString(aByteArray) + \", \"", " + \"aNullableIntArray=\" + Arrays.toString(aNullableIntArray) + \", \"", - " + \"aList=\" + aList", + " + \"aList=\" + aList + \", \"", + " + \"anImmutableList=\" + anImmutableList", " + \"}\";", " }", "", @@ -597,7 +609,8 @@ public class CompilationTest extends TestCase { " && (Arrays.equals(this.aNullableIntArray, " + "(that instanceof AutoValue_Baz) " + "? ((AutoValue_Baz) that).aNullableIntArray : that.aNullableIntArray()))", - " && (this.aList.equals(that.aList()));", + " && (this.aList.equals(that.aList()))", + " && (this.anImmutableList.equals(that.anImmutableList()));", " }", " return false;", " }", @@ -612,6 +625,8 @@ public class CompilationTest extends TestCase { " h ^= Arrays.hashCode(aNullableIntArray);", " h *= 1000003;", " h ^= aList.hashCode();", + " h *= 1000003;", + " h ^= anImmutableList.hashCode();", " return h;", " }", "", @@ -624,6 +639,7 @@ public class CompilationTest extends TestCase { " private byte[] aByteArray;", " private int[] aNullableIntArray;", " private List<T> aList;", + " private ImmutableList.Builder<T> anImmutableList = ImmutableList.builder();", "", " Builder() {", " }", @@ -633,6 +649,7 @@ public class CompilationTest extends TestCase { " aByteArray(source.aByteArray());", " aNullableIntArray(source.aNullableIntArray());", " aList(source.aList());", + " anImmutableList.addAll(source.anImmutableList());", " }", "", " @Override", @@ -661,6 +678,24 @@ public class CompilationTest extends TestCase { " }", "", " @Override", + " public List<T> aList() {", + " if (aList == null) {", + " throw new IllegalStateException(\"Property \\\"aList\\\" has not been set\");", + " }", + " return aList;", + " }", + "", + " @Override", + " public ImmutableList.Builder<T> anImmutableListBuilder() {", + " return anImmutableList;", + " }", + "", + " @Override", + " public ImmutableList<T> anImmutableList() {", + " return anImmutableList.build();", + " }", + "", + " @Override", " public Baz<T> build() {", " String missing = \"\";", " if (anInt == null) {", @@ -675,10 +710,12 @@ public class CompilationTest extends TestCase { " if (!missing.isEmpty()) {", " throw new IllegalStateException(\"Missing required properties:\" + missing);", " }", - " Baz<T> result = new AutoValue_Baz<T>(", - " this.anInt, this.aByteArray, this.aNullableIntArray, this.aList);", - " result.validate();", - " return result;", + " return new AutoValue_Baz<T>(", + " this.anInt,", + " this.aByteArray,", + " this.aNullableIntArray,", + " this.aList,", + " this.anImmutableList.build());", " }", " }", "}"); @@ -952,7 +989,7 @@ public class CompilationTest extends TestCase { .in(javaFileObject).onLine(12); } - public void testAutoValueBuilderAlienMethod() { + public void testAutoValueBuilderWrongTypeGetter() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "foo.bar.Baz", "package foo.bar;", @@ -960,12 +997,44 @@ public class CompilationTest extends TestCase { "import com.google.auto.value.AutoValue;", "", "@AutoValue", + "public abstract class Baz<T, U> {", + " abstract T blim();", + " abstract U blam();", + "", + " @AutoValue.Builder", + " public interface Builder<T, U> {", + " Builder<T, U> blim(T x);", + " Builder<T, U> blam(U x);", + " T blim();", + " T blam();", + " Baz<T, U> build();", + " }", + "}"); + assertAbout(javaSource()) + .that(javaFileObject) + .processedWith(new AutoValueProcessor(), new AutoValueBuilderProcessor()) + .failsToCompile() + .withErrorContaining( + "Method matches a property of foo.bar.Baz but has return type T instead of U") + .in(javaFileObject).onLine(15); + } + + public void testAutoValueBuilderPropertyBuilderAndSetter() { + JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( + "foo.bar.Baz", + "package foo.bar;", + "", + "import com.google.auto.value.AutoValue;", + "import com.google.common.collect.ImmutableSet;", + "", + "@AutoValue", "public abstract class Baz {", - " abstract String blam();", + " abstract ImmutableSet<String> blim();", "", " @AutoValue.Builder", " public interface Builder {", - " Builder blam(String x, String y);", + " abstract ImmutableSet.Builder<String> blimBuilder();", + " abstract Builder setBlim(ImmutableSet<String> blim);", " Baz build();", " }", "}"); @@ -973,11 +1042,11 @@ public class CompilationTest extends TestCase { .that(javaFileObject) .processedWith(new AutoValueProcessor(), new AutoValueBuilderProcessor()) .failsToCompile() - .withErrorContaining("Builder methods must have 0 or 1 parameters") + .withErrorContaining("Property blim cannot have both a setter and a builder") .in(javaFileObject).onLine(11); } - public void testAutoValueBuilderMissingBuildMethod() { + public void testAutoValueBuilderPropertyBuilderInvalidType() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "foo.bar.Baz", "package foo.bar;", @@ -985,12 +1054,13 @@ public class CompilationTest extends TestCase { "import com.google.auto.value.AutoValue;", "", "@AutoValue", - "public abstract class Baz<T> {", - " abstract T blam();", + "public abstract class Baz<T, U> {", + " abstract String blim();", "", " @AutoValue.Builder", - " public interface Builder<T> {", - " Builder<T> blam(T x);", + " public interface Builder<T, U> {", + " StringBuilder blimBuilder();", + " Baz<T, U> build();", " }", "}"); assertAbout(javaSource()) @@ -998,54 +1068,55 @@ public class CompilationTest extends TestCase { .processedWith(new AutoValueProcessor(), new AutoValueBuilderProcessor()) .failsToCompile() .withErrorContaining( - "Builder must have a single no-argument method returning foo.bar.Baz<T>") - .in(javaFileObject).onLine(10); + "Method looks like a property builder, but its return type is not a builder for an " + + "immutable type in com.google.common.collect") + .in(javaFileObject).onLine(11); } - public void testAutoValueBuilderDuplicateBuildMethods() { + public void testAutoValueBuilderPropertyBuilderRawType() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "foo.bar.Baz", "package foo.bar;", "", "import com.google.auto.value.AutoValue;", + "import com.google.common.collect.ImmutableList;", "", "@AutoValue", - "public abstract class Baz {", - " abstract String blam();", + "public abstract class Baz<T, U> {", + " abstract ImmutableList blim();", "", " @AutoValue.Builder", - " public interface Builder {", - " Builder blam(String x);", - " Baz build();", - " Baz create();", + " public interface Builder<T, U> {", + " ImmutableList.Builder blimBuilder();", + " Baz<T, U> build();", " }", "}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith(new AutoValueProcessor(), new AutoValueBuilderProcessor()) .failsToCompile() - .withErrorContaining("Builder must have a single no-argument method returning foo.bar.Baz") - .in(javaFileObject).onLine(12) - .and() - .withErrorContaining("Builder must have a single no-argument method returning foo.bar.Baz") - .in(javaFileObject).onLine(13); + .withErrorContaining( + "Property builder type cannot be raw (missing <...>)") + .in(javaFileObject).onLine(12); } - public void testAutoValueBuilderWrongTypeBuildMethod() { + public void testAutoValueBuilderPropertyBuilderWrongCollectionType() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "foo.bar.Baz", "package foo.bar;", "", "import com.google.auto.value.AutoValue;", + "import com.google.common.collect.ImmutableList;", + "import com.google.common.collect.ImmutableSet;", "", "@AutoValue", - "public abstract class Baz {", - " abstract String blam();", + "public abstract class Baz<T, U> {", + " abstract ImmutableList<T> blim();", "", " @AutoValue.Builder", - " public interface Builder {", - " Builder blam(String x);", - " String build();", + " public interface Builder<T, U> {", + " ImmutableSet.Builder<T> blimBuilder();", + " Baz<T, U> build();", " }", "}"); assertAbout(javaSource()) @@ -1053,37 +1124,42 @@ public class CompilationTest extends TestCase { .processedWith(new AutoValueProcessor(), new AutoValueBuilderProcessor()) .failsToCompile() .withErrorContaining( - "Method without arguments should be a build method returning foo.bar.Baz") - .in(javaFileObject).onLine(12); + "Return type of property-builder method implies a property of type " + + "com.google.common.collect.ImmutableSet<T>, but property blim has type " + + "com.google.common.collect.ImmutableList<T>") + .in(javaFileObject).onLine(13); } - public void testAutoValueBuilderTypeParametersDontMatch1() { + public void testAutoValueBuilderPropertyBuilderWrongElementType() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "foo.bar.Baz", "package foo.bar;", "", "import com.google.auto.value.AutoValue;", + "import com.google.common.collect.ImmutableSet;", "", "@AutoValue", - "public abstract class Baz<T> {", - " abstract String blam();", + "public abstract class Baz<T, U> {", + " abstract ImmutableSet<T> blim();", "", " @AutoValue.Builder", - " public interface Builder {", - " Builder blam(String x);", - " Baz build();", + " public interface Builder<T, U> {", + " ImmutableSet.Builder<U> blimBuilder();", + " Baz<T, U> build();", " }", "}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith(new AutoValueProcessor(), new AutoValueBuilderProcessor()) .failsToCompile() - .withErrorContaining("Type parameters of foo.bar.Baz.Builder must have same names and " - + "bounds as type parameters of foo.bar.Baz") - .in(javaFileObject).onLine(10); + .withErrorContaining( + "Return type of property-builder method implies a property of type " + + "com.google.common.collect.ImmutableSet<U>, but property blim has type " + + "com.google.common.collect.ImmutableSet<T>") + .in(javaFileObject).onLine(12); } - public void testAutoValueBuilderTypeParametersDontMatch2() { + public void testAutoValueBuilderAlienMethod0() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "foo.bar.Baz", "package foo.bar;", @@ -1091,12 +1167,13 @@ public class CompilationTest extends TestCase { "import com.google.auto.value.AutoValue;", "", "@AutoValue", - "public abstract class Baz<T> {", - " abstract T blam();", + "public abstract class Baz {", + " abstract String blam();", "", " @AutoValue.Builder", - " public interface Builder<E> {", - " Builder<E> blam(E x);", + " public interface Builder {", + " Builder blam(String x);", + " Builder whut();", " Baz build();", " }", "}"); @@ -1104,12 +1181,13 @@ public class CompilationTest extends TestCase { .that(javaFileObject) .processedWith(new AutoValueProcessor(), new AutoValueBuilderProcessor()) .failsToCompile() - .withErrorContaining("Type parameters of foo.bar.Baz.Builder must have same names and " - + "bounds as type parameters of foo.bar.Baz") - .in(javaFileObject).onLine(10); + .withErrorContaining( + "Method without arguments should be a build method returning foo.bar.Baz" + + " or a getter method with the same name and type as a getter method of foo.bar.Baz") + .in(javaFileObject).onLine(12); } - public void testAutoValueBuilderTypeParametersDontMatch3() { + public void testAutoValueBuilderAlienMethod1() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "foo.bar.Baz", "package foo.bar;", @@ -1117,12 +1195,12 @@ public class CompilationTest extends TestCase { "import com.google.auto.value.AutoValue;", "", "@AutoValue", - "public abstract class Baz<T extends Number & Comparable<T>> {", - " abstract T blam();", + "public abstract class Baz {", + " abstract String blam();", "", " @AutoValue.Builder", - " public interface Builder<T extends Number> {", - " Builder<T> blam(T x);", + " public interface Builder {", + " void whut(String x);", " Baz build();", " }", "}"); @@ -1130,12 +1208,11 @@ public class CompilationTest extends TestCase { .that(javaFileObject) .processedWith(new AutoValueProcessor(), new AutoValueBuilderProcessor()) .failsToCompile() - .withErrorContaining("Type parameters of foo.bar.Baz.Builder must have same names and " - + "bounds as type parameters of foo.bar.Baz") - .in(javaFileObject).onLine(10); + .withErrorContaining("Method does not correspond to a property of foo.bar.Baz") + .in(javaFileObject).onLine(11); } - public void testAutoValueBuilderToBuilderWrongTypeParameters() { + public void testAutoValueBuilderAlienMethod2() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "foo.bar.Baz", "package foo.bar;", @@ -1143,27 +1220,24 @@ public class CompilationTest extends TestCase { "import com.google.auto.value.AutoValue;", "", "@AutoValue", - "abstract class Baz<K extends Comparable<K>, V> {", - " abstract K key();", - " abstract V value();", - " abstract Builder<V, K> toBuilder1();", + "public abstract class Baz {", + " abstract String blam();", "", " @AutoValue.Builder", - " interface Builder<K extends Comparable<K>, V> {", - " Builder<K, V> key(K key);", - " Builder<K, V> value(V value);", - " Baz<K, V> build();", + " public interface Builder {", + " Builder blam(String x, String y);", + " Baz build();", " }", "}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith(new AutoValueProcessor(), new AutoValueBuilderProcessor()) .failsToCompile() - .withErrorContaining("Builder converter method should return foo.bar.Baz.Builder<K, V>") - .in(javaFileObject).onLine(9); + .withErrorContaining("Builder methods must have 0 or 1 parameters") + .in(javaFileObject).onLine(11); } - public void testAutoValueBuilderToBuilderDuplicate() { + public void testAutoValueBuilderMissingBuildMethod() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "foo.bar.Baz", "package foo.bar;", @@ -1171,55 +1245,53 @@ public class CompilationTest extends TestCase { "import com.google.auto.value.AutoValue;", "", "@AutoValue", - "abstract class Baz<K extends Comparable<K>, V> {", - " abstract K key();", - " abstract V value();", - " abstract Builder<K, V> toBuilder1();", - " abstract Builder<K, V> toBuilder2();", + "public abstract class Baz<T> {", + " abstract T blam();", "", " @AutoValue.Builder", - " interface Builder<K extends Comparable<K>, V> {", - " Builder<K, V> key(K key);", - " Builder<K, V> value(V value);", - " Baz<K, V> build();", + " public interface Builder<T> {", + " Builder<T> blam(T x);", " }", "}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith(new AutoValueProcessor(), new AutoValueBuilderProcessor()) .failsToCompile() - .withErrorContaining("There can be at most one builder converter method") - .in(javaFileObject).onLine(9); + .withErrorContaining( + "Builder must have a single no-argument method returning foo.bar.Baz<T>") + .in(javaFileObject).onLine(10); } - public void testAutoValueValidateNotInAutoValue() { + public void testAutoValueBuilderDuplicateBuildMethods() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "foo.bar.Baz", "package foo.bar;", "", "import com.google.auto.value.AutoValue;", "", + "@AutoValue", "public abstract class Baz {", " abstract String blam();", "", - " @AutoValue.Validate", - " void validate() {}", - "", + " @AutoValue.Builder", " public interface Builder {", " Builder blam(String x);", " Baz build();", + " Baz create();", " }", "}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith(new AutoValueProcessor(), new AutoValueBuilderProcessor()) .failsToCompile() - .withErrorContaining( - "@AutoValue.Validate can only be applied to a method inside an @AutoValue class") - .in(javaFileObject).onLine(9); + .withErrorContaining("Builder must have a single no-argument method returning foo.bar.Baz") + .in(javaFileObject).onLine(12) + .and() + .withErrorContaining("Builder must have a single no-argument method returning foo.bar.Baz") + .in(javaFileObject).onLine(13); } - public void testAutoValueValidateWithoutBuilder() { + public void testAutoValueBuilderWrongTypeBuildMethod() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "foo.bar.Baz", "package foo.bar;", @@ -1230,12 +1302,10 @@ public class CompilationTest extends TestCase { "public abstract class Baz {", " abstract String blam();", "", - " @AutoValue.Validate", - " void validate() {}", - "", + " @AutoValue.Builder", " public interface Builder {", " Builder blam(String x);", - " Baz build();", + " String build();", " }", "}"); assertAbout(javaSource()) @@ -1243,11 +1313,11 @@ public class CompilationTest extends TestCase { .processedWith(new AutoValueProcessor(), new AutoValueBuilderProcessor()) .failsToCompile() .withErrorContaining( - "@AutoValue.Validate is only meaningful if there is an @AutoValue.Builder") - .in(javaFileObject).onLine(10); + "Method without arguments should be a build method returning foo.bar.Baz") + .in(javaFileObject).onLine(12); } - public void testAutoValueBuilderValidateMethodStatic() { + public void testAutoValueBuilderTypeParametersDontMatch1() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "foo.bar.Baz", "package foo.bar;", @@ -1255,12 +1325,9 @@ public class CompilationTest extends TestCase { "import com.google.auto.value.AutoValue;", "", "@AutoValue", - "public abstract class Baz {", + "public abstract class Baz<T> {", " abstract String blam();", "", - " @AutoValue.Validate", - " static void validate() {}", - "", " @AutoValue.Builder", " public interface Builder {", " Builder blam(String x);", @@ -1271,11 +1338,12 @@ public class CompilationTest extends TestCase { .that(javaFileObject) .processedWith(new AutoValueProcessor(), new AutoValueBuilderProcessor()) .failsToCompile() - .withErrorContaining("@AutoValue.Validate cannot apply to a static method") + .withErrorContaining("Type parameters of foo.bar.Baz.Builder must have same names and " + + "bounds as type parameters of foo.bar.Baz") .in(javaFileObject).onLine(10); } - public void testAutoValueBuilderValidateMethodNotVoid() { + public void testAutoValueBuilderTypeParametersDontMatch2() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "foo.bar.Baz", "package foo.bar;", @@ -1283,17 +1351,12 @@ public class CompilationTest extends TestCase { "import com.google.auto.value.AutoValue;", "", "@AutoValue", - "public abstract class Baz {", - " abstract String blam();", - "", - " @AutoValue.Validate", - " Baz validate() {", - " return this;", - " }", + "public abstract class Baz<T> {", + " abstract T blam();", "", " @AutoValue.Builder", - " public interface Builder {", - " Builder blam(String x);", + " public interface Builder<E> {", + " Builder<E> blam(E x);", " Baz build();", " }", "}"); @@ -1301,11 +1364,12 @@ public class CompilationTest extends TestCase { .that(javaFileObject) .processedWith(new AutoValueProcessor(), new AutoValueBuilderProcessor()) .failsToCompile() - .withErrorContaining("@AutoValue.Validate method must be void") + .withErrorContaining("Type parameters of foo.bar.Baz.Builder must have same names and " + + "bounds as type parameters of foo.bar.Baz") .in(javaFileObject).onLine(10); } - public void testAutoValueBuilderValidateMethodWithParameters() { + public void testAutoValueBuilderTypeParametersDontMatch3() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "foo.bar.Baz", "package foo.bar;", @@ -1313,15 +1377,12 @@ public class CompilationTest extends TestCase { "import com.google.auto.value.AutoValue;", "", "@AutoValue", - "public abstract class Baz {", - " abstract String blam();", - "", - " @AutoValue.Validate", - " void validate(boolean why) {}", + "public abstract class Baz<T extends Number & Comparable<T>> {", + " abstract T blam();", "", " @AutoValue.Builder", - " public interface Builder {", - " Builder blam(String x);", + " public interface Builder<T extends Number> {", + " Builder<T> blam(T x);", " Baz build();", " }", "}"); @@ -1329,11 +1390,12 @@ public class CompilationTest extends TestCase { .that(javaFileObject) .processedWith(new AutoValueProcessor(), new AutoValueBuilderProcessor()) .failsToCompile() - .withErrorContaining("@AutoValue.Validate method must not have parameters") + .withErrorContaining("Type parameters of foo.bar.Baz.Builder must have same names and " + + "bounds as type parameters of foo.bar.Baz") .in(javaFileObject).onLine(10); } - public void testAutoValueBuilderValidateMethodDuplicate() { + public void testAutoValueBuilderToBuilderWrongTypeParameters() { JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( "foo.bar.Baz", "package foo.bar;", @@ -1341,27 +1403,53 @@ public class CompilationTest extends TestCase { "import com.google.auto.value.AutoValue;", "", "@AutoValue", - "public abstract class Baz {", - " abstract String blam();", + "abstract class Baz<K extends Comparable<K>, V> {", + " abstract K key();", + " abstract V value();", + " abstract Builder<V, K> toBuilder1();", + "", + " @AutoValue.Builder", + " interface Builder<K extends Comparable<K>, V> {", + " Builder<K, V> key(K key);", + " Builder<K, V> value(V value);", + " Baz<K, V> build();", + " }", + "}"); + assertAbout(javaSource()) + .that(javaFileObject) + .processedWith(new AutoValueProcessor(), new AutoValueBuilderProcessor()) + .failsToCompile() + .withErrorContaining("Builder converter method should return foo.bar.Baz.Builder<K, V>") + .in(javaFileObject).onLine(9); + } + + public void testAutoValueBuilderToBuilderDuplicate() { + JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( + "foo.bar.Baz", + "package foo.bar;", "", - " @AutoValue.Validate", - " void validate() {}", + "import com.google.auto.value.AutoValue;", "", - " @AutoValue.Validate", - " void validateSomeMore() {}", + "@AutoValue", + "abstract class Baz<K extends Comparable<K>, V> {", + " abstract K key();", + " abstract V value();", + " abstract Builder<K, V> toBuilder1();", + " abstract Builder<K, V> toBuilder2();", "", " @AutoValue.Builder", - " public interface Builder {", - " Builder blam(String x);", - " Baz build();", + " interface Builder<K extends Comparable<K>, V> {", + " Builder<K, V> key(K key);", + " Builder<K, V> value(V value);", + " Baz<K, V> build();", " }", "}"); assertAbout(javaSource()) .that(javaFileObject) .processedWith(new AutoValueProcessor(), new AutoValueBuilderProcessor()) .failsToCompile() - .withErrorContaining("There can only be one @AutoValue.Validate method") - .in(javaFileObject).onLine(13); + .withErrorContaining("There can be at most one builder converter method") + .in(javaFileObject).onLine(9); } public void testGetFooIsFoo() throws Exception { diff --git a/value/src/test/java/com/google/auto/value/processor/GuavaCollectionBuildersTest.java b/value/src/test/java/com/google/auto/value/processor/GuavaCollectionBuildersTest.java new file mode 100644 index 00000000..7bf77957 --- /dev/null +++ b/value/src/test/java/com/google/auto/value/processor/GuavaCollectionBuildersTest.java @@ -0,0 +1,99 @@ +package com.google.auto.value.processor; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableSet; +import com.google.common.reflect.ClassPath; +import com.google.common.truth.Expect; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; + +/** + * Validates the assumptions AutoValue makes about Guava immutable collection builders. We expect + * for each public class {@code com.google.common.collect.ImmutableFoo} that: + * <ul> + * <li>it contains a public nested class {@code ImmutableFoo.Builder} with the same type parameters; + * <li>there is a public static method {@code ImmutableFoo.builder()} that returns + * {@code ImmutableFoo.Builder}; + * <li>there is a method {@code ImmutableFoo.Builder.build()} that returns {@code ImmutableFoo}; + * <li>and there is a method in {@code ImmutableFoo.Builder} called either {@code addAll} + * or {@code putAll} with a single parameter to which {@code ImmutableFoo} can be assigned. + * </ul> + * + * @author emcmanus@google.com (Éamonn McManus) + */ +@RunWith(JUnit4.class) +public class GuavaCollectionBuildersTest { + private static final ImmutableSet<String> NON_BUILDABLE_COLLECTIONS = + ImmutableSet.of("ImmutableCollection", "ImmutableMapBuilder"); + + @Rule public final Expect expect = Expect.create(); + + @Test + public void testImmutableBuilders() throws Exception { + ClassPath classPath = ClassPath.from(getClass().getClassLoader()); + ImmutableSet<ClassPath.ClassInfo> classes = classPath.getAllClasses(); + int checked = 0; + for (ClassPath.ClassInfo classInfo : classes) { + if (classInfo.getPackageName().equals("com.google.common.collect") + && classInfo.getSimpleName().startsWith("Immutable") + && !NON_BUILDABLE_COLLECTIONS.contains(classInfo.getSimpleName())) { + Class<?> c = Class.forName(classInfo.getName()); + if (Modifier.isPublic(c.getModifiers())) { + checked++; + checkImmutableClass(c); + } + } + } + expect.that(checked).isGreaterThan(10); + } + + private void checkImmutableClass(Class<?> c) + throws ClassNotFoundException, NoSuchMethodException { + if (!Modifier.isPublic(c.getModifiers())) { + return; + } + + // We have a public static ImmutableFoo.builder() + Method builderMethod = c.getMethod("builder"); + assertThat(Modifier.isStatic(builderMethod.getModifiers())).isTrue(); + + // Its return type is Builder with the same type parameters. + Type builderMethodReturn = builderMethod.getGenericReturnType(); + expect.that(builderMethodReturn).isInstanceOf(ParameterizedType.class); + ParameterizedType builderMethodParameterizedReturn = (ParameterizedType) builderMethodReturn; + Class<?> builderClass = Class.forName(c.getName() + "$Builder"); + expect.that(builderMethod.getReturnType()).isEqualTo(builderClass); + expect.that(Arrays.toString(builderMethodParameterizedReturn.getActualTypeArguments())) + .named(c.getName()) + .isEqualTo(Arrays.toString(builderClass.getTypeParameters())); + + // The Builder has a public build() method that returns ImmutableFoo. + Method buildMethod = builderClass.getMethod("build"); + expect.that(buildMethod.getReturnType()).isEqualTo(c); + + // The Builder has either an addAll or a putAll public method with a parameter that + // ImmutableFoo can be assigned to. + boolean found = false; + for (Method m : builderClass.getMethods()) { + if ((m.getName().equals("addAll") || m.getName().equals("putAll")) + && m.getParameterTypes().length == 1) { + Class<?> parameter = m.getParameterTypes()[0]; + if (parameter.isAssignableFrom(c)) { + found = true; + break; + } + } + } + expect.that(found).named(builderClass.getName() + " has addAll or putAll").isTrue(); + } +} |