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

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

The 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 com.google.common.collect.Iterables.getOnlyElement;
import static dagger.internal.codegen.base.Util.reentrantComputeIfAbsent;
import static dagger.internal.codegen.binding.AssistedInjectionAnnotations.assistedInjectedConstructors;
import static dagger.internal.codegen.binding.InjectionAnnotations.injectedConstructors;
import static dagger.internal.codegen.binding.SourceFiles.factoryNameForElement;
import static dagger.internal.codegen.binding.SourceFiles.membersInjectorNameForType;
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList;
import static dagger.internal.codegen.xprocessing.XElements.closestEnclosingTypeElement;
import static dagger.internal.codegen.xprocessing.XElements.getAnyAnnotation;
import static dagger.internal.codegen.xprocessing.XMethodElements.hasTypeParameters;
import static dagger.internal.codegen.xprocessing.XTypes.isSubtype;

import androidx.room.compiler.processing.XAnnotation;
import androidx.room.compiler.processing.XConstructorElement;
import androidx.room.compiler.processing.XElement;
import androidx.room.compiler.processing.XExecutableParameterElement;
import androidx.room.compiler.processing.XFieldElement;
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 androidx.room.compiler.processing.XVariableElement;
import com.google.common.collect.ImmutableSet;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.TypeName;
import dagger.internal.codegen.base.ClearableCache;
import dagger.internal.codegen.base.DaggerSuperficialValidation;
import dagger.internal.codegen.binding.InjectionAnnotations;
import dagger.internal.codegen.binding.MethodSignatureFormatter;
import dagger.internal.codegen.compileroption.CompilerOptions;
import dagger.internal.codegen.javapoet.TypeNames;
import dagger.internal.codegen.langmodel.Accessibility;
import dagger.internal.codegen.model.Scope;
import dagger.internal.codegen.xprocessing.XAnnotations;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.tools.Diagnostic;
import javax.tools.Diagnostic.Kind;

/**
 * A {@linkplain ValidationReport validator} for {@link Inject}-annotated elements and the types
 * that contain them.
 */
@Singleton
public final class InjectValidator implements ClearableCache {

  private final XProcessingEnv processingEnv;
  private final CompilerOptions compilerOptions;
  private final DependencyRequestValidator dependencyRequestValidator;
  private final Optional privateAndStaticInjectionDiagnosticKind;
  private final InjectionAnnotations injectionAnnotations;
  private final DaggerSuperficialValidation superficialValidation;
  private final Map provisionReports = new HashMap<>();
  private final Map membersInjectionReports = new HashMap<>();
  private final MethodSignatureFormatter methodSignatureFormatter;

  @Inject
  InjectValidator(
      XProcessingEnv processingEnv,
      DependencyRequestValidator dependencyRequestValidator,
      CompilerOptions compilerOptions,
      InjectionAnnotations injectionAnnotations,
      DaggerSuperficialValidation superficialValidation,
      MethodSignatureFormatter methodSignatureFormatter) {
    this(
        processingEnv,
        compilerOptions,
        dependencyRequestValidator,
        Optional.empty(),
        injectionAnnotations,
        superficialValidation,
        methodSignatureFormatter);
  }

  private InjectValidator(
      XProcessingEnv processingEnv,
      CompilerOptions compilerOptions,
      DependencyRequestValidator dependencyRequestValidator,
      Optional privateAndStaticInjectionDiagnosticKind,
      InjectionAnnotations injectionAnnotations,
      DaggerSuperficialValidation superficialValidation,
      MethodSignatureFormatter methodSignatureFormatter) {
    this.processingEnv = processingEnv;
    this.compilerOptions = compilerOptions;
    this.dependencyRequestValidator = dependencyRequestValidator;
    this.privateAndStaticInjectionDiagnosticKind = privateAndStaticInjectionDiagnosticKind;
    this.injectionAnnotations = injectionAnnotations;
    this.superficialValidation = superficialValidation;
    this.methodSignatureFormatter = methodSignatureFormatter;
  }

  @Override
  public void clearCache() {
    provisionReports.clear();
    membersInjectionReports.clear();
  }

