diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f385485f2..f9821dfa0a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,20 +18,20 @@ jobs: steps: # Cancel any previous runs for the same branch that are still running. - name: 'Cancel previous runs' - uses: styfle/cancel-workflow-action@b173b6ec0100793626c2d9e6b90435061f4fc3e5 + uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa with: access_token: ${{ github.token }} - name: 'Check out repository' - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 - name: 'Cache local Maven repository' - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 with: path: ~/.m2/repository key: maven-${{ hashFiles('**/pom.xml') }} restore-keys: | maven- - name: 'Set up JDK ${{ matrix.java }}' - uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 with: java-version: ${{ matrix.java }} distribution: 'zulu' @@ -49,16 +49,16 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Check out repository' - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 - name: 'Cache local Maven repository' - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 with: path: ~/.m2/repository key: maven-${{ hashFiles('**/pom.xml') }} restore-keys: | maven- - name: 'Set up JDK 11' - uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 with: java-version: 11 distribution: 'zulu' @@ -78,16 +78,16 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Check out repository' - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 - name: 'Cache local Maven repository' - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 with: path: ~/.m2/repository key: maven-${{ hashFiles('**/pom.xml') }} restore-keys: | maven- - name: 'Set up JDK 11' - uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 with: java-version: 11 distribution: 'zulu' diff --git a/common/pom.xml b/common/pom.xml index f4fe00e480..54cd4e2498 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -36,8 +36,8 @@ UTF-8 1.8 - 32.1.2-jre - 1.1.3 + 33.2.0-jre + 1.4.2 @@ -89,7 +89,7 @@ com.google.testing.compile compile-testing - 0.19 + 0.21.0 test @@ -110,7 +110,7 @@ maven-compiler-plugin - 3.11.0 + 3.13.0 ${java.version} ${java.version} @@ -123,14 +123,14 @@ org.codehaus.plexus plexus-java - 1.1.2 + 1.2.0 org.apache.maven.plugins maven-jar-plugin - 3.3.0 + 3.4.1 @@ -146,7 +146,7 @@ org.eclipse.jdt ecj - 3.34.0 + 3.37.0 test diff --git a/common/src/main/java/com/google/auto/common/BasicAnnotationProcessor.java b/common/src/main/java/com/google/auto/common/BasicAnnotationProcessor.java index 7b69d33271..9dd1a39ce1 100644 --- a/common/src/main/java/com/google/auto/common/BasicAnnotationProcessor.java +++ b/common/src/main/java/com/google/auto/common/BasicAnnotationProcessor.java @@ -16,21 +16,24 @@ package com.google.auto.common; import static com.google.auto.common.MoreElements.asExecutable; -import static com.google.auto.common.MoreElements.asPackage; +import static com.google.auto.common.MoreElements.asType; import static com.google.auto.common.MoreElements.isAnnotationPresent; +import static com.google.auto.common.MoreElements.isType; +import static com.google.auto.common.MoreStreams.toImmutableList; import static com.google.auto.common.MoreStreams.toImmutableMap; import static com.google.auto.common.MoreStreams.toImmutableSet; import static com.google.auto.common.SuperficialValidation.validateElement; -import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; -import static com.google.common.collect.Iterables.transform; +import static com.google.common.collect.MoreCollectors.onlyElement; import static com.google.common.collect.Multimaps.filterKeys; import static java.util.Objects.requireNonNull; +import static javax.lang.model.element.ElementKind.CONSTRUCTOR; +import static javax.lang.model.element.ElementKind.METHOD; import static javax.lang.model.element.ElementKind.PACKAGE; import static javax.tools.Diagnostic.Kind.ERROR; +import static javax.tools.Diagnostic.Kind.WARNING; import com.google.common.base.Ascii; -import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -42,25 +45,26 @@ import java.lang.annotation.Annotation; import java.util.LinkedHashSet; import java.util.Objects; -import java.util.Optional; import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Messager; import javax.annotation.processing.ProcessingEnvironment; -import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; 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.PackageElement; +import javax.lang.model.element.Parameterizable; import javax.lang.model.element.TypeElement; -import javax.lang.model.type.ErrorType; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.element.VariableElement; import javax.lang.model.util.Elements; -import javax.lang.model.util.SimpleElementVisitor8; import org.checkerframework.checker.nullness.qual.Nullable; /** - * An abstract {@link Processor} implementation that defers processing of {@link Element}s to later - * rounds if they cannot be processed. + * An abstract {@link javax.annotation.processing.Processor Processor} implementation that defers + * processing of {@link Element}s to later rounds if they cannot be processed. * *

