All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dagger.internal.codegen.validation.InjectBindingRegistryImpl Maven / Gradle / Ivy

There is a newer version: 2.52
Show newest version
/*
 * Copyright (C) 2014 The Dagger Authors.
 *
 * 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 dagger.internal.codegen.validation;

import static androidx.room.compiler.processing.XElementKt.isTypeElement;
import static androidx.room.compiler.processing.compat.XConverters.toKS;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static dagger.internal.codegen.base.Keys.isValidImplicitProvisionKey;
import static dagger.internal.codegen.base.Keys.isValidMembersInjectionKey;
import static dagger.internal.codegen.binding.AssistedInjectionAnnotations.assistedInjectedConstructors;
import static dagger.internal.codegen.binding.InjectionAnnotations.injectedConstructors;
import static dagger.internal.codegen.binding.SourceFiles.generatedClassNameForBinding;
import static dagger.internal.codegen.extension.DaggerCollectors.toOptional;
import static dagger.internal.codegen.xprocessing.XElements.asTypeElement;
import static dagger.internal.codegen.xprocessing.XElements.closestEnclosingTypeElement;
import static dagger.internal.codegen.xprocessing.XTypes.erasedTypeName;
import static dagger.internal.codegen.xprocessing.XTypes.isDeclared;
import static dagger.internal.codegen.xprocessing.XTypes.nonObjectSuperclass;
import static dagger.internal.codegen.xprocessing.XTypes.unwrapType;

import androidx.room.compiler.processing.XConstructorElement;
import androidx.room.compiler.processing.XFieldElement;
import androidx.room.compiler.processing.XMessager;
import androidx.room.compiler.processing.XMethodElement;
import androidx.room.compiler.processing.XProcessingEnv;
import androidx.room.compiler.processing.XType;
import androidx.room.compiler.processing.XTypeElement;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.devtools.ksp.symbol.Origin;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.squareup.javapoet.ClassName;
import dagger.Component;
import dagger.Provides;
import dagger.internal.codegen.base.SourceFileGenerationException;
import dagger.internal.codegen.base.SourceFileGenerator;
import dagger.internal.codegen.binding.Binding;
import dagger.internal.codegen.binding.BindingFactory;
import dagger.internal.codegen.binding.InjectBindingRegistry;
import dagger.internal.codegen.binding.KeyFactory;
import dagger.internal.codegen.binding.MembersInjectionBinding;
import dagger.internal.codegen.binding.ProvisionBinding;
import dagger.internal.codegen.compileroption.CompilerOptions;
import dagger.internal.codegen.javapoet.TypeNames;
import dagger.internal.codegen.model.Key;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.tools.Diagnostic.Kind;

/**
 * Maintains the collection of provision bindings from {@link Inject} constructors and members
 * injection bindings from {@link Inject} fields and methods known to the annotation processor. Note
 * that this registry does not handle any explicit bindings (those from {@link Provides}
 * methods, {@link Component} dependencies, etc.).
 */
@Singleton
final class InjectBindingRegistryImpl implements InjectBindingRegistry {
  private final XProcessingEnv processingEnv;
  private final XMessager messager;
  private final InjectValidator injectValidator;
  private final InjectValidator injectValidatorWhenGeneratingCode;
  private final KeyFactory keyFactory;
  private final BindingFactory bindingFactory;
  private final CompilerOptions compilerOptions;

  private final class BindingsCollection {
    private final ClassName factoryClass;
    private final Map bindingsByKey = Maps.newLinkedHashMap();
    private final Deque bindingsRequiringGeneration = new ArrayDeque<>();
    private final Set materializedBindingKeys = Sets.newLinkedHashSet();

    BindingsCollection(ClassName factoryClass) {
      this.factoryClass = factoryClass;
    }