  /**
   * Returns a new validator that performs the same validation as this one, but is strict about
   * rejecting optionally-specified JSR 330 behavior that Dagger doesn't support (unless {@code
   * -Adagger.ignorePrivateAndStaticInjectionForComponent=enabled} was set in the javac options).
   */
  public InjectValidator whenGeneratingCode() {
    return compilerOptions.ignorePrivateAndStaticInjectionForComponent()
        ? this
        : new InjectValidator(
            processingEnv,
            compilerOptions,
            dependencyRequestValidator,
            Optional.of(Diagnostic.Kind.ERROR),
            injectionAnnotations,
            superficialValidation,
            methodSignatureFormatter);
  }

  public ValidationReport validate(XTypeElement typeElement) {
    return reentrantComputeIfAbsent(provisionReports, typeElement, this::validateUncached);
  }

  private ValidationReport validateUncached(XTypeElement typeElement) {
    ValidationReport.Builder builder = ValidationReport.about(typeElement);
    builder.addSubreport(validateForMembersInjectionInternal(typeElement));

    ImmutableSet injectConstructors =
        ImmutableSet.builder()
            .addAll(injectedConstructors(typeElement))
            .addAll(assistedInjectedConstructors(typeElement))
            .build();

    switch (injectConstructors.size()) {
      case 0:
        break; // Nothing to validate.
      case 1:
        builder.addSubreport(validateConstructor(getOnlyElement(injectConstructors)));
        break;
      default:
        builder.addError(
            String.format(
                "Type %s may only contain one injected constructor. Found: %s",
                typeElement.getQualifiedName(),
                injectConstructors.stream()
                    .map(methodSignatureFormatter::format)
                    .collect(toImmutableList())),
            typeElement);
    }

    return builder.build();
  }

  private ValidationReport validateConstructor(XConstructorElement constructorElement) {
    superficialValidation.validateTypeOf(constructorElement);
    ValidationReport.Builder builder =
        ValidationReport.about(constructorElement.getEnclosingElement());

    if (InjectionAnnotations.hasInjectAnnotation(constructorElement)
        && constructorElement.hasAnnotation(TypeNames.ASSISTED_INJECT)) {
      builder.addError("Constructors cannot be annotated with both @Inject and @AssistedInject");
    }

    ClassName injectAnnotation =
        getAnyAnnotation(
                constructorElement,
                TypeNames.INJECT,
                TypeNames.INJECT_JAVAX,
                TypeNames.ASSISTED_INJECT)
            .map(XAnnotations::getClassName)
            .get();

    if (constructorElement.isPrivate()) {
      builder.addError(
          "Dagger does not support injection into private constructors", constructorElement);
    }

    // If this type has already been processed in a previous round or compilation unit then there
    // is no reason to recheck for invalid scope annotations since it's already been checked.
    // This allows us to skip superficial validation of constructor annotations in subsequent
    // compilations where the annotation types may no longer be on the classpath.
    if (!processedInPreviousRoundOrCompilationUnit(constructorElement)) {
      superficialValidation.validateAnnotationsOf(constructorElement);
      for (XAnnotation qualifier : injectionAnnotations.getQualifiers(constructorElement)) {
        builder.addError(
            String.format(
                "@Qualifier annotations are not allowed on @%s constructors",
                injectAnnotation.simpleName()),
            constructorElement,
            qualifier);
      }

      String scopeErrorMsg =
          String.format(
              "@Scope annotations are not allowed on @%s constructors",
              injectAnnotation.simpleName());

      if (injectAnnotation.equals(TypeNames.INJECT)
          || injectAnnotation.equals(TypeNames.INJECT_JAVAX)) {
        scopeErrorMsg += "; annotate the class instead";
      }

      for (Scope scope : injectionAnnotations.getScopes(constructorElement)) {
        builder.addError(scopeErrorMsg, constructorElement, scope.scopeAnnotation().xprocessing());
      }
    }

    for (XExecutableParameterElement parameter : constructorElement.getParameters()) {
      superficialValidation.validateTypeOf(parameter);
      validateDependencyRequest(builder, parameter);
    }

    if (throwsCheckedExceptions(constructorElement)) {
      builder.addItem(
          String.format(
              "Dagger does not support checked exceptions on @%s constructors",
              injectAnnotation.simpleName()),
          privateMemberDiagnosticKind(),
          constructorElement);
    }

    checkInjectIntoPrivateClass(constructorElement, builder);

    XTypeElement enclosingElement = constructorElement.getEnclosingElement();
    if (enclosingElement.isAbstract()) {
      builder.addError(
          String.format(
              "@%s is nonsense on the constructor of an abstract class",
              injectAnnotation.simpleName()),
          constructorElement);
    }

    if (enclosingElement.isNested() && !enclosingElement.isStatic()) {
      builder.addError(
          String.format(
              "@%s constructors are invalid on inner classes. "
                  + "Did you mean to make the class static?",
              injectAnnotation.simpleName()),
          constructorElement);
    }

    // Note: superficial validation of the annotations is done as part of getting the scopes.
    ImmutableSet scopes =
        injectionAnnotations.getScopes(constructorElement.getEnclosingElement());
    if (injectAnnotation.equals(TypeNames.ASSISTED_INJECT)) {
      for (Scope scope : scopes) {
        builder.addError(
            "A type with an @AssistedInject-annotated constructor cannot be scoped",
            enclosingElement,
            scope.scopeAnnotation().xprocessing());
      }
    } else if (scopes.size() > 1) {
      for (Scope scope : scopes) {
        builder.addError(
            "A single binding may not declare more than one @Scope",
            enclosingElement,
            scope.scopeAnnotation().xprocessing());
      }
    }

    return builder.build();
  }