Subclasses put their processing logic in {@link Step} implementations. The steps are passed to * the processor by returning them in the {@link #steps()} method, and can access the {@link @@ -69,11 +73,12 @@ *

Any logic that needs to happen once per round can be specified by overriding {@link * #postRound(RoundEnvironment)}. * - *

Ill-formed elements are deferred

+ *

Ill-formed elements are deferred

* * Any annotated element whose nearest enclosing type is not well-formed is deferred, and not passed * to any {@code Step}. This helps processors to avoid many common pitfalls, such as {@link - * ErrorType} instances, {@link ClassCastException}s and badly coerced types. + * javax.lang.model.type.ErrorType ErrorType} instances, {@link ClassCastException}s and badly + * coerced types. * *

A non-package element is considered well-formed if its type, type parameters, parameters, * default values, supertypes, annotations, and enclosed elements are. Package elements are treated @@ -85,7 +90,7 @@ * because the element will never be fully complete. All such compilations will fail with an error * message on the offending type that describes the issue. * - *

Each {@code Step} can defer elements

+ *

Each {@code Step} can defer elements

* *

Each {@code Step} can defer elements by including them in the set returned by {@link * Step#process(ImmutableSetMultimap)}; elements deferred by a step will be passed back to that step @@ -106,18 +111,32 @@ */ public abstract class BasicAnnotationProcessor extends AbstractProcessor { - private final Set deferredElementNames = new LinkedHashSet<>(); - private final SetMultimap elementsDeferredBySteps = + /* For every element that is not module/package, to be well-formed its + * enclosing-type in its entirety should be well-formed. Since modules + * don't get annotated (and are not supported here) they can be ignored. + */ + + /** + * Packages and types that have been deferred because either they themselves reference + * as-yet-undefined types, or at least one of their contained elements does. + */ + private final Set deferredEnclosingElements = new LinkedHashSet<>(); + + /** + * Elements that were explicitly deferred in some {@link Step} by being returned from {@link + * Step#process}. + */ + private final SetMultimap elementsDeferredBySteps = LinkedHashMultimap.create(); - private Elements elements; + private Elements elementUtils; private Messager messager; private ImmutableList steps; @Override public final synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); - this.elements = processingEnv.getElementUtils(); + this.elementUtils = processingEnv.getElementUtils(); this.messager = processingEnv.getMessager(); this.steps = ImmutableList.copyOf(steps()); } @@ -168,7 +187,7 @@ private ImmutableSet getSupportedAnnotationTypeElements() { private ImmutableSet getSupportedAnnotationTypeElements(Step step) { return step.annotations().stream() - .map(elements::getTypeElement) + .map(elementUtils::getTypeElement) .filter(Objects::nonNull) .collect(toImmutableSet()); } @@ -185,26 +204,26 @@ public final ImmutableSet getSupportedAnnotationTypes() { @Override public final boolean process(Set annotations, RoundEnvironment roundEnv) { - checkState(elements != null); + checkState(elementUtils != null); checkState(messager != null); checkState(steps != null); // If this is the last round, report all of the missing elements if there // were no errors raised in the round; otherwise reporting the missing - // elements just adds noise the output. + // elements just adds noise to the output. if (roundEnv.processingOver()) { postRound(roundEnv); if (!roundEnv.errorRaised()) { reportMissingElements( - ImmutableSet.builder() - .addAll(deferredElementNames) + ImmutableSet.builder() + .addAll(deferredEnclosingElements) .addAll(elementsDeferredBySteps.values()) .build()); } return false; } - process(validElements(roundEnv)); + process(getWellFormedElementsByAnnotationType(roundEnv)); postRound(roundEnv); @@ -212,13 +231,13 @@ public final boolean process(Set annotations, RoundEnviro } /** Processes the valid elements, including those previously deferred by each step. */ - private void process(ImmutableSetMultimap validElements) { + private void process(ImmutableSetMultimap wellFormedElements) { for (Step step : steps) { ImmutableSet annotationTypes = getSupportedAnnotationTypeElements(step); ImmutableSetMultimap stepElements = new ImmutableSetMultimap.Builder() .putAll(indexByAnnotation(elementsDeferredBySteps.get(step), annotationTypes)) - .putAll(filterKeys(validElements, Predicates.in(annotationTypes))) + .putAll(filterKeys(wellFormedElements, annotationTypes::contains)) .build(); if (stepElements.isEmpty()) { elementsDeferredBySteps.removeAll(step); @@ -226,22 +245,24 @@ private void process(ImmutableSetMultimap validElements) { Set rejectedElements = step.process(toClassNameKeyedMultimap(stepElements)); elementsDeferredBySteps.replaceValues( - step, transform(rejectedElements, ElementName::forAnnotatedElement)); + step, + rejectedElements.stream() + .map(element -> ElementFactory.forAnnotatedElement(element, messager)) + .collect(toImmutableList())); } } } - private void reportMissingElements(Set missingElementNames) { - for (ElementName missingElementName : missingElementNames) { - Optional missingElement = missingElementName.getElement(elements); - if (missingElement.isPresent()) { + private void reportMissingElements(Set missingElementFactories) { + for (ElementFactory missingElementFactory : missingElementFactories) { + Element missingElement = missingElementFactory.getElement(elementUtils); + if (missingElement != null) { messager.printMessage( ERROR, - processingErrorMessage( - "this " + Ascii.toLowerCase(missingElement.get().getKind().name())), - missingElement.get()); + processingErrorMessage("this " + Ascii.toLowerCase(missingElement.getKind().name())), + missingElement); } else { - messager.printMessage(ERROR, processingErrorMessage(missingElementName.name())); + messager.printMessage(ERROR, processingErrorMessage(missingElementFactory.toString)); } } } @@ -255,69 +276,88 @@ private String processingErrorMessage(String target) { } /** - * Returns the valid annotated elements contained in all of the deferred elements. If none are - * found for a deferred element, defers it again. + * Returns the superficially validated annotated elements of this round, including the validated + * previously ill-formed elements. Also update {@link #deferredEnclosingElements}. + * + *

Note that the elements deferred by processing steps are guaranteed to be well-formed; + * therefore, they are ignored (not returned) here, and they will be considered directly in the + * {@link #process(ImmutableSetMultimap)} method. */ - private ImmutableSetMultimap validElements(RoundEnvironment roundEnv) { - ImmutableSet prevDeferredElementNames = ImmutableSet.copyOf(deferredElementNames); - deferredElementNames.clear(); + private ImmutableSetMultimap getWellFormedElementsByAnnotationType( + RoundEnvironment roundEnv) { + ImmutableSet deferredEnclosingElementsCopy = + ImmutableSet.copyOf(deferredEnclosingElements); + deferredEnclosingElements.clear(); - ImmutableSetMultimap.Builder deferredElementsByAnnotationBuilder = + ImmutableSetMultimap.Builder prevIllFormedElementsBuilder = ImmutableSetMultimap.builder(); - for (ElementName deferredElementName : prevDeferredElementNames) { - Optional deferredElement = deferredElementName.getElement(elements); - if (deferredElement.isPresent()) { + for (ElementFactory deferredElementFactory : deferredEnclosingElementsCopy) { + Element deferredElement = deferredElementFactory.getElement(elementUtils); + if (deferredElement != null) { findAnnotatedElements( - deferredElement.get(), - getSupportedAnnotationTypeElements(), - deferredElementsByAnnotationBuilder); + deferredElement, getSupportedAnnotationTypeElements(), prevIllFormedElementsBuilder); } else { - deferredElementNames.add(deferredElementName); + deferredEnclosingElements.add(deferredElementFactory); } } - ImmutableSetMultimap deferredElementsByAnnotation = - deferredElementsByAnnotationBuilder.build(); + ImmutableSetMultimap prevIllFormedElements = + prevIllFormedElementsBuilder.build(); - ImmutableSetMultimap.Builder validElements = + ImmutableSetMultimap.Builder wellFormedElementsBuilder = ImmutableSetMultimap.builder(); - Set validElementNames = new LinkedHashSet<>(); + // For optimization purposes, the ElementFactory instances for packages and types that have + // already been verified to be well-formed are stored. + Set wellFormedPackageOrTypeElements = new LinkedHashSet<>(); - // Look at the elements we've found and the new elements from this round and validate them. + /* Look at + * 1. the previously ill-formed elements which have a present enclosing type (in case of + * Package element, the package itself), and + * 2. the new elements from this round + * and validate them. + */ for (TypeElement annotationType : getSupportedAnnotationTypeElements()) { Set roundElements = roundEnv.getElementsAnnotatedWith(annotationType); - ImmutableSet prevRoundElements = deferredElementsByAnnotation.get(annotationType); - for (Element element : Sets.union(roundElements, prevRoundElements)) { - ElementName elementName = ElementName.forAnnotatedElement(element); - boolean isValidElement = - validElementNames.contains(elementName) - || (!deferredElementNames.contains(elementName) - && validateElement( - element.getKind().equals(PACKAGE) ? element : getEnclosingType(element))); - if (isValidElement) { - validElements.put(annotationType, element); - validElementNames.add(elementName); + + for (Element element : Sets.union(roundElements, prevIllFormedElements.get(annotationType))) { + // For every element that is not module/package, to be well-formed its + // enclosing-type in its entirety should be well-formed. Since modules + // don't get annotated (and not supported here) they can be ignored. + Element enclosing = (element.getKind() == PACKAGE) ? element : getEnclosingType(element); + ElementFactory enclosingFactory = ElementFactory.forAnnotatedElement(enclosing, messager); + + boolean isWellFormedElement = + wellFormedPackageOrTypeElements.contains(enclosingFactory) + || (!deferredEnclosingElements.contains(enclosingFactory) + && validateElement(enclosing)); + if (isWellFormedElement) { + wellFormedElementsBuilder.put(annotationType, element); + wellFormedPackageOrTypeElements.add(enclosingFactory); } else { - deferredElementNames.add(elementName); + deferredEnclosingElements.add(enclosingFactory); } } } - return validElements.build(); + return wellFormedElementsBuilder.build(); } private ImmutableSetMultimap indexByAnnotation( - Set annotatedElements, ImmutableSet annotationTypes) { - ImmutableSetMultimap.Builder deferredElements = + Set annotatedElementFactories, ImmutableSet annotationTypes) { + ImmutableSetMultimap.Builder deferredElementsByAnnotationTypeBuilder = ImmutableSetMultimap.builder(); - for (ElementName elementName : annotatedElements) { - Optional element = elementName.getElement(elements); - if (element.isPresent()) { - findAnnotatedElements(element.get(), annotationTypes, deferredElements); + for (ElementFactory elementFactory : annotatedElementFactories) { + Element element = elementFactory.getElement(elementUtils); + if (element != null) { + for (TypeElement annotationType : annotationTypes) { + if (isAnnotationPresent(element, annotationType)) { + deferredElementsByAnnotationTypeBuilder.put(annotationType, element); + } + } } } - return deferredElements.build(); + return deferredElementsByAnnotationTypeBuilder.build(); } /** @@ -348,16 +388,24 @@ private static void findAnnotatedElements( } } - // element.getEnclosedElements() does NOT return parameter elements - switch (element.getKind()) { - case METHOD: - case CONSTRUCTOR: - for (Element parameterElement : asExecutable(element).getParameters()) { - findAnnotatedElements(parameterElement, annotationTypes, annotatedElements); - } - break; - default: // do nothing + // element.getEnclosedElements() does NOT return parameter or type parameter elements + + Parameterizable parameterizable = null; + if (isType(element)) { + parameterizable = asType(element); + } else if (isExecutable(element)) { + ExecutableElement executableElement = asExecutable(element); + parameterizable = executableElement; + for (VariableElement parameterElement : executableElement.getParameters()) { + findAnnotatedElements(parameterElement, annotationTypes, annotatedElements); + } + } + if (parameterizable != null) { + for (TypeParameterElement parameterElement : parameterizable.getTypeParameters()) { + findAnnotatedElements(parameterElement, annotationTypes, annotatedElements); + } } + for (TypeElement annotationType : annotationTypes) { if (isAnnotationPresent(element, annotationType)) { annotatedElements.put(annotationType, element); @@ -367,29 +415,19 @@ private static void findAnnotatedElements( /** * Returns the nearest enclosing {@link TypeElement} to the current element, throwing an {@link - * IllegalArgumentException} if the provided {@link Element} is a {@link PackageElement} or is - * otherwise not enclosed by a type. + * IllegalArgumentException} if the provided {@link Element} is not enclosed by a type. */ // TODO(user) move to MoreElements and make public. private static TypeElement getEnclosingType(Element element) { - return element.accept( - new SimpleElementVisitor8() { - @Override - protected TypeElement defaultAction(Element e, Void p) { - return e.getEnclosingElement().accept(this, p); - } - - @Override - public TypeElement visitType(TypeElement e, Void p) { - return e; - } + Element enclosingTypeElement = element; + while (enclosingTypeElement != null && !isType(enclosingTypeElement)) { + enclosingTypeElement = enclosingTypeElement.getEnclosingElement(); + } - @Override - public TypeElement visitPackage(PackageElement e, Void p) { - throw new IllegalArgumentException(); - } - }, - null); + if (enclosingTypeElement == null) { + throw new IllegalArgumentException(element + " is not enclosed in any TypeElement."); + } + return asType(enclosingTypeElement); } private static ImmutableSetMultimap toClassNameKeyedMultimap( @@ -403,6 +441,10 @@ private static ImmutableSetMultimap toClassNameKeyedMultimap( return builder.build(); } + private static boolean isExecutable(Element element) { + return element.getKind() == METHOD || element.getKind() == CONSTRUCTOR; + } + /** * Wraps the passed {@link ProcessingStep} in a {@link Step}. This is a convenience method to * allow incremental migration to a String-based API. This method can be used to return a not yet @@ -505,74 +547,328 @@ private ImmutableSetMultimap, Element> toClassKeyedM } } + /* Element Factories */ + /** - * A package or type name. - * - *

It's unfortunate that we have to track types and packages separately, but since there are - * two different methods to look them up in {@link Elements}, we end up with a lot of parallel - * logic. :( + * A factory for an annotated element. * - *

Packages declared (and annotated) in {@code package-info.java} are tracked as deferred - * packages, type elements are tracked directly, and all other elements are tracked via their - * nearest enclosing type. + *

Instead of saving elements, an {@code ElementFactory} is saved since there is no guarantee + * that any particular element will always be represented by the same object. (Reference: {@link + * Element}) For example, Eclipse compiler uses different {@code Element} instances per round. The + * factory allows us to reconstruct an equivalent element in a later round. */ - private static final class ElementName { - private enum Kind { - PACKAGE_NAME, - TYPE_NAME, + private abstract static class ElementFactory { + final String toString; + + private ElementFactory(Element element) { + this.toString = element.toString(); + } + + /** An {@link ElementFactory} for an annotated element. */ + static ElementFactory forAnnotatedElement(Element element, Messager messager) { + /* The name of the ElementKind constants is used instead to accommodate for RECORD + * and RECORD_COMPONENT kinds, which are introduced in Java 16. + */ + switch (element.getKind().name()) { + case "PACKAGE": + return new PackageElementFactory(element); + case "CLASS": + case "ENUM": + case "INTERFACE": + case "ANNOTATION_TYPE": + case "RECORD": + return new TypeElementFactory(element); + case "TYPE_PARAMETER": + return new TypeParameterElementFactory(element, messager); + case "FIELD": + case "ENUM_CONSTANT": + case "RECORD_COMPONENT": + return new FieldOrRecordComponentElementFactory(element); + case "CONSTRUCTOR": + case "METHOD": + return new ExecutableElementFactory(element); + case "PARAMETER": + return new ParameterElementFactory(element); + default: + messager.printMessage( + WARNING, + String.format( + "%s does not support element type %s.", + ElementFactory.class.getCanonicalName(), element.getKind())); + return new UnsupportedElementFactory(element); + } } - private final Kind kind; - private final String name; + @Override + public boolean equals(@Nullable Object object) { + if (this == object) { + return true; + } else if (!(object instanceof ElementFactory)) { + return false; + } - private ElementName(Kind kind, Name name) { - this.kind = checkNotNull(kind); - this.name = name.toString(); + ElementFactory that = (ElementFactory) object; + return this.toString.equals(that.toString); + } + + @Override + public int hashCode() { + return toString.hashCode(); } /** - * An {@link ElementName} for an annotated element. If {@code element} is a package, uses the - * fully qualified name of the package. If it's a type, uses its fully qualified name. - * Otherwise, uses the fully-qualified name of the nearest enclosing type. - * - *

A package can be annotated if it has a {@code package-info.java} with annotations on the - * package declaration. + * Returns the {@link Element} corresponding to the name information saved in this factory, or + * null if none exists. */ - static ElementName forAnnotatedElement(Element element) { - return element.getKind() == PACKAGE - ? new ElementName(Kind.PACKAGE_NAME, asPackage(element).getQualifiedName()) - : new ElementName(Kind.TYPE_NAME, getEnclosingType(element).getQualifiedName()); + abstract @Nullable Element getElement(Elements elementUtils); + } + + /** + * Saves the Element reference and returns it when inquired, with the hope that the same object + * still represents that element, or the required information is present. + */ + private static final class UnsupportedElementFactory extends ElementFactory { + private final Element element; + + private UnsupportedElementFactory(Element element) { + super(element); + this.element = element; + } + + @Override + Element getElement(Elements elementUtils) { + return element; + } + } + + /* It's unfortunate that we have to track types and packages separately, but since there are + * two different methods to look them up in {@link Elements}, we end up with a lot of parallel + * logic. :( + */ + private static final class PackageElementFactory extends ElementFactory { + private PackageElementFactory(Element element) { + super(element); + } + + @Override + @Nullable PackageElement getElement(Elements elementUtils) { + return elementUtils.getPackageElement(toString); + } + } + + private static final class TypeElementFactory extends ElementFactory { + private TypeElementFactory(Element element) { + super(element); + } + + @Override + @Nullable TypeElement getElement(Elements elementUtils) { + return elementUtils.getTypeElement(toString); + } + } + + private static final class TypeParameterElementFactory extends ElementFactory { + private final ElementFactory enclosingElementFactory; + + private TypeParameterElementFactory(Element element, Messager messager) { + super(element); + this.enclosingElementFactory = + ElementFactory.forAnnotatedElement(element.getEnclosingElement(), messager); + } + + @Override + @Nullable TypeParameterElement getElement(Elements elementUtils) { + Parameterizable enclosingElement = + (Parameterizable) enclosingElementFactory.getElement(elementUtils); + if (enclosingElement == null) { + return null; + } + return enclosingElement.getTypeParameters().stream() + .filter(typeParamElement -> toString.equals(typeParamElement.toString())) + .collect(onlyElement()); + } + + @Override + public boolean equals(@Nullable Object object) { + if (this == object) { + return true; + } else if (!(object instanceof TypeParameterElementFactory)) { + return false; + } + + TypeParameterElementFactory that = (TypeParameterElementFactory) object; + return this.toString.equals(that.toString) + && this.enclosingElementFactory.equals(that.enclosingElementFactory); + } + + @Override + public int hashCode() { + return Objects.hash(toString, enclosingElementFactory); } + } + + /** Represents FIELD, ENUM_CONSTANT, and RECORD_COMPONENT */ + private static class FieldOrRecordComponentElementFactory extends ElementFactory { + private final TypeElementFactory enclosingTypeElementFactory; + private final ElementKind elementKind; - /** The fully-qualified name of the element. */ - String name() { - return name; + private FieldOrRecordComponentElementFactory(Element element) { + super(element); // toString is its simple name. + this.enclosingTypeElementFactory = new TypeElementFactory(getEnclosingType(element)); + this.elementKind = element.getKind(); } + @Override + @Nullable Element getElement(Elements elementUtils) { + TypeElement enclosingTypeElement = enclosingTypeElementFactory.getElement(elementUtils); + if (enclosingTypeElement == null) { + return null; + } + return enclosingTypeElement.getEnclosedElements().stream() + .filter( + element -> + elementKind.equals(element.getKind()) && toString.equals(element.toString())) + .collect(onlyElement()); + } + + @Override + public boolean equals(@Nullable Object object) { + if (!super.equals(object) || !(object instanceof FieldOrRecordComponentElementFactory)) { + return false; + } + // To distinguish between a field and record_component + FieldOrRecordComponentElementFactory that = (FieldOrRecordComponentElementFactory) object; + return this.elementKind == that.elementKind; + } + + @Override + public int hashCode() { + return Objects.hash(toString, elementKind); + } + } + + /** + * Represents METHOD and CONSTRUCTOR. + * + *

The {@code equals()} and {@code hashCode()} have been overridden since the {@code toString} + * alone is not sufficient to make a distinction in all overloaded cases. For example, {@code > void m(C c) {}}, and {@code > void m(C c) {}} + * both have the same toString {@code m(C)} but are valid cases for overloading methods. + * Moreover, the needed enclosing type-element information is not included in the toString. + * + *

The executable element is retrieved by saving its enclosing type element, simple name, and + * ordinal position in the source code relative to other related overloaded methods, meaning those + * with the same simple name. This is possible because according to Java language specification + * for {@link TypeElement#getEnclosedElements()}: "As a particular instance of the general + * accuracy requirements and the ordering behavior required of this interface, the list of + * enclosed elements will be returned to the natural order for the originating source of + * information about the type. For example, if the information about the type is originating from + * a source file, the elements will be returned in source code order. (However, in that case the + * ordering of implicitly declared elements, such as default constructors, is not specified.)" + * + *

Simple name is saved since comparing the toString is not reliable when at least one + * parameter references ERROR, possibly because it is not generated yet. For example, method + * {@code void m(SomeGeneratedClass sgc)}, before the generation of {@code SomeGeneratedClass} has + * the toString {@code m(SomeGeneratedClass)}; however, after the generation it will have toString + * equal to {@code m(test.SomeGeneratedClass)} assuming that the package name is "test". + */ + private static final class ExecutableElementFactory extends ElementFactory { + private final TypeElementFactory enclosingTypeElementFactory; + private final Name simpleName; + /** - * The {@link Element} whose fully-qualified name is {@link #name()}. Empty if the relevant - * method on {@link Elements} returns {@code null}. + * The index of the element among all elements of the same kind within the enclosing type. If + * this is method {@code foo(...)} and the index is 0, that means that the method is the first + * method called {@code foo} in the enclosing type. */ - Optional getElement(Elements elements) { - return Optional.ofNullable( - kind == Kind.PACKAGE_NAME - ? elements.getPackageElement(name) - : elements.getTypeElement(name)); + private final int sameNameIndex; + + private ExecutableElementFactory(Element element) { + super(element); + TypeElement enclosingTypeElement = getEnclosingType(element); + this.enclosingTypeElementFactory = new TypeElementFactory(enclosingTypeElement); + this.simpleName = element.getSimpleName(); + + ImmutableList methods = sameNameMethods(enclosingTypeElement, simpleName); + this.sameNameIndex = methods.indexOf(element); + checkState(this.sameNameIndex >= 0, "Did not find %s in %s", element, methods); + } + + @Override + @Nullable ExecutableElement getElement(Elements elementUtils) { + TypeElement enclosingTypeElement = enclosingTypeElementFactory.getElement(elementUtils); + if (enclosingTypeElement == null) { + return null; + } + ImmutableList methods = sameNameMethods(enclosingTypeElement, simpleName); + return asExecutable(methods.get(sameNameIndex)); + } + + private static ImmutableList sameNameMethods( + TypeElement enclosingTypeElement, Name simpleName) { + return enclosingTypeElement.getEnclosedElements().stream() + .filter(element -> element.getSimpleName().equals(simpleName) && isExecutable(element)) + .collect(toImmutableList()); + } + + @Override + public boolean equals(@Nullable Object object) { + if (this == object) { + return true; + } else if (!(object instanceof ExecutableElementFactory)) { + return false; + } + + ExecutableElementFactory that = (ExecutableElementFactory) object; + return this.simpleName.equals(that.simpleName) + && this.sameNameIndex == that.sameNameIndex + && this.enclosingTypeElementFactory.equals(that.enclosingTypeElementFactory); + } + + @Override + public int hashCode() { + return Objects.hash(simpleName, sameNameIndex, enclosingTypeElementFactory); + } + } + + private static final class ParameterElementFactory extends ElementFactory { + private final ExecutableElementFactory enclosingExecutableElementFactory; + + private ParameterElementFactory(Element element) { + super(element); + this.enclosingExecutableElementFactory = + new ExecutableElementFactory(element.getEnclosingElement()); + } + + @Override + @Nullable VariableElement getElement(Elements elementUtils) { + ExecutableElement enclosingExecutableElement = + enclosingExecutableElementFactory.getElement(elementUtils); + if (enclosingExecutableElement == null) { + return null; + } else { + return enclosingExecutableElement.getParameters().stream() + .filter(paramElement -> toString.equals(paramElement.toString())) + .collect(onlyElement()); + } } @Override public boolean equals(@Nullable Object object) { - if (!(object instanceof ElementName)) { + if (this == object) { + return true; + } else if (!(object instanceof ParameterElementFactory)) { return false; } - ElementName that = (ElementName) object; - return this.kind == that.kind && this.name.equals(that.name); + ParameterElementFactory that = (ParameterElementFactory) object; + return this.toString.equals(that.toString) + && this.enclosingExecutableElementFactory.equals(that.enclosingExecutableElementFactory); } @Override public int hashCode() { - return Objects.hash(kind, name); + return Objects.hash(toString, enclosingExecutableElementFactory); } } } diff --git a/common/src/test/java/com/google/auto/common/BasicAnnotationProcessorTest.java b/common/src/test/java/com/google/auto/common/BasicAnnotationProcessorTest.java index 03944e5df9..8a17b680a7 100644 --- a/common/src/test/java/com/google/auto/common/BasicAnnotationProcessorTest.java +++ b/common/src/test/java/com/google/auto/common/BasicAnnotationProcessorTest.java @@ -15,15 +15,13 @@ */ package com.google.auto.common; +import static com.google.common.base.StandardSystemProperty.JAVA_SPECIFICATION_VERSION; import static com.google.common.collect.Multimaps.transformValues; -import static com.google.common.truth.Truth.assertAbout; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; -import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource; -import static com.google.testing.compile.JavaSourcesSubjectFactory.javaSources; import static javax.tools.Diagnostic.Kind.ERROR; -import static javax.tools.StandardLocation.SOURCE_OUTPUT; import com.google.auto.common.BasicAnnotationProcessor.ProcessingStep; import com.google.auto.common.BasicAnnotationProcessor.Step; @@ -32,17 +30,24 @@ import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.SetMultimap; import com.google.common.truth.Correspondence; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.testing.compile.Compilation; import com.google.testing.compile.CompilationRule; import com.google.testing.compile.JavaFileObjects; import java.io.IOException; import java.io.PrintWriter; import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import javax.annotation.processing.Filer; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; +import javax.lang.model.element.Name; import javax.lang.model.element.TypeElement; import javax.lang.model.util.Elements; import javax.tools.JavaFileObject; @@ -68,6 +73,10 @@ public final SourceVersion getSupportedSourceVersion() { @Retention(RetentionPolicy.SOURCE) public @interface RequiresGeneratedCode {} + @Retention(RetentionPolicy.SOURCE) + @Target(ElementType.TYPE_PARAMETER) + public @interface TypeParameterRequiresGeneratedCode {} + /** * Rejects elements unless the class generated by {@link GeneratesCode}'s processor is present. */ @@ -97,19 +106,119 @@ public ImmutableSet process( @Override public ImmutableSet annotations() { - return ImmutableSet.of(ENCLOSING_CLASS_NAME + ".RequiresGeneratedCode"); + return ImmutableSet.of( + ENCLOSING_CLASS_NAME + ".RequiresGeneratedCode", + ENCLOSING_CLASS_NAME + ".TypeParameterRequiresGeneratedCode"); } - }, + }); + } + + ImmutableList> processArguments() { + return processArguments.build(); + } + } + + @Retention(RetentionPolicy.SOURCE) + @Target(ElementType.METHOD) + public @interface OneMethodAtATime {} + + private static class OneMethodAtATimeProcessor extends BaseAnnotationProcessor { + + int rejectedRounds; + final ImmutableList.Builder> processArguments = + ImmutableList.builder(); + + @Override + protected Iterable steps() { + return ImmutableSet.of( new Step() { @Override public ImmutableSet process( ImmutableSetMultimap elementsByAnnotation) { - return ImmutableSet.of(); + processArguments.add(ImmutableSetMultimap.copyOf(elementsByAnnotation)); + int numberOfAnnotatedElements = elementsByAnnotation.size(); + if (numberOfAnnotatedElements == 0) { + return ImmutableSet.of(); + } + + generateClass( + processingEnv.getFiler(), + "GeneratedByOneMethodAtATimeProcessor_" + + elementsByAnnotation.values().iterator().next().getSimpleName()); + if (numberOfAnnotatedElements > 1) { + rejectedRounds++; + } + return ImmutableSet.copyOf( + elementsByAnnotation.values().asList().subList(1, numberOfAnnotatedElements)); } @Override public ImmutableSet annotations() { - return ImmutableSet.of(ENCLOSING_CLASS_NAME + ".AnAnnotation"); + return ImmutableSet.of(ENCLOSING_CLASS_NAME + ".OneMethodAtATime"); + } + }); + } + + ImmutableList> processArguments() { + return processArguments.build(); + } + } + + /** + * This processor processes the OneMethodAtATime annotated methods, one at a time in the following + * fashion. If the number of annotated methods is more than two, the second annotated method is + * processed and the rest are deferred. Otherwise, the first annotated method is processed. + */ + private static class OneOverloadedMethodAtATimeProcessor extends BaseAnnotationProcessor { + + int rejectedRounds; + final ImmutableList.Builder> processArguments = + ImmutableList.builder(); + + @Override + protected Iterable steps() { + return ImmutableSet.of( + new Step() { + @Override + public ImmutableSet process( + ImmutableSetMultimap elementsByAnnotation) { + processArguments.add(ImmutableSetMultimap.copyOf(elementsByAnnotation)); + + List annotatedElements = new ArrayList<>(elementsByAnnotation.values()); + int numberOfAnnotatedElements = annotatedElements.size(); + if (numberOfAnnotatedElements == 0) { + return ImmutableSet.of(); + } + if (numberOfAnnotatedElements > 1) { + rejectedRounds++; + } + + Name nameOfToBeProcessedElement; + ImmutableSet rejectedElements; + if (numberOfAnnotatedElements > 2) { + // Skip the first Element + nameOfToBeProcessedElement = annotatedElements.get(1).getSimpleName(); + annotatedElements.remove(1); + rejectedElements = ImmutableSet.copyOf(annotatedElements); + } else { + nameOfToBeProcessedElement = annotatedElements.get(0).getSimpleName(); + annotatedElements.remove(0); + rejectedElements = ImmutableSet.copyOf(annotatedElements); + } + + generateClass( + processingEnv.getFiler(), + String.format( + "GeneratedByOneMethodAtATimeProcessor_%d_%s", + numberOfAnnotatedElements > 1 ? rejectedRounds : rejectedRounds + 1, + Objects.requireNonNull(nameOfToBeProcessedElement))); + + return Objects.requireNonNull(rejectedElements); + } + + @Override + public ImmutableSet annotations() { + return ImmutableSet.of(ENCLOSING_CLASS_NAME + ".OneMethodAtATime"); } }); } @@ -256,9 +365,33 @@ SetMultimap, Element> getElementsByAnnotation() { @Rule public CompilationRule compilation = new CompilationRule(); + private void requiresGeneratedCodeDeferralTest( + JavaFileObject dependentTestFileObject, JavaFileObject generatesCodeFileObject) { + RequiresGeneratedCodeProcessor requiresGeneratedCodeProcessor = + new RequiresGeneratedCodeProcessor(); + Compilation compilation = + javac() + .withProcessors(requiresGeneratedCodeProcessor, new GeneratesCodeProcessor()) + .compile(dependentTestFileObject, generatesCodeFileObject); + assertThat(compilation).succeeded(); + assertThat(compilation).generatedSourceFile("test.GeneratedByRequiresGeneratedCodeProcessor"); + assertThat(requiresGeneratedCodeProcessor.rejectedRounds).isEqualTo(0); + } + + private void requiresGeneratedCodeDeferralTest(JavaFileObject dependentTestFileObject) { + JavaFileObject generatesCodeFileObject = + JavaFileObjects.forSourceLines( + "test.ClassB", + "package test;", + "", + "@" + GeneratesCode.class.getCanonicalName(), + "public class ClassB {}"); + requiresGeneratedCodeDeferralTest(dependentTestFileObject, generatesCodeFileObject); + } + @Test public void properlyDefersProcessing_typeElement() { - JavaFileObject classAFileObject = + JavaFileObject dependentTestFileObject = JavaFileObjects.forSourceLines( "test.ClassA", "package test;", @@ -267,23 +400,96 @@ public void properlyDefersProcessing_typeElement() { "public class ClassA {", " SomeGeneratedClass sgc;", "}"); - JavaFileObject classBFileObject = + requiresGeneratedCodeDeferralTest(dependentTestFileObject); + } + + @Test + public void properlyDefersProcessing_packageElement() { + JavaFileObject dependentTestFileObject = JavaFileObjects.forSourceLines( - "test.ClassB", + "test.ClassA", "package test;", "", "@" + GeneratesCode.class.getCanonicalName(), - "public class ClassB {}"); - RequiresGeneratedCodeProcessor requiresGeneratedCodeProcessor = - new RequiresGeneratedCodeProcessor(); - assertAbout(javaSources()) - .that(ImmutableList.of(classAFileObject, classBFileObject)) - .processedWith(requiresGeneratedCodeProcessor, new GeneratesCodeProcessor()) - .compilesWithoutError() - .and() - .generatesFileNamed( - SOURCE_OUTPUT, "test", "GeneratedByRequiresGeneratedCodeProcessor.java"); - assertThat(requiresGeneratedCodeProcessor.rejectedRounds).isEqualTo(0); + "public class ClassA {", + "}"); + JavaFileObject generatesCodeFileObject = + JavaFileObjects.forSourceLines( + "test.package-info", + "@" + RequiresGeneratedCode.class.getCanonicalName(), + "@" + ReferencesAClass.class.getCanonicalName() + "(SomeGeneratedClass.class)", + "package test;"); + requiresGeneratedCodeDeferralTest(dependentTestFileObject, generatesCodeFileObject); + } + + @Test + public void properlyDefersProcessing_argumentElement() { + JavaFileObject dependentTestFileObject = + JavaFileObjects.forSourceLines( + "test.ClassA", + "package test;", + "", + "public class ClassA {", + " SomeGeneratedClass sgc;", + " public void myMethod(@" + + RequiresGeneratedCode.class.getCanonicalName() + + " int myInt)", + " {}", + "}"); + JavaFileObject generatesCodeFileObject = + JavaFileObjects.forSourceLines( + "test.ClassB", + "package test;", + "", + "public class ClassB {", + " public void myMethod(@" + GeneratesCode.class.getCanonicalName() + " int myInt) {}", + "}"); + requiresGeneratedCodeDeferralTest(dependentTestFileObject, generatesCodeFileObject); + } + + @Test + public void properlyDefersProcessing_recordComponent() { + double version = Double.parseDouble(Objects.requireNonNull(JAVA_SPECIFICATION_VERSION.value())); + assume().that(version).isAtLeast(16.0); + JavaFileObject dependentTestFileObject = + JavaFileObjects.forSourceLines( + "test.RecordA", + "package test;", + "", + "public record RecordA( @" + + RequiresGeneratedCode.class.getCanonicalName() + + " SomeGeneratedClass sgc) {", + "}"); + requiresGeneratedCodeDeferralTest(dependentTestFileObject); + } + + @Test + public void properlyDefersProcessing_typeParameter() { + JavaFileObject dependentTestFileObject = + JavaFileObjects.forSourceLines( + "test.ClassA", + "package test;", + "", + "public class ClassA <@" + + TypeParameterRequiresGeneratedCode.class.getCanonicalName() + + " T extends SomeGeneratedClass> {", + "}"); + requiresGeneratedCodeDeferralTest(dependentTestFileObject); + } + + @Test + public void properlyDefersProcessing_methodTypeParameter() { + JavaFileObject dependentTestFileObject = + JavaFileObjects.forSourceLines( + "test.ClassA", + "package test;", + "", + "public class ClassA {", + " <@" + + TypeParameterRequiresGeneratedCode.class.getCanonicalName() + + " T extends SomeGeneratedClass> void foo(T t) {}", + "}"); + requiresGeneratedCodeDeferralTest(dependentTestFileObject); } @Test @@ -299,88 +505,118 @@ public void properlyDefersProcessing_nestedTypeValidBeforeOuterType() { " @" + AnAnnotation.class.getCanonicalName(), " static class ValidInRound1 {}", "}"); - assertAbout(javaSource()) - .that(source) - .processedWith(new AnAnnotationProcessor()) - .compilesWithoutError() - .and() - .generatesFileNamed(SOURCE_OUTPUT, "test", "ValidInRound2XYZ.java"); + Compilation compilation = javac().withProcessors(new AnAnnotationProcessor()).compile(source); + assertThat(compilation).succeeded(); + assertThat(compilation).generatedSourceFile("test.ValidInRound2XYZ"); } @Test - public void properlyDefersProcessing_packageElement() { + public void properlyDefersProcessing_rejectsTypeElement() { JavaFileObject classAFileObject = JavaFileObjects.forSourceLines( "test.ClassA", "package test;", "", - "@" + GeneratesCode.class.getCanonicalName(), + "@" + RequiresGeneratedCode.class.getCanonicalName(), "public class ClassA {", + " @" + AnAnnotation.class.getCanonicalName(), + " public void method() {}", "}"); - JavaFileObject packageFileObject = - JavaFileObjects.forSourceLines( - "test.package-info", - "@" + RequiresGeneratedCode.class.getCanonicalName(), - "@" + ReferencesAClass.class.getCanonicalName() + "(SomeGeneratedClass.class)", - "package test;"); RequiresGeneratedCodeProcessor requiresGeneratedCodeProcessor = - new RequiresGeneratedCodeProcessor(); - assertAbout(javaSources()) - .that(ImmutableList.of(classAFileObject, packageFileObject)) - .processedWith(requiresGeneratedCodeProcessor, new GeneratesCodeProcessor()) - .compilesWithoutError() - .and() - .generatesFileNamed( - SOURCE_OUTPUT, "test", "GeneratedByRequiresGeneratedCodeProcessor.java"); - assertThat(requiresGeneratedCodeProcessor.rejectedRounds).isEqualTo(0); + requiresGeneratedCodeRejectionTest(classAFileObject); + + // Re b/118372780: Assert that the right deferred elements are passed back, and not any enclosed + // elements annotated with annotations from a different step. + assertThat(requiresGeneratedCodeProcessor.processArguments()) + .comparingElementsUsing(setMultimapValuesByString()) + .containsExactly( + ImmutableSetMultimap.of(RequiresGeneratedCode.class.getCanonicalName(), "test.ClassA"), + ImmutableSetMultimap.of(RequiresGeneratedCode.class.getCanonicalName(), "test.ClassA")) + .inOrder(); } @Test - public void properlyDefersProcessing_argumentElement() { + public void properlyDefersProcessing_rejectsTypeParameterElement() { + JavaFileObject classAFileObject = + JavaFileObjects.forSourceLines( + "test.ClassA", + "package test;", + "", + "public class ClassA<@" + + TypeParameterRequiresGeneratedCode.class.getCanonicalName() + + " T> {", + " @" + AnAnnotation.class.getCanonicalName(), + " public void method() {}", + "}"); + requiresGeneratedCodeRejectionTest(classAFileObject); + } + + @Test + public void properlyDefersProcessing_rejectsArgumentElement() { JavaFileObject classAFileObject = JavaFileObjects.forSourceLines( "test.ClassA", "package test;", "", "public class ClassA {", - " SomeGeneratedClass sgc;", " public void myMethod(@" + RequiresGeneratedCode.class.getCanonicalName() + " int myInt)", " {}", "}"); - JavaFileObject classBFileObject = + requiresGeneratedCodeRejectionTest(classAFileObject); + } + + @Test + public void properlyDefersProcessing_rejectsField() { + double version = Double.parseDouble(Objects.requireNonNull(JAVA_SPECIFICATION_VERSION.value())); + assume().that(version).isAtLeast(16.0); + JavaFileObject classAFileObject = JavaFileObjects.forSourceLines( - "test.ClassB", + "test.ClassA", "package test;", "", - "public class ClassB {", - " public void myMethod(@" + GeneratesCode.class.getCanonicalName() + " int myInt) {}", + "public class ClassA {", + "@" + RequiresGeneratedCode.class.getCanonicalName() + " String s;", "}"); - RequiresGeneratedCodeProcessor requiresGeneratedCodeProcessor = - new RequiresGeneratedCodeProcessor(); - assertAbout(javaSources()) - .that(ImmutableList.of(classAFileObject, classBFileObject)) - .processedWith(requiresGeneratedCodeProcessor, new GeneratesCodeProcessor()) - .compilesWithoutError() - .and() - .generatesFileNamed( - SOURCE_OUTPUT, "test", "GeneratedByRequiresGeneratedCodeProcessor.java"); - assertThat(requiresGeneratedCodeProcessor.rejectedRounds).isEqualTo(0); + requiresGeneratedCodeRejectionTest(classAFileObject); + } + + @Test + public void properlyDefersProcessing_rejectsRecordComponent() { + double version = Double.parseDouble(Objects.requireNonNull(JAVA_SPECIFICATION_VERSION.value())); + assume().that(version).isAtLeast(16.0); + JavaFileObject classAFileObject = + JavaFileObjects.forSourceLines( + "test.RecordA", + "package test;", + "", + "public record RecordA(@" + + RequiresGeneratedCode.class.getCanonicalName() + + " String s) {", + "}"); + requiresGeneratedCodeRejectionTest(classAFileObject); } @Test - public void properlyDefersProcessing_rejectsElement() { + public void properlyDefersProcessing_rejectsTypeParameterElementInMethod() { JavaFileObject classAFileObject = JavaFileObjects.forSourceLines( "test.ClassA", "package test;", "", - "@" + RequiresGeneratedCode.class.getCanonicalName(), "public class ClassA {", " @" + AnAnnotation.class.getCanonicalName(), - " public void method() {}", + " <@" + + TypeParameterRequiresGeneratedCode.class.getCanonicalName() + + " T> void method(T t) {}", "}"); + requiresGeneratedCodeRejectionTest(classAFileObject); + } + + @CanIgnoreReturnValue + private RequiresGeneratedCodeProcessor requiresGeneratedCodeRejectionTest( + JavaFileObject classAFileObject) { JavaFileObject classBFileObject = JavaFileObjects.forSourceLines( "test.ClassB", @@ -390,23 +626,14 @@ public void properlyDefersProcessing_rejectsElement() { "public class ClassB {}"); RequiresGeneratedCodeProcessor requiresGeneratedCodeProcessor = new RequiresGeneratedCodeProcessor(); - assertAbout(javaSources()) - .that(ImmutableList.of(classAFileObject, classBFileObject)) - .processedWith(requiresGeneratedCodeProcessor, new GeneratesCodeProcessor()) - .compilesWithoutError() - .and() - .generatesFileNamed( - SOURCE_OUTPUT, "test", "GeneratedByRequiresGeneratedCodeProcessor.java"); + Compilation compilation = + javac() + .withProcessors(requiresGeneratedCodeProcessor, new GeneratesCodeProcessor()) + .compile(classAFileObject, classBFileObject); + assertThat(compilation).succeeded(); + assertThat(compilation).generatedSourceFile("test.GeneratedByRequiresGeneratedCodeProcessor"); assertThat(requiresGeneratedCodeProcessor.rejectedRounds).isEqualTo(1); - - // Re b/118372780: Assert that the right deferred elements are passed back, and not any enclosed - // elements annotated with annotations from a different step. - assertThat(requiresGeneratedCodeProcessor.processArguments()) - .comparingElementsUsing(setMultimapValuesByString()) - .containsExactly( - ImmutableSetMultimap.of(RequiresGeneratedCode.class.getCanonicalName(), "test.ClassA"), - ImmutableSetMultimap.of(RequiresGeneratedCode.class.getCanonicalName(), "test.ClassA")) - .inOrder(); + return requiresGeneratedCodeProcessor; } private static @@ -417,6 +644,196 @@ Correspondence, SetMultimap> setMultimapValuesByStr "is equivalent comparing multimap values by `toString()` to"); } + @Test + public void properlyDefersProcessing_stepRejectingExecutableElements() { + JavaFileObject classAFileObject = + JavaFileObjects.forSourceLines( + "test.ClassA", + "package test;", + "", + "public class ClassA {", + " @" + OneMethodAtATime.class.getCanonicalName(), + " public void method0() {}", + " @" + OneMethodAtATime.class.getCanonicalName(), + " public void method1() {}", + " @" + OneMethodAtATime.class.getCanonicalName(), + " public void method2() {}", + "}"); + OneMethodAtATimeProcessor oneMethodAtATimeProcessor = new OneMethodAtATimeProcessor(); + Compilation compilation = + javac().withProcessors(oneMethodAtATimeProcessor).compile(classAFileObject); + + assertThat(compilation).succeeded(); + assertThat(oneMethodAtATimeProcessor.rejectedRounds).isEqualTo(2); + + assertThat(oneMethodAtATimeProcessor.processArguments()) + .comparingElementsUsing(setMultimapValuesByString()) + .containsExactly( + ImmutableSetMultimap.of( + OneMethodAtATime.class.getCanonicalName(), "method0()", + OneMethodAtATime.class.getCanonicalName(), "method1()", + OneMethodAtATime.class.getCanonicalName(), "method2()"), + ImmutableSetMultimap.of( + OneMethodAtATime.class.getCanonicalName(), "method1()", + OneMethodAtATime.class.getCanonicalName(), "method2()"), + ImmutableSetMultimap.of(OneMethodAtATime.class.getCanonicalName(), "method2()")) + .inOrder(); + } + + @Test + public void properlyDefersProcessing_stepRejectingOverloadedExecutableElements() { + JavaFileObject classAFileObject = + JavaFileObjects.forSourceLines( + "test.ClassA", + "package test;", + "", + "public class ClassA {", + " @" + OneMethodAtATime.class.getCanonicalName(), + " public void overloadedMethod(int x) {}", + " @" + OneMethodAtATime.class.getCanonicalName(), + " public void overloadedMethod(float x) {}", + " @" + OneMethodAtATime.class.getCanonicalName(), + " public void overloadedMethod(double x) {}", + "}"); + OneOverloadedMethodAtATimeProcessor oneOverloadedMethodAtATimeProcessor = + new OneOverloadedMethodAtATimeProcessor(); + Compilation compilation = + javac().withProcessors(oneOverloadedMethodAtATimeProcessor).compile(classAFileObject); + + assertThat(compilation).succeeded(); + assertThat(oneOverloadedMethodAtATimeProcessor.rejectedRounds).isEqualTo(2); + + assertThat(oneOverloadedMethodAtATimeProcessor.processArguments()) + .comparingElementsUsing(setMultimapValuesByString()) + .containsExactly( + ImmutableSetMultimap.of( + OneMethodAtATime.class.getCanonicalName(), "overloadedMethod(int)", + OneMethodAtATime.class.getCanonicalName(), "overloadedMethod(float)", + OneMethodAtATime.class.getCanonicalName(), "overloadedMethod(double)"), + ImmutableSetMultimap.of( + OneMethodAtATime.class.getCanonicalName(), "overloadedMethod(int)", + OneMethodAtATime.class.getCanonicalName(), "overloadedMethod(double)"), + ImmutableSetMultimap.of( + OneMethodAtATime.class.getCanonicalName(), "overloadedMethod(double)")) + .inOrder(); + } + + @Test + public void properlyDefersProcessing_stepAndIllFormedRejectingExecutableElements() { + JavaFileObject testFileObject = + JavaFileObjects.forSourceLines( + "test.ClassA", + "package test;", + "import java.util.Set;", + "import java.util.SortedSet;", + "", + "public class ClassA {", + " @" + OneMethodAtATime.class.getCanonicalName(), + " > void overloadedMethod(C x) {}", + " @" + OneMethodAtATime.class.getCanonicalName(), + " > void overloadedMethod(C c) {}", + " @" + OneMethodAtATime.class.getCanonicalName(), + " void overloadedMethod(SomeGeneratedClass c) {}", + " @" + OneMethodAtATime.class.getCanonicalName(), + " void method0(SomeGeneratedClass c) {}", + "}"); + + JavaFileObject generatesCodeFileObject = + JavaFileObjects.forSourceLines( + "test.ClassB", + "package test;", + "", + "@" + GeneratesCode.class.getCanonicalName(), + "public class ClassB {}"); + + OneOverloadedMethodAtATimeProcessor oneOverloadedMethodAtATimeProcessor = + new OneOverloadedMethodAtATimeProcessor(); + Compilation compilation = + javac() + .withProcessors(oneOverloadedMethodAtATimeProcessor, new GeneratesCodeProcessor()) + .compile(testFileObject, generatesCodeFileObject); + + assertThat(compilation).succeeded(); + assertThat(oneOverloadedMethodAtATimeProcessor.rejectedRounds).isEqualTo(3); + + assertThat(oneOverloadedMethodAtATimeProcessor.processArguments()) + .comparingElementsUsing(setMultimapValuesByString()) + .containsExactly( + ImmutableSetMultimap.of( + OneMethodAtATime.class.getCanonicalName(), "overloadedMethod(C)", + OneMethodAtATime.class.getCanonicalName(), "overloadedMethod(C)", + OneMethodAtATime.class.getCanonicalName(), + "overloadedMethod(test.SomeGeneratedClass)", + OneMethodAtATime.class.getCanonicalName(), "method0(test.SomeGeneratedClass)"), + ImmutableSetMultimap.of( + OneMethodAtATime.class.getCanonicalName(), "overloadedMethod(C)", + OneMethodAtATime.class.getCanonicalName(), + "overloadedMethod(test.SomeGeneratedClass)", + OneMethodAtATime.class.getCanonicalName(), "method0(test.SomeGeneratedClass)"), + ImmutableSetMultimap.of( + OneMethodAtATime.class.getCanonicalName(), "overloadedMethod(C)", + OneMethodAtATime.class.getCanonicalName(), "method0(test.SomeGeneratedClass)"), + ImmutableSetMultimap.of( + OneMethodAtATime.class.getCanonicalName(), "method0(test.SomeGeneratedClass)")) + .inOrder(); + } + + /** + * In the following example, at least open-jdk does not report the second method if {@code + * SomeGeneratedClass} references a {@link TypeKind#ERROR} when {@link + * RoundEnvironment#getElementsAnnotatedWith} is called, or even when {@link + * TypeElement#getEnclosedElements()} is called. Therefore, our implementation should be vigilant + * that the second method is captured for processing at some point. + * + *

Note that for a method to get "hidden" like this, it should reside after the ERROR + * referencing method, and it should not have any distinguishing characteristic like different + * name, different number of parameter, or a clear parameter type mismatch. + */ + @Test + public void properlyDefersProcessing_errorTypeReferencingOverloadedMethods() { + JavaFileObject testFileObject = + JavaFileObjects.forSourceLines( + "test.ClassA", + "package test;", + "", + "public class ClassA {", + " @" + OneMethodAtATime.class.getCanonicalName(), + " void overloadedMethod(SomeGeneratedClass c) {}", + " @" + OneMethodAtATime.class.getCanonicalName(), + " void overloadedMethod(int c) {}", + "}"); + + JavaFileObject generatesCodeFileObject = + JavaFileObjects.forSourceLines( + "test.ClassB", + "package test;", + "", + "@" + GeneratesCode.class.getCanonicalName(), + "public class ClassB {}"); + + OneOverloadedMethodAtATimeProcessor oneOverloadedMethodAtATimeProcessor = + new OneOverloadedMethodAtATimeProcessor(); + Compilation compilation = + javac() + .withProcessors(oneOverloadedMethodAtATimeProcessor, new GeneratesCodeProcessor()) + .compile(testFileObject, generatesCodeFileObject); + + assertThat(compilation).succeeded(); + assertThat(oneOverloadedMethodAtATimeProcessor.rejectedRounds).isEqualTo(1); + + assertThat(oneOverloadedMethodAtATimeProcessor.processArguments()) + .comparingElementsUsing(setMultimapValuesByString()) + .containsExactly( + ImmutableSetMultimap.of( + OneMethodAtATime.class.getCanonicalName(), + "overloadedMethod(test.SomeGeneratedClass)", + OneMethodAtATime.class.getCanonicalName(), + "overloadedMethod(int)"), + ImmutableSetMultimap.of( + OneMethodAtATime.class.getCanonicalName(), "overloadedMethod(int)")) + .inOrder(); + } + @Test public void properlySkipsMissingAnnotations_generatesClass() { JavaFileObject source = @@ -464,13 +881,12 @@ public void reportsMissingType() { "public class ClassA {", " SomeGeneratedClass bar;", "}"); - assertAbout(javaSources()) - .that(ImmutableList.of(classAFileObject)) - .processedWith(new RequiresGeneratedCodeProcessor()) - .failsToCompile() - .withErrorContaining(RequiresGeneratedCodeProcessor.class.getCanonicalName()) - .in(classAFileObject) - .onLine(4); + Compilation compilation = + javac().withProcessors(new RequiresGeneratedCodeProcessor()).compile(classAFileObject); + assertThat(compilation) + .hadErrorContaining(RequiresGeneratedCodeProcessor.class.getCanonicalName()) + .inFile(classAFileObject) + .onLineContaining("class ClassA"); } @Test @@ -482,12 +898,9 @@ public void reportsMissingTypeSuppressedWhenOtherErrors() { "", "@" + CauseError.class.getCanonicalName(), "public class ClassA {}"); - assertAbout(javaSources()) - .that(ImmutableList.of(classAFileObject)) - .processedWith(new CauseErrorProcessor()) - .failsToCompile() - .withErrorCount(1) - .withErrorContaining("purposeful"); + Compilation compilation = + javac().withProcessors(new CauseErrorProcessor()).compile(classAFileObject); + assertThat(compilation).hadErrorContaining("purposeful"); } @Test diff --git a/factory/pom.xml b/factory/pom.xml index ee9144896a..d55eba510f 100644 --- a/factory/pom.xml +++ b/factory/pom.xml @@ -31,10 +31,10 @@ UTF-8 1.1.1 - 1.10.3 + 1.10.4 1.8 - 32.1.2-jre - 1.1.5 + 33.2.0-jre + 1.4.2 @@ -61,6 +61,31 @@ http://www.google.com + + + gk5885 + Gregory Kick + gk5885@gmail.com + + owner + developer + + -6 + + + eamonnmcmanus + Éamonn McManus + emcmanus@google.com + Google + http://www.google.com + + owner + developer + + -8 + + + sonatype-nexus-snapshots @@ -106,12 +131,19 @@ javapoet 1.13.0 + javax.inject javax.inject 1 + test + + + jakarta.inject + jakarta.inject-api + 2.0.1 + test - com.google.testing.compile compile-testing @@ -121,7 +153,7 @@ com.google.testparameterinjector test-parameter-injector - 1.12 + 1.16 test @@ -145,7 +177,7 @@ org.checkerframework checker-compat-qual - 2.5.5 + 2.5.6 test @@ -154,7 +186,7 @@ maven-compiler-plugin - 3.11.0 + 3.13.0 ${java.version} ${java.version} @@ -183,25 +215,25 @@ org.codehaus.plexus plexus-java - 1.1.2 + 1.2.0 org.apache.maven.plugins maven-surefire-plugin - 3.1.2 + 3.2.5 ${test.jvm.flags} maven-jar-plugin - 3.3.0 + 3.4.1 maven-invoker-plugin - 3.6.0 + 3.7.0 true ${project.build.directory}/it @@ -240,5 +272,55 @@ --add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED + + sonatype-oss-release + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.6.3 + + false + + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.4 + + + sign-artifacts + verify + + sign + + + + + + + diff --git a/factory/src/it/functional/pom.xml b/factory/src/it/functional/pom.xml index 7359ec1aaa..696ba67136 100644 --- a/factory/src/it/functional/pom.xml +++ b/factory/src/it/functional/pom.xml @@ -45,7 +45,7 @@ com.google.guava guava - 32.1.2-jre + 33.2.0-jre com.google.inject @@ -72,7 +72,7 @@ com.google.truth truth - 1.1.3 + 1.4.2 test diff --git a/factory/src/main/java/com/google/auto/factory/Provided.java b/factory/src/main/java/com/google/auto/factory/Provided.java index 226a16f48c..18029af557 100644 --- a/factory/src/main/java/com/google/auto/factory/Provided.java +++ b/factory/src/main/java/com/google/auto/factory/Provided.java @@ -20,8 +20,13 @@ import java.lang.annotation.Target; /** - * An annotation to be applied to parameters that should be provided by an - * {@linkplain javax.inject.Inject injected} {@link javax.inject.Provider} in a generated factory. + * An annotation to be applied to parameters that should be provided by an injected {@code Provider} + * in a generated factory. + * + *

The {@code @Inject} and {@code Provider} classes come from either the legacy package {@code + * javax.inject} or the updated package {@code jakarta.inject}. {@code jakarta.inject} is used if it + * is on the classpath. Compile with {@code -Acom.google.auto.factory.InjectApi=javax} if you want + * to use {@code javax.inject} even when {@code jakarta.inject} is available. * * @author Gregory Kick */ 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 0654eed10e..05f631898c 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 @@ -41,11 +41,13 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Messager; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedOptions; import javax.lang.model.SourceVersion; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; @@ -60,6 +62,7 @@ import javax.tools.Diagnostic.Kind; import net.ltgt.gradle.incap.IncrementalAnnotationProcessor; import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType; +import org.checkerframework.checker.nullness.qual.Nullable; /** * The annotation processor that generates factories for {@link AutoFactory} annotations. @@ -68,13 +71,25 @@ */ @IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.ISOLATING) @AutoService(Processor.class) +@SupportedOptions(AutoFactoryProcessor.INJECT_API_OPTION) public final class AutoFactoryProcessor extends AbstractProcessor { + static final String INJECT_API_OPTION = "com.google.auto.factory.InjectApi"; + + private static final ImmutableSet INJECT_APIS = ImmutableSet.of("jakarta", "javax"); + private FactoryDescriptorGenerator factoryDescriptorGenerator; private AutoFactoryDeclaration.Factory declarationFactory; private ProvidedChecker providedChecker; private Messager messager; private Elements elements; private Types types; + private InjectApi injectApi; + + /** + * If non-null, we will call this whenever the {@link #process} method is called, giving it one of + * the {@code @AutoFactory} elements, and do nothing else. + */ + private @Nullable Consumer<@Nullable Element> errorFunction; @Override public synchronized void init(ProcessingEnvironment processingEnv) { @@ -82,14 +97,40 @@ public synchronized void init(ProcessingEnvironment processingEnv) { elements = processingEnv.getElementUtils(); types = processingEnv.getTypeUtils(); messager = processingEnv.getMessager(); + String api = processingEnv.getOptions().get(INJECT_API_OPTION); + if (api != null && !INJECT_APIS.contains(api)) { + messager.printMessage( + Kind.ERROR, + "Usage: -A" + + INJECT_API_OPTION + + "=, where is " + + String.join(" or ", INJECT_APIS)); + errorFunction = unused -> {}; + return; + } + try { + injectApi = InjectApi.from(elements, api); + } catch (IllegalStateException e) { + errorFunction = element -> { + messager.printMessage(Kind.ERROR, e.getMessage(), element); + errorFunction = unused -> {}; // Only print the error once. + }; + return; + } providedChecker = new ProvidedChecker(messager); declarationFactory = new AutoFactoryDeclaration.Factory(elements, messager); factoryDescriptorGenerator = - new FactoryDescriptorGenerator(messager, types, declarationFactory); + new FactoryDescriptorGenerator(messager, types, declarationFactory, injectApi); } @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { + if (errorFunction != null) { + Set elements = roundEnv.getElementsAnnotatedWith(AutoFactory.class); + Element anElement = elements.isEmpty() ? null : elements.iterator().next(); + errorFunction.accept(anElement); + return false; + } try { doProcess(roundEnv); } catch (Throwable e) { @@ -141,7 +182,8 @@ private void doProcess(RoundEnvironment roundEnv) { indexedMethodsBuilder.build(); ImmutableSetMultimap factoriesBeingCreated = simpleNamesToNames(indexedMethods.keySet()); - FactoryWriter factoryWriter = new FactoryWriter(processingEnv, factoriesBeingCreated); + FactoryWriter factoryWriter = + new FactoryWriter(processingEnv, injectApi, factoriesBeingCreated); indexedMethods .asMap() @@ -216,7 +258,10 @@ private ImmutableSet implementationMethods( Elements2.getExecutableElementAsMemberOf(types, implementationMethod, supertype); ImmutableSet passedParameters = Parameter.forParameterList( - implementationMethod.getParameters(), methodType.getParameterTypes(), types); + implementationMethod.getParameters(), + methodType.getParameterTypes(), + types, + injectApi); implementationMethodsBuilder.add( ImplementationMethodDescriptor.builder() .name(implementationMethod.getSimpleName().toString()) @@ -270,7 +315,9 @@ private void checkAnnotationsToApply(Element annotation) { Set seenAnnotations = new HashSet<>(); for (ExecutableElement annotationMember : methodsIn(annotation.getEnclosedElements())) { TypeMirror memberType = annotationMember.getReturnType(); - boolean isAnnotation = memberType.getKind().equals(DECLARED) && asElement(memberType).getKind().equals(ANNOTATION_TYPE); + boolean isAnnotation = + memberType.getKind().equals(DECLARED) + && asElement(memberType).getKind().equals(ANNOTATION_TYPE); if (!isAnnotation && !memberType.getKind().equals(ERROR)) { messager.printMessage( Kind.ERROR, 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 70a21ea263..584b342eb8 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 @@ -52,12 +52,17 @@ final class FactoryDescriptorGenerator { private final Messager messager; private final Types types; private final AutoFactoryDeclaration.Factory declarationFactory; + private final InjectApi injectApi; FactoryDescriptorGenerator( - Messager messager, Types types, AutoFactoryDeclaration.Factory declarationFactory) { + Messager messager, + Types types, + AutoFactoryDeclaration.Factory declarationFactory, + InjectApi injectApi) { this.messager = messager; this.types = types; this.declarationFactory = declarationFactory; + this.injectApi = injectApi; } ImmutableSet generateDescriptor(Element element) { @@ -132,16 +137,17 @@ FactoryMethodDescriptor generateDescriptorForConstructor( // The map returned by partitioningBy always has entries for both key values but our // null-checker isn't yet smart enough to know that. ImmutableSet providedParameters = - Parameter.forParameterList(requireNonNull(parameterMap.get(true)), types); + Parameter.forParameterList(requireNonNull(parameterMap.get(true)), types, injectApi); ImmutableSet passedParameters = - Parameter.forParameterList(requireNonNull(parameterMap.get(false)), types); + Parameter.forParameterList(requireNonNull(parameterMap.get(false)), types, injectApi); return FactoryMethodDescriptor.builder(declaration) .name("create") .returnType(classElement.asType()) .publicMethod(classElement.getModifiers().contains(PUBLIC)) .providedParameters(providedParameters) .passedParameters(passedParameters) - .creationParameters(Parameter.forParameterList(constructor.getParameters(), types)) + .creationParameters( + Parameter.forParameterList(constructor.getParameters(), types, injectApi)) .isVarArgs(constructor.isVarArgs()) .exceptions(constructor.getThrownTypes()) .overridingMethod(false) 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 a887c57715..c0a7959133 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 @@ -49,8 +49,6 @@ import java.util.List; import javax.annotation.processing.Filer; import javax.annotation.processing.ProcessingEnvironment; -import javax.inject.Inject; -import javax.inject.Provider; import javax.lang.model.SourceVersion; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; @@ -60,6 +58,7 @@ final class FactoryWriter { + private final InjectApi injectApi; private final Filer filer; private final Elements elements; private final SourceVersion sourceVersion; @@ -67,7 +66,9 @@ final class FactoryWriter { FactoryWriter( ProcessingEnvironment processingEnv, + InjectApi injectApi, ImmutableSetMultimap factoriesBeingCreated) { + this.injectApi = injectApi; this.filer = processingEnv.getFiler(); this.elements = processingEnv.getElementUtils(); this.sourceVersion = processingEnv.getSourceVersion(); @@ -118,7 +119,8 @@ private static void addFactoryTypeParameters( private void addConstructorAndProviderFields( TypeSpec.Builder factory, FactoryDescriptor descriptor) { - MethodSpec.Builder constructor = constructorBuilder().addAnnotation(Inject.class); + MethodSpec.Builder constructor = + constructorBuilder().addAnnotation(ClassName.get(injectApi.inject())); if (descriptor.publicType()) { constructor.addModifiers(PUBLIC); } @@ -127,7 +129,8 @@ private void addConstructorAndProviderFields( for (ProviderField provider : providerFields) { ++argumentNumber; TypeName typeName = resolveTypeName(provider.key().type().get()).box(); - TypeName providerType = ParameterizedTypeName.get(ClassName.get(Provider.class), typeName); + TypeName providerType = + ParameterizedTypeName.get(ClassName.get(injectApi.provider()), typeName); factory.addField(providerType, provider.name(), PRIVATE, FINAL); if (provider.key().qualifier().isPresent()) { // only qualify the constructor parameter @@ -181,7 +184,7 @@ private void addFactoryMethods( } else { ProviderField provider = requireNonNull(descriptor.providers().get(parameter.key())); argument = CodeBlock.of(provider.name()); - if (parameter.isProvider()) { + if (injectApi.isProvider(parameter.type().get())) { // Providers are checked for nullness in the Factory's constructor. checkNotNull = false; } else { diff --git a/factory/src/main/java/com/google/auto/factory/processor/InjectApi.java b/factory/src/main/java/com/google/auto/factory/processor/InjectApi.java new file mode 100644 index 0000000000..d1689fed25 --- /dev/null +++ b/factory/src/main/java/com/google/auto/factory/processor/InjectApi.java @@ -0,0 +1,78 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.auto.factory.processor; + +import static com.google.auto.common.MoreStreams.toImmutableMap; +import static java.util.stream.Collectors.joining; + +import com.google.auto.common.MoreTypes; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.util.AbstractMap.SimpleEntry; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Encapsulates the choice of {@code jakarta.inject} or {@code javax.inject}. */ +@AutoValue +abstract class InjectApi { + abstract TypeElement inject(); + + abstract TypeElement provider(); + + abstract TypeElement qualifier(); + + private static final ImmutableList PREFIXES_IN_ORDER = + ImmutableList.of("jakarta.inject.", "javax.inject."); + + static InjectApi from(Elements elementUtils, @Nullable String apiPrefix) { + ImmutableList apiPackages = + (apiPrefix == null) ? PREFIXES_IN_ORDER : ImmutableList.of(apiPrefix + ".inject."); + for (String apiPackage : apiPackages) { + ImmutableMap apiMap = apiMap(elementUtils, apiPackage); + TypeElement inject = apiMap.get("Inject"); + TypeElement provider = apiMap.get("Provider"); + TypeElement qualifier = apiMap.get("Qualifier"); + if (inject != null && provider != null && qualifier != null) { + return new AutoValue_InjectApi(inject, provider, qualifier); + } + } + String classes = "{" + String.join(",", API_CLASSES) + "}"; + String missing = apiPackages.stream().sorted().map(s -> s + classes).collect(joining(" or ")); + throw new IllegalStateException("Class path for AutoFactory class must include " + missing); + } + + /** True if {@code type} is a {@code Provider}. */ + boolean isProvider(TypeMirror type) { + return type.getKind().equals(TypeKind.DECLARED) + && MoreTypes.asTypeElement(type).equals(provider()); + } + + private static ImmutableMap apiMap( + Elements elementUtils, String apiPackage) { + return API_CLASSES.stream() + .map(name -> new SimpleEntry<>(name, elementUtils.getTypeElement(apiPackage + name))) + .filter(entry -> entry.getValue() != null) + .collect(toImmutableMap(SimpleEntry::getKey, SimpleEntry::getValue)); + } + + private static final ImmutableSet API_CLASSES = + ImmutableSet.of("Inject", "Provider", "Qualifier"); +} diff --git a/factory/src/main/java/com/google/auto/factory/processor/Key.java b/factory/src/main/java/com/google/auto/factory/processor/Key.java index 6dc7644554..8347ce95f2 100644 --- a/factory/src/main/java/com/google/auto/factory/processor/Key.java +++ b/factory/src/main/java/com/google/auto/factory/processor/Key.java @@ -16,7 +16,6 @@ package com.google.auto.factory.processor; import static com.google.auto.common.MoreElements.isAnnotationPresent; -import static com.google.auto.factory.processor.Mirrors.isProvider; import static com.google.auto.factory.processor.Mirrors.unwrapOptionalEquivalence; import static com.google.auto.factory.processor.Mirrors.wrapOptionalInEquivalence; @@ -26,7 +25,6 @@ import com.google.common.base.Equivalence; import java.util.Collection; import java.util.Optional; -import javax.inject.Qualifier; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; @@ -37,7 +35,6 @@ * @author Gregory Kick */ @AutoValue -// TODO(ronshapiro): reuse dagger.model.Key? abstract class Key { abstract Equivalence.Wrapper type(); @@ -49,7 +46,7 @@ Optional qualifier() { } /** - * Constructs a key based on the type {@code type} and any {@link Qualifier}s in {@code + * Constructs a key based on the type {@code type} and any {@code Qualifier}s in {@code * annotations}. * *

If {@code type} is a {@code Provider}, the returned {@link Key}'s {@link #type()} is @@ -57,6 +54,7 @@ Optional qualifier() { * corresponding {@linkplain Types#boxedClass(PrimitiveType) boxed type}. * *

For example: + * * *
Input type {@code Key.type()} *
{@code String} {@code String} @@ -64,18 +62,19 @@ Optional qualifier() { *
{@code int} {@code Integer} *
*/ - static Key create(TypeMirror type, Collection annotations, Types types) { + static Key create( + TypeMirror type, Collection annotations, Types types, InjectApi injectApi) { // TODO(gak): check for only one qualifier rather than using the first Optional qualifier = annotations.stream() .filter( annotation -> isAnnotationPresent( - annotation.getAnnotationType().asElement(), Qualifier.class)) + annotation.getAnnotationType().asElement(), injectApi.qualifier())) .findFirst(); TypeMirror keyType = - isProvider(type) + injectApi.isProvider(type) ? MoreTypes.asDeclared(type).getTypeArguments().get(0) : boxedType(type, types); return new AutoValue_Key( diff --git a/factory/src/main/java/com/google/auto/factory/processor/Mirrors.java b/factory/src/main/java/com/google/auto/factory/processor/Mirrors.java index 5739511ecf..c81623d469 100644 --- a/factory/src/main/java/com/google/auto/factory/processor/Mirrors.java +++ b/factory/src/main/java/com/google/auto/factory/processor/Mirrors.java @@ -15,14 +15,12 @@ */ package com.google.auto.factory.processor; -import com.google.auto.common.MoreTypes; import com.google.common.base.Equivalence; import com.google.common.collect.ImmutableMap; import java.lang.annotation.Annotation; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; -import javax.inject.Provider; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; @@ -30,7 +28,6 @@ import javax.lang.model.element.Name; import javax.lang.model.element.TypeElement; import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeMirror; import javax.lang.model.util.SimpleElementVisitor6; final class Mirrors { @@ -53,11 +50,6 @@ public Name visitType(TypeElement e, Void p) { null); } - /** {@code true} if {@code type} is a {@link Provider}. */ - static boolean isProvider(TypeMirror type) { - return MoreTypes.isType(type) && MoreTypes.isTypeOf(Provider.class, type); - } - /** * Returns an annotation value map with {@link String} keys instead of {@link ExecutableElement} * instances. diff --git a/factory/src/main/java/com/google/auto/factory/processor/Parameter.java b/factory/src/main/java/com/google/auto/factory/processor/Parameter.java index bb586a1cf3..16a419a8c3 100644 --- a/factory/src/main/java/com/google/auto/factory/processor/Parameter.java +++ b/factory/src/main/java/com/google/auto/factory/processor/Parameter.java @@ -34,7 +34,6 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Stream; -import javax.inject.Provider; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; @@ -49,15 +48,11 @@ abstract class Parameter { /** - * The original type of the parameter, while {@code key().type()} erases the wrapped {@link + * The original type of the parameter, while {@code key().type()} erases the wrapped {@code * Provider}, if any. */ abstract Equivalence.Wrapper type(); - boolean isProvider() { - return Mirrors.isProvider(type().get()); - } - boolean isPrimitive() { return type().get().getKind().isPrimitive(); } @@ -79,15 +74,16 @@ ImmutableList annotations() { Optional nullable() { return unwrapOptionalEquivalence(nullableWrapper()); } + private static Parameter forVariableElement( - VariableElement variable, TypeMirror type, Types types) { + VariableElement variable, TypeMirror type, Types types, InjectApi injectApi) { ImmutableList allAnnotations = Stream.of(variable.getAnnotationMirrors(), type.getAnnotationMirrors()) .flatMap(List::stream) .collect(toImmutableList()); Optional nullable = allAnnotations.stream().filter(Parameter::isNullable).findFirst(); - Key key = Key.create(type, allAnnotations, types); + Key key = Key.create(type, allAnnotations, types, injectApi); ImmutableSet> typeAnnotationWrappers = type.getAnnotationMirrors().stream() @@ -120,12 +116,14 @@ private static boolean isNullable(AnnotationMirror annotation) { static ImmutableSet forParameterList( List variables, List variableTypes, - Types types) { + Types types, + InjectApi injectApi) { checkArgument(variables.size() == variableTypes.size()); ImmutableSet.Builder builder = ImmutableSet.builder(); Set names = Sets.newHashSetWithExpectedSize(variables.size()); for (int i = 0; i < variables.size(); i++) { - Parameter parameter = forVariableElement(variables.get(i), variableTypes.get(i), types); + Parameter parameter = + forVariableElement(variables.get(i), variableTypes.get(i), types, injectApi); checkArgument(names.add(parameter.name()), "Duplicate parameter name: %s", parameter.name()); builder.add(parameter); } @@ -135,11 +133,11 @@ static ImmutableSet forParameterList( } static ImmutableSet forParameterList( - List variables, Types types) { + List variables, Types types, InjectApi injectApi) { List variableTypes = Lists.newArrayListWithExpectedSize(variables.size()); for (VariableElement var : variables) { variableTypes.add(var.asType()); } - return forParameterList(variables, variableTypes, types); + return forParameterList(variables, variableTypes, types, injectApi); } } diff --git a/factory/src/test/java/com/google/auto/factory/processor/AutoFactoryProcessorNegativeTest.java b/factory/src/test/java/com/google/auto/factory/processor/AutoFactoryProcessorNegativeTest.java index 4998aa57e6..c9907b057d 100644 --- a/factory/src/test/java/com/google/auto/factory/processor/AutoFactoryProcessorNegativeTest.java +++ b/factory/src/test/java/com/google/auto/factory/processor/AutoFactoryProcessorNegativeTest.java @@ -15,12 +15,17 @@ */ package com.google.auto.factory.processor; +import static com.google.common.truth.Truth.assertThat; import static com.google.testing.compile.CompilationSubject.assertThat; +import com.google.common.collect.ImmutableList; import com.google.testing.compile.Compilation; import com.google.testing.compile.Compiler; import com.google.testing.compile.JavaFileObjects; +import java.io.File; +import java.net.URL; import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -178,4 +183,46 @@ public void annotationsToApplyNotAnnotations() { .inFile(file) .onLineContaining("andWhatIsThis"); } + + @Test + public void noInjectApi() throws Exception { + URL autoFactoryUrl = + Class.forName("com.google.auto.factory.AutoFactory") + .getProtectionDomain() + .getCodeSource() + .getLocation(); + assertThat(autoFactoryUrl.getProtocol()).isEqualTo("file"); + File autoFactoryFile = new File(autoFactoryUrl.getPath()); + Compiler compiler = + Compiler.javac() + .withProcessors(new AutoFactoryProcessor()) + .withClasspath(ImmutableList.of(autoFactoryFile)); + JavaFileObject file = JavaFileObjects.forResource("good/SimpleClass.java"); + Compilation compilation = compiler.compile(file); + assertThat(compilation).failed(); + assertThat(compilation) + .hadErrorContaining( + "Class path for AutoFactory class must include" + + " jakarta.inject.{Inject,Provider,Qualifier} or" + + " javax.inject.{Inject,Provider,Qualifier}"); + assertThat(compilation).hadErrorCount(1); + } + + /** + * AutoFactoryProcessor shouldn't complain about the absence of {@code javax.inject} if there are + * no {@code @AutoFactory} classes being compiled. Its {@code init} will be called and will see + * the problem, but will say nothing. + */ + @Test + public void noInjectApiButNoAutoFactoryEither() { + Compiler compiler = + Compiler.javac() + .withProcessors(new AutoFactoryProcessor()) + .withClasspath(ImmutableList.of()); + JavaFileObject file = + JavaFileObjects.forSourceString("test.Foo", "package test; public class Foo {}"); + Compilation compilation = compiler.compile(file); + assertThat(compilation).succeededWithoutWarnings(); + assertThat(compilation).generatedFile(StandardLocation.CLASS_OUTPUT, "test", "Foo.class"); + } } diff --git a/factory/src/test/java/com/google/auto/factory/processor/AutoFactoryProcessorTest.java b/factory/src/test/java/com/google/auto/factory/processor/AutoFactoryProcessorTest.java index 845ea58a42..14026b17d6 100644 --- a/factory/src/test/java/com/google/auto/factory/processor/AutoFactoryProcessorTest.java +++ b/factory/src/test/java/com/google/auto/factory/processor/AutoFactoryProcessorTest.java @@ -63,16 +63,36 @@ public AutoFactoryProcessorTest(@TestParameter Config config) { private enum InjectPackage { JAVAX, + JAKARTA } + /** + * Each test configuration specifies whether javax or jakarta or both are on the classpath, which + * one is expected to be chosen, and any {@code -A} options. + */ private enum Config { - JAVAX_ONLY_ON_CLASSPATH(ImmutableList.of(InjectPackage.JAVAX), InjectPackage.JAVAX); - - /** Config that is used for negative tests, and to update the golden files. */ - static final Config DEFAULT = JAVAX_ONLY_ON_CLASSPATH; + JAVAX_ONLY_ON_CLASSPATH(ImmutableList.of(InjectPackage.JAVAX), InjectPackage.JAVAX), + JAKARTA_ONLY_ON_CLASSPATH(ImmutableList.of(InjectPackage.JAKARTA), InjectPackage.JAKARTA), + BOTH_ON_CLASSPATH( + ImmutableList.of(InjectPackage.JAVAX, InjectPackage.JAKARTA), + InjectPackage.JAKARTA), + EXPLICIT_JAVAX( + ImmutableList.of(InjectPackage.JAVAX, InjectPackage.JAKARTA), + InjectPackage.JAVAX, + ImmutableList.of("-A" + AutoFactoryProcessor.INJECT_API_OPTION + "=javax")), + EXPLICIT_JAKARTA( + ImmutableList.of(InjectPackage.JAVAX, InjectPackage.JAKARTA), + InjectPackage.JAKARTA, + ImmutableList.of("-A" + AutoFactoryProcessor.INJECT_API_OPTION + "=jakarta")); + + /** + * Config that is used for negative tests, and to update the golden files. Since those files use + * {@code javax.inject}, we need a config that specifies that package. + */ + static final Config DEFAULT = EXPLICIT_JAVAX; final ImmutableList packagesOnClasspath; - final InjectPackage unusedExpectedPackage; + final InjectPackage expectedPackage; final ImmutableList options; Config(ImmutableList packagesOnClasspath, InjectPackage expectedPackage) { @@ -84,7 +104,7 @@ private enum Config { InjectPackage expectedPackage, ImmutableList options) { this.packagesOnClasspath = packagesOnClasspath; - this.unusedExpectedPackage = expectedPackage; + this.expectedPackage = expectedPackage; this.options = options; } @@ -95,6 +115,7 @@ private enum Config { fileForClass("javax.annotation.Nullable"), fileForClass("org.checkerframework.checker.nullness.compatqual.NullableType")); static final File JAVAX_CLASSPATH = fileForClass("javax.inject.Provider"); + static final File JAKARTA_CLASSPATH = fileForClass("jakarta.inject.Provider"); static File fileForClass(String className) { Class c; @@ -114,6 +135,9 @@ ImmutableList classpath() { if (packagesOnClasspath.contains(InjectPackage.JAVAX)) { classpathBuilder.add(JAVAX_CLASSPATH); } + if (packagesOnClasspath.contains(InjectPackage.JAKARTA)) { + classpathBuilder.add(JAKARTA_CLASSPATH); + } return classpathBuilder.build(); } @@ -184,6 +208,9 @@ private JavaFileObject goldenFile(String resourceName) { try { URL resourceUrl = Resources.getResource(resourceName); String source = Resources.toString(resourceUrl, UTF_8); + if (config.expectedPackage.equals(InjectPackage.JAKARTA)) { + source = source.replace("javax.inject", "jakarta.inject"); + } String className = resourceName.replaceFirst("\\.java$", "").replace('/', '.'); return JavaFileObjects.forSourceString(className, source); } catch (IOException e) { @@ -369,57 +396,6 @@ public void mixedDepsImplementingInterfaces() { "expected/MixedDepsImplementingInterfacesFactory.java")); } - @Test - public void failsWithMixedFinals() { - JavaFileObject file = JavaFileObjects.forResource("bad/MixedFinals.java"); - Compilation compilation = config.javac().compile(file); - assertThat(compilation).failed(); - assertThat(compilation) - .hadErrorContaining( - "Cannot mix allowSubclasses=true and allowSubclasses=false in one factory.") - .inFile(file) - .onLine(24); - assertThat(compilation) - .hadErrorContaining( - "Cannot mix allowSubclasses=true and allowSubclasses=false in one factory.") - .inFile(file) - .onLine(27); - } - - @Test - public void providedButNoAutoFactory() { - JavaFileObject file = JavaFileObjects.forResource("bad/ProvidedButNoAutoFactory.java"); - Compilation compilation = config.javac().compile(file); - assertThat(compilation).failed(); - assertThat(compilation) - .hadErrorContaining( - "@Provided may only be applied to constructors requesting an auto-factory") - .inFile(file) - .onLineContaining("@Provided"); - } - - @Test - public void providedOnMethodParameter() { - JavaFileObject file = JavaFileObjects.forResource("bad/ProvidedOnMethodParameter.java"); - Compilation compilation = config.javac().compile(file); - assertThat(compilation).failed(); - assertThat(compilation) - .hadErrorContaining("@Provided may only be applied to constructor parameters") - .inFile(file) - .onLineContaining("@Provided"); - } - - @Test - public void invalidCustomName() { - JavaFileObject file = JavaFileObjects.forResource("bad/InvalidCustomName.java"); - Compilation compilation = config.javac().compile(file); - assertThat(compilation).failed(); - assertThat(compilation) - .hadErrorContaining("\"SillyFactory!\" is not a valid Java identifier") - .inFile(file) - .onLineContaining("SillyFactory!"); - } - @Test public void factoryExtendingAbstractClass() { goldenTest( @@ -438,21 +414,6 @@ public void factoryWithConstructorThrowsClauseExtendingAbstractClass() { "expected/FactoryExtendingAbstractClassThrowsFactory.java")); } - @Test - public void factoryExtendingAbstractClass_withConstructorParams() { - JavaFileObject file = - JavaFileObjects.forResource("bad/FactoryExtendingAbstractClassWithConstructorParams.java"); - Compilation compilation = config.javac().compile(file); - assertThat(compilation).failed(); - assertThat(compilation) - .hadErrorContaining( - "tests.FactoryExtendingAbstractClassWithConstructorParams.AbstractFactory is not a" - + " valid supertype for a factory. Factory supertypes must have a no-arg" - + " constructor.") - .inFile(file) - .onLineContaining("@AutoFactory"); - } - @Test public void factoryExtendingAbstractClass_multipleConstructors() { goldenTest( @@ -460,45 +421,6 @@ public void factoryExtendingAbstractClass_multipleConstructors() { ImmutableMap.of()); } - @Test - public void factoryExtendingInterface() { - JavaFileObject file = JavaFileObjects.forResource("bad/InterfaceSupertype.java"); - Compilation compilation = config.javac().compile(file); - assertThat(compilation).failed(); - assertThat(compilation) - .hadErrorContaining( - "java.lang.Runnable is not a valid supertype for a factory. Supertypes must be" - + " non-final classes.") - .inFile(file) - .onLineContaining("@AutoFactory"); - } - - @Test - public void factoryExtendingEnum() { - JavaFileObject file = JavaFileObjects.forResource("bad/EnumSupertype.java"); - Compilation compilation = config.javac().compile(file); - assertThat(compilation).failed(); - assertThat(compilation) - .hadErrorContaining( - "java.util.concurrent.TimeUnit is not a valid supertype for a factory. Supertypes must" - + " be non-final classes.") - .inFile(file) - .onLineContaining("@AutoFactory"); - } - - @Test - public void factoryExtendingFinalClass() { - JavaFileObject file = JavaFileObjects.forResource("bad/FinalSupertype.java"); - Compilation compilation = config.javac().compile(file); - assertThat(compilation).failed(); - assertThat(compilation) - .hadErrorContaining( - "java.lang.Boolean is not a valid supertype for a factory. Supertypes must be" - + " non-final classes.") - .inFile(file) - .onLineContaining("@AutoFactory"); - } - @Test public void factoryImplementingGenericInterfaceExtension() { goldenTest( @@ -684,6 +606,9 @@ private void rewriteImports(List sourceLines) { ? "import javax.annotation.Generated;" : line); } + if (config.expectedPackage.equals(InjectPackage.JAKARTA)) { + importLines.replaceAll(line -> line.replace("javax.inject", "jakarta.inject")); + } Collections.sort(importLines); } } diff --git a/service/README.md b/service/README.md index 5016456135..dbcf4560da 100644 --- a/service/README.md +++ b/service/README.md @@ -20,6 +20,7 @@ Say you have: ```java package foo.bar; +import com.google.auto.service.AutoService; import javax.annotation.processing.Processor; @AutoService(Processor.class) diff --git a/service/pom.xml b/service/pom.xml index a6e0d0710a..a2838f575d 100644 --- a/service/pom.xml +++ b/service/pom.xml @@ -37,8 +37,8 @@ UTF-8 1.8 - 32.1.2-jre - 1.1.5 + 33.2.0-jre + 1.4.2 @@ -111,7 +111,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.13.0 ${java.version} ${java.version} @@ -123,14 +123,14 @@ org.codehaus.plexus plexus-java - 1.1.2 + 1.2.0 org.apache.maven.plugins maven-jar-plugin - 3.3.0 + 3.4.1 diff --git a/service/processor/src/main/java/com/google/auto/service/processor/ServicesFiles.java b/service/processor/src/main/java/com/google/auto/service/processor/ServicesFiles.java index b08431e809..8e51ec7753 100644 --- a/service/processor/src/main/java/com/google/auto/service/processor/ServicesFiles.java +++ b/service/processor/src/main/java/com/google/auto/service/processor/ServicesFiles.java @@ -28,17 +28,14 @@ import java.util.HashSet; import java.util.Set; -/** - * A helper class for reading and writing Services files. - */ +/** A helper class for reading and writing Services files. */ final class ServicesFiles { public static final String SERVICES_PATH = "META-INF/services"; private ServicesFiles() {} /** - * Returns an absolute path to a service file given the class - * name of the service. + * Returns an absolute path to a service file given the class name of the service. * * @param serviceName not {@code null} * @return SERVICES_PATH + serviceName @@ -84,7 +81,7 @@ static void writeServiceFile(Collection services, OutputStream output) BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, UTF_8)); for (String service : services) { writer.write(service); - writer.newLine(); + writer.write('\n'); } writer.flush(); } diff --git a/value/README.md b/value/README.md index bdf936c4af..16fd0f9259 100644 --- a/value/README.md +++ b/value/README.md @@ -1,6 +1,6 @@ # AutoValue -*Generated immutable value classes for Java 7+*
+*Generated immutable value classes for Java 8+*
***Kevin Bourrillion, Éamonn McManus***
**Google, Inc.** @@ -20,13 +20,14 @@ AutoValue provides an easier way to create immutable value classes, with a lot less code and less room for error, while **not restricting your freedom** to code almost any aspect of your class exactly the way you want it. -For more information, consult the -[detailed documentation](userguide/index.md). +For more information, consult the [detailed documentation]. **Note:** If you are using Kotlin then its [data classes](https://kotlinlang.org/docs/data-classes.html) are usually more appropriate than AutoValue. Likewise, if you are using a version of Java that -has [records](https://docs.oracle.com/en/java/javase/16/language/records.html), -then those are usually more appropriate. You can still use -[AutoBuilder](userguide/autobuilder.md) -to make builders for data classes or records. +has [records] then those are usually more appropriate. You can still +use [AutoBuilder] to make builders for data classes or records. + +[detailed documentation]: userguide/index.md +[records]: userguide/records.md +[AutoBuilder]: userguide/autobuilder.md diff --git a/value/annotations/pom.xml b/value/annotations/pom.xml index 48f33dac90..f045c328fa 100644 --- a/value/annotations/pom.xml +++ b/value/annotations/pom.xml @@ -21,19 +21,19 @@ com.google.auto.value auto-value-parent - HEAD-SNAPSHOT + 1.11.0 auto-value-annotations - HEAD-SNAPSHOT + 1.11.0 AutoValue Annotations - Immutable value-type code generation for Java 1.7+. + Immutable value-type code generation for Java 8+. https://github.com/google/auto/tree/main/value - 1.7 + 1.8 diff --git a/value/pom.xml b/value/pom.xml index f44edd6ba6..7cd2d71ad1 100644 --- a/value/pom.xml +++ b/value/pom.xml @@ -20,10 +20,10 @@ com.google.auto.value auto-value-parent - HEAD-SNAPSHOT + 1.11.0 AutoValue Parent - Immutable value-type code generation for Java 7+. + Immutable value-type code generation for Java 8+. pom https://github.com/google/auto/tree/main/value @@ -31,8 +31,8 @@ UTF-8 1.8 - 32.1.2-jre - 1.1.5 + 33.2.0-jre + 1.4.2 @@ -155,7 +155,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.13.0 ${java.version} ${java.version} @@ -167,19 +167,19 @@ org.codehaus.plexus plexus-java - 1.1.2 + 1.2.0 org.apache.maven.plugins maven-jar-plugin - 3.3.0 + 3.4.1 org.apache.maven.plugins maven-invoker-plugin - 3.6.0 + 3.7.0 true ${project.build.directory}/it @@ -215,7 +215,7 @@ org.apache.maven.plugins maven-source-plugin - 3.3.0 + 3.3.1 attach-sources @@ -228,7 +228,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.5.0 + 3.6.3 false @@ -244,7 +244,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.1.0 + 3.2.4 sign-artifacts diff --git a/value/processor/pom.xml b/value/processor/pom.xml index a93c776512..a19a4ecbac 100644 --- a/value/processor/pom.xml +++ b/value/processor/pom.xml @@ -21,14 +21,14 @@ com.google.auto.value auto-value-parent - HEAD-SNAPSHOT + 1.11.0 auto-value - HEAD-SNAPSHOT + 1.11.0 AutoValue Processor - Immutable value-type code generation for Java 1.7+. + Immutable value-type code generation for Java 8+. https://github.com/google/auto/tree/main/value @@ -41,7 +41,7 @@ 1.1.1 - 2.21.1 + 2.27.1 @@ -78,15 +78,10 @@ com.squareup javapoet - - org.jetbrains.kotlinx - kotlinx-metadata-jvm - 0.7.0 - org.ow2.asm asm - 9.5 + 9.7 @@ -200,7 +195,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.1.2 + 3.2.5 ${test.jvm.flags} @@ -216,7 +211,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.5.0 + 3.5.3 package @@ -230,21 +225,6 @@ com.google.code.findbugs:jsr305 - - - *:* - - - META-INF/*.kotlin_module - - - - - - - com.google @@ -257,14 +237,6 @@ com.squareup.javapoet autovalue.shaded.com.squareup.javapoet - - kotlin - autovalue.shaded.kotlin - - - kotlinx - autovalue.shaded.kotlinx - net.ltgt.gradle.incap autovalue.shaded.net.ltgt.gradle.incap diff --git a/value/src/it/functional/pom.xml b/value/src/it/functional/pom.xml index bf92f4a1df..de02c8c382 100644 --- a/value/src/it/functional/pom.xml +++ b/value/src/it/functional/pom.xml @@ -22,17 +22,17 @@ com.google.auto.value auto-value-parent - HEAD-SNAPSHOT + 1.11.0 ../../../pom.xml https://github.com/google/auto/tree/main/value com.google.auto.value.it.functional functional - HEAD-SNAPSHOT + 1.11.0 Auto-Value Functional Integration Test - 1.9.10 + 2.0.0 @@ -40,11 +40,6 @@ auto-value-annotations ${project.version} - - com.google.auto.value - auto-value - ${project.version} - com.google.auto.service auto-service @@ -62,7 +57,7 @@ org.gwtproject gwt-user - 2.10.0 + 2.11.0 junit @@ -92,7 +87,7 @@ dev.gradleplugins gradle-test-kit - 8.3 + 8.6 test @@ -112,7 +107,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.3.0 + 3.4.1 org.jetbrains.kotlin @@ -149,15 +144,27 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.13.0 org.codehaus.plexus plexus-java - 1.1.2 + 1.2.0 + + + com.google.auto.value + auto-value + ${project.version} + + + org.jetbrains.kotlin + kotlin-metadata-jvm + 2.0.0 + + ${java.specification.version} ${java.specification.version} @@ -173,7 +180,7 @@ org.apache.maven.plugins maven-deploy-plugin - 3.1.1 + 3.1.2 true @@ -182,12 +189,12 @@ org.apache.maven.plugins maven-surefire-plugin - 3.1.2 + 3.2.5 org.apache.maven.plugins maven-failsafe-plugin - 3.1.2 + 3.2.5 ${project.version} @@ -202,7 +209,7 @@ - + @@ -214,15 +221,27 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.13.0 org.codehaus.plexus plexus-java - 1.1.2 + 1.2.0 + + + com.google.auto.value + auto-value + ${project.version} + + + org.jetbrains.kotlin + kotlin-metadata-jvm + 2.0.0 + + ${java.specification.version} ${java.specification.version} @@ -249,7 +268,7 @@ org.eclipse.jdt ecj - 3.34.0 + 3.37.0 test @@ -274,25 +293,6 @@ - - exclude-java8-tests - - (,1.7] - - - - - maven-compiler-plugin - - - **/AutoValueJava8Test.java - - - - - - - open-modules diff --git a/value/src/it/functional/src/test/java/com/google/auto/value/AutoAnnotationTest.java b/value/src/it/functional/src/test/java/com/google/auto/value/AutoAnnotationTest.java index de6885cf38..11849ff067 100644 --- a/value/src/it/functional/src/test/java/com/google/auto/value/AutoAnnotationTest.java +++ b/value/src/it/functional/src/test/java/com/google/auto/value/AutoAnnotationTest.java @@ -86,16 +86,8 @@ public void testEqualsParameterAnnotation() throws ReflectiveOperationException @SuppressWarnings("GetClassOnAnnotation") // yes, I really want the implementation class Class autoAnnotationImpl = newStringValues(new String[0]).getClass(); Method equals = autoAnnotationImpl.getDeclaredMethod("equals", Object.class); - // The remaining faffing around with reflection is there because we have a Google-internal test - // that runs this code with -source 7 -target 7. We're really just doing this: - // assertThat(equals.getAnnotatedParameterTypes()[0].isAnnotationPresent(jspecifyNullable)) - // .isTrue(); - Method getAnnotatedParameterTypes = Method.class.getMethod("getAnnotatedParameterTypes"); - Object[] annotatedParameterTypes = (Object[]) getAnnotatedParameterTypes.invoke(equals); - Method isAnnotationPresent = - annotatedParameterTypes[0].getClass().getMethod("isAnnotationPresent", Class.class); - assertThat(isAnnotationPresent.invoke(annotatedParameterTypes[0], jspecifyNullable)) - .isEqualTo(true); + assertThat(equals.getAnnotatedParameterTypes()[0].isAnnotationPresent(jspecifyNullable)) + .isTrue(); } @Test diff --git a/value/src/it/functional/src/test/java/com/google/auto/value/AutoBuilderTest.java b/value/src/it/functional/src/test/java/com/google/auto/value/AutoBuilderTest.java index 4581f6e1e2..e90f33cd22 100644 --- a/value/src/it/functional/src/test/java/com/google/auto/value/AutoBuilderTest.java +++ b/value/src/it/functional/src/test/java/com/google/auto/value/AutoBuilderTest.java @@ -17,8 +17,8 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import static java.lang.annotation.ElementType.TYPE_USE; +import static java.util.stream.Collectors.joining; import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; @@ -27,6 +27,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.truth.Truth; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -412,14 +413,10 @@ public void staticMethodOfContainingClass() { @Test public void missingRequiredProperty() { - // This test is compiled at source level 7 by CompileWithEclipseTest, so we can't use - // assertThrows with a lambda. - try { - localTimeBuilder().hour(12).minute(34).build(); - fail(); - } catch (IllegalStateException e) { - assertThat(e).hasMessageThat().isEqualTo("Missing required properties: second"); - } + IllegalStateException e = + assertThrows( + IllegalStateException.class, () -> localTimeBuilder().hour(12).minute(34).build()); + assertThat(e).hasMessageThat().isEqualTo("Missing required properties: second"); } static void throwException() throws IOException { @@ -507,12 +504,7 @@ public void propertyBuilder() { } static String concatList(ImmutableList list) { - // We're avoiding streams for now since we compile this in Java 7 mode in CompileWithEclipseTest - StringBuilder sb = new StringBuilder(); - for (T element : list) { - sb.append(element); - } - return sb.toString(); + return list.stream().map(String::valueOf).collect(joining()); } @AutoBuilder(callMethod = "concatList") @@ -666,9 +658,9 @@ static PairBuilder pairBuilder() { @Test public void genericGetters() { PairBuilder builder = pairBuilder(); - assertThat(builder.getSecond()).isEmpty(); + Truth.assertThat(builder.getSecond()).isEmpty(); builder.setSecond(2); - assertThat(builder.getSecond()).hasValue(2); + Truth.assertThat(builder.getSecond()).hasValue(2); try { builder.getFirst(); fail(); diff --git a/value/src/it/functional/src/test/java/com/google/auto/value/AutoValueJava8Test.java b/value/src/it/functional/src/test/java/com/google/auto/value/AutoValueJava8Test.java index dfd825ca30..d1bdb1bf2c 100644 --- a/value/src/it/functional/src/test/java/com/google/auto/value/AutoValueJava8Test.java +++ b/value/src/it/functional/src/test/java/com/google/auto/value/AutoValueJava8Test.java @@ -16,17 +16,20 @@ package com.google.auto.value; import static com.google.common.base.StandardSystemProperty.JAVA_SPECIFICATION_VERSION; +import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; -import static com.google.common.truth.Truth8.assertThat; import static com.google.common.truth.TruthJUnit.assume; import static com.google.testing.compile.CompilationSubject.assertThat; +import static java.util.Arrays.stream; import static org.junit.Assert.assertThrows; import static org.junit.Assume.assumeTrue; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.testing.EqualsTester; +import com.google.common.truth.Truth; import com.google.testing.compile.Compilation; import com.google.testing.compile.Compiler; import com.google.testing.compile.JavaFileObjects; @@ -36,8 +39,10 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedParameterizedType; import java.lang.reflect.AnnotatedType; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.TypeVariable; import java.util.ArrayList; @@ -47,6 +52,7 @@ import java.util.OptionalDouble; import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Stream; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; @@ -70,7 +76,10 @@ * @author emcmanus@google.com (Éamonn McManus) */ @RunWith(JUnit4.class) +@SuppressWarnings({"SameNameButDifferent", "NullableTypeParameter", "TruthSelfEquals"}) +// We are deliberately doing some shady stuff to test edge cases. public class AutoValueJava8Test { + @SuppressWarnings("NonFinalStaticField") // b/314784069 private static boolean javacHandlesTypeAnnotationsCorrectly; // This is appalling. Some versions of javac do not correctly report annotations on type uses in @@ -169,11 +178,17 @@ public void testEqualsParameterIsAnnotated() throws NoSuchMethodException { // Some versions do, some don't. So skip the test unless we are on at least JDK 9. double javaVersion = Double.parseDouble(JAVA_SPECIFICATION_VERSION.value()); assume().that(javaVersion).isAtLeast(9.0); - Method equals = - NullableProperties.create(null, 23).getClass().getMethod("equals", Object.class); - AnnotatedType[] parameterTypes = equals.getAnnotatedParameterTypes(); - assertThat(parameterTypes).hasLength(1); - assertThat(parameterTypes[0].getAnnotation(Nullable.class)).isNotNull(); + NullableProperties nullableProperties = NullableProperties.create(null, 23); + Method equals = nullableProperties.getClass().getMethod("equals", Object.class); + // If `java.lang.Object.equals` is itself annotated, for example with the JSpecify `@Nullable`, + // then we will copy that annotation onto the parameter of the generated `equals` + // implementation. Otherwise we will copy the `@Nullable` from the return type of the + // `nullableString()` method. So we accept either @Nullable here. + // (You might think we could just reflect on Object.equals to see if it has @Nullable, but in + // some environments we have nullness annotations at compile time but not at run time.) + assertThat(equals.getAnnotatedParameterTypes()[0].getAnnotations()) + .asList() + .containsAnyIn(nullables(nullableProperties.getClass())); } @AutoAnnotation @@ -181,6 +196,26 @@ static Nullable nullable() { return new AutoAnnotation_AutoValueJava8Test_nullable(); } + /** + * Returns a set containing this test's {@link Nullable @Nullable} annotation, plus possibly + * another {@code @Nullable} that is present on the parameter of {@link Object#equals}. + */ + static ImmutableSet nullables(Class autoValueImplClass) { + try { + return Stream.concat( + Stream.of(nullable()), + stream( + autoValueImplClass + .getMethod("equals", Object.class) + .getAnnotatedParameterTypes()[0] + .getAnnotations()) + .filter(a -> a.annotationType().getSimpleName().equals("Nullable"))) + .collect(toImmutableSet()); + } catch (ReflectiveOperationException e) { + throw new LinkageError(e.getMessage(), e); + } + } + @Test public void testNullablePropertyImplementationIsNullable() throws NoSuchMethodException { Method method = @@ -275,6 +310,71 @@ public void testEqualsWithNullable() throws Exception { .testEquals(); } + interface GenericGrandparent { + T thing(); + } + + interface GenericParent extends GenericGrandparent { + @Override + @Nullable T thing(); + } + + @AutoValue + abstract static class StringThing implements GenericParent { + } + + @AutoValue + abstract static class StringThingWithBuilder implements GenericParent { + static Builder builder() { + return new AutoValue_AutoValueJava8Test_StringThingWithBuilder.Builder(); + } + + @AutoValue.Builder + interface Builder { + Builder setThing(String thing); + @Nullable String thing(); + StringThingWithBuilder build(); + } + } + + @Test + public void testInheritedGetterRemainsNullable() throws NoSuchMethodException { + // Ensure that the implementation has `@Nullable String thing()`. + StringThing instance = new AutoValue_AutoValueJava8Test_StringThing(null); + Method getter = instance.getClass().getDeclaredMethod("thing"); + assertThat(getter.getAnnotatedReturnType().getAnnotations()).asList().contains(nullable()); + } + + @Test + public void testInheritedBuilderGetterRemainsNullable() throws NoSuchMethodException { + StringThingWithBuilder instance = StringThingWithBuilder.builder().setThing(null).build(); + Method getter = instance.getClass().getDeclaredMethod("thing"); + assertThat(getter.getAnnotatedReturnType().getAnnotations()).asList().contains(nullable()); + } + + interface GenericListParent { + List<@Nullable T> things(); + } + + @AutoValue + abstract static class StringList implements GenericListParent {} + + // We'd like AutoValue to realize that the effective type of `things()` in `StringList` is + // `List<@Nullable String>`. Unfortunately it doesn't, because Types.asMemberOf deletes + // annotations. The workaround that we have to restore them only works for top-level annotations, + // like the `@Nullable T` in `GenericParent`, but not like the `List<@Nullable T>` here. + @Test + public void testInheritedListGetterRemainsNullable() throws NoSuchMethodException { + StringList instance = new AutoValue_AutoValueJava8Test_StringList(ImmutableList.of()); + Method getter = instance.getClass().getDeclaredMethod("things"); + AnnotatedParameterizedType returnType = + (AnnotatedParameterizedType) getter.getAnnotatedReturnType(); + assertThat(returnType.getAnnotatedActualTypeArguments()[0].getAnnotations()) + .asList() + .doesNotContain(nullable()); + // This should be .contains(nullable()). + } + public static class Nested {} @Retention(RetentionPolicy.RUNTIME) @@ -448,7 +548,7 @@ public void testOmitNullableWithBuilder() { assertThat(e).hasMessageThat().contains("notNullable"); NullablePropertyWithBuilder.Builder builder = NullablePropertyWithBuilder.builder(); - assertThat(builder.nullable()).isEmpty(); + Truth.assertThat(builder.nullable()).isEmpty(); } @AutoValue @@ -476,12 +576,12 @@ public void testOmitOptionalWithNullableBuilder() { OptionalPropertyWithNullableBuilder instance1 = OptionalPropertyWithNullableBuilder.builder().notOptional("hello").build(); assertThat(instance1.notOptional()).isEqualTo("hello"); - assertThat(instance1.optional()).isEmpty(); + Truth.assertThat(instance1.optional()).isEmpty(); OptionalPropertyWithNullableBuilder instance2 = OptionalPropertyWithNullableBuilder.builder().notOptional("hello").optional(null).build(); assertThat(instance2.notOptional()).isEqualTo("hello"); - assertThat(instance2.optional()).isEmpty(); + Truth.assertThat(instance2.optional()).isEmpty(); assertThat(instance1).isEqualTo(instance2); OptionalPropertyWithNullableBuilder instance3 = @@ -490,7 +590,7 @@ public void testOmitOptionalWithNullableBuilder() { .optional("world") .build(); assertThat(instance3.notOptional()).isEqualTo("hello"); - assertThat(instance3.optional()).hasValue("world"); + Truth.assertThat(instance3.optional()).hasValue("world"); assertThrows( IllegalStateException.class, () -> OptionalPropertyWithNullableBuilder.builder().build()); @@ -518,19 +618,19 @@ public interface Builder { public void testNullableOptional() { NullableOptionalPropertyWithNullableBuilder instance1 = NullableOptionalPropertyWithNullableBuilder.builder().build(); - assertThat(instance1.optional()).isNull(); + Truth.assertThat(instance1.optional()).isNull(); NullableOptionalPropertyWithNullableBuilder instance2 = NullableOptionalPropertyWithNullableBuilder.builder().optional(null).build(); - assertThat(instance2.optional()).isEmpty(); + Truth.assertThat(instance2.optional()).isEmpty(); NullableOptionalPropertyWithNullableBuilder instance3 = NullableOptionalPropertyWithNullableBuilder.builder().optional("haruspex").build(); - assertThat(instance3.optional()).hasValue("haruspex"); + Truth.assertThat(instance3.optional()).hasValue("haruspex"); NullableOptionalPropertyWithNullableBuilder.Builder builder = NullableOptionalPropertyWithNullableBuilder.builder(); - assertThat(builder.optional()).isNull(); + Truth.assertThat(builder.optional()).isNull(); } @AutoValue @@ -831,20 +931,20 @@ abstract static class Builder { @Test public void testOptionalOptional_empty() { OptionalOptional empty = OptionalOptional.builder().build(); - assertThat(empty.maybeJustMaybe()).isEmpty(); + Truth.assertThat(empty.maybeJustMaybe()).isEmpty(); } @Test public void testOptionalOptional_ofEmpty() { OptionalOptional ofEmpty = OptionalOptional.builder().maybeJustMaybe(Optional.empty()).build(); - assertThat(ofEmpty.maybeJustMaybe()).hasValue(Optional.empty()); + Truth.assertThat(ofEmpty.maybeJustMaybe()).hasValue(Optional.empty()); } @Test public void testOptionalOptional_ofSomething() { OptionalOptional ofSomething = OptionalOptional.builder().maybeJustMaybe(Optional.of("foo")).build(); - assertThat(ofSomething.maybeJustMaybe()).hasValue(Optional.of("foo")); + Truth.assertThat(ofSomething.maybeJustMaybe()).hasValue(Optional.of("foo")); } @AutoValue @@ -867,7 +967,7 @@ abstract static class Builder { public void testOptionalExtends() { Predicate predicate = n -> n.toString().equals("0"); OptionalExtends t = OptionalExtends.builder().setPredicate(predicate).build(); - assertThat(t.predicate()).hasValue(predicate); + Truth.assertThat(t.predicate()).hasValue(predicate); } @AutoValue @@ -1047,4 +1147,63 @@ public void nullableVariableBound() { assertThat(x.nullOne()).isNull(); assertThat(x.nullTwo()).isNull(); } + + @AutoValue + public abstract static class NotNullableVariableBound { + public abstract T t(); + + public abstract @Nullable T nullableT(); + + public abstract String string(); + + public static Builder builder() { + return new AutoValue_AutoValueJava8Test_NotNullableVariableBound.Builder<>(); + } + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setT(T t); + + public abstract Builder setNullableT(@Nullable T nullableT); + + public abstract Builder setString(String string); + + public abstract NotNullableVariableBound build(); + } + } + + @Test + public void typeParameterBuilderFieldsAreNullable() throws ReflectiveOperationException { + assertThrows(NullPointerException.class, () -> NotNullableVariableBound.builder().setT(null)); + + // Even though neither t() nor string() has a @Nullable return type, the corresponding builder + // fields should be @Nullable. This test depends on the knowledge that for a property `t`, we + // will have a field also called `t`. + // For `nullableT`, the @Nullable in question should be the same as the one on the return type + // of nullableT(). For `t`, it is a @Nullable that AutoValue found somewhere, either there or + // possibly on the parameter of the inherited Object.equals method. + Field builderT = NotNullableVariableBound.builder().getClass().getDeclaredField("t"); + assertThat(builderT.getAnnotatedType().getAnnotations()) + .asList() + .containsAnyIn(nullables(AutoValue_AutoValueJava8Test_NotNullableVariableBound.class)); + Field builderNullableT = + NotNullableVariableBound.builder().getClass().getDeclaredField("nullableT"); + assertThat(builderNullableT.getAnnotatedType().getAnnotations()).asList().contains(nullable()); + + // Meanwhile the AutoValue class itself should have @Nullable on the private field, the getter + // method, and the constructor parameter for nullableT. This @Nullable should be the same as + // the one on the return type of nullableT(). + Class autoValueClass = AutoValue_AutoValueJava8Test_NotNullableVariableBound.class; + Field nullableTField = autoValueClass.getDeclaredField("nullableT"); + assertThat(nullableTField.getAnnotatedType().getAnnotations()).asList().contains(nullable()); + Method nullableTMethod = autoValueClass.getMethod("nullableT"); + assertThat(nullableTMethod.getAnnotatedReturnType().getAnnotations()) + .asList() + .contains(nullable()); + Constructor autoValueConstructor = + autoValueClass.getDeclaredConstructor(Object.class, Object.class, String.class); + assertThat(autoValueConstructor.getAnnotatedParameterTypes()[1].getAnnotations()) + .asList() + .contains(nullable()); + } } diff --git a/value/src/it/functional/src/test/java/com/google/auto/value/AutoValueNotEclipseTest.java b/value/src/it/functional/src/test/java/com/google/auto/value/AutoValueNotEclipseTest.java index 4d008c713a..de8eb80a1b 100644 --- a/value/src/it/functional/src/test/java/com/google/auto/value/AutoValueNotEclipseTest.java +++ b/value/src/it/functional/src/test/java/com/google/auto/value/AutoValueNotEclipseTest.java @@ -15,7 +15,7 @@ */ package com.google.auto.value; -import static com.google.common.truth.Truth8.assertThat; +import static com.google.common.truth.Truth.assertThat; import java.util.Optional; import javax.annotation.Nullable; diff --git a/value/src/it/functional/src/test/java/com/google/auto/value/CompileWithEclipseTest.java b/value/src/it/functional/src/test/java/com/google/auto/value/CompileWithEclipseTest.java index a42929076e..a46ca245cc 100644 --- a/value/src/it/functional/src/test/java/com/google/auto/value/CompileWithEclipseTest.java +++ b/value/src/it/functional/src/test/java/com/google/auto/value/CompileWithEclipseTest.java @@ -80,32 +80,16 @@ public static void setSourceRoot() { private static final Predicate JAVA_FILE = f -> f.getName().endsWith(".java") && !IGNORED_TEST_FILES.contains(f.getName()); - private static final ImmutableSet JAVA8_TEST_FILES = - ImmutableSet.of( - "AutoBuilderTest.java", - "AutoOneOfJava8Test.java", - "AutoValueJava8Test.java", - "EmptyExtension.java"); - private static final Predicate JAVA8_TEST = f -> JAVA8_TEST_FILES.contains(f.getName()); - @Test - public void compileWithEclipseJava7() throws Exception { - compileWithEclipse("7", JAVA_FILE.and(JAVA8_TEST.negate())); - } - - @Test - public void compileWithEclipseJava8() throws Exception { - compileWithEclipse("8", JAVA_FILE); - } - - private void compileWithEclipse(String version, Predicate predicate) throws IOException { + public void compileWithEclipse() throws IOException { + String version = "8"; File sourceRootFile = new File(SOURCE_ROOT); File javaDir = new File(sourceRootFile, "src/main/java"); File javatestsDir = new File(sourceRootFile, "src/test/java"); Set sources = new ImmutableSet.Builder() - .addAll(filesUnderDirectory(javaDir, predicate)) - .addAll(filesUnderDirectory(javatestsDir, predicate)) + .addAll(filesUnderDirectory(javaDir, JAVA_FILE)) + .addAll(filesUnderDirectory(javatestsDir, JAVA_FILE)) .build(); assertThat(sources).isNotEmpty(); JavaCompiler compiler = new EclipseCompiler(); diff --git a/value/src/it/gwtserializer/pom.xml b/value/src/it/gwtserializer/pom.xml index 91aade2127..68ac3854cd 100644 --- a/value/src/it/gwtserializer/pom.xml +++ b/value/src/it/gwtserializer/pom.xml @@ -22,21 +22,21 @@ com.google.auto.value auto-value-parent - HEAD-SNAPSHOT + 1.11.0 ../../../pom.xml https://github.com/google/auto/tree/main/value com.google.auto.value.it.gwtserializer gwtserializer - HEAD-SNAPSHOT + 1.11.0 Auto-Value GWT-RPC Serialization Integration Test org.gwtproject gwt - 2.10.0 + 2.11.0 pom import @@ -94,12 +94,12 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.13.0 org.codehaus.plexus plexus-java - 1.1.2 + 1.2.0 @@ -132,7 +132,7 @@ net.ltgt.gwt.maven gwt-maven-plugin - 1.0.1 + 1.1.0 @@ -144,7 +144,7 @@ org.apache.maven.plugins maven-deploy-plugin - 3.1.1 + 3.1.2 true @@ -160,7 +160,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.3.0 + 3.4.1 default-jar diff --git a/value/src/main/java/com/google/auto/value/extension/AutoValueExtension.java b/value/src/main/java/com/google/auto/value/extension/AutoValueExtension.java index 375864e30f..7b5e40f7ec 100644 --- a/value/src/main/java/com/google/auto/value/extension/AutoValueExtension.java +++ b/value/src/main/java/com/google/auto/value/extension/AutoValueExtension.java @@ -41,10 +41,10 @@ * compiler's {@code -classpath} or {@code -processorpath}. * *

When the AutoValue processor runs for a class {@code Foo}, it will ask each Extension whether - * it is {@linkplain #applicable applicable}. Suppose two Extensions reply that they are. Then - * the processor will generate the AutoValue logic in a direct subclass of {@code Foo}, and it - * will ask the first Extension to generate a subclass of that, and the second Extension to generate - * a subclass of the subclass. So we might have this hierarchy: + * it is {@linkplain #applicable applicable}. Suppose two Extensions reply that they are. Then the + * processor will generate the AutoValue logic in a direct subclass of {@code Foo}, and it will ask + * the first Extension to generate a subclass of that, and the second Extension to generate a + * subclass of the subclass. So we might have this hierarchy: * *

  * @AutoValue abstract class Foo {...}                          // the hand-written class
@@ -63,8 +63,8 @@
  * 

The first generated class in the hierarchy will always be the one generated by the AutoValue * processor and the last one will always be the one generated by the Extension that {@code * mustBeFinal}, if any. Other than that, the order of the classes in the hierarchy is unspecified. - * The last class in the hierarchy is {@code AutoValue_Foo} and that is the one that the - * {@code Foo} class will reference, for example with {@code new AutoValue_Foo(...)}. + * The last class in the hierarchy is {@code AutoValue_Foo} and that is the one that the {@code Foo} + * class will reference, for example with {@code new AutoValue_Foo(...)}. * *

Each Extension must also be sure to generate a constructor with arguments corresponding to all * properties in {@link com.google.auto.value.extension.AutoValueExtension.Context#propertyTypes()}, @@ -72,8 +72,8 @@ * have at least package visibility. * *

Because the class generated by the AutoValue processor is at the top of the generated - * hierarchy, Extensions can override its methods, for example {@code hashCode()}, - * {@code toString()}, or the implementations of the various {@code bar()} property methods. + * hierarchy, Extensions can override its methods, for example {@code hashCode()}, {@code + * toString()}, or the implementations of the various {@code bar()} property methods. */ public abstract class AutoValueExtension { @@ -99,10 +99,10 @@ public interface Context { /** * The fully-qualified name of the last class in the {@code AutoValue} hierarchy. For an - * {@code @AutoValue} class {@code foo.bar.Baz}, this will be {@code foo.bar.AutoValue_Baz}. - * The class may be generated by an extension, which will be the current extension if the - * {@code isFinal} parameter to {@link AutoValueExtension#generateClass} is true and the - * returned string is not {@code null}. + * {@code @AutoValue} class {@code foo.bar.Baz}, this will be {@code foo.bar.AutoValue_Baz}. The + * class may be generated by an extension, which will be the current extension if the {@code + * isFinal} parameter to {@link AutoValueExtension#generateClass} is true and the returned + * string is not {@code null}. * *

For compatibility reasons, this method has a default implementation that throws an * exception. The AutoValue processor supplies an implementation that behaves as documented. @@ -159,6 +159,14 @@ default Map propertyTypes() { */ Set abstractMethods(); + /** + * Returns the complete set of abstract methods defined in or inherited by the {@code @Builder} + * class. This includes methods that have been consumed by this or another Extension. + * + *

If there is no builder class, then this set is empty. + */ + Set builderAbstractMethods(); + /** * Returns the complete list of annotations defined on the {@code classToCopyFrom} that should * be added to any generated subclass. Only annotations visible to the {@code @AutoValue} will @@ -203,9 +211,7 @@ default Optional builder() { } } - /** - * Represents a {@code Builder} associated with an {@code @AutoValue} class. - */ + /** Represents a {@code Builder} associated with an {@code @AutoValue} class. */ public interface BuilderContext { /** * Returns the {@code @AutoValue.Builder} interface or abstract class that this object @@ -218,6 +224,7 @@ public interface BuilderContext { * type. * *

Consider a class like this: + * *

      *   {@code @AutoValue} abstract class Foo {
      *     abstract String bar();
@@ -230,8 +237,8 @@ public interface BuilderContext {
      *   }
      * 
* - *

Here {@code toBuilderMethods()} will return a set containing the method - * {@code Foo.toBuilder()}. + *

Here {@code toBuilderMethods()} will return a set containing the method {@code + * Foo.toBuilder()}. */ Set toBuilderMethods(); @@ -240,6 +247,7 @@ public interface BuilderContext { * type. * *

Consider a class like this: + * *

      *   {@code @AutoValue} abstract class Foo {
      *     abstract String bar();
@@ -257,19 +265,19 @@ public interface BuilderContext {
      *   }
      * 
* - *

Here {@code builderMethods()} will return a set containing the method - * {@code Foo.builder()}. Generated code should usually call this method in preference to - * constructing {@code AutoValue_Foo.Builder()} directly, because this method can establish - * default values for properties, as it does here. + *

Here {@code builderMethods()} will return a set containing the method {@code + * Foo.builder()}. Generated code should usually call this method in preference to constructing + * {@code AutoValue_Foo.Builder()} directly, because this method can establish default values + * for properties, as it does here. */ Set builderMethods(); /** * Returns the method {@code build()} in the builder class, if it exists and returns the - * {@code @AutoValue} type. This is the method that generated code for - * {@code @AutoValue class Foo} should call in order to get an instance of {@code Foo} from its - * builder. The returned method is called {@code build()}; if the builder uses some other name - * then extensions have no good way to guess how they should build. + * {@code @AutoValue} type. This is the method that generated code for {@code @AutoValue class + * Foo} should call in order to get an instance of {@code Foo} from its builder. The returned + * method is called {@code build()}; if the builder uses some other name then extensions have no + * good way to guess how they should build. * *

A common convention is for {@code build()} to be a concrete method in the * {@code @AutoValue.Builder} class, which calls an abstract method {@code autoBuild()} that is @@ -281,9 +289,9 @@ public interface BuilderContext { /** * Returns the abstract build method. If the {@code @AutoValue} class is {@code Foo}, this is an * abstract no-argument method in the builder class that returns {@code Foo}. This might be - * called {@code build()}, or, following a common convention, it might be called - * {@code autoBuild()} and used in the implementation of a {@code build()} method that is - * defined in the builder class. + * called {@code build()}, or, following a common convention, it might be called {@code + * autoBuild()} and used in the implementation of a {@code build()} method that is defined in + * the builder class. * *

Extensions should call the {@code build()} method in preference to this one. But they * should override this one if they want to customize build-time behaviour. @@ -292,19 +300,18 @@ public interface BuilderContext { /** * Returns a map from property names to the corresponding setters. A property may have more than - * one setter. For example, an {@code ImmutableList} might be set by - * {@code setFoo(ImmutableList)} and {@code setFoo(String[])}. + * one setter. For example, an {@code ImmutableList} might be set by {@code + * setFoo(ImmutableList)} and {@code setFoo(String[])}. */ Map> setters(); /** * Returns a map from property names to property builders. For example, if there is a property - * {@code foo} defined by {@code abstract ImmutableList foo();} or - * {@code abstract ImmutableList getFoo();} in the {@code @AutoValue} class, - * then there can potentially be a builder defined by - * {@code abstract ImmutableList.Builder fooBuilder();} in the - * {@code @AutoValue.Builder} class. This map would then map {@code "foo"} to the - * {@link ExecutableElement} representing {@code fooBuilder()}. + * {@code foo} defined by {@code abstract ImmutableList foo();} or {@code abstract + * ImmutableList getFoo();} in the {@code @AutoValue} class, then there can potentially + * be a builder defined by {@code abstract ImmutableList.Builder fooBuilder();} in the + * {@code @AutoValue.Builder} class. This map would then map {@code "foo"} to the {@link + * ExecutableElement} representing {@code fooBuilder()}. */ Map propertyBuilders(); } @@ -328,8 +335,8 @@ public enum IncrementalExtensionType { /** * This extension is aggregating, meaning that it may generate outputs based on several - * annotated input classes and it respects the constraints imposed on aggregating processors. - * It is unusual for AutoValue extensions to be aggregating. + * annotated input classes and it respects the constraints imposed on aggregating processors. It + * is unusual for AutoValue extensions to be aggregating. * * @see Gradle @@ -436,10 +443,10 @@ public Set consumeProperties(Context context) { } /** - * Returns a possible empty set of abstract methods that this Extension intends to implement. This + * Returns a possibly empty set of abstract methods that this Extension intends to implement. This * will prevent AutoValue from generating an implementation, in cases where it would have, and it - * will also avoid warnings about abstract methods that AutoValue doesn't expect. The default set - * returned by this method is empty. + * will also avoid complaints about abstract methods that AutoValue doesn't expect. The default + * set returned by this method is empty. * *

Each returned method must be one of the abstract methods in {@link * Context#abstractMethods()}. @@ -457,6 +464,21 @@ public Set consumeMethods(Context context) { return ImmutableSet.of(); } + /** + * Returns a possibly empty set of abstract methods that this Extension intends to implement. This + * will prevent AutoValue from generating an implementation, in cases where it would have, and it + * will also avoid complaints about abstract methods that AutoValue doesn't expect. The default + * set returned by this method is empty. + * + *

Each returned method must be one of the abstract methods in {@link + * Context#builderAbstractMethods()}. + * + * @param context the Context of the code generation for this class. + */ + public Set consumeBuilderMethods(Context context) { + return ImmutableSet.of(); + } + /** * Returns the generated source code of the class named {@code className} to extend {@code * classToExtend}, or {@code null} if this extension does not generate a class in the hierarchy. @@ -476,7 +498,8 @@ public Set consumeMethods(Context context) { * ... * } * ... - * }}

+ * } + * } * *

Here, {@code } is {@link Context#packageName()}; {@code } is the * keyword {@code final} if {@code isFinal} is true or {@code abstract} otherwise; and {@code @@ -484,6 +507,26 @@ public Set consumeMethods(Context context) { * name. The {@code } and {@code } are typically * derived from {@link Context#propertyTypes()}. * + *

An extension can also generate a subclass of the nested {@code Builder} class if there is + * one. In that case, it should check if {@link BuilderContext#toBuilderMethods()} is empty. If + * not, the {@code Builder} subclass should include a "copy constructor", like this: + * + *

{@code
+   * ...
+   *  class  extends  {
+   *   ...
+   *   static class Builder extends .Builder {
+   *     Builder() {}
+   *     Builder( copyFrom) {
+   *       super(copyFrom);
+   *     }
+   *     ...
+   *   }
+   * }
+   * }
+ * + *

Here, {@code } is {@link Context#autoValueClass()}.} + * * @param context The {@link Context} of the code generation for this class. * @param className The simple name of the resulting class. The returned code will be written to a * file named accordingly. diff --git a/value/src/main/java/com/google/auto/value/extension/memoized/Memoized.java b/value/src/main/java/com/google/auto/value/extension/memoized/Memoized.java index cf24cb74f3..6eefd07c35 100644 --- a/value/src/main/java/com/google/auto/value/extension/memoized/Memoized.java +++ b/value/src/main/java/com/google/auto/value/extension/memoized/Memoized.java @@ -47,7 +47,7 @@ * href="https://errorprone.info/bugpattern/DoubleCheckedLocking">double-checked locking to * ensure that the annotated method is called at most once. * - *

Example

+ *

Example

* *
  *   {@code @AutoValue}
diff --git a/value/src/main/java/com/google/auto/value/extension/toprettystring/ToPrettyString.java b/value/src/main/java/com/google/auto/value/extension/toprettystring/ToPrettyString.java
index cd2762a25b..9532fc0e46 100644
--- a/value/src/main/java/com/google/auto/value/extension/toprettystring/ToPrettyString.java
+++ b/value/src/main/java/com/google/auto/value/extension/toprettystring/ToPrettyString.java
@@ -41,7 +41,7 @@
  *
  * 

{@code @ToPrettyString} is valid on overridden {@code toString()} and other methods alike. * - *

Example

+ *

Example

* *
  *   {@code @AutoValue}
diff --git a/value/src/main/java/com/google/auto/value/processor/AnnotatedTypeMirror.java b/value/src/main/java/com/google/auto/value/processor/AnnotatedTypeMirror.java
new file mode 100644
index 0000000000..1b0e83b78d
--- /dev/null
+++ b/value/src/main/java/com/google/auto/value/processor/AnnotatedTypeMirror.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.auto.value.processor;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import java.util.Objects;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * A {@link TypeMirror} and associated annotations.
+ *
+ * 

The reason this class is needed is that certain methods in the {@code javax.lang.model} API + * return modified versions of types, for example {@link javax.lang.model.util.Types#asMemberOf}. + * Historically, Java compilers were a bit inconsistent about whether those modified types preserve + * annotations that were in the original type, but the recent consensus is that they should not. + * Suppose for example if we have this: + * + *

{@code
+ * interface Parent {
+ *   @Nullable T thing();
+ * }
+ * abstract class Child implements Parent {}
+ * }
+ * + *

If we use {@code asMemberOf} to determine what the return type of {@code Child.thing()} is, we + * will discover it is {@code String}. But we really wanted {@code @Nullable String}. To fix that, + * we combine the annotations from {@code Parent.thing()} with the resolved type from {@code + * Child.thing()}. + * + *

This is only a partial workaround. We aren't able to splice the {@code @Nullable} from {@code + * List<@Nullable T>} into a type like {@code List} that might be returned from {@code + * asMemberOf}. Probably a more complete solution would be to adapt the type substitution logic in + * {@link TypeVariables} so that it can be used instead of {@code Types.asMemberOf} and so that it + * reattaches annotations on type parameters to the corresponding type arguments, like the {@code + * List<@Nullable String>} in the example. + * + *

https://bugs.openjdk.org/browse/JDK-8174126 would potentially provide the basis for a cleaner + * solution, via a new {@code Types.withAnnotations(type, annotations)} method. + * + *

This class deliberately does not implement {@link TypeMirror}. Making "mutant" {@code + * TypeMirror} instances is a bit dangerous, because if you give such a thing to one of the {@code + * javax.lang.model} APIs then you will almost certainly get a {@code ClassCastException}. Those + * APIs only expect objects that they themselves produced. + */ +final class AnnotatedTypeMirror { + private final TypeMirror originalType; + private final TypeMirror rewrittenType; + + AnnotatedTypeMirror(TypeMirror originalType, TypeMirror rewrittenType) { + this.originalType = originalType; + this.rewrittenType = rewrittenType; + } + + AnnotatedTypeMirror(TypeMirror type) { + this(type, type); + } + + ImmutableList annotations() { + return ImmutableList.copyOf(originalType.getAnnotationMirrors()); + } + + TypeMirror getType() { + return rewrittenType; + } + + TypeKind getKind() { + return rewrittenType.getKind(); + } + + @Override + public String toString() { + String annotations = Joiner.on(' ').join(originalType.getAnnotationMirrors()); + return annotations.isEmpty() ? rewrittenType.toString() : annotations + " " + rewrittenType; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof AnnotatedTypeMirror) { + AnnotatedTypeMirror that = (AnnotatedTypeMirror) obj; + // This is just for tests. If we wanted to have a genuinely useful `equals` method we would + // probably want it to use something like `Types.isSameType`. + return this.originalType == that.originalType && this.rewrittenType == that.rewrittenType; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(originalType, rewrittenType); + } +} diff --git a/value/src/main/java/com/google/auto/value/processor/AutoBuilderProcessor.java b/value/src/main/java/com/google/auto/value/processor/AutoBuilderProcessor.java index 607e7367b9..9699975cb8 100644 --- a/value/src/main/java/com/google/auto/value/processor/AutoBuilderProcessor.java +++ b/value/src/main/java/com/google/auto/value/processor/AutoBuilderProcessor.java @@ -21,11 +21,9 @@ import static com.google.auto.common.MoreStreams.toImmutableList; import static com.google.auto.common.MoreStreams.toImmutableMap; import static com.google.auto.common.MoreStreams.toImmutableSet; -import static com.google.auto.common.MoreTypes.asTypeElement; import static com.google.auto.value.processor.AutoValueProcessor.OMIT_IDENTIFIERS_OPTION; import static com.google.auto.value.processor.ClassNames.AUTO_ANNOTATION_NAME; import static com.google.auto.value.processor.ClassNames.AUTO_BUILDER_NAME; -import static com.google.auto.value.processor.ClassNames.KOTLIN_METADATA_NAME; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toMap; import static javax.lang.model.util.ElementFilter.constructorsIn; @@ -48,7 +46,6 @@ import java.lang.reflect.Field; import java.util.AbstractMap.SimpleEntry; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.NavigableSet; @@ -72,12 +69,6 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.tools.JavaFileObject; -import kotlinx.metadata.Flag; -import kotlinx.metadata.KmClass; -import kotlinx.metadata.KmConstructor; -import kotlinx.metadata.KmValueParameter; -import kotlinx.metadata.jvm.KotlinClassHeader; -import kotlinx.metadata.jvm.KotlinClassMetadata; import net.ltgt.gradle.incap.IncrementalAnnotationProcessor; import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType; @@ -104,11 +95,13 @@ public Set getSupportedOptions() { } private TypeMirror javaLangVoid; + private KotlinMetadata kotlinMetadata; @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); javaLangVoid = elementUtils().getTypeElement("java.lang.Void").asType(); + kotlinMetadata = new KotlinMetadata(errorReporter()); } // The handling of @AutoBuilder to generate annotation implementations needs some explanation. @@ -298,13 +291,20 @@ private ImmutableSet propertySet( .map( v -> { String name = v.getSimpleName().toString(); - return newProperty( - v, - identifiers.get(v), - propertyToGetterName.get(name), - Optional.ofNullable(builderInitializers.get(name)), - executable.isOptional(name), - nullables); + Property p = + newProperty( + v, + identifiers.get(v), + propertyToGetterName.get(name), + Optional.ofNullable(builderInitializers.get(name)), + executable.isOptional(name), + nullables); + if (p.isNullable() && v.asType().getKind().isPrimitive()) { + errorReporter() + .reportError( + v, "[AutoBuilderNullPrimitive] Primitive types cannot be @Nullable"); + } + return p; }) .collect(toImmutableSet()); } @@ -323,7 +323,7 @@ private Property newProperty( name, identifier, TypeEncoder.encode(type), - type, + new AnnotatedTypeMirror(type), nullableAnnotation, nullables, getterName, @@ -457,12 +457,13 @@ private Executable findExecutable( private ImmutableList findRelevantExecutables( TypeElement ofClass, String callMethod, TypeElement autoBuilderType) { - Optional kotlinMetadata = kotlinMetadataAnnotation(ofClass); + Optional kotlinMetadataAnnotation = + kotlinMetadata.kotlinMetadataAnnotation(ofClass); List elements = ofClass.getEnclosedElements(); Stream relevantExecutables = callMethod.isEmpty() - ? kotlinMetadata - .map(a -> kotlinConstructorsIn(a, ofClass).stream()) + ? kotlinMetadataAnnotation + .map(a -> kotlinMetadata.kotlinConstructorsIn(a, ofClass).stream()) .orElseGet(() -> constructorsIn(elements).stream().map(Executable::of)) : methodsIn(elements).stream() .filter(m -> m.getSimpleName().contentEquals(callMethod)) @@ -575,91 +576,6 @@ private boolean visibleFrom(Element element, PackageElement fromPackage) { } } - private Optional kotlinMetadataAnnotation(Element element) { - // It would be MUCH simpler if we could just use ofClass.getAnnotation(Metadata.class). - // However that would be unsound. We want to shade the Kotlin runtime, including - // kotlin.Metadata, so as not to interfere with other things on the annotation classpath that - // might have a different version of the runtime. That means that if we referenced - // kotlin.Metadata.class here we would actually be referencing - // autovalue.shaded.kotlin.Metadata.class. Obviously the Kotlin class doesn't have that - // annotation. - return element.getAnnotationMirrors().stream() - .filter( - a -> - asTypeElement(a.getAnnotationType()) - .getQualifiedName() - .contentEquals(KOTLIN_METADATA_NAME)) - .map(a -> a) // get rid of that stupid wildcard - .findFirst(); - } - - /** - * Use Kotlin reflection to build {@link Executable} instances for the constructors in {@code - * ofClass} that include information about which parameters have default values. - */ - private ImmutableList kotlinConstructorsIn( - AnnotationMirror metadata, TypeElement ofClass) { - ImmutableMap annotationValues = - AnnotationMirrors.getAnnotationValuesWithDefaults(metadata).entrySet().stream() - .collect(toImmutableMap(e -> e.getKey().getSimpleName().toString(), e -> e.getValue())); - // We match the KmConstructor instances with the ExecutableElement instances based on the - // parameter names. We could possibly just assume that the constructors are in the same order. - Map, ExecutableElement> map = - constructorsIn(ofClass.getEnclosedElements()).stream() - .collect(toMap(c -> parameterNames(c), c -> c, (a, b) -> a, LinkedHashMap::new)); - ImmutableMap, ExecutableElement> paramNamesToConstructor = - ImmutableMap.copyOf(map); - KotlinClassHeader header = - new KotlinClassHeader( - (Integer) annotationValues.get("k").getValue(), - intArrayValue(annotationValues.get("mv")), - stringArrayValue(annotationValues.get("d1")), - stringArrayValue(annotationValues.get("d2")), - (String) annotationValues.get("xs").getValue(), - (String) annotationValues.get("pn").getValue(), - (Integer) annotationValues.get("xi").getValue()); - KotlinClassMetadata.Class classMetadata = - (KotlinClassMetadata.Class) KotlinClassMetadata.read(header); - KmClass kmClass = classMetadata.toKmClass(); - ImmutableList.Builder kotlinConstructorsBuilder = ImmutableList.builder(); - for (KmConstructor constructor : kmClass.getConstructors()) { - ImmutableSet.Builder allBuilder = ImmutableSet.builder(); - ImmutableSet.Builder optionalBuilder = ImmutableSet.builder(); - for (KmValueParameter param : constructor.getValueParameters()) { - String name = param.getName(); - allBuilder.add(name); - if (Flag.ValueParameter.DECLARES_DEFAULT_VALUE.invoke(param.getFlags())) { - optionalBuilder.add(name); - } - } - ImmutableSet optional = optionalBuilder.build(); - ImmutableSet all = allBuilder.build(); - ExecutableElement javaConstructor = paramNamesToConstructor.get(all); - if (javaConstructor != null) { - kotlinConstructorsBuilder.add(Executable.of(javaConstructor, optional)); - } - } - return kotlinConstructorsBuilder.build(); - } - - private static int[] intArrayValue(AnnotationValue value) { - @SuppressWarnings("unchecked") - List list = (List) value.getValue(); - return list.stream().mapToInt(v -> (int) v.getValue()).toArray(); - } - - private static String[] stringArrayValue(AnnotationValue value) { - @SuppressWarnings("unchecked") - List list = (List) value.getValue(); - return list.stream().map(AnnotationValue::getValue).toArray(String[]::new); - } - - private static ImmutableSet parameterNames(ExecutableElement executableElement) { - return executableElement.getParameters().stream() - .map(v -> v.getSimpleName().toString()) - .collect(toImmutableSet()); - } - private static final ElementKind ELEMENT_KIND_RECORD = elementKindRecord(); private static ElementKind elementKindRecord() { @@ -781,7 +697,7 @@ private static Property annotationBuilderProperty( name, name, TypeEncoder.encode(type), - type, + new AnnotatedTypeMirror(type), /* nullableAnnotation= */ Optional.empty(), nullables, /* getter= */ "", diff --git a/value/src/main/java/com/google/auto/value/processor/AutoOneOfProcessor.java b/value/src/main/java/com/google/auto/value/processor/AutoOneOfProcessor.java index 72550b712c..343fd34989 100644 --- a/value/src/main/java/com/google/auto/value/processor/AutoOneOfProcessor.java +++ b/value/src/main/java/com/google/auto/value/processor/AutoOneOfProcessor.java @@ -99,7 +99,7 @@ void processType(TypeElement autoOneOfType) { Set otherMethods = new LinkedHashSet<>(abstractMethods); otherMethods.remove(kindGetter); - ImmutableMap propertyMethodsAndTypes = + ImmutableMap propertyMethodsAndTypes = propertyMethodsIn(otherMethods, autoOneOfType); ImmutableBiMap properties = propertyNameToMethodMap(propertyMethodsAndTypes.keySet()); @@ -257,7 +257,7 @@ && objectMethodToOverride(method) == ObjectMethod.NONE) { private void defineVarsForType( TypeElement type, AutoOneOfTemplateVars vars, - ImmutableMap propertyMethodsAndTypes, + ImmutableMap propertyMethodsAndTypes, ExecutableElement kindGetter, Nullables nullables) { vars.props = diff --git a/value/src/main/java/com/google/auto/value/processor/AutoValueOrBuilderTemplateVars.java b/value/src/main/java/com/google/auto/value/processor/AutoValueOrBuilderTemplateVars.java index db711857bd..b2aad04109 100644 --- a/value/src/main/java/com/google/auto/value/processor/AutoValueOrBuilderTemplateVars.java +++ b/value/src/main/java/com/google/auto/value/processor/AutoValueOrBuilderTemplateVars.java @@ -147,4 +147,10 @@ abstract class AutoValueOrBuilderTemplateVars extends AutoValueishTemplateVars { * subclasses) */ Boolean isFinal = false; + + /** + * The modifiers (for example {@code final} or {@code abstract}) for the generated builder + * subclass, followed by a space if they are not empty. + */ + String builderClassModifiers = ""; } 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 73815f283a..12574d2305 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 @@ -206,24 +206,36 @@ void processType(TypeElement type) { BuilderSpec builderSpec = new BuilderSpec(type, processingEnv, errorReporter()); Optional builder = builderSpec.getBuilder(); ImmutableSet toBuilderMethods; + ImmutableSet builderAbstractMethods; if (builder.isPresent()) { toBuilderMethods = builder.get().toBuilderMethods(typeUtils(), type, abstractMethods); + builderAbstractMethods = builder.get().builderAbstractMethods(); } else { toBuilderMethods = ImmutableSet.of(); + builderAbstractMethods = ImmutableSet.of(); } - ImmutableMap propertyMethodsAndTypes = + ImmutableMap propertyMethodsAndTypes = propertyMethodsIn(immutableSetDifference(abstractMethods, toBuilderMethods), type); ImmutableMap properties = propertyNameToMethodMap(propertyMethodsAndTypes.keySet()); ExtensionContext context = new ExtensionContext( - processingEnv, type, properties, propertyMethodsAndTypes, abstractMethods); + this, + processingEnv, + type, + properties, + propertyMethodsAndTypes, + abstractMethods, + builderAbstractMethods); ImmutableList applicableExtensions = applicableExtensions(type, context); ImmutableSet consumedMethods = methodsConsumedByExtensions( type, applicableExtensions, context, abstractMethods, properties); + ImmutableSet consumedBuilderMethods = + builderMethodsConsumedByExtensions( + type, applicableExtensions, context, builderAbstractMethods); if (!consumedMethods.isEmpty()) { ImmutableSet allAbstractMethods = abstractMethods; @@ -234,7 +246,13 @@ void processType(TypeElement type) { properties = propertyNameToMethodMap(propertyMethodsAndTypes.keySet()); context = new ExtensionContext( - processingEnv, type, properties, propertyMethodsAndTypes, allAbstractMethods); + this, + processingEnv, + type, + properties, + propertyMethodsAndTypes, + allAbstractMethods, + builderAbstractMethods); } ImmutableSet propertyMethods = propertyMethodsAndTypes.keySet(); @@ -252,7 +270,8 @@ void processType(TypeElement type) { toBuilderMethods, propertyMethodsAndTypes, builder, - nullables); + nullables, + consumedBuilderMethods); vars.builtType = vars.origClass + vars.actualTypes; vars.build = "new " + finalSubclass + vars.actualTypes; @@ -268,8 +287,13 @@ void processType(TypeElement type) { int subclassDepth = writeExtensions(type, context, applicableExtensions); String subclass = generatedSubclassName(type, subclassDepth); vars.subclass = TypeSimplifier.simpleNameOf(subclass); + vars.finalSubclass = finalSubclass; vars.isFinal = (subclassDepth == 0); vars.modifiers = vars.isFinal ? "final " : "abstract "; + vars.builderClassModifiers = + consumedBuilderMethods.isEmpty() + ? vars.isFinal ? "static final " : "static " + : "abstract static "; String text = vars.toText(); text = TypeEncoder.decode(text, processingEnv, vars.pkg, type.asType()); @@ -393,6 +417,40 @@ private ImmutableSet methodsConsumedByExtensions( return ImmutableSet.copyOf(consumed); } + private ImmutableSet builderMethodsConsumedByExtensions( + TypeElement type, + ImmutableList applicableExtensions, + ExtensionContext context, + ImmutableSet builderAbstractMethods) { + Set consumed = new HashSet<>(); + for (AutoValueExtension extension : applicableExtensions) { + Set consumedHere = new HashSet<>(); + for (ExecutableElement consumedMethod : extension.consumeBuilderMethods(context)) { + if (!builderAbstractMethods.contains(consumedMethod)) { + errorReporter() + .reportError( + type, + "[AutoValueBuilderConsumeNotAbstract] Extension %s wants to consume a method that" + + " is not one of the abstract methods in this class: %s", + extensionName(extension), + consumedMethod); + } else { + consumedHere.add(consumedMethod); + } + } + for (ExecutableElement repeat : intersection(consumed, consumedHere)) { + errorReporter() + .reportError( + repeat, + "[AutoValueBuilderConsumeNotAbstract] Extension %s wants to consume a method that" + + " was already consumed by another extension", + extensionName(extension)); + } + consumed.addAll(consumedHere); + } + return ImmutableSet.copyOf(consumed); + } + private void validateMethods( TypeElement type, ImmutableSet abstractMethods, @@ -428,9 +486,10 @@ private void defineVarsForType( TypeElement type, AutoValueTemplateVars vars, ImmutableSet toBuilderMethods, - ImmutableMap propertyMethodsAndTypes, + ImmutableMap propertyMethodsAndTypes, Optional maybeBuilder, - Nullables nullables) { + Nullables nullables, + ImmutableSet consumedBuilderAbstractMethods) { ImmutableSet propertyMethods = propertyMethodsAndTypes.keySet(); vars.toBuilderMethods = toBuilderMethods.stream().map(SimpleMethod::new).collect(toImmutableList()); @@ -438,7 +497,7 @@ private void defineVarsForType( ImmutableListMultimap annotatedPropertyFields = propertyFieldAnnotationMap(type, propertyMethods); ImmutableListMultimap annotatedPropertyMethods = - propertyMethodAnnotationMap(type, propertyMethods, typeUtils()); + propertyMethodAnnotationMap(type, propertyMethods); vars.props = propertySet( propertyMethodsAndTypes, @@ -450,7 +509,8 @@ private void defineVarsForType( builder -> { ImmutableBiMap methodToPropertyName = propertyNameToMethodMap(propertyMethods).inverse(); - builder.defineVarsForAutoValue(vars, methodToPropertyName, nullables); + builder.defineVarsForAutoValue( + vars, methodToPropertyName, nullables, consumedBuilderAbstractMethods); vars.builderName = "Builder"; vars.builderAnnotations = copiedClassAnnotations(builder.builderType()); }); 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 e53b92e930..9e811c1fb6 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 @@ -35,6 +35,14 @@ class AutoValueTemplateVars extends AutoValueOrBuilderTemplateVars { /** The simple name of the generated subclass. */ String subclass; + + /** + * The simple name of the final subclass. This is the same as {@link #subclass} unless there are + * extensions. If there are extensions, then {@link #subclass} might be something like {@code + * $$AutoValue_Foo} while {@code finalSubclass} will be {@code AutoValue_Foo}. + */ + String finalSubclass; + /** * The modifiers (for example {@code final} or {@code abstract}) for the generated subclass, * followed by a space if they are not empty. diff --git a/value/src/main/java/com/google/auto/value/processor/AutoValueishProcessor.java b/value/src/main/java/com/google/auto/value/processor/AutoValueishProcessor.java index 82d31bf8c7..c147266abf 100644 --- a/value/src/main/java/com/google/auto/value/processor/AutoValueishProcessor.java +++ b/value/src/main/java/com/google/auto/value/processor/AutoValueishProcessor.java @@ -109,10 +109,9 @@ abstract class AutoValueishProcessor extends AbstractProcessor { this.appliesToInterfaces = appliesToInterfaces; } - /** - * The annotation we are processing, for example {@code AutoValue} or {@code AutoBuilder}. - */ + /** The annotation we are processing, for example {@code AutoValue} or {@code AutoBuilder}. */ private TypeElement annotationType; + /** The simple name of {@link #annotationType}. */ private String simpleAnnotationName; @@ -155,17 +154,17 @@ public final SourceVersion getSupportedSourceVersion() { } /** - * A property of an {@code @AutoValue} (etc) class, defined by one of its abstract - * methods. An instance of this class is made available to the Velocity template engine for each - * property. The public methods of this class define JavaBeans-style properties that are - * accessible from templates. For example {@link #getType()} means we can write {@code $p.type} - * for a Velocity variable {@code $p} that is a {@code Property}. + * A property of an {@code @AutoValue} (etc) class, defined by one of its abstract methods. An + * instance of this class is made available to the Velocity template engine for each property. The + * public methods of this class define JavaBeans-style properties that are accessible from + * templates. For example {@link #getType()} means we can write {@code $p.type} for a Velocity + * variable {@code $p} that is a {@code Property}. */ public static class Property { private final String name; private final String identifier; private final String type; - private final TypeMirror typeMirror; + private final AnnotatedTypeMirror annotatedType; private final Optional nullableAnnotation; private final ImmutableList availableNullableTypeAnnotations; // 0 or 1 private final Optionalish optional; @@ -177,7 +176,7 @@ public static class Property { String name, String identifier, String type, - TypeMirror typeMirror, + AnnotatedTypeMirror annotatedType, Optional nullableAnnotation, Nullables nullables, String getter, @@ -186,14 +185,14 @@ public static class Property { this.name = name; this.identifier = identifier; this.type = type; - this.typeMirror = typeMirror; + this.annotatedType = annotatedType; this.nullableAnnotation = nullableAnnotation; this.availableNullableTypeAnnotations = nullables.nullableTypeAnnotations(); - this.optional = Optionalish.createIfOptional(typeMirror); + this.optional = Optionalish.createIfOptional(annotatedType.getType()); this.builderInitializer = maybeBuilderInitializer.isPresent() ? " = " + maybeBuilderInitializer.get() - : builderInitializer(typeMirror, nullableAnnotation); + : builderInitializer(annotatedType, nullableAnnotation); this.getter = getter; this.hasDefault = hasDefault; } @@ -205,11 +204,11 @@ public static class Property { * initializer sets it to {@code Optional.empty()}. */ private static String builderInitializer( - TypeMirror typeMirror, Optional nullableAnnotation) { + AnnotatedTypeMirror annotatedType, Optional nullableAnnotation) { if (nullableAnnotation.isPresent()) { return ""; } - Optionalish optional = Optionalish.createIfOptional(typeMirror); + Optionalish optional = Optionalish.createIfOptional(annotatedType.getType()); if (optional == null) { return ""; } @@ -225,20 +224,20 @@ private static String builderInitializer( * *

    *
  • the property is not primitive; - *
  • the property is not already nullable; + *
  • the property type does not already have a {@code @Nullable} annotation; *
  • there is no explicit initializer (for example {@code Optional} properties start off as * {@code Optional.empty()}); *
  • we have found a {@code @Nullable} type annotation that can be applied. *
*/ public String getBuilderFieldType() { - if (typeMirror.getKind().isPrimitive() + if (annotatedType.getType().getKind().isPrimitive() || nullableAnnotation.isPresent() || !builderInitializer.isEmpty() || availableNullableTypeAnnotations.isEmpty()) { return type; } - return TypeEncoder.encodeWithAnnotations(typeMirror, availableNullableTypeAnnotations); + return TypeEncoder.encodeWithAnnotations(annotatedType, availableNullableTypeAnnotations); } /** @@ -263,7 +262,7 @@ public String getName() { } TypeMirror getTypeMirror() { - return typeMirror; + return annotatedType.getType(); } public String getType() { @@ -271,7 +270,7 @@ public String getType() { } public TypeKind getKind() { - return typeMirror.getKind(); + return annotatedType.getType().getKind(); } /** @@ -332,7 +331,7 @@ public static class GetterProperty extends Property { String name, String identifier, ExecutableElement method, - TypeMirror typeMirror, + AnnotatedTypeMirror annotatedType, String typeString, ImmutableList fieldAnnotations, ImmutableList methodAnnotations, @@ -342,7 +341,7 @@ public static class GetterProperty extends Property { name, identifier, typeString, - typeMirror, + annotatedType, nullableAnnotation, nullables, method.getSimpleName().toString(), @@ -527,7 +526,7 @@ private void validateType(TypeElement type) { * AutoValue.CopyAnnotations} also do not appear here. */ final ImmutableSet propertySet( - ImmutableMap propertyMethodsAndTypes, + ImmutableMap propertyMethodsAndTypes, ImmutableListMultimap annotatedPropertyFields, ImmutableListMultimap annotatedPropertyMethods, Nullables nullables) { @@ -595,8 +594,7 @@ final void defineSharedVarsForType( vars.toString = methodsToGenerate.containsKey(ObjectMethod.TO_STRING); vars.equals = methodsToGenerate.containsKey(ObjectMethod.EQUALS); vars.hashCode = methodsToGenerate.containsKey(ObjectMethod.HASH_CODE); - vars.equalsParameterType = - equalsParameterType(methodsToGenerate, nullables); + vars.equalsParameterType = equalsParameterType(methodsToGenerate, nullables); vars.serialVersionUID = getSerialVersionUID(type); } @@ -956,18 +954,20 @@ static ImmutableSet abstractMethodsIn(Iterable propertyMethodsIn( + ImmutableMap propertyMethodsIn( Set abstractMethods, TypeElement autoValueOrOneOfType) { - DeclaredType declaredType = MoreTypes.asDeclared(autoValueOrOneOfType.asType()); - ImmutableSet.Builder properties = ImmutableSet.builder(); - for (ExecutableElement method : abstractMethods) { - if (method.getParameters().isEmpty() - && (method.getReturnType().getKind() != TypeKind.VOID || propertiesCanBeVoid()) - && objectMethodToOverride(method) == ObjectMethod.NONE) { - properties.add(method); - } - } - return new EclipseHack(processingEnv).methodReturnTypes(properties.build(), declaredType); + return abstractMethods.stream() + .filter( + method -> + method.getParameters().isEmpty() + && (method.getReturnType().getKind() != TypeKind.VOID || propertiesCanBeVoid()) + && objectMethodToOverride(method) == ObjectMethod.NONE) + .collect( + toImmutableMap( + method -> method, + method -> + MethodSignature.asMemberOf(typeUtils(), autoValueOrOneOfType, method) + .returnType())); } /** True if void properties are allowed. */ @@ -1065,11 +1065,8 @@ final String getSerialVersionUID(TypeElement type) { } /** Implements the semantics of {@code AutoValue.CopyAnnotations}; see its javadoc. */ - static ImmutableList annotationsToCopy( - Element autoValueType, - Element typeOrMethod, - Set excludedAnnotations, - Types typeUtils) { + ImmutableList annotationsToCopy( + Element autoValueType, Element typeOrMethod, Set excludedAnnotations) { ImmutableList.Builder result = ImmutableList.builder(); for (AnnotationMirror annotation : typeOrMethod.getAnnotationMirrors()) { String annotationFqName = getAnnotationFqName(annotation); @@ -1077,7 +1074,7 @@ static ImmutableList annotationsToCopy( // and it should not be in the excludedAnnotations set. if (!isInAutoValuePackage(annotationFqName) && !excludedAnnotations.contains(annotationFqName) - && annotationVisibleFrom(annotation, autoValueType, typeUtils)) { + && annotationVisibleFrom(annotation, autoValueType)) { result.add(annotation); } } @@ -1128,7 +1125,7 @@ ImmutableList copiedClassAnnotations(TypeElement type) { ImmutableList copyAnnotations( Element autoValueType, Element typeOrMethod, Set excludedAnnotations) { ImmutableList annotationsToCopy = - annotationsToCopy(autoValueType, typeOrMethod, excludedAnnotations, typeUtils()); + annotationsToCopy(autoValueType, typeOrMethod, excludedAnnotations); return annotationStrings(annotationsToCopy); } @@ -1179,18 +1176,18 @@ private static String getAnnotationFqName(AnnotationMirror annotation) { .toString(); } - static ImmutableListMultimap propertyMethodAnnotationMap( - TypeElement type, ImmutableSet propertyMethods, Types typeUtils) { + ImmutableListMultimap propertyMethodAnnotationMap( + TypeElement type, ImmutableSet propertyMethods) { ImmutableListMultimap.Builder builder = ImmutableListMultimap.builder(); for (ExecutableElement propertyMethod : propertyMethods) { - builder.putAll(propertyMethod, propertyMethodAnnotations(type, propertyMethod, typeUtils)); + builder.putAll(propertyMethod, propertyMethodAnnotations(type, propertyMethod)); } return builder.build(); } - static ImmutableList propertyMethodAnnotations( - TypeElement type, ExecutableElement method, Types typeUtils) { + ImmutableList propertyMethodAnnotations( + TypeElement type, ExecutableElement method) { ImmutableSet excludedAnnotations = ImmutableSet.builder() .addAll(getExcludedAnnotationClassNames(method)) @@ -1201,7 +1198,7 @@ static ImmutableList propertyMethodAnnotations( // they will be output as part of the method's return type. Set returnTypeAnnotations = getReturnTypeAnnotations(method, a -> true); Set excluded = union(excludedAnnotations, returnTypeAnnotations); - return annotationsToCopy(type, method, excluded, typeUtils); + return annotationsToCopy(type, method, excluded); } final ImmutableListMultimap propertyFieldAnnotationMap( @@ -1255,7 +1252,7 @@ private ImmutableList propertyFieldAnnotations( .addAll(returnTypeAnnotations) .addAll(nonFieldAnnotations) .build(); - return annotationsToCopy(type, method, excluded, typeUtils()); + return annotationsToCopy(type, method, excluded); } private static Set getReturnTypeAnnotations( @@ -1273,13 +1270,12 @@ private boolean annotationAppliesToFields(TypeElement annotation) { return target == null || Arrays.asList(target.value()).contains(ElementType.FIELD); } - private static boolean annotationVisibleFrom( - AnnotationMirror annotation, Element from, Types typeUtils) { + private boolean annotationVisibleFrom(AnnotationMirror annotation, Element from) { Element annotationElement = annotation.getAnnotationType().asElement(); Visibility visibility = Visibility.effectiveVisibilityOfElement(annotationElement); switch (visibility) { case PUBLIC: - return true; + break; case PROTECTED: // If the annotation is protected, it must be inside another class, call it C. If our // @AutoValue class is Foo then, for the annotation to be visible, either Foo must be in the @@ -1292,13 +1288,18 @@ private static boolean annotationVisibleFrom( // https://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.6.2.1 // AutoValue_Foo is a top-level class, so an annotation on it cannot be in the body of a // subclass of anything. - return getPackage(annotationElement).equals(getPackage(from)) - || typeUtils.isSubtype(from.asType(), annotationElement.getEnclosingElement().asType()); + if (!getPackage(annotationElement).equals(getPackage(from)) + && !typeUtils() + .isSubtype(from.asType(), annotationElement.getEnclosingElement().asType())) { + return false; + } + break; case DEFAULT: return getPackage(annotationElement).equals(getPackage(from)); default: return false; } + return true; } /** 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 591b0cc8cc..472d22d221 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 @@ -45,7 +45,6 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.ExecutableType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; @@ -79,12 +78,8 @@ abstract class BuilderMethodClassifier { * Foo.Builder} are two separate variables. Originally {@code bar()} returned the {@code T} of * {@code Foo}, but in this map we have rewritten it to be the {@code T} of {@code * Foo.Builder}. - * - *

Importantly, this rewrite loses type annotations, so when those are important we must - * be careful to look at the original type as reported by the {@link #originalPropertyType} - * method. */ - private final ImmutableMap rewrittenPropertyTypes; + private final ImmutableMap rewrittenPropertyTypes; private final Set buildMethods = new LinkedHashSet<>(); private final Map builderGetters = new LinkedHashMap<>(); @@ -93,7 +88,6 @@ abstract class BuilderMethodClassifier { LinkedListMultimap.create(); private final Multimap propertyNameToUnprefixedSetters = LinkedListMultimap.create(); - private final EclipseHack eclipseHack; private final Nullables nullables; private boolean settersPrefixed; @@ -103,7 +97,7 @@ abstract class BuilderMethodClassifier { ProcessingEnvironment processingEnv, TypeMirror builtType, TypeElement builderType, - ImmutableMap rewrittenPropertyTypes, + ImmutableMap rewrittenPropertyTypes, ImmutableSet propertiesWithDefaults, Nullables nullables) { this.errorReporter = errorReporter; @@ -113,7 +107,6 @@ abstract class BuilderMethodClassifier { this.builderType = builderType; this.rewrittenPropertyTypes = rewrittenPropertyTypes; this.propertiesWithDefaults = propertiesWithDefaults; - this.eclipseHack = new EclipseHack(processingEnv); this.nullables = nullables; } @@ -176,7 +169,7 @@ boolean classifyMethods(Iterable methods, boolean autoValueHa return false; } for (String property : rewrittenPropertyTypes.keySet()) { - TypeMirror propertyType = rewrittenPropertyTypes.get(property); + TypeMirror propertyType = rewrittenPropertyTypes.get(property).getType(); boolean hasSetter = propertyNameToSetter.containsKey(property); PropertyBuilder propertyBuilder = propertyNameToPropertyBuilder.get(property); boolean hasBuilder = propertyBuilder != null; @@ -247,7 +240,7 @@ private void classifyMethodNoArgs(ExecutableElement method) { } String methodName = method.getSimpleName().toString(); - TypeMirror returnType = builderMethodReturnType(method); + TypeMirror returnType = builderMethodReturnType(method).getType(); if (methodName.endsWith("Builder")) { String prefix = methodName.substring(0, methodName.length() - "Builder".length()); @@ -267,7 +260,6 @@ private void classifyMethodNoArgs(ExecutableElement method) { this, this::propertyIsNullable, rewrittenPropertyTypes, - eclipseHack, nullables); Optional propertyBuilder = propertyBuilderClassifier.makePropertyBuilder(method, property); @@ -295,16 +287,16 @@ private void classifyMethodNoArgs(ExecutableElement method) { } private void classifyGetter(ExecutableElement builderGetter, String propertyName) { - TypeMirror originalGetterType = rewrittenPropertyTypes.get(propertyName); - TypeMirror builderGetterType = builderMethodReturnType(builderGetter); + TypeMirror originalGetterType = rewrittenPropertyTypes.get(propertyName).getType(); + AnnotatedTypeMirror builderGetterType = builderMethodReturnType(builderGetter); String builderGetterTypeString = TypeEncoder.encodeWithAnnotations(builderGetterType); - if (TYPE_EQUIVALENCE.equivalent(builderGetterType, originalGetterType)) { + if (TYPE_EQUIVALENCE.equivalent(builderGetterType.getType(), originalGetterType)) { builderGetters.put( propertyName, new BuilderSpec.PropertyGetter(builderGetter, builderGetterTypeString, null)); return; } - Optionalish optional = Optionalish.createIfOptional(builderGetterType); + Optionalish optional = Optionalish.createIfOptional(builderGetterType.getType()); if (optional != null) { TypeMirror containedType = optional.getContainedType(typeUtils); // If the original method is int getFoo() then we allow Optional here. @@ -397,10 +389,9 @@ private void classifyMethodOneArg(ExecutableElement method) { } Optional function = getSetterFunction(propertyElement, method); if (function.isPresent()) { - DeclaredType builderTypeMirror = MoreTypes.asDeclared(builderType.asType()); - ExecutableType methodMirror = - MoreTypes.asExecutable(typeUtils.asMemberOf(builderTypeMirror, method)); - TypeMirror returnType = methodMirror.getReturnType(); + MethodSignature methodSignature = + MethodSignature.asMemberOf(typeUtils, builderType, method); + TypeMirror returnType = methodSignature.returnType().getType(); if (typeUtils.isSubtype(builderType.asType(), returnType) && !MoreTypes.isTypeOf(Object.class, returnType)) { if (nullableAnnotationFor(method, returnType).isPresent()) { @@ -411,7 +402,8 @@ private void classifyMethodOneArg(ExecutableElement method) { + " is not appropriate", autoWhat()); } // We allow the return type to be a supertype (other than Object), to support step builders. - TypeMirror parameterType = Iterables.getOnlyElement(methodMirror.getParameterTypes()); + TypeMirror parameterType = + Iterables.getOnlyElement(methodSignature.parameterTypes()).getType(); propertyNameToSetters.put( propertyName, new PropertySetter(method, parameterType, function.get())); } else { @@ -448,7 +440,6 @@ private boolean classifyPropertyBuilderOneArg(ExecutableElement method) { this, this::propertyIsNullable, rewrittenPropertyTypes, - eclipseHack, nullables); Optional maybePropertyBuilder = propertyBuilderClassifier.makePropertyBuilder(method, property); @@ -470,11 +461,12 @@ private Optional getSetterFunction(E propertyElement, ExecutableElement boolean nullableParameter = nullableAnnotationFor(parameterElement, parameterElement.asType()).isPresent(); String property = propertyElements().inverse().get(propertyElement); - TypeMirror targetType = rewrittenPropertyTypes.get(property); - ExecutableType finalSetter = - MoreTypes.asExecutable( - typeUtils.asMemberOf(MoreTypes.asDeclared(builderType.asType()), setter)); - TypeMirror parameterType = finalSetter.getParameterTypes().get(0); + TypeMirror targetType = rewrittenPropertyTypes.get(property).getType(); + TypeMirror parameterType = + MethodSignature.asMemberOf(typeUtils, builderType, setter) + .parameterTypes() + .get(0) + .getType(); // Two types are assignable to each other if they are the same type, or if one is primitive and // the other is the corresponding boxed type. There might be other cases where this is true, but // we're likely to want to accept those too. @@ -522,7 +514,7 @@ private Optional getConvertingSetterFunction( ExecutableElement setter, TypeMirror parameterType) { String property = propertyElements().inverse().get(propertyElement); - DeclaredType targetType = MoreTypes.asDeclared(rewrittenPropertyTypes.get(property)); + DeclaredType targetType = MoreTypes.asDeclared(rewrittenPropertyTypes.get(property).getType()); for (ExecutableElement copyOfMethod : copyOfMethods) { Optional function = getConvertingSetterFunction(copyOfMethod, targetType, parameterType); @@ -646,25 +638,9 @@ private ImmutableList copyOfMethods( * * If the builder is {@code ChildBuilder} then the return type of {@code setFoo} is also {@code * ChildBuilder}, and not {@code B} as its {@code getReturnType()} method would claim. - * - *

If the caller is in a version of Eclipse with this bug then the {@code - * asMemberOf} call will fail if the method is inherited from an interface. We work around that - * for methods in the {@code @AutoValue} class using {@link EclipseHack#methodReturnTypes} but we - * don't try to do so here because it should be much less likely. You might need to change {@code - * ParentBuilder} from an interface to an abstract class to make it work, but you'll often need to - * do that anyway. */ - TypeMirror builderMethodReturnType(ExecutableElement builderMethod) { - DeclaredType builderTypeMirror = MoreTypes.asDeclared(builderType.asType()); - TypeMirror methodMirror; - try { - methodMirror = typeUtils.asMemberOf(builderTypeMirror, builderMethod); - } catch (IllegalArgumentException e) { - // Presumably we've hit the Eclipse bug cited. - return builderMethod.getReturnType(); - } - return MoreTypes.asExecutable(methodMirror).getReturnType(); + AnnotatedTypeMirror builderMethodReturnType(ExecutableElement builderMethod) { + return MethodSignature.asMemberOf(typeUtils, builderType, builderMethod).returnType(); } private static String prefixWithSet(String propertyName) { @@ -696,9 +672,7 @@ private boolean propertyIsNullable(String property) { /** * Returns the property type as it appears on the original source program element. This can be * different from the type stored in {@link #rewrittenPropertyTypes} since that one will refer to - * type variables in the builder rather than in the original class. Also, {@link - * #rewrittenPropertyTypes} will not have type annotations even if they were present on the - * original element, so {@code originalPropertyType} is the right thing to use for those. + * type variables in the builder rather than in the original class. */ abstract TypeMirror originalPropertyType(E propertyElement); diff --git a/value/src/main/java/com/google/auto/value/processor/BuilderMethodClassifierForAutoBuilder.java b/value/src/main/java/com/google/auto/value/processor/BuilderMethodClassifierForAutoBuilder.java index d9900b57df..a83f8e1c12 100644 --- a/value/src/main/java/com/google/auto/value/processor/BuilderMethodClassifierForAutoBuilder.java +++ b/value/src/main/java/com/google/auto/value/processor/BuilderMethodClassifierForAutoBuilder.java @@ -30,7 +30,6 @@ import java.util.Optional; import java.util.function.Function; import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.TypeParameterElement; @@ -50,7 +49,7 @@ private BuilderMethodClassifierForAutoBuilder( TypeMirror builtType, TypeElement builderType, ImmutableBiMap paramToPropertyName, - ImmutableMap rewrittenPropertyTypes, + ImmutableMap rewrittenPropertyTypes, ImmutableSet propertiesWithDefaults, Nullables nullables) { super( @@ -91,7 +90,7 @@ static Optional> classify( ImmutableBiMap paramToPropertyName = executable.parameters().stream() .collect(toImmutableBiMap(v -> v, v -> v.getSimpleName().toString())); - ImmutableMap rewrittenPropertyTypes = + ImmutableMap rewrittenPropertyTypes = rewriteParameterTypes(executable, builderType, errorReporter, processingEnv.getTypeUtils()); BuilderMethodClassifier classifier = new BuilderMethodClassifierForAutoBuilder( @@ -147,7 +146,7 @@ static Optional> classify( // the return type Set of SingletonSetBuilder.build(). But in fact we only use // MoreTypes.equivalence to compare those, and that returns true for distinct type variables if // they have the same name and bounds. - private static ImmutableMap rewriteParameterTypes( + private static ImmutableMap rewriteParameterTypes( Executable executable, TypeElement builderType, ErrorReporter errorReporter, @@ -166,7 +165,9 @@ private static ImmutableMap rewriteParameterTypes( // Optimization for a common case. No point in doing all that type visiting if we have no // variables to substitute. return executable.parameters().stream() - .collect(toImmutableMap(v -> v.getSimpleName().toString(), Element::asType)); + .collect( + toImmutableMap( + v -> v.getSimpleName().toString(), v -> new AnnotatedTypeMirror(v.asType()))); } Map, TypeMirror> typeVariables = new LinkedHashMap<>(); for (int i = 0; i < executableTypeParams.size(); i++) { @@ -180,7 +181,10 @@ private static ImmutableMap rewriteParameterTypes( .collect( toImmutableMap( v -> v.getSimpleName().toString(), - v -> TypeVariables.substituteTypeVariables(v.asType(), substitute, typeUtils))); + v -> + new AnnotatedTypeMirror( + v.asType(), + TypeVariables.substituteTypeVariables(v.asType(), substitute, typeUtils)))); } @Override diff --git a/value/src/main/java/com/google/auto/value/processor/BuilderMethodClassifierForAutoValue.java b/value/src/main/java/com/google/auto/value/processor/BuilderMethodClassifierForAutoValue.java index 67b7c01bf7..d35b35459b 100644 --- a/value/src/main/java/com/google/auto/value/processor/BuilderMethodClassifierForAutoValue.java +++ b/value/src/main/java/com/google/auto/value/processor/BuilderMethodClassifierForAutoValue.java @@ -40,7 +40,7 @@ private BuilderMethodClassifierForAutoValue( TypeMirror builtType, TypeElement builderType, ImmutableBiMap getterToPropertyName, - ImmutableMap rewrittenPropertyTypes, + ImmutableMap rewrittenPropertyTypes, Nullables nullables) { super( errorReporter, @@ -81,7 +81,7 @@ static Optional> classify( TypeElement autoValueClass, TypeElement builderType, ImmutableBiMap getterToPropertyName, - ImmutableMap rewrittenPropertyTypes, + ImmutableMap rewrittenPropertyTypes, Nullables nullables, boolean autoValueHasToBuilder) { BuilderMethodClassifier classifier = 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 5ecee2b0a3..265dc5ccd8 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 @@ -37,6 +37,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; +import com.google.common.collect.Sets; import java.util.List; import java.util.Map; import java.util.Optional; @@ -130,6 +131,7 @@ private Optional findBuilderError(TypeElement builderTypeElement) { class Builder implements AutoValueExtension.BuilderContext { private final TypeElement builderTypeElement; private ImmutableSet toBuilderMethods; + private ImmutableSet builderAbstractMethods; private ExecutableElement buildMethod; private BuilderMethodClassifier classifier; @@ -205,6 +207,13 @@ public Set toBuilderMethods() { return toBuilderMethods; } + ImmutableSet builderAbstractMethods() { + if (builderAbstractMethods == null) { + builderAbstractMethods = abstractMethods(builderTypeElement, processingEnv); + } + return builderAbstractMethods; + } + /** * Finds any methods in the set that return the builder type. If the builder has type parameters * {@code }, then the return type of the method must be {@code Builder} with the @@ -271,18 +280,19 @@ ImmutableSet toBuilderMethods( void defineVarsForAutoValue( AutoValueOrBuilderTemplateVars vars, ImmutableBiMap getterToPropertyName, - Nullables nullables) { + Nullables nullables, + ImmutableSet consumedBuilderAbstractMethods) { Iterable builderMethods = - abstractMethods(builderTypeElement, processingEnv); + Sets.difference(builderAbstractMethods, consumedBuilderAbstractMethods); boolean autoValueHasToBuilder = toBuilderMethods != null && !toBuilderMethods.isEmpty(); - ImmutableMap getterToPropertyType = + ImmutableMap getterToPropertyType = TypeVariables.rewriteReturnTypes( - processingEnv.getElementUtils(), processingEnv.getTypeUtils(), getterToPropertyName.keySet(), autoValueClass, builderTypeElement); - ImmutableMap.Builder rewrittenPropertyTypes = ImmutableMap.builder(); + ImmutableMap.Builder rewrittenPropertyTypes = + ImmutableMap.builder(); getterToPropertyType.forEach( (getter, type) -> rewrittenPropertyTypes.put(getterToPropertyName.get(getter), type)); Optional> optionalClassifier = diff --git a/value/src/main/java/com/google/auto/value/processor/EclipseHack.java b/value/src/main/java/com/google/auto/value/processor/EclipseHack.java index 727e32875c..421a4efbbf 100644 --- a/value/src/main/java/com/google/auto/value/processor/EclipseHack.java +++ b/value/src/main/java/com/google/auto/value/processor/EclipseHack.java @@ -15,27 +15,17 @@ */ package com.google.auto.value.processor; -import static java.util.stream.Collectors.toList; +import static com.google.common.collect.ImmutableList.toImmutableList; -import com.google.auto.common.MoreElements; import com.google.auto.common.MoreTypes; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import java.util.LinkedHashMap; +import com.google.common.collect.ImmutableList; import java.util.List; -import java.util.Map; -import java.util.Set; -import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Element; -import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Name; import javax.lang.model.element.TypeElement; 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; /** * Hacks needed to work around various bugs and incompatibilities in Eclipse's implementation of @@ -44,17 +34,7 @@ * @author Éamonn McManus */ class EclipseHack { - private final Elements elementUtils; - private final Types typeUtils; - - EclipseHack(ProcessingEnvironment processingEnv) { - this(processingEnv.getElementUtils(), processingEnv.getTypeUtils()); - } - - EclipseHack(Elements elementUtils, Types typeUtils) { - this.elementUtils = elementUtils; - this.typeUtils = typeUtils; - } + private EclipseHack() {} /** * Returns the enclosing type of {@code type}, if {@code type} is an inner class. Otherwise @@ -96,15 +76,15 @@ static TypeMirror getEnclosingType(DeclaredType type) { if (!arguments.isEmpty()) { boolean allVariables = arguments.stream().allMatch(t -> t.getKind().equals(TypeKind.TYPEVAR)); if (allVariables) { - List argumentNames = + ImmutableList argumentNames = arguments.stream() .map(t -> MoreTypes.asTypeVariable(t).asElement().getSimpleName()) - .collect(toList()); + .collect(toImmutableList()); TypeElement enclosingElement = MoreTypes.asTypeElement(declared); - List parameterNames = + ImmutableList parameterNames = enclosingElement.getTypeParameters().stream() .map(Element::getSimpleName) - .collect(toList()); + .collect(toImmutableList()); if (argumentNames.equals(parameterNames)) { // We're going to return a NoType. We don't have a Types to hand so we can't call // Types.getNoType(). Instead, just keep going through outer types until we get to @@ -118,70 +98,4 @@ static TypeMirror getEnclosingType(DeclaredType type) { } return declared; } - - TypeMirror methodReturnType(ExecutableElement method, DeclaredType in) { - try { - TypeMirror methodMirror = typeUtils.asMemberOf(in, method); - return MoreTypes.asExecutable(methodMirror).getReturnType(); - } catch (IllegalArgumentException e) { - return methodReturnTypes(ImmutableSet.of(method), in).get(method); - } - } - - /** - * Returns a map containing the real return types of the given methods, knowing that they appear - * in the given type. This means that if the given type is say {@code StringIterator implements - * Iterator} then we want the {@code next()} method to map to String, rather than the - * {@code T} that it returns as inherited from {@code Iterator}. This method is in EclipseHack - * because if it weren't for this - * Eclipse bug it would be trivial. Unfortunately, versions of Eclipse up to at least 4.5 have - * a bug where the {@link Types#asMemberOf} method throws IllegalArgumentException if given a - * method that is inherited from an interface. Fortunately, Eclipse's implementation of {@link - * Elements#getAllMembers} does the type substitution that {@code asMemberOf} would have done. But - * javac's implementation doesn't. So we try the way that would work if Eclipse weren't buggy, and - * only if we get IllegalArgumentException do we use {@code getAllMembers}. - */ - ImmutableMap methodReturnTypes( - Set methods, DeclaredType in) { - ImmutableMap.Builder map = ImmutableMap.builder(); - Map noArgMethods = null; - for (ExecutableElement method : methods) { - TypeMirror returnType = null; - try { - TypeMirror methodMirror = typeUtils.asMemberOf(in, method); - returnType = MoreTypes.asExecutable(methodMirror).getReturnType(); - } catch (IllegalArgumentException e) { - if (method.getParameters().isEmpty()) { - if (noArgMethods == null) { - noArgMethods = noArgMethodsIn(in); - } - returnType = noArgMethods.get(method.getSimpleName()).getReturnType(); - } - } - if (returnType == null) { - returnType = method.getReturnType(); - } - map.put(method, returnType); - } - return map.build(); - } - - /** - * Constructs a map from name to method of the no-argument methods in the given type. We need this - * because an ExecutableElement returned by {@link Elements#getAllMembers} will not compare equal - * to the original ExecutableElement if {@code getAllMembers} substituted type parameters, as it - * does in Eclipse. - */ - private Map noArgMethodsIn(DeclaredType in) { - TypeElement autoValueType = MoreElements.asType(typeUtils.asElement(in)); - List allMethods = - ElementFilter.methodsIn(elementUtils.getAllMembers(autoValueType)); - Map map = new LinkedHashMap(); - for (ExecutableElement method : allMethods) { - if (method.getParameters().isEmpty()) { - map.put(method.getSimpleName(), method); - } - } - return map; - } } diff --git a/value/src/main/java/com/google/auto/value/processor/ExtensionContext.java b/value/src/main/java/com/google/auto/value/processor/ExtensionContext.java index ca48fdbeac..27b961f352 100644 --- a/value/src/main/java/com/google/auto/value/processor/ExtensionContext.java +++ b/value/src/main/java/com/google/auto/value/processor/ExtensionContext.java @@ -35,25 +35,31 @@ class ExtensionContext implements AutoValueExtension.Context { + private final AutoValueProcessor autoValueProcessor; private final ProcessingEnvironment processingEnvironment; private final TypeElement autoValueClass; private final ImmutableMap properties; - private final ImmutableMap propertyTypes; + private final ImmutableMap propertyTypes; private final ImmutableSet abstractMethods; + private final ImmutableSet builderAbstractMethods; private Optional builderContext = Optional.empty(); ExtensionContext( + AutoValueProcessor autoValueProcessor, ProcessingEnvironment processingEnvironment, TypeElement autoValueClass, ImmutableMap properties, - ImmutableMap propertyMethodsAndTypes, - ImmutableSet abstractMethods) { + ImmutableMap propertyMethodsAndTypes, + ImmutableSet abstractMethods, + ImmutableSet builderAbstractMethods) { + this.autoValueProcessor = autoValueProcessor; this.processingEnvironment = processingEnvironment; this.autoValueClass = autoValueClass; this.properties = properties; this.propertyTypes = ImmutableMap.copyOf(Maps.transformValues(properties, propertyMethodsAndTypes::get)); this.abstractMethods = abstractMethods; + this.builderAbstractMethods = builderAbstractMethods; } void setBuilderContext(BuilderContext builderContext) { @@ -87,7 +93,7 @@ public Map properties() { @Override public Map propertyTypes() { - return propertyTypes; + return Maps.transformValues(propertyTypes, AnnotatedTypeMirror::getType); } @Override @@ -95,6 +101,11 @@ public Set abstractMethods() { return abstractMethods; } + @Override + public Set builderAbstractMethods() { + return builderAbstractMethods; + } + @Override public List classAnnotationsToCopy(TypeElement classToCopyFrom) { // Only copy annotations from a class if it has @AutoValue.CopyAnnotations. @@ -123,14 +134,13 @@ public List classAnnotationsToCopy(TypeElement classToCopyFrom .add(ClassNames.KOTLIN_METADATA_NAME) .build(); - return AutoValueishProcessor.annotationsToCopy( - autoValueClass, classToCopyFrom, excludedAnnotations, processingEnvironment.getTypeUtils()); + return autoValueProcessor.annotationsToCopy( + autoValueClass, classToCopyFrom, excludedAnnotations); } @Override public List methodAnnotationsToCopy(ExecutableElement method) { - return AutoValueishProcessor.propertyMethodAnnotations( - autoValueClass, method, processingEnvironment.getTypeUtils()); + return autoValueProcessor.propertyMethodAnnotations(autoValueClass, method); } @Override diff --git a/value/src/main/java/com/google/auto/value/processor/ForwardingClassGenerator.java b/value/src/main/java/com/google/auto/value/processor/ForwardingClassGenerator.java index 71f035d0a3..946b4ec0cb 100644 --- a/value/src/main/java/com/google/auto/value/processor/ForwardingClassGenerator.java +++ b/value/src/main/java/com/google/auto/value/processor/ForwardingClassGenerator.java @@ -31,7 +31,7 @@ import static org.objectweb.asm.Opcodes.INVOKESPECIAL; import static org.objectweb.asm.Opcodes.LLOAD; import static org.objectweb.asm.Opcodes.NEW; -import static org.objectweb.asm.Opcodes.V1_7; +import static org.objectweb.asm.Opcodes.V1_8; import com.google.auto.common.MoreElements; import com.google.common.collect.ImmutableList; @@ -90,7 +90,7 @@ static byte[] makeConstructorForwarder( ClassWriter classWriter = new ClassWriter(COMPUTE_MAXS); classWriter.visit( - V1_7, + V1_8, ACC_FINAL | ACC_SUPER, internalName(forwardingClassName), null, diff --git a/value/src/main/java/com/google/auto/value/processor/KotlinMetadata.java b/value/src/main/java/com/google/auto/value/processor/KotlinMetadata.java new file mode 100644 index 0000000000..bbe68a67dc --- /dev/null +++ b/value/src/main/java/com/google/auto/value/processor/KotlinMetadata.java @@ -0,0 +1,311 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.auto.value.processor; + +import static com.google.auto.common.MoreStreams.toImmutableList; +import static com.google.auto.common.MoreStreams.toImmutableMap; +import static com.google.auto.common.MoreStreams.toImmutableSet; +import static com.google.auto.common.MoreTypes.asTypeElement; +import static com.google.auto.value.processor.ClassNames.KOTLIN_METADATA_NAME; +import static com.google.common.base.Throwables.throwIfUnchecked; +import static java.util.stream.Collectors.toMap; +import static javax.lang.model.util.ElementFilter.constructorsIn; + +import com.google.auto.common.AnnotationMirrors; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; + +/** + * Utilities for working with Kotlin metadata. + * + *

We use reflection to avoid referencing the Kotlin metadata API directly. AutoBuilder clients + * that don't use Kotlin shouldn't have to have the Kotlin runtime on their classpath, even if it is + * only the annotation-processing classpath. + */ +final class KotlinMetadata { + private final ErrorReporter errorReporter; + private boolean warnedAboutMissingMetadataApi; + + KotlinMetadata(ErrorReporter errorReporter) { + this.errorReporter = errorReporter; + } + + /** + * Use Kotlin reflection to build {@link Executable} instances for the constructors in {@code + * ofClass} that include information about which parameters have default values. + * + * @param metadata the {@code @kotlin.Metadata} annotation on {@code ofClass} + * @param ofClass the class whose constructors should be returned + */ + ImmutableList kotlinConstructorsIn(AnnotationMirror metadata, TypeElement ofClass) { + if (!KOTLIN_METADATA_AVAILABLE) { + if (!warnedAboutMissingMetadataApi) { + warnedAboutMissingMetadataApi = true; + errorReporter.reportWarning( + ofClass, + "[AutoBuilderNoMetadataApi] The Kotlin metadata API (kotlinx.metadata or" + + " kotlin.metadata) is not available. You may need to add a dependency on" + + " org.jetbrains.kotlin:kotlin-metadata-jvm."); + } + return ImmutableList.of(); + } + try { + return kotlinConstructorsFromReflection(metadata, ofClass); + } catch (InvocationTargetException e) { + throwIfUnchecked(e.getCause()); + // We don't expect the Kotlin API to throw checked exceptions. + throw new LinkageError(e.getMessage(), e); + } catch (ReflectiveOperationException e) { + throw new LinkageError(e.getMessage(), e); + } + } + + private static ImmutableList kotlinConstructorsFromReflection( + AnnotationMirror metadata, TypeElement ofClass) throws ReflectiveOperationException { + ImmutableMap annotationValues = + AnnotationMirrors.getAnnotationValuesWithDefaults(metadata).entrySet().stream() + .collect(toImmutableMap(e -> e.getKey().getSimpleName().toString(), e -> e.getValue())); + // We match the KmConstructor instances with the ExecutableElement instances based on the + // parameter names. We could possibly just assume that the constructors are in the same order. + Map, ExecutableElement> map = + constructorsIn(ofClass.getEnclosedElements()).stream() + .collect(toMap(c -> parameterNames(c), c -> c, (a, b) -> a, LinkedHashMap::new)); + ImmutableMap, ExecutableElement> paramNamesToConstructor = + ImmutableMap.copyOf(map); + KotlinClassHeader header = + new KotlinClassHeader( + (Integer) annotationValues.get("k").getValue(), + intArrayValue(annotationValues.get("mv")), + stringArrayValue(annotationValues.get("d1")), + stringArrayValue(annotationValues.get("d2")), + (String) annotationValues.get("xs").getValue(), + (String) annotationValues.get("pn").getValue(), + (Integer) annotationValues.get("xi").getValue()); + KotlinClassMetadata.Class classMetadata = KotlinClassMetadata.readLenient(header); + KmClass kmClass = classMetadata.getKmClass(); + ImmutableList.Builder kotlinConstructorsBuilder = ImmutableList.builder(); + for (KmConstructor constructor : kmClass.getConstructors()) { + ImmutableSet.Builder allBuilder = ImmutableSet.builder(); + ImmutableSet.Builder optionalBuilder = ImmutableSet.builder(); + for (KmValueParameter param : constructor.getValueParameters()) { + String name = param.getName(); + allBuilder.add(name); + if (Attributes.getDeclaresDefaultValue(param)) { + optionalBuilder.add(name); + } + } + ImmutableSet optional = optionalBuilder.build(); + ImmutableSet all = allBuilder.build(); + ExecutableElement javaConstructor = paramNamesToConstructor.get(all); + if (javaConstructor != null) { + kotlinConstructorsBuilder.add(Executable.of(javaConstructor, optional)); + } + } + return kotlinConstructorsBuilder.build(); + } + + private static ImmutableSet parameterNames(ExecutableElement executableElement) { + return executableElement.getParameters().stream() + .map(v -> v.getSimpleName().toString()) + .collect(toImmutableSet()); + } + + Optional kotlinMetadataAnnotation(Element element) { + return element.getAnnotationMirrors().stream() + .filter( + a -> + asTypeElement(a.getAnnotationType()) + .getQualifiedName() + .contentEquals(KOTLIN_METADATA_NAME)) + .map(a -> a) // get rid of that stupid wildcard + .findFirst(); + } + + private static int[] intArrayValue(AnnotationValue value) { + @SuppressWarnings("unchecked") + List list = (List) value.getValue(); + return list.stream().mapToInt(v -> (int) v.getValue()).toArray(); + } + + private static String[] stringArrayValue(AnnotationValue value) { + @SuppressWarnings("unchecked") + List list = (List) value.getValue(); + return list.stream().map(AnnotationValue::getValue).toArray(String[]::new); + } + + // Wrapper classes for the Kotlin metadata API. These classes have the same names as the ones + // from that API (minus the package of course), and use reflection to access the real API. This + // allows us to write client code that is essentially the same as if we were using the real API. + // Otherwise the logic would be obscured by all the reflective calls. + + private static class KotlinClassHeader { + final Object /* KotlinClassHeader */ wrapped; + + KotlinClassHeader( + Integer k, int[] mv, String[] d1, String[] d2, String xs, String pn, Integer xi) + throws ReflectiveOperationException { + this.wrapped = NEW_KOTLIN_CLASS_HEADER.newInstance(k, mv, d1, d2, xs, pn, xi); + } + } + + @SuppressWarnings({"JavaLangClash", "SameNameButDifferent"}) // "Class" + private static class KotlinClassMetadata { + static Class readLenient(KotlinClassHeader kotlinClassHeader) + throws ReflectiveOperationException { + return new Class( + KOTLIN_CLASS_METADATA_READ_LENIENT.invoke(null, kotlinClassHeader.wrapped)); + } + + static class Class { + final Object /* KotlinClassMetadata.Class */ wrapped; + + Class(Object /* KotlinClassMetadata.Class */ wrapped) { + this.wrapped = wrapped; + } + + KmClass getKmClass() throws ReflectiveOperationException { + return new KmClass(KOTLIN_CLASS_METADATA_CLASS_GET_KM_CLASS.invoke(wrapped)); + } + } + } + + private static class KmClass { + final Object /* KmClass */ wrapped; + + KmClass(Object wrapped) { + this.wrapped = wrapped; + } + + List getConstructors() throws ReflectiveOperationException { + return ((List) KM_CLASS_GET_CONSTRUCTORS.invoke(wrapped)) + .stream().map(KmConstructor::new).collect(toImmutableList()); + } + } + + private static class KmConstructor { + final Object /* KmConstructor */ wrapped; + + KmConstructor(Object wrapped) { + this.wrapped = wrapped; + } + + List getValueParameters() throws ReflectiveOperationException { + return ((List) KM_CONSTRUCTOR_GET_VALUE_PARAMETERS.invoke(wrapped)) + .stream().map(KmValueParameter::new).collect(toImmutableList()); + } + } + + private static class KmValueParameter { + final Object /* KmValueParameter */ wrapped; + + KmValueParameter(Object wrapped) { + this.wrapped = wrapped; + } + + String getName() throws ReflectiveOperationException { + return (String) KM_VALUE_PARAMETER_GET_NAME.invoke(wrapped); + } + } + + private static class Attributes { + private Attributes() {} + + static boolean getDeclaresDefaultValue(KmValueParameter kmValueParameter) + throws ReflectiveOperationException { + return (boolean) ATTRIBUTES_GET_DECLARES_DEFAULT_VALUE.invoke(null, kmValueParameter.wrapped); + } + } + + private static final Constructor NEW_KOTLIN_CLASS_HEADER; + private static final Method KOTLIN_CLASS_METADATA_READ_LENIENT; + private static final Method KOTLIN_CLASS_METADATA_CLASS_GET_KM_CLASS; + private static final Method KM_CLASS_GET_CONSTRUCTORS; + private static final Method KM_CONSTRUCTOR_GET_VALUE_PARAMETERS; + private static final Method KM_VALUE_PARAMETER_GET_NAME; + private static final Method ATTRIBUTES_GET_DECLARES_DEFAULT_VALUE; + private static final boolean KOTLIN_METADATA_AVAILABLE; + + static { + Constructor newKotlinClassHeader = null; + Method kotlinClassMetadataReadLenient = null; + Method kotlinClassMetadataClassGetKmClass = null; + Method kmClassGetConstructors = null; + Method kmConstructorGetValueParameters = null; + Method kmValueParameterGetName = null; + Method attributeGetDeclaresDefaultValue = null; + boolean kotlinMetadataAvailable = false; + for (String prefix : new String[] {"kotlin.metadata.", "kotlinx.metadata."}) { + try { + Class kotlinClassHeaderClass = Class.forName(prefix + "jvm.KotlinClassHeader"); + newKotlinClassHeader = + kotlinClassHeaderClass.getConstructor( + Integer.class, + int[].class, + String[].class, + String[].class, + String.class, + String.class, + Integer.class); + Class kotlinClassMetadataClass = Class.forName(prefix + "jvm.KotlinClassMetadata"); + // Load `kotlin.Metadata` in the same classloader as `kotlinClassHeaderClass`. They are + // potentially from different artifacts so we could otherwise end up with a + // `kotlin.Metadata` that is not actually the type of the `readLenient` parameter because of + // differing classloaders. + Class kotlinMetadataClass = + Class.forName("kotlin.Metadata", false, kotlinClassHeaderClass.getClassLoader()); + kotlinClassMetadataReadLenient = + kotlinClassMetadataClass.getMethod("readLenient", kotlinMetadataClass); + Class kotlinClassMetadataClassClass = + Class.forName(prefix + "jvm.KotlinClassMetadata$Class"); + Class kmClassClass = Class.forName(prefix + "KmClass"); + kotlinClassMetadataClassGetKmClass = kotlinClassMetadataClassClass.getMethod("getKmClass"); + kmClassGetConstructors = kmClassClass.getMethod("getConstructors"); + Class kmConstructorClass = Class.forName(prefix + "KmConstructor"); + kmConstructorGetValueParameters = kmConstructorClass.getMethod("getValueParameters"); + Class kmValueParameterClass = Class.forName(prefix + "KmValueParameter"); + kmValueParameterGetName = kmValueParameterClass.getMethod("getName"); + Class attributeClass = Class.forName(prefix + "Attributes"); + attributeGetDeclaresDefaultValue = + attributeClass.getMethod("getDeclaresDefaultValue", kmValueParameterClass); + kotlinMetadataAvailable = true; + break; + } catch (ReflectiveOperationException e) { + // OK: The metadata API is unavailable with this prefix, and possibly with any prefix. + } + } + NEW_KOTLIN_CLASS_HEADER = newKotlinClassHeader; + KOTLIN_CLASS_METADATA_READ_LENIENT = kotlinClassMetadataReadLenient; + KOTLIN_CLASS_METADATA_CLASS_GET_KM_CLASS = kotlinClassMetadataClassGetKmClass; + KM_CLASS_GET_CONSTRUCTORS = kmClassGetConstructors; + KM_CONSTRUCTOR_GET_VALUE_PARAMETERS = kmConstructorGetValueParameters; + KM_VALUE_PARAMETER_GET_NAME = kmValueParameterGetName; + ATTRIBUTES_GET_DECLARES_DEFAULT_VALUE = attributeGetDeclaresDefaultValue; + KOTLIN_METADATA_AVAILABLE = kotlinMetadataAvailable; + } +} diff --git a/value/src/main/java/com/google/auto/value/processor/MethodSignature.java b/value/src/main/java/com/google/auto/value/processor/MethodSignature.java new file mode 100644 index 0000000000..ee3f39ca58 --- /dev/null +++ b/value/src/main/java/com/google/auto/value/processor/MethodSignature.java @@ -0,0 +1,66 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.auto.value.processor; + +import static com.google.auto.common.MoreStreams.toImmutableList; +import static com.google.auto.common.MoreTypes.asDeclared; +import static com.google.auto.common.MoreTypes.asExecutable; + +import com.google.common.collect.ImmutableList; +import java.util.stream.IntStream; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.util.Types; + +/** + * Represents the signature of a method: the types of its parameters and of its return value. The + * types in question are represented using {@link AnnotatedTypeMirror}, which is a {@link + * TypeMirror} and associated type annotations. + */ +final class MethodSignature { + private final ExecutableType originalMethod; + private final ExecutableType rewrittenMethod; + + private MethodSignature(ExecutableType originalMethod, ExecutableType rewrittenMethod) { + this.originalMethod = originalMethod; + this.rewrittenMethod = rewrittenMethod; + } + + ImmutableList parameterTypes() { + return IntStream.range(0, originalMethod.getParameterTypes().size()) + .mapToObj( + i -> + new AnnotatedTypeMirror( + originalMethod.getParameterTypes().get(i), + rewrittenMethod.getParameterTypes().get(i))) + .collect(toImmutableList()); + } + + AnnotatedTypeMirror returnType() { + return new AnnotatedTypeMirror(originalMethod.getReturnType(), rewrittenMethod.getReturnType()); + } + + static MethodSignature asMemberOf(Types typeUtils, DeclaredType in, ExecutableElement method) { + return new MethodSignature( + asExecutable(method.asType()), asExecutable(typeUtils.asMemberOf(in, method))); + } + + static MethodSignature asMemberOf(Types typeUtils, TypeElement in, ExecutableElement method) { + return asMemberOf(typeUtils, asDeclared(in.asType()), method); + } +} diff --git a/value/src/main/java/com/google/auto/value/processor/PropertyBuilderClassifier.java b/value/src/main/java/com/google/auto/value/processor/PropertyBuilderClassifier.java index f1c7533acd..5bb368bc20 100644 --- a/value/src/main/java/com/google/auto/value/processor/PropertyBuilderClassifier.java +++ b/value/src/main/java/com/google/auto/value/processor/PropertyBuilderClassifier.java @@ -33,7 +33,6 @@ import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.ExecutableType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; @@ -55,8 +54,7 @@ class PropertyBuilderClassifier { private final Elements elementUtils; private final BuilderMethodClassifier builderMethodClassifier; private final Predicate propertyIsNullable; - private final ImmutableMap propertyTypes; - private final EclipseHack eclipseHack; + private final ImmutableMap propertyTypes; private final Nullables nullables; PropertyBuilderClassifier( @@ -65,8 +63,7 @@ class PropertyBuilderClassifier { Elements elementUtils, BuilderMethodClassifier builderMethodClassifier, Predicate propertyIsNullable, - ImmutableMap propertyTypes, - EclipseHack eclipseHack, + ImmutableMap propertyTypes, Nullables nullables) { this.errorReporter = errorReporter; this.typeUtils = typeUtils; @@ -74,7 +71,6 @@ class PropertyBuilderClassifier { this.builderMethodClassifier = builderMethodClassifier; this.propertyIsNullable = propertyIsNullable; this.propertyTypes = propertyTypes; - this.eclipseHack = eclipseHack; this.nullables = nullables; } @@ -89,7 +85,7 @@ public static class PropertyBuilder { private final String name; private final String builderType; private final String nullableBuilderType; - private final TypeMirror builderTypeMirror; + private final AnnotatedTypeMirror builderAnnotatedType; private final String build; private final String initializer; private final String beforeInitDefault; @@ -101,7 +97,7 @@ public static class PropertyBuilder { ExecutableElement propertyBuilderMethod, String builderType, String nullableBuilderType, - TypeMirror builderTypeMirror, + AnnotatedTypeMirror builderAnnotatedType, String build, String initializer, String beforeInitDefault, @@ -112,7 +108,7 @@ public static class PropertyBuilder { this.name = propertyBuilderMethod.getSimpleName() + "$"; this.builderType = builderType; this.nullableBuilderType = nullableBuilderType; - this.builderTypeMirror = builderTypeMirror; + this.builderAnnotatedType = builderAnnotatedType; this.build = build; this.initializer = initializer; this.beforeInitDefault = beforeInitDefault; @@ -159,7 +155,7 @@ public String getNullableBuilderType() { } TypeMirror getBuilderTypeMirror() { - return builderTypeMirror; + return builderAnnotatedType.getType(); } /** The name of the build method, {@code build} or {@code buildOrThrow}. */ @@ -229,19 +225,19 @@ public String getCopyAll() { // This method outputs an error and returns Optional.empty() if the barBuilder() method has a // problem. Optional makePropertyBuilder(ExecutableElement method, String property) { - TypeMirror barBuilderTypeMirror = builderMethodClassifier.builderMethodReturnType(method); - if (barBuilderTypeMirror.getKind() != TypeKind.DECLARED) { + AnnotatedTypeMirror barBuilderAnnotatedType = builderMethodClassifier.builderMethodReturnType(method); + if (barBuilderAnnotatedType.getKind() != TypeKind.DECLARED) { errorReporter.reportError( method, "[AutoValueOddBuilderMethod] Method looks like a property builder, but its return type" + " is not a class or interface"); return Optional.empty(); } - DeclaredType barBuilderDeclaredType = MoreTypes.asDeclared(barBuilderTypeMirror); - TypeElement barBuilderTypeElement = MoreTypes.asTypeElement(barBuilderTypeMirror); + DeclaredType barBuilderDeclaredType = MoreTypes.asDeclared(barBuilderAnnotatedType.getType()); + TypeElement barBuilderTypeElement = MoreTypes.asTypeElement(barBuilderDeclaredType); Map barBuilderNoArgMethods = noArgMethodsOf(barBuilderTypeElement); - TypeMirror barTypeMirror = propertyTypes.get(property); + TypeMirror barTypeMirror = propertyTypes.get(property).getType(); if (barTypeMirror.getKind() != TypeKind.DECLARED) { errorReporter.reportError( method, @@ -276,7 +272,8 @@ Optional makePropertyBuilder(ExecutableElement method, String p // We've determined that `BarBuilder` has a method `build()` or `buildOrThrow(). But it must // return `Bar`. And if the type of `bar()` is Bar then `BarBuilder.build()` must return // something that can be assigned to Bar. - TypeMirror buildType = eclipseHack.methodReturnType(build, barBuilderDeclaredType); + TypeMirror buildType = + MethodSignature.asMemberOf(typeUtils, barBuilderDeclaredType, build).returnType().getType(); if (!typeUtils.isAssignable(buildType, barTypeMirror)) { errorReporter.reportError( method, @@ -310,10 +307,10 @@ Optional makePropertyBuilder(ExecutableElement method, String p } ExecutableElement builderMaker = maybeBuilderMaker.get(); - String barBuilderType = TypeEncoder.encodeWithAnnotations(barBuilderTypeMirror); + String barBuilderType = TypeEncoder.encodeWithAnnotations(barBuilderAnnotatedType); String nullableBarBuilderType = TypeEncoder.encodeWithAnnotations( - barBuilderTypeMirror, nullables.nullableTypeAnnotations()); + barBuilderAnnotatedType, nullables.nullableTypeAnnotations()); String rawBarType = TypeEncoder.encodeRaw(barTypeMirror); String arguments = method.getParameters().isEmpty() @@ -330,7 +327,7 @@ Optional makePropertyBuilder(ExecutableElement method, String p && !toBuilder.getModifiers().contains(Modifier.STATIC) && typeUtils.isAssignable( typeUtils.erasure(toBuilder.getReturnType()), - typeUtils.erasure(barBuilderTypeMirror))) { + typeUtils.erasure(barBuilderDeclaredType))) { builtToBuilder = toBuilder.getSimpleName().toString(); } else { Optional maybeCopyAll = @@ -364,7 +361,7 @@ Optional makePropertyBuilder(ExecutableElement method, String p method, barBuilderType, nullableBarBuilderType, - barBuilderTypeMirror, + barBuilderAnnotatedType, build.getSimpleName().toString(), initializer, beforeInitDefault, @@ -480,9 +477,12 @@ private Optional addAllPutAll( && method.getParameters().size() == 1) .filter( method -> { - ExecutableType methodMirror = - MoreTypes.asExecutable(typeUtils.asMemberOf(barBuilderDeclaredType, method)); - return typeUtils.isAssignable(barTypeMirror, methodMirror.getParameterTypes().get(0)); + TypeMirror parameterType = + MethodSignature.asMemberOf(typeUtils, barBuilderDeclaredType, method) + .parameterTypes() + .get(0) + .getType(); + return typeUtils.isAssignable(barTypeMirror, parameterType); }) .findFirst(); } diff --git a/value/src/main/java/com/google/auto/value/processor/TemplateVars.java b/value/src/main/java/com/google/auto/value/processor/TemplateVars.java index 73253629dd..a3abb190cd 100644 --- a/value/src/main/java/com/google/auto/value/processor/TemplateVars.java +++ b/value/src/main/java/com/google/auto/value/processor/TemplateVars.java @@ -145,7 +145,7 @@ private static Reader readerFromUrl(String resourceName) throws IOException { } else if (Ascii.equalsIgnoreCase(resourceUrl.getProtocol(), "jar")) { return readerFromJar(resourceUrl); } else { - throw new AssertionError("Template search logic fails for: " + resourceUrl); + return readerFromOther(resourceUrl); } } catch (URISyntaxException e) { throw new IOException(e); @@ -174,12 +174,18 @@ private static Reader readerFromJar(URL resourceUrl) throws URISyntaxException, // In most execution environments, we'll be dealing with a jar, but we handle individual files // just for cases like running our tests with Maven. - private static Reader readerFromFile(URL resourceUrl) - throws IOException, URISyntaxException { + private static Reader readerFromFile(URL resourceUrl) throws IOException, URISyntaxException { File resourceFile = new File(resourceUrl.toURI()); return new InputStreamReader(new FileInputStream(resourceFile), UTF_8); } + // As a fallback, we handle other kinds of URL naively. For example, if we're executing in GraalVM + // code, we might have a `resource:` URL. This code is not currently covered by unit tests. + // See https://github.com/google/auto/issues/1783. + private static Reader readerFromOther(URL resourceUrl) throws IOException { + return new InputStreamReader(resourceUrl.openStream(), UTF_8); + } + private static Object fieldValue(Field field, Object container) { try { return field.get(container); diff --git a/value/src/main/java/com/google/auto/value/processor/TypeEncoder.java b/value/src/main/java/com/google/auto/value/processor/TypeEncoder.java index 2643d12d4e..83df64e954 100644 --- a/value/src/main/java/com/google/auto/value/processor/TypeEncoder.java +++ b/value/src/main/java/com/google/auto/value/processor/TypeEncoder.java @@ -102,6 +102,14 @@ static String encodeWithAnnotations(TypeMirror type) { return encodeWithAnnotations(type, ImmutableList.of(), ImmutableSet.of()); } + /** + * Encodes the given type and its type annotations. The class comment for {@link TypeEncoder} + * covers the details of annotation encoding. + */ + static String encodeWithAnnotations(AnnotatedTypeMirror type) { + return encodeWithAnnotations(type, ImmutableList.of(), ImmutableSet.of()); + } + /** * Encodes the given type and its type annotations. The class comment for {@link TypeEncoder} * covers the details of annotation encoding. @@ -114,6 +122,18 @@ static String encodeWithAnnotations( return encodeWithAnnotations(type, extraAnnotations, ImmutableSet.of()); } + /** + * Encodes the given type and its type annotations. The class comment for {@link TypeEncoder} + * covers the details of annotation encoding. + * + * @param extraAnnotations additional type annotations to include with the type + */ + static String encodeWithAnnotations( + AnnotatedTypeMirror type, + ImmutableList extraAnnotations) { + return encodeWithAnnotations(type, extraAnnotations, ImmutableSet.of()); + } + /** * Encodes the given type and its type annotations. The class comment for {@link TypeEncoder} * covers the details of annotation encoding. @@ -123,7 +143,7 @@ static String encodeWithAnnotations( * {@code com.example.Nullable} is in this set then the encoding will not include this * {@code @Nullable} annotation. */ - static String encodeWithAnnotations( + private static String encodeWithAnnotations( TypeMirror type, ImmutableList extraAnnotations, Set excludedAnnotationTypes) { @@ -143,6 +163,35 @@ static String encodeWithAnnotations( .toString(); } + /** + * Encodes the given type and its type annotations. The class comment for {@link TypeEncoder} + * covers the details of annotation encoding. + * + * @param extraAnnotations additional type annotations to include with the type + * @param excludedAnnotationTypes annotations not to include in the encoding. For example, if + * {@code com.example.Nullable} is in this set then the encoding will not include this + * {@code @Nullable} annotation. + */ + static String encodeWithAnnotations( + AnnotatedTypeMirror type, + ImmutableList extraAnnotations, + Set excludedAnnotationTypes) { + StringBuilder sb = new StringBuilder(); + // A function that is equivalent to t.getAnnotationMirrors() except when the t in question is + // our starting type. In that case we also add extraAnnotations to the result. + Function> getTypeAnnotations = + t -> + (t == type.getType()) + ? ImmutableList.builder() + .addAll(type.annotations()) + .addAll(extraAnnotations) + .build() + : t.getAnnotationMirrors(); + return new AnnotatedEncodingTypeVisitor(excludedAnnotationTypes, getTypeAnnotations) + .visit2(type.getType(), sb) + .toString(); + } + /** * Decodes the given string, respelling class names appropriately. The text is scanned for tokens * like {@code `java.util.Locale`} or {@code `«java.util.Locale`} to determine which classes are diff --git a/value/src/main/java/com/google/auto/value/processor/TypeVariables.java b/value/src/main/java/com/google/auto/value/processor/TypeVariables.java index ae27f91b46..cc0bb13545 100644 --- a/value/src/main/java/com/google/auto/value/processor/TypeVariables.java +++ b/value/src/main/java/com/google/auto/value/processor/TypeVariables.java @@ -37,7 +37,6 @@ import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; import javax.lang.model.type.WildcardType; -import javax.lang.model.util.Elements; import javax.lang.model.util.SimpleTypeVisitor8; import javax.lang.model.util.Types; @@ -79,8 +78,7 @@ private TypeVariables() {} * @param targetType the class to translate the methods into ({@code Foo.Builder}) in the * example. */ - static ImmutableMap rewriteReturnTypes( - Elements elementUtils, + static ImmutableMap rewriteReturnTypes( Types typeUtils, Collection methods, TypeElement sourceType, @@ -95,14 +93,16 @@ static ImmutableMap rewriteReturnTypes( // What we're doing is only valid if the type parameters are "the same". The check here even // requires the names to be the same. The logic would still work without that, but we impose // that requirement elsewhere and it means we can check in this simple way. - EclipseHack eclipseHack = new EclipseHack(elementUtils, typeUtils); TypeMirror[] targetTypeParameterMirrors = new TypeMirror[targetTypeParameters.size()]; for (int i = 0; i < targetTypeParameters.size(); i++) { targetTypeParameterMirrors[i] = targetTypeParameters.get(i).asType(); } DeclaredType parallelSource = typeUtils.getDeclaredType(sourceType, targetTypeParameterMirrors); return methods.stream() - .collect(toImmutableMap(m -> m, m -> eclipseHack.methodReturnType(m, parallelSource))); + .collect( + toImmutableMap( + m -> m, + m -> MethodSignature.asMemberOf(typeUtils, parallelSource, m).returnType())); } /** 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 18ca827aea..ec0910c2a0 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 @@ -181,7 +181,7 @@ ${modifiers}class $subclass$formalTypes extends $origClass$actualTypes { @`java.lang.Override` ${m.access}${builderTypeName}${builderActualTypes} ${m.name}() { - return new Builder${builderActualTypes}(this); + return new ${finalSubclass}.Builder${builderActualTypes}(this); } #end diff --git a/value/src/main/java/com/google/auto/value/processor/builder.vm b/value/src/main/java/com/google/auto/value/processor/builder.vm index 3b90619634..30989b7843 100644 --- a/value/src/main/java/com/google/auto/value/processor/builder.vm +++ b/value/src/main/java/com/google/auto/value/processor/builder.vm @@ -30,9 +30,7 @@ #foreach ($a in $builderAnnotations) $a #end -#if (!$autoBuilder) static #end## -#if ($isFinal) final #end## -class ${builderName}${builderFormalTypes} ## +${builderClassModifiers}class ${builderName}${builderFormalTypes} ## #if ($builderIsInterface) implements #else extends #end ${builderTypeName}${builderActualTypes} { @@ -60,7 +58,6 @@ class ${builderName}${builderFormalTypes} ## #if ($toBuilderConstructor) - #if (!$autoBuilder) private #end## ${builderName}($builtType source) { #foreach ($p in $props) @@ -252,28 +249,28 @@ class ${builderName}${builderFormalTypes} ## #if ($identifiers) ## build a friendly message showing all missing properties #if ($builderRequiredProperties.requiredProperties.size() == 1) - `java.lang.String` missing = " $builderRequiredProperties.requiredProperties.iterator().next()"; + `java.lang.String` missing = " $builderRequiredProperties.requiredProperties.iterator().next()"; #else - `java.lang.StringBuilder` missing = new `java.lang.StringBuilder`(); + `java.lang.StringBuilder` missing = new `java.lang.StringBuilder`(); #foreach ($p in $builderRequiredProperties.requiredProperties) - if ($builderRequiredProperties.missingRequiredProperty($p)) { - missing.append(" $p.name"); - } + if ($builderRequiredProperties.missingRequiredProperty($p)) { + missing.append(" $p.name"); + } #end #end - throw new IllegalStateException("Missing required properties:" + missing); + throw new IllegalStateException("Missing required properties:" + missing); #else ## just throw an exception if anything is missing - throw new IllegalStateException(); + throw new IllegalStateException(); #end - } + } #end diff --git a/value/src/test/java/com/google/auto/value/extension/serializable/processor/SerializableAutoValueExtensionTest.java b/value/src/test/java/com/google/auto/value/extension/serializable/processor/SerializableAutoValueExtensionTest.java index cbfdcafd45..3f0858b681 100644 --- a/value/src/test/java/com/google/auto/value/extension/serializable/processor/SerializableAutoValueExtensionTest.java +++ b/value/src/test/java/com/google/auto/value/extension/serializable/processor/SerializableAutoValueExtensionTest.java @@ -16,7 +16,6 @@ package com.google.auto.value.extension.serializable.processor; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import static org.junit.Assert.assertThrows; import com.google.auto.value.AutoValue; diff --git a/value/src/test/java/com/google/auto/value/extension/serializable/serializer/impl/ImmutableListSerializerExtensionTest.java b/value/src/test/java/com/google/auto/value/extension/serializable/serializer/impl/ImmutableListSerializerExtensionTest.java index 159059d31d..331b260a2c 100644 --- a/value/src/test/java/com/google/auto/value/extension/serializable/serializer/impl/ImmutableListSerializerExtensionTest.java +++ b/value/src/test/java/com/google/auto/value/extension/serializable/serializer/impl/ImmutableListSerializerExtensionTest.java @@ -16,7 +16,6 @@ package com.google.auto.value.extension.serializable.serializer.impl; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import com.google.auto.value.extension.serializable.serializer.interfaces.Serializer; import com.google.auto.value.extension.serializable.serializer.utils.CompilationAbstractTest; diff --git a/value/src/test/java/com/google/auto/value/extension/serializable/serializer/impl/ImmutableMapSerializerExtensionTest.java b/value/src/test/java/com/google/auto/value/extension/serializable/serializer/impl/ImmutableMapSerializerExtensionTest.java index a7006f54ce..9995665839 100644 --- a/value/src/test/java/com/google/auto/value/extension/serializable/serializer/impl/ImmutableMapSerializerExtensionTest.java +++ b/value/src/test/java/com/google/auto/value/extension/serializable/serializer/impl/ImmutableMapSerializerExtensionTest.java @@ -16,7 +16,6 @@ package com.google.auto.value.extension.serializable.serializer.impl; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import com.google.auto.value.extension.serializable.serializer.interfaces.Serializer; import com.google.auto.value.extension.serializable.serializer.utils.CompilationAbstractTest; diff --git a/value/src/test/java/com/google/auto/value/extension/serializable/serializer/impl/OptionalSerializerExtensionTest.java b/value/src/test/java/com/google/auto/value/extension/serializable/serializer/impl/OptionalSerializerExtensionTest.java index e817911e16..87ee644ec5 100644 --- a/value/src/test/java/com/google/auto/value/extension/serializable/serializer/impl/OptionalSerializerExtensionTest.java +++ b/value/src/test/java/com/google/auto/value/extension/serializable/serializer/impl/OptionalSerializerExtensionTest.java @@ -16,7 +16,6 @@ package com.google.auto.value.extension.serializable.serializer.impl; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import com.google.auto.value.extension.serializable.serializer.interfaces.Serializer; import com.google.auto.value.extension.serializable.serializer.utils.CompilationAbstractTest; diff --git a/value/src/test/java/com/google/auto/value/processor/AutoAnnotationCompilationTest.java b/value/src/test/java/com/google/auto/value/processor/AutoAnnotationCompilationTest.java index 1f79a074b5..6492fa009e 100644 --- a/value/src/test/java/com/google/auto/value/processor/AutoAnnotationCompilationTest.java +++ b/value/src/test/java/com/google/auto/value/processor/AutoAnnotationCompilationTest.java @@ -15,7 +15,7 @@ */ package com.google.auto.value.processor; -import static com.google.common.truth.Truth8.assertThat; +import static com.google.common.truth.Truth.assertThat; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; @@ -243,7 +243,7 @@ public void testGwtSimple() { "package com.example.factories;", "", "import com.example.annotations.MyAnnotation;", - "import java.io.Serializable", + "import java.io.Serializable;", "import java.util.Arrays;", GeneratedImport.importGeneratedAnnotationType(), "", @@ -376,7 +376,7 @@ public void testCollectionsForArrays() { " if (enums == null) {", " throw new NullPointerException(\"Null enums\");", " }", - " this.enums = enums.toArray(new MyEnum[0];", + " this.enums = enums.toArray(new MyEnum[0]);", " }", "", " @Override public Class annotationType() {", @@ -414,7 +414,7 @@ public void testCollectionsForArrays() { " && Arrays.equals(enums,", " (that instanceof AutoAnnotation_AnnotationFactory_newMyAnnotation)", " ? ((AutoAnnotation_AnnotationFactory_newMyAnnotation) that).enums", - " : that.enums())", + " : that.enums());", " }", " return false;", " }", diff --git a/value/src/test/java/com/google/auto/value/processor/AutoBuilderCompilationTest.java b/value/src/test/java/com/google/auto/value/processor/AutoBuilderCompilationTest.java index c85deb5a46..a850d171cb 100644 --- a/value/src/test/java/com/google/auto/value/processor/AutoBuilderCompilationTest.java +++ b/value/src/test/java/com/google/auto/value/processor/AutoBuilderCompilationTest.java @@ -872,6 +872,46 @@ public void nullableSetterForNonNullableParameter() { .onLineContaining("thing(@Nullable String x)"); } + @Test + public void nullablePrimitiveParameter() { + JavaFileObject javaFileObject = + JavaFileObjects.forSourceLines( + "foo.bar.Baz", + "package foo.bar;", + "", + "import com.google.auto.value.AutoBuilder;", + "import com.example.annotations.Nullable;", + "", + "class Baz {", + " Baz(@Nullable int thing) {}", + "", + " @AutoBuilder", + " interface Builder {", + " abstract Builder thing(int x);", + " abstract Baz build();", + " }", + "}"); + JavaFileObject nullableFileObject = + JavaFileObjects.forSourceLines( + "com.example.annotations.Nullable", + "package com.example.annotations;", + "", + "import java.lang.annotation.ElementType;", + "import java.lang.annotation.Target;", + "", + "@Target(ElementType.TYPE_USE)", + "public @interface Nullable {}"); + Compilation compilation = + javac() + .withProcessors(new AutoBuilderProcessor()) + .compile(javaFileObject, nullableFileObject); + assertThat(compilation).failed(); + assertThat(compilation) + .hadErrorContaining("[AutoBuilderNullPrimitive] Primitive types cannot be @Nullable") + .inFile(javaFileObject) + .onLineContaining("Baz(@Nullable int thing)"); + } + @Test public void setterWrongType() { JavaFileObject javaFileObject = diff --git a/value/src/test/java/com/google/auto/value/processor/AutoOneOfCompilationTest.java b/value/src/test/java/com/google/auto/value/processor/AutoOneOfCompilationTest.java index a55b74d0f2..d729a9dc73 100644 --- a/value/src/test/java/com/google/auto/value/processor/AutoOneOfCompilationTest.java +++ b/value/src/test/java/com/google/auto/value/processor/AutoOneOfCompilationTest.java @@ -27,7 +27,9 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -/** @author emcmanus@google.com (Éamonn McManus) */ +/** + * @author emcmanus@google.com (Éamonn McManus) + */ @RunWith(JUnit4.class) public class AutoOneOfCompilationTest { @Rule public final Expect expect = Expect.create(); @@ -233,7 +235,8 @@ public void success() { " public TaskResult.Kind getKind() {", " return TaskResult.Kind.EMPTY;", " }", - " }"); + " }", + "}"); Compilation compilation = javac() .withProcessors(new AutoOneOfProcessor()) diff --git a/value/src/test/java/com/google/auto/value/processor/AutoValueCompilationTest.java b/value/src/test/java/com/google/auto/value/processor/AutoValueCompilationTest.java index d30198cfa9..e4224913cf 100644 --- a/value/src/test/java/com/google/auto/value/processor/AutoValueCompilationTest.java +++ b/value/src/test/java/com/google/auto/value/processor/AutoValueCompilationTest.java @@ -1271,7 +1271,7 @@ public void correctBuilder() { " }", "", " @Override public Baz.Builder toBuilder() {", - " return new Builder(this);", + " return new AutoValue_Baz.Builder(this);", " }", "", " static final class Builder extends Baz.Builder {", @@ -1289,7 +1289,7 @@ public void correctBuilder() { " Builder() {", " }", "", - " private Builder(Baz source) {", + " Builder(Baz source) {", " this.anInt = source.anInt();", " this.aByteArray = source.aByteArray();", " this.aNullableIntArray = source.aNullableIntArray();", @@ -1621,7 +1621,7 @@ public void builderWithNullableTypeAnnotation() { " }", "", " @Override public Baz.Builder toBuilder() {", - " return new Builder(this);", + " return new AutoValue_Baz.Builder(this);", " }", "", " static final class Builder extends Baz.Builder {", @@ -1637,7 +1637,7 @@ public void builderWithNullableTypeAnnotation() { " Builder() {", " }", "", - " private Builder(Baz source) {", + " Builder(Baz source) {", " this.anInt = source.anInt();", " this.aByteArray = source.aByteArray();", " this.aNullableIntArray = source.aNullableIntArray();", diff --git a/value/src/test/java/com/google/auto/value/processor/AutoValueModuleCompilationTest.java b/value/src/test/java/com/google/auto/value/processor/AutoValueModuleCompilationTest.java deleted file mode 100644 index e2362a7f7b..0000000000 --- a/value/src/test/java/com/google/auto/value/processor/AutoValueModuleCompilationTest.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.auto.value.processor; - -import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.truth.Truth.assertThat; - -import com.google.common.collect.ImmutableList; -import com.google.testing.compile.JavaFileObjects; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import javax.tools.Diagnostic; -import javax.tools.JavaCompiler; -import javax.tools.JavaFileObject; -import javax.tools.ToolProvider; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public final class AutoValueModuleCompilationTest { - @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); - - /** - * This test currently confirms that AutoValue has a bug with module handling. If your AutoValue - * class inherits an abstract method from a module, and that abstract method has an annotation - * that is not exported from the module, then AutoValue will incorrectly try to copy the - * annotation onto the generated implementation of the abstract method. - */ - @Test - public void nonVisibleMethodAnnotationFromOtherModule() throws Exception { - JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler(); - Path tempDir = temporaryFolder.newFolder().toPath(); - Path srcDir = tempDir.resolve("src"); - Path outDir = tempDir.resolve("out"); - Path fooOut = outDir.resolve("foo"); - writeFile(srcDir, "foo/module-info.java", "module foo {", " exports foo.exported;", "}"); - writeFile( - srcDir, - "foo/exported/Foo.java", - "package foo.exported;", - "", - "import foo.unexported.UnexportedAnnotation;", - "", - "public interface Foo {", - " @ExportedAnnotation", - " @UnexportedAnnotation", - " String name();", - "}"); - writeFile( - srcDir, - "foo/exported/ExportedAnnotation.java", - "package foo.exported;", - "", - "import java.lang.annotation.*;", - "", - "@Retention(RetentionPolicy.RUNTIME)", - "public @interface ExportedAnnotation {}"); - writeFile( - srcDir, - "foo/unexported/UnexportedAnnotation.java", - "package foo.unexported;", - "", - "import java.lang.annotation.*;", - "", - "@Retention(RetentionPolicy.RUNTIME)", - "public @interface UnexportedAnnotation {}"); - JavaCompiler.CompilationTask fooCompilationTask = - javaCompiler.getTask( - /* out= */ null, - /* fileManager= */ null, - /* diagnosticListener= */ null, - /* options= */ ImmutableList.of( - "--module", - "foo", - "-d", - outDir.toString(), - "--module-source-path", - srcDir.toString()), - /* classes= */ null, - /* compilationUnits= */ null); - fooCompilationTask.setProcessors(ImmutableList.of(new AutoValueProcessor())); - boolean fooSuccess = fooCompilationTask.call(); - assertThat(fooSuccess).isTrue(); - - JavaFileObject barFile = - JavaFileObjects.forSourceLines( - "bar.Value", - "package bar;", - "", - "import com.google.auto.value.AutoValue;", - "import foo.exported.Foo;", - "", - "@AutoValue", - "public abstract class Value implements Foo {", - " public static Value of(String name) {", - " return new AutoValue_Value(name);", - " }", - "}"); - List> diagnostics = new ArrayList<>(); - JavaCompiler.CompilationTask barCompilationTask = - javaCompiler.getTask( - /* out= */ null, - /* fileManager= */ null, - /* diagnosticListener= */ diagnostics::add, - /* options= */ ImmutableList.of( - "-d", - outDir.toString(), - "--module-path", - fooOut.toString(), - "--add-modules", - "foo"), - /* classes= */ null, - /* compilationUnits= */ ImmutableList.of(barFile)); - boolean barSuccess = barCompilationTask.call(); - assertThat(barSuccess).isFalse(); - assertThat( - diagnostics.stream() - .map(d -> d.getMessage(Locale.ROOT)) - .collect(toImmutableList()) - .toString()) - .contains("package foo.unexported is declared in module foo, which does not export it"); - } - - private static void writeFile(Path srcDir, String relativeName, String... lines) - throws IOException { - Path path = srcDir.resolve(relativeName); - Files.createDirectories(path.getParent()); - Files.writeString(path, String.join("\n", lines)); - } -} diff --git a/value/src/test/java/com/google/auto/value/processor/BuilderRequiredPropertiesTest.java b/value/src/test/java/com/google/auto/value/processor/BuilderRequiredPropertiesTest.java index fe7ad321ca..f854c60a02 100644 --- a/value/src/test/java/com/google/auto/value/processor/BuilderRequiredPropertiesTest.java +++ b/value/src/test/java/com/google/auto/value/processor/BuilderRequiredPropertiesTest.java @@ -341,7 +341,7 @@ private Property fakeProperty(String name, TypeMirror type, boolean hasDefault) /* name= */ name, /* identifier= */ name, /* type= */ type.toString(), - /* typeMirror= */ type, + /* annotatedType= */ new AnnotatedTypeMirror(type), /* nullableAnnotation= */ Optional.empty(), /* nullables= */ Nullables.fromMethods(null, ImmutableList.of()), /* getter= */ name, diff --git a/value/src/test/java/com/google/auto/value/processor/ExtensionTest.java b/value/src/test/java/com/google/auto/value/processor/ExtensionTest.java index 56eaad25f3..b692c2726e 100644 --- a/value/src/test/java/com/google/auto/value/processor/ExtensionTest.java +++ b/value/src/test/java/com/google/auto/value/processor/ExtensionTest.java @@ -15,8 +15,8 @@ */ package com.google.auto.value.processor; +import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; @@ -205,7 +205,7 @@ public void testDoesntRaiseWarningForConsumedProperties() { } @Test - public void testDoesntRaiseWarningForToBuilder() { + public void testDoesntRaiseWarningForToBuilder() throws IOException { JavaFileObject impl = JavaFileObjects.forSourceLines( "foo.bar.Baz", @@ -227,6 +227,18 @@ public void testDoesntRaiseWarningForToBuilder() { .withProcessors(new AutoValueProcessor(ImmutableList.of(new FooExtension()))) .compile(impl); assertThat(compilation).succeededWithoutWarnings(); + + // $AutoValue_Baz is the source file generated by AutoValue itself, which is then subclassed as + // AutoValue_Baz by the extension. The implementation of toBuilder() should call + // `new AutoValue_Baz.Builder` in case the extension includes its own subclass of the Builder + // class. + Optional generatedSourceFile = + compilation.generatedSourceFile("foo.bar.$AutoValue_Baz"); + assertThat(generatedSourceFile).isPresent(); + String content = + generatedSourceFile.get().getCharContent(/* ignoreEncodingErrors= */ false).toString(); + assertThat(content).doesNotContain("new Builder"); + assertThat(content).contains("new AutoValue_Baz.Builder"); } @Test @@ -384,6 +396,60 @@ public void testExtensionWithoutConsumedPropertiesFails() { .onLineContaining("abstract Double[] bad()"); } + @Test + public void testConsumeBuilderMethod() { + JavaFileObject javaFileObject = + JavaFileObjects.forSourceLines( + "foo.bar.Baz", + "package foo.bar;", + "", + "import com.google.auto.value.AutoValue;", + "", + "@AutoValue", + "public abstract class Baz {", + " abstract String foo();", + " @AutoValue.Builder", + " public static abstract class Builder {", + " abstract Builder setFoo(String value);", + " abstract int doSomething();", + " abstract Baz build();", + " }", + "}"); + BuilderExtension extension = new BuilderExtension(); + extension.consumeMethod("doSomething"); + Compilation compilation = + javac() + .withProcessors(new AutoValueProcessor(ImmutableList.of(extension))) + .compile(javaFileObject); + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void testAbstractBuilderMethodNotConsumedFails() { + JavaFileObject javaFileObject = + JavaFileObjects.forSourceLines( + "foo.bar.Baz", + "package foo.bar;", + "", + "import com.google.auto.value.AutoValue;", + "", + "@AutoValue", + "public abstract class Baz {", + " abstract String foo();", + " @AutoValue.Builder", + " public static abstract class Builder {", + " abstract Builder setFoo(String value);", + " abstract int doSomething();", + " abstract Baz build();", + " }", + "}"); + Compilation compilation = + javac() + .withProcessors(new AutoValueProcessor(ImmutableList.of(new BuilderExtension()))) + .compile(javaFileObject); + assertThat(compilation).hadErrorContaining("AutoValueBuilderNoArg"); + } + @Test public void testConsumeMethodWithArguments() { JavaFileObject javaFileObject = @@ -778,6 +844,83 @@ public String generateClass( } } + private static class BuilderExtension extends AutoValueExtension { + + private Optional consumedMethodName; + + BuilderExtension() { + this.consumedMethodName = Optional.empty(); + } + + public void consumeMethod(String methodName) { + consumedMethodName = Optional.of(methodName); + } + + @Override + public boolean applicable(Context context) { + return true; + } + + @Override + public Set consumeBuilderMethods(Context context) { + return consumedMethodName.map(s -> context.builderAbstractMethods().stream() + .filter(element -> element.getSimpleName().toString().endsWith(s)) + .collect(toImmutableSet())).orElseGet(ImmutableSet::of); + } + + @Override + public String generateClass( + Context context, String className, String classToExtend, boolean isFinal) { + StringBuilder constructor = + new StringBuilder().append(" public ").append(className).append("("); + + boolean first = true; + for (Map.Entry el : context.properties().entrySet()) { + if (first) { + first = false; + } else { + constructor.append(", "); + } + constructor.append("String ").append(el.getKey()); + } + + constructor.append(") {\n"); + constructor.append(" super("); + + first = true; + for (Map.Entry el : context.properties().entrySet()) { + if (first) { + first = false; + } else { + constructor.append(", "); + } + constructor.append(el.getKey()); + } + constructor.append(");\n"); + constructor.append(" }\n"); + + return String.format( + "package %s;\n" + + "\n" + + "%s class %s extends %s {\n" + + constructor + + " @Override public String foo() {\n" + + " return \"foo\";\n" + + " }\n" + + " public static class Builder extends $AutoValue_Baz.Builder {\n" + + " @Override\n" + + " public int doSomething() {\n" + + " return 5;\n" + + " }\n" + + " }\n" + + "}", + context.packageName(), + isFinal ? "final" : "abstract", + className, + classToExtend); + } + } + // Extension that generates a class that just forwards to the parent constructor. // We will make subclasses that are respectively final and non-final. private abstract static class EmptyExtension extends AutoValueExtension { diff --git a/value/src/test/java/com/google/auto/value/processor/TypeVariablesTest.java b/value/src/test/java/com/google/auto/value/processor/TypeVariablesTest.java index 895bed2535..821c9beb51 100644 --- a/value/src/test/java/com/google/auto/value/processor/TypeVariablesTest.java +++ b/value/src/test/java/com/google/auto/value/processor/TypeVariablesTest.java @@ -60,9 +60,11 @@ public void noTypeParameters() { TypeElement source1 = elementUtils.getTypeElement(Source1.class.getCanonicalName()); TypeElement target1 = elementUtils.getTypeElement(Target1.class.getCanonicalName()); List sourceMethods = ElementFilter.methodsIn(source1.getEnclosedElements()); - Map types = - TypeVariables.rewriteReturnTypes(elementUtils, typeUtils, sourceMethods, source1, target1); - assertThat(types).containsExactly(sourceMethods.get(0), sourceMethods.get(0).getReturnType()); + ImmutableMap types = + TypeVariables.rewriteReturnTypes(typeUtils, sourceMethods, source1, target1); + assertThat(types) + .containsExactly( + sourceMethods.get(0), new AnnotatedTypeMirror(sourceMethods.get(0).getReturnType())); } abstract static class Source2 { @@ -78,13 +80,13 @@ public void simpleTypeParameter() { TypeElement source2 = elementUtils.getTypeElement(Source2.class.getCanonicalName()); TypeElement target2 = elementUtils.getTypeElement(Target2.class.getCanonicalName()); List sourceMethods = ElementFilter.methodsIn(source2.getEnclosedElements()); - Map types = - TypeVariables.rewriteReturnTypes(elementUtils, typeUtils, sourceMethods, source2, target2); + ImmutableMap types = + TypeVariables.rewriteReturnTypes(typeUtils, sourceMethods, source2, target2); List targetMethods = ElementFilter.methodsIn(target2.getEnclosedElements()); TypeMirror setFooParameter = targetMethods.get(0).getParameters().get(0).asType(); ExecutableElement getFoo = sourceMethods.get(0); TypeMirror originalGetFooReturn = getFoo.getReturnType(); - TypeMirror rewrittenGetFooReturn = types.get(getFoo); + TypeMirror rewrittenGetFooReturn = types.get(getFoo).getType(); assertThat(typeUtils.isAssignable(setFooParameter, originalGetFooReturn)).isFalse(); assertThat(typeUtils.isAssignable(setFooParameter, rewrittenGetFooReturn)).isTrue(); } @@ -102,13 +104,13 @@ public void hairyTypeParameters() { TypeElement source3 = elementUtils.getTypeElement(Source3.class.getCanonicalName()); TypeElement target3 = elementUtils.getTypeElement(Target3.class.getCanonicalName()); List sourceMethods = ElementFilter.methodsIn(source3.getEnclosedElements()); - Map types = - TypeVariables.rewriteReturnTypes(elementUtils, typeUtils, sourceMethods, source3, target3); + ImmutableMap types = + TypeVariables.rewriteReturnTypes(typeUtils, sourceMethods, source3, target3); List targetMethods = ElementFilter.methodsIn(target3.getEnclosedElements()); TypeMirror setFooParameter = targetMethods.get(0).getParameters().get(0).asType(); ExecutableElement getFoo = sourceMethods.get(0); TypeMirror originalGetFooReturn = getFoo.getReturnType(); - TypeMirror rewrittenGetFooReturn = types.get(getFoo); + TypeMirror rewrittenGetFooReturn = types.get(getFoo).getType(); assertThat(typeUtils.isAssignable(setFooParameter, originalGetFooReturn)).isFalse(); assertThat(typeUtils.isAssignable(setFooParameter, rewrittenGetFooReturn)).isTrue(); } @@ -130,8 +132,8 @@ public void nestedClasses() { TypeElement outer = elementUtils.getTypeElement(Outer.class.getCanonicalName()); TypeElement inner = elementUtils.getTypeElement(Outer.Inner.class.getCanonicalName()); List outerMethods = ElementFilter.methodsIn(outer.getEnclosedElements()); - Map types = - TypeVariables.rewriteReturnTypes(elementUtils, typeUtils, outerMethods, outer, inner); + ImmutableMap types = + TypeVariables.rewriteReturnTypes(typeUtils, outerMethods, outer, inner); List innerMethods = ElementFilter.methodsIn(inner.getEnclosedElements()); ExecutableElement getFoo = methodNamed(outerMethods, "getFoo"); ExecutableElement getBar = methodNamed(outerMethods, "getBar"); @@ -139,12 +141,12 @@ public void nestedClasses() { ExecutableElement setBar = methodNamed(innerMethods, "setBar"); TypeMirror setFooParameter = setFoo.getParameters().get(0).asType(); TypeMirror originalGetFooReturn = getFoo.getReturnType(); - TypeMirror rewrittenGetFooReturn = types.get(getFoo); + TypeMirror rewrittenGetFooReturn = types.get(getFoo).getType(); assertThat(typeUtils.isAssignable(setFooParameter, originalGetFooReturn)).isFalse(); assertThat(typeUtils.isAssignable(setFooParameter, rewrittenGetFooReturn)).isTrue(); TypeMirror setBarParameter = setBar.getParameters().get(0).asType(); TypeMirror originalGetBarReturn = getBar.getReturnType(); - TypeMirror rewrittenGetBarReturn = types.get(getBar); + TypeMirror rewrittenGetBarReturn = types.get(getBar).getType(); assertThat(typeUtils.isAssignable(setBarParameter, originalGetBarReturn)).isFalse(); assertThat(typeUtils.isAssignable(setBarParameter, rewrittenGetBarReturn)).isTrue(); } diff --git a/value/userguide/autobuilder.md b/value/userguide/autobuilder.md index 8045192d9d..e427c8865d 100644 --- a/value/userguide/autobuilder.md +++ b/value/userguide/autobuilder.md @@ -119,6 +119,13 @@ The example also implements a `toBuilder()` method to get a builder that starts out with values from the given instance. See [below](#to_builder) for more details on that. +### Required configuration to understand Kotlin classes + +In order for AutoBuilder to understand Kotlin classes, you will typically need +to add a dependency on the `org.jetbrains.kotlin:kotlin-metadata-jvm` package, +in the same place where you depend on `com.google.auto.value:auto-value`. The +older `org.jetbrains.kotlinx:kotlinx-metadata-jvm` should also work. + ## The generated subclass Like `@AutoValue.Builder`, compiling an `@AutoBuilder` class will generate a diff --git a/value/userguide/builders-howto.md b/value/userguide/builders-howto.md index f1f948092b..62b45235a5 100644 --- a/value/userguide/builders-howto.md +++ b/value/userguide/builders-howto.md @@ -117,9 +117,9 @@ abstract class Animal { } ``` -Occasionally you may want to supply a default value, but only if the property is -not set explicitly. This is covered in the section on -[normalization](#normalize). +Occasionally you may want to supply a more complex default value, possibly +derived from other fields and only if the property is not set explicitly. This +is covered in the section on [normalization](#normalize). ## ... initialize a builder to the same property values as an existing value instance diff --git a/value/userguide/howto.md b/value/userguide/howto.md index 5acbd024a7..6dde3dc1f5 100644 --- a/value/userguide/howto.md +++ b/value/userguide/howto.md @@ -337,7 +337,7 @@ public class Names { } public static NamedBuilder namedBuilder() { - return new AutoBuilder_Names_namedBuilder(); + return new AutoBuilder_Names_NamedBuilder(); } ... diff --git a/value/userguide/index.md b/value/userguide/index.md index 1a86aee05a..f1fba0c900 100644 --- a/value/userguide/index.md +++ b/value/userguide/index.md @@ -1,7 +1,7 @@ # AutoValue -*Generated immutable value classes for Java 7+*
+*Generated immutable value classes for Java 8+*
***Éamonn McManus, Kevin Bourrillion***
**Google, Inc.** @@ -241,9 +241,7 @@ See [Why AutoValue?](why.md). ## What Java versions does it work with? -AutoValue requires that your compiler be at least Java 8. However, the code that -it generates is compatible with Java 7. That means that you can use it with -`-source 7 -target 7` or (for Java 9+) `--release 7`. +AutoValue requires that your compiler be at least Java 8. ## How do I...