Skip to content

Commit

Permalink
Handle both javax.inject.* and jakarta.inject.*.
Browse files Browse the repository at this point in the history
Previously AutoFactory recognized and generated `javax.inject` only. Now, it will prefer `jakarta.inject` if that is on the classpath, but it will still recognize `javax.inject` if only that is on the classpath. A new compiler option can be used to select `javax.inject` even if `jakarta.inject` is also available.

Also remove some obsolete tests that would otherwise have had to be updated.

Closes #1184.

RELNOTES=AutoFactory now recognizes and generates both `javax.inject` and `jakarta.inject`. If `jakarta.inject` is on the classpath then it will be used, and otherwise `javax.inject`. A new compiler option `-Acom.google.auto.factory.InjectApi` can be set to either `javax` or `jakarta` to force the use of one or the other.
PiperOrigin-RevId: 575915977
  • Loading branch information
eamonnmcmanus authored and Google Java Core Libraries committed Oct 23, 2023
1 parent ef19949 commit 67772b2
Show file tree
Hide file tree
Showing 11 changed files with 261 additions and 154 deletions.
9 changes: 8 additions & 1 deletion factory/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,19 @@
<artifactId>javapoet</artifactId>
<version>1.13.0</version>
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
<version>2.0.1</version>
<scope>test</scope>
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>com.google.testing.compile</groupId>
<artifactId>compile-testing</artifactId>
Expand Down
9 changes: 7 additions & 2 deletions factory/src/main/java/com/google/auto/factory/Provided.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -68,28 +71,66 @@
*/
@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<String> 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) {
super.init(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
+ "=<api>, where <api> 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<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (errorFunction != null) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AutoFactory.class);
Element anElement = elements.isEmpty() ? null : elements.iterator().next();
errorFunction.accept(anElement);
return false;
}
try {
doProcess(roundEnv);
} catch (Throwable e) {
Expand Down Expand Up @@ -141,7 +182,8 @@ private void doProcess(RoundEnvironment roundEnv) {
indexedMethodsBuilder.build();
ImmutableSetMultimap<String, PackageAndClass> factoriesBeingCreated =
simpleNamesToNames(indexedMethods.keySet());
FactoryWriter factoryWriter = new FactoryWriter(processingEnv, factoriesBeingCreated);
FactoryWriter factoryWriter =
new FactoryWriter(processingEnv, injectApi, factoriesBeingCreated);

indexedMethods
.asMap()
Expand Down Expand Up @@ -216,7 +258,10 @@ private ImmutableSet<ImplementationMethodDescriptor> implementationMethods(
Elements2.getExecutableElementAsMemberOf(types, implementationMethod, supertype);
ImmutableSet<Parameter> passedParameters =
Parameter.forParameterList(
implementationMethod.getParameters(), methodType.getParameterTypes(), types);
implementationMethod.getParameters(),
methodType.getParameterTypes(),
types,
injectApi);
implementationMethodsBuilder.add(
ImplementationMethodDescriptor.builder()
.name(implementationMethod.getSimpleName().toString())
Expand Down Expand Up @@ -270,7 +315,9 @@ private void checkAnnotationsToApply(Element annotation) {
Set<TypeElement> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<FactoryMethodDescriptor> generateDescriptor(Element element) {
Expand Down Expand Up @@ -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<Parameter> providedParameters =
Parameter.forParameterList(requireNonNull(parameterMap.get(true)), types);
Parameter.forParameterList(requireNonNull(parameterMap.get(true)), types, injectApi);
ImmutableSet<Parameter> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -60,14 +58,17 @@

final class FactoryWriter {

private final InjectApi injectApi;
private final Filer filer;
private final Elements elements;
private final SourceVersion sourceVersion;
private final ImmutableSetMultimap<String, PackageAndClass> factoriesBeingCreated;

FactoryWriter(
ProcessingEnvironment processingEnv,
InjectApi injectApi,
ImmutableSetMultimap<String, PackageAndClass> factoriesBeingCreated) {
this.injectApi = injectApi;
this.filer = processingEnv.getFiler();
this.elements = processingEnv.getElementUtils();
this.sourceVersion = processingEnv.getSourceVersion();
Expand Down Expand Up @@ -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);
}
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> PREFIXES_IN_ORDER =
ImmutableList.of("jakarta.inject.", "javax.inject.");

static InjectApi from(Elements elementUtils, @Nullable String apiPrefix) {
ImmutableList<String> apiPackages =
(apiPrefix == null) ? PREFIXES_IN_ORDER : ImmutableList.of(apiPrefix + ".inject.");
for (String apiPackage : apiPackages) {
ImmutableMap<String, TypeElement> 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<String, TypeElement> 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<String> API_CLASSES =
ImmutableSet.of("Inject", "Provider", "Qualifier");
}
13 changes: 6 additions & 7 deletions factory/src/main/java/com/google/auto/factory/processor/Key.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -37,7 +35,6 @@
* @author Gregory Kick
*/
@AutoValue
// TODO(ronshapiro): reuse dagger.model.Key?
abstract class Key {

abstract Equivalence.Wrapper<TypeMirror> type();
Expand All @@ -49,33 +46,35 @@ Optional<AnnotationMirror> 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}.
*
* <p>If {@code type} is a {@code Provider<T>}, the returned {@link Key}'s {@link #type()} is
* {@code T}. If {@code type} is a primitive, the returned {@link Key}'s {@link #type()} is the
* corresponding {@linkplain Types#boxedClass(PrimitiveType) boxed type}.
*
* <p>For example:
*
* <table>
* <tr><th>Input type <th>{@code Key.type()}
* <tr><td>{@code String} <td>{@code String}
* <tr><td>{@code Provider<String>} <td>{@code String}
* <tr><td>{@code int} <td>{@code Integer}
* </table>
*/
static Key create(TypeMirror type, Collection<AnnotationMirror> annotations, Types types) {
static Key create(
TypeMirror type, Collection<AnnotationMirror> annotations, Types types, InjectApi injectApi) {
// TODO(gak): check for only one qualifier rather than using the first
Optional<AnnotationMirror> 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(
Expand Down
Loading

0 comments on commit 67772b2

Please sign in to comment.