  private ValidationReport validateField(XFieldElement fieldElement) {
    superficialValidation.validateTypeOf(fieldElement);
    ValidationReport.Builder builder = ValidationReport.about(fieldElement);
    if (fieldElement.isFinal()) {
      builder.addError("@Inject fields may not be final", fieldElement);
    }

    if (fieldElement.isPrivate()) {
      builder.addItem(
          "Dagger does not support injection into private fields",
          privateMemberDiagnosticKind(),
          fieldElement);
    }

    if (fieldElement.isStatic()) {
      builder.addItem(
          "Dagger does not support injection into static fields",
          staticMemberDiagnosticKind(),
          fieldElement);
    }

    if (fieldElement.isProtected()
        && fieldElement.getEnclosingElement().isFromKotlin()
        ) {
      builder.addError(
          "Dagger injector does not have access to kotlin protected fields", fieldElement);
    }

    validateDependencyRequest(builder, fieldElement);

    return builder.build();
  }

  private ValidationReport validateMethod(XMethodElement methodElement) {
    superficialValidation.validateTypeOf(methodElement);
    ValidationReport.Builder builder = ValidationReport.about(methodElement);
    if (methodElement.isAbstract()) {
      builder.addError("Methods with @Inject may not be abstract", methodElement);
    }

    if (methodElement.isPrivate()) {
      builder.addItem(
          "Dagger does not support injection into private methods",
          privateMemberDiagnosticKind(),
          methodElement);
    }

    if (methodElement.isStatic()) {
      builder.addItem(
          "Dagger does not support injection into static methods",
          staticMemberDiagnosticKind(),
          methodElement);
    }

    // No need to resolve type parameters since we're only checking existence.
    if (hasTypeParameters(methodElement)) {
      builder.addError("Methods with @Inject may not declare type parameters", methodElement);
    }

    // No need to resolve thrown types since we're only checking existence.
    if (!methodElement.getThrownTypes().isEmpty()) {
      builder.addError(
          "Methods with @Inject may not throw checked exceptions. "
              + "Please wrap your exceptions in a RuntimeException instead.",
          methodElement);
    }

    for (XExecutableParameterElement parameter : methodElement.getParameters()) {
      superficialValidation.validateTypeOf(parameter);
      validateDependencyRequest(builder, parameter);
    }

    return builder.build();
  }

  private void validateDependencyRequest(
      ValidationReport.Builder builder, XVariableElement parameter) {
    dependencyRequestValidator.validateDependencyRequest(builder, parameter, parameter.getType());
    dependencyRequestValidator.checkNotProducer(builder, parameter);
  }