    void generateBindings(SourceFileGenerator generator) throws SourceFileGenerationException {
      for (B binding = bindingsRequiringGeneration.poll();
          binding != null;
          binding = bindingsRequiringGeneration.poll()) {
        checkState(!binding.unresolved().isPresent());
        XType type = binding.key().type().xprocessing();
        if (!isDeclared(type)
            || injectValidatorWhenGeneratingCode.validate(type.getTypeElement()).isClean()) {
          generator.generate(binding);
        }
        materializedBindingKeys.add(binding.key());
      }
      // Because Elements instantiated across processing rounds are not guaranteed to be equals() to
      // the logically same element, clear the cache after generating
      bindingsByKey.clear();
    }

    /** Returns a previously cached binding. */
    B getBinding(Key key) {
      return bindingsByKey.get(key);
    }

    /** Caches the binding and generates it if it needs generation. */
    void tryRegisterBinding(B binding, boolean isCalledFromInjectProcessor) {
      if (processingEnv.getBackend() == XProcessingEnv.Backend.KSP) {
        Origin origin =
            toKS(closestEnclosingTypeElement(binding.bindingElement().get())).getOrigin();
        // If the origin of the element is from a source file in the current compilation unit then
        // we're guaranteed that the InjectProcessor should run over the element so only generate
        // the Factory/MembersInjector if we're being called from the InjectProcessor.
        //
        // TODO(bcorso): generally, this isn't something we would need to keep track of manually.
        // However, KSP incremental processing has a bug that will overwrite the cache for the
        // element if we generate files for it, which can lead to missing generated files from
        // other processors. See https://github.com/google/dagger/issues/4063 and
        // https://github.com/google/dagger/issues/4054. Remove this once that bug is fixed.
        if (!isCalledFromInjectProcessor && (origin == Origin.JAVA || origin == Origin.KOTLIN)) {
          return;
        }
      }
      tryToCacheBinding(binding);

      @SuppressWarnings("unchecked")
      B maybeUnresolved =
          binding.unresolved().isPresent() ? (B) binding.unresolved().get() : binding;
      tryToGenerateBinding(maybeUnresolved, isCalledFromInjectProcessor);
    }

    /**
     * Tries to generate a binding, not generating if it already is generated. For resolved
     * bindings, this will try to generate the unresolved version of the binding.
     */
    void tryToGenerateBinding(B binding, boolean isCalledFromInjectProcessor) {
      if (shouldGenerateBinding(binding)) {
        bindingsRequiringGeneration.offer(binding);
        if (compilerOptions.warnIfInjectionFactoryNotGeneratedUpstream()
                && !isCalledFromInjectProcessor) {
          messager.printMessage(
              Kind.NOTE,
              String.format(
                  "Generating a %s for %s. "
                      + "Prefer to run the dagger processor over that class instead.",
                  factoryClass.simpleName(),
                  // erasure to strip  from msgs.
                  erasedTypeName(binding.key().type().xprocessing())));
        }
      }
    }

    /** Returns true if the binding needs to be generated. */
    private boolean shouldGenerateBinding(B binding) {
      if (binding instanceof MembersInjectionBinding) {
        MembersInjectionBinding membersInjectionBinding = (MembersInjectionBinding) binding;
        // Empty members injection bindings are special and don't need source files.
        if (membersInjectionBinding.injectionSites().isEmpty()) {
          return false;
        }
        // Members injectors for classes with no local injection sites and no @Inject
        // constructor are unused.
        boolean hasInjectConstructor =
            !(injectedConstructors(membersInjectionBinding.membersInjectedType()).isEmpty()
                && assistedInjectedConstructors(
                    membersInjectionBinding.membersInjectedType()).isEmpty());
        if (!membersInjectionBinding.hasLocalInjectionSites() && !hasInjectConstructor) {
          return false;
        }
      }
      return !binding.unresolved().isPresent()
          && !materializedBindingKeys.contains(binding.key())
          && !bindingsRequiringGeneration.contains(binding)
          && processingEnv.findTypeElement(generatedClassNameForBinding(binding)) == null;
    }

    /** Caches the binding for future lookups by key. */
    private void tryToCacheBinding(B binding) {
      // We only cache resolved bindings or unresolved bindings w/o type arguments.
      // Unresolved bindings w/ type arguments aren't valid for the object graph.
      if (binding.unresolved().isPresent()
          || binding.bindingTypeElement().get().getType().getTypeArguments().isEmpty()) {
        Key key = binding.key();
        Binding previousValue = bindingsByKey.put(key, binding);
        checkState(previousValue == null || binding.equals(previousValue),
            "couldn't register %s. %s was already registered for %s",
            binding, previousValue, key);
      }
    }
  }

  private final BindingsCollection provisionBindings =
      new BindingsCollection<>(TypeNames.PROVIDER);
  private final BindingsCollection membersInjectionBindings =
      new BindingsCollection<>(TypeNames.MEMBERS_INJECTOR);

  @Inject
  InjectBindingRegistryImpl(
      XProcessingEnv processingEnv,
      XMessager messager,
      InjectValidator injectValidator,
      KeyFactory keyFactory,
      BindingFactory bindingFactory,
      CompilerOptions compilerOptions) {
    this.processingEnv = processingEnv;
    this.messager = messager;
    this.injectValidator = injectValidator;
    this.injectValidatorWhenGeneratingCode = injectValidator.whenGeneratingCode();
    this.keyFactory = keyFactory;
    this.bindingFactory = bindingFactory;
    this.compilerOptions = compilerOptions;
  }

  // TODO(dpb): make the SourceFileGenerators fields so they don't have to be passed in
  @Override
  public void generateSourcesForRequiredBindings(
      SourceFileGenerator factoryGenerator,
      SourceFileGenerator membersInjectorGenerator)
      throws SourceFileGenerationException {
    provisionBindings.generateBindings(factoryGenerator);
    membersInjectionBindings.generateBindings(membersInjectorGenerator);
  }

  @Override
  public Optional tryRegisterInjectConstructor(
      XConstructorElement constructorElement) {
    return tryRegisterConstructor(
        constructorElement,
        Optional.empty(),
        /* isCalledFromInjectProcessor= */ true);
  }

  @CanIgnoreReturnValue
  private Optional tryRegisterConstructor(
      XConstructorElement constructorElement,
      Optional resolvedType,
      boolean isCalledFromInjectProcessor) {
    XTypeElement typeElement = constructorElement.getEnclosingElement();

    // Validating here shouldn't have a performance penalty because the validator caches its reports
    ValidationReport report = injectValidator.validate(typeElement);
    report.printMessagesTo(messager);
    if (!report.isClean()) {
      return Optional.empty();
    }

    XType type = typeElement.getType();
    Key key = keyFactory.forInjectConstructorWithResolvedType(type);
    ProvisionBinding cachedBinding = provisionBindings.getBinding(key);
    if (cachedBinding != null) {
      return Optional.of(cachedBinding);
    }

    ProvisionBinding binding = bindingFactory.injectionBinding(constructorElement, resolvedType);
    provisionBindings.tryRegisterBinding(binding, isCalledFromInjectProcessor);
    if (!binding.injectionSites().isEmpty()) {
      tryRegisterMembersInjectedType(typeElement, resolvedType, isCalledFromInjectProcessor);
    }
    return Optional.of(binding);
  }

  @Override
  public Optional tryRegisterInjectField(XFieldElement fieldElement) {
    // TODO(b/204116636): Add a test for this once we're able to test kotlin sources.
    // TODO(b/204208307): Add validation for KAPT to test if this came from a top-level field.
    if (!isTypeElement(fieldElement.getEnclosingElement())) {
      messager.printMessage(
          Kind.ERROR,
          "@Inject fields must be enclosed in a type.",
          fieldElement);
    }
    return tryRegisterMembersInjectedType(
        asTypeElement(fieldElement.getEnclosingElement()),
        Optional.empty(),
        /* isCalledFromInjectProcessor= */ true);
  }

  @Override
  public Optional tryRegisterInjectMethod(XMethodElement methodElement) {
    // TODO(b/204116636): Add a test for this once we're able to test kotlin sources.
    // TODO(b/204208307): Add validation for KAPT to test if this came from a top-level method.
    if (!isTypeElement(methodElement.getEnclosingElement())) {
      messager.printMessage(
          Kind.ERROR,
          "@Inject methods must be enclosed in a type.",
          methodElement);
    }
    return tryRegisterMembersInjectedType(
        asTypeElement(methodElement.getEnclosingElement()),
        Optional.empty(),
        /* isCalledFromInjectProcessor= */ true);
  }

  @CanIgnoreReturnValue
  private Optional tryRegisterMembersInjectedType(
      XTypeElement typeElement,
      Optional resolvedType,
      boolean isCalledFromInjectProcessor) {
    // Validating here shouldn't have a performance penalty because the validator caches its reports
    ValidationReport report = injectValidator.validateForMembersInjection(typeElement);
    report.printMessagesTo(messager);
    if (!report.isClean()) {
      return Optional.empty();
    }

    XType type = typeElement.getType();
    Key key = keyFactory.forInjectConstructorWithResolvedType(type);
    MembersInjectionBinding cachedBinding = membersInjectionBindings.getBinding(key);
    if (cachedBinding != null) {
      return Optional.of(cachedBinding);
    }

    MembersInjectionBinding binding = bindingFactory.membersInjectionBinding(type, resolvedType);
    membersInjectionBindings.tryRegisterBinding(binding, isCalledFromInjectProcessor);
    for (Optional supertype = nonObjectSuperclass(type);
        supertype.isPresent();
        supertype = nonObjectSuperclass(supertype.get())) {
      getOrFindMembersInjectionBinding(keyFactory.forMembersInjectedType(supertype.get()));
    }
    return Optional.of(binding);
  }

  @CanIgnoreReturnValue
  @Override
  public Optional getOrFindProvisionBinding(Key key) {
    checkNotNull(key);
    if (!isValidImplicitProvisionKey(key)) {
      return Optional.empty();
    }
    ProvisionBinding binding = provisionBindings.getBinding(key);
    if (binding != null) {
      return Optional.of(binding);
    }

    XType type = key.type().xprocessing();
    XTypeElement element = type.getTypeElement();

    ValidationReport report = injectValidator.validate(element);
    report.printMessagesTo(messager);
    if (!report.isClean()) {
      return Optional.empty();
    }

    return Stream.concat(
            injectedConstructors(element).stream(),
            assistedInjectedConstructors(element).stream())
        // We're guaranteed that there's at most 1 @Inject constructors from above validation.
        .collect(toOptional())
        .flatMap(
            constructor ->
                tryRegisterConstructor(
                    constructor,
                    Optional.of(type),
                    /* isCalledFromInjectProcessor= */ false));
  }

  @CanIgnoreReturnValue
  @Override
  public Optional getOrFindMembersInjectionBinding(Key key) {
    checkNotNull(key);
    // TODO(gak): is checking the kind enough?
    checkArgument(isValidMembersInjectionKey(key));
    MembersInjectionBinding binding = membersInjectionBindings.getBinding(key);
    if (binding != null) {
      return Optional.of(binding);
    }
    return tryRegisterMembersInjectedType(
        key.type().xprocessing().getTypeElement(),
        Optional.of(key.type().xprocessing()),
        /* isCalledFromInjectProcessor= */ false);
  }

  @Override
  public Optional getOrFindMembersInjectorProvisionBinding(Key key) {
    if (!isValidMembersInjectionKey(key)) {
      return Optional.empty();
    }
    Key membersInjectionKey =
        keyFactory.forMembersInjectedType(unwrapType(key.type().xprocessing()));
    return getOrFindMembersInjectionBinding(membersInjectionKey)
        .map(binding -> bindingFactory.membersInjectorBinding(key, binding));
  }
}