  public ValidationReport validateForMembersInjection(XTypeElement typeElement) {
    return !processedInPreviousRoundOrCompilationUnit(typeElement)
        ? validate(typeElement) // validate everything
        : validateForMembersInjectionInternal(typeElement); // validate only inject members
  }

  private ValidationReport validateForMembersInjectionInternal(XTypeElement typeElement) {
    return reentrantComputeIfAbsent(
        membersInjectionReports, typeElement, this::validateForMembersInjectionInternalUncached);
  }

  private ValidationReport validateForMembersInjectionInternalUncached(XTypeElement typeElement) {
    superficialValidation.validateTypeOf(typeElement);
    // TODO(beder): This element might not be currently compiled, so this error message could be
    // left in limbo. Find an appropriate way to display the error message in that case.
    ValidationReport.Builder builder = ValidationReport.about(typeElement);
    boolean hasInjectedMembers = false;
    for (XFieldElement field : typeElement.getDeclaredFields()) {
      if (InjectionAnnotations.hasInjectAnnotation(field)) {
        hasInjectedMembers = true;
        ValidationReport report = validateField(field);
        if (!report.isClean()) {
          builder.addSubreport(report);
        }
      }
    }
    for (XMethodElement method : typeElement.getDeclaredMethods()) {
      if (InjectionAnnotations.hasInjectAnnotation(method)) {
        hasInjectedMembers = true;
        ValidationReport report = validateMethod(method);
        if (!report.isClean()) {
          builder.addSubreport(report);
        }
      }
    }

    if (hasInjectedMembers) {
      checkInjectIntoPrivateClass(typeElement, builder);
      checkInjectIntoKotlinObject(typeElement, builder);
    }

    Optional.ofNullable(typeElement.getSuperType())
        .filter(supertype -> !supertype.getTypeName().equals(TypeName.OBJECT))
        .ifPresent(
            supertype -> {
              superficialValidation.validateSuperTypeOf(typeElement);
              ValidationReport report = validateForMembersInjection(supertype.getTypeElement());
              if (!report.isClean()) {
                builder.addSubreport(report);
              }
            });

    return builder.build();
  }

  /** Returns true if the given method element declares a checked exception. */
  private boolean throwsCheckedExceptions(XConstructorElement constructorElement) {
    XType runtimeException = processingEnv.findType(TypeNames.RUNTIME_EXCEPTION);
    XType error = processingEnv.findType(TypeNames.ERROR);
    superficialValidation.validateThrownTypesOf(constructorElement);
    return !constructorElement.getThrownTypes().stream()
        .allMatch(type -> isSubtype(type, runtimeException) || isSubtype(type, error));
  }

  private void checkInjectIntoPrivateClass(XElement element, ValidationReport.Builder builder) {
    if (!Accessibility.isElementAccessibleFromOwnPackage(closestEnclosingTypeElement(element))) {
      builder.addItem(
          "Dagger does not support injection into private classes",
          privateMemberDiagnosticKind(),
          element);
    }
  }

  private void checkInjectIntoKotlinObject(XTypeElement element, ValidationReport.Builder builder) {
    if (element.isKotlinObject() || element.isCompanionObject()) {
      builder.addError("Dagger does not support injection into Kotlin objects", element);
    }
  }

  private Diagnostic.Kind privateMemberDiagnosticKind() {
    return privateAndStaticInjectionDiagnosticKind.orElse(
        compilerOptions.privateMemberValidationKind());
  }

  private Diagnostic.Kind staticMemberDiagnosticKind() {
    return privateAndStaticInjectionDiagnosticKind.orElse(
        compilerOptions.staticMemberValidationKind());
  }

  private boolean processedInPreviousRoundOrCompilationUnit(XConstructorElement injectConstructor) {
    return processingEnv.findTypeElement(factoryNameForElement(injectConstructor)) != null;
  }

  private boolean processedInPreviousRoundOrCompilationUnit(XTypeElement membersInjectedType) {
    return processingEnv.findTypeElement(membersInjectorNameForType(membersInjectedType)) != null;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy