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

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

There is a newer version: 2.45-kim-rc1
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.XTypeKt.isVoid;
import static androidx.room.compiler.processing.compat.XConverters.toJavac;
import static androidx.room.compiler.processing.compat.XConverters.toXProcessing;
import static com.google.common.base.Verify.verify;
import static com.google.common.collect.Iterables.consumingIterable;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.collect.Multimaps.asMap;
import static com.google.common.collect.Sets.intersection;
import static dagger.internal.codegen.base.ComponentAnnotation.anyComponentAnnotation;
import static dagger.internal.codegen.base.ModuleAnnotation.moduleAnnotation;
import static dagger.internal.codegen.base.Util.reentrantComputeIfAbsent;
import static dagger.internal.codegen.binding.ComponentCreatorAnnotation.creatorAnnotationsFor;
import static dagger.internal.codegen.binding.ComponentCreatorAnnotation.productionCreatorAnnotations;
import static dagger.internal.codegen.binding.ComponentCreatorAnnotation.subcomponentCreatorAnnotations;
import static dagger.internal.codegen.binding.ComponentKind.annotationsFor;
import static dagger.internal.codegen.binding.ConfigurationAnnotations.enclosedAnnotatedTypes;
import static dagger.internal.codegen.binding.ErrorMessages.ComponentCreatorMessages.builderMethodRequiresNoArgs;
import static dagger.internal.codegen.binding.ErrorMessages.ComponentCreatorMessages.moreThanOneRefToSubcomponent;
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet;
import static dagger.internal.codegen.xprocessing.XElements.asMethod;
import static dagger.internal.codegen.xprocessing.XElements.asTypeElement;
import static dagger.internal.codegen.xprocessing.XElements.getAnyAnnotation;
import static dagger.internal.codegen.xprocessing.XElements.getSimpleName;
import static dagger.internal.codegen.xprocessing.XTypeElements.getAllUnimplementedMethods;
import static dagger.internal.codegen.xprocessing.XTypes.isDeclared;
import static java.util.Comparator.comparing;
import static javax.lang.model.util.ElementFilter.methodsIn;

import androidx.room.compiler.processing.XAnnotation;
import androidx.room.compiler.processing.XExecutableParameterElement;
import androidx.room.compiler.processing.XMethodElement;
import androidx.room.compiler.processing.XMethodType;
import androidx.room.compiler.processing.XProcessingEnv;
import androidx.room.compiler.processing.XType;
import androidx.room.compiler.processing.XTypeElement;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.TypeName;
import dagger.Component;
import dagger.internal.codegen.base.ClearableCache;
import dagger.internal.codegen.base.ComponentAnnotation;
import dagger.internal.codegen.binding.ComponentKind;
import dagger.internal.codegen.binding.DependencyRequestFactory;
import dagger.internal.codegen.binding.ErrorMessages;
import dagger.internal.codegen.binding.MethodSignatureFormatter;
import dagger.internal.codegen.binding.ModuleKind;
import dagger.internal.codegen.javapoet.TypeNames;
import dagger.internal.codegen.kotlin.KotlinMetadataUtil;
import dagger.internal.codegen.langmodel.DaggerElements;
import dagger.spi.model.DependencyRequest;
import dagger.spi.model.Key;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.lang.model.SourceVersion;

/**
 * Performs superficial validation of the contract of the {@link Component} and {@link
 * dagger.producers.ProductionComponent} annotations.
 */
@Singleton
public final class ComponentValidator implements ClearableCache {
  private final XProcessingEnv processingEnv;
  private final DaggerElements elements;
  private final ModuleValidator moduleValidator;
  private final ComponentCreatorValidator creatorValidator;
  private final DependencyRequestValidator dependencyRequestValidator;
  private final MembersInjectionValidator membersInjectionValidator;
  private final MethodSignatureFormatter methodSignatureFormatter;
  private final DependencyRequestFactory dependencyRequestFactory;
  private final Map reports = new HashMap<>();
  private final KotlinMetadataUtil metadataUtil;

  @Inject
  ComponentValidator(
      XProcessingEnv processingEnv,
      DaggerElements elements,
      ModuleValidator moduleValidator,
      ComponentCreatorValidator creatorValidator,
      DependencyRequestValidator dependencyRequestValidator,
      MembersInjectionValidator membersInjectionValidator,
      MethodSignatureFormatter methodSignatureFormatter,
      DependencyRequestFactory dependencyRequestFactory,
      KotlinMetadataUtil metadataUtil) {
    this.processingEnv = processingEnv;
    this.elements = elements;
    this.moduleValidator = moduleValidator;
    this.creatorValidator = creatorValidator;
    this.dependencyRequestValidator = dependencyRequestValidator;
    this.membersInjectionValidator = membersInjectionValidator;
    this.methodSignatureFormatter = methodSignatureFormatter;
    this.dependencyRequestFactory = dependencyRequestFactory;
    this.metadataUtil = metadataUtil;
  }

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

  /** Validates the given component. */
  public ValidationReport validate(XTypeElement component) {
    return reentrantComputeIfAbsent(reports, component, this::validateUncached);
  }

  private ValidationReport validateUncached(XTypeElement component) {
    return new ElementValidator(component).validateElement();
  }

  private class ElementValidator {
    private final XTypeElement component;
    private final ValidationReport.Builder report;
    private final ImmutableSet componentKinds;

    // Populated by ComponentMethodValidators
    private final SetMultimap referencedSubcomponents =
        LinkedHashMultimap.create();

    ElementValidator(XTypeElement component) {
      this.component = component;
      this.report = ValidationReport.about(component);
      this.componentKinds = ComponentKind.getComponentKinds(component);
    }

    private ComponentKind componentKind() {
      return getOnlyElement(componentKinds);
    }

    private ComponentAnnotation componentAnnotation() {
      return anyComponentAnnotation(component).get();
    }

    ValidationReport validateElement() {
      if (componentKinds.size() > 1) {
        return moreThanOneComponentAnnotation();
      }

      validateUseOfCancellationPolicy();
      validateIsAbstractType();
      validateCreators();
      validateNoReusableAnnotation();
      validateComponentMethods();
      validateNoConflictingEntryPoints();
      validateSubcomponentReferences();
      validateComponentDependencies();
      validateReferencedModules();
      validateSubcomponents();

      return report.build();
    }

    private ValidationReport moreThanOneComponentAnnotation() {
      String error =
          "Components may not be annotated with more than one component annotation: found "
              + annotationsFor(componentKinds);
      report.addError(error, component);
      return report.build();
    }

    private void validateUseOfCancellationPolicy() {
      if (component.hasAnnotation(TypeNames.CANCELLATION_POLICY) && !componentKind().isProducer()) {
        report.addError(
            "@CancellationPolicy may only be applied to production components and subcomponents",
            component);
      }
    }

    private void validateIsAbstractType() {
      if (!component.isInterface() && !(component.isClass() && component.isAbstract())) {
        report.addError(
            String.format(
                "@%s may only be applied to an interface or abstract class",
                componentKind().annotation().simpleName()),
            component);
      }
    }

    private void validateCreators() {
      ImmutableSet creators =
          enclosedAnnotatedTypes(component, creatorAnnotationsFor(componentAnnotation()));
      creators.forEach(creator -> report.addSubreport(creatorValidator.validate(creator)));
      if (creators.size() > 1) {
        report.addError(
            String.format(
                ErrorMessages.componentMessagesFor(componentKind()).moreThanOne(), creators),
            component);
      }
    }

    private void validateNoReusableAnnotation() {
      if (component.hasAnnotation(TypeNames.REUSABLE)) {
        report.addError(
            "@Reusable cannot be applied to components or subcomponents",
            component,
            component.getAnnotation(TypeNames.REUSABLE));
      }
    }

    private void validateComponentMethods() {
      validateClassMethodName();
      getAllUnimplementedMethods(component).stream()
          .map(ComponentMethodValidator::new)
          .forEachOrdered(ComponentMethodValidator::validateMethod);
    }

    private void validateClassMethodName() {
      if (metadataUtil.hasMetadata(toJavac(component))) {
        metadataUtil
            .getAllMethodNamesBySignature(toJavac(component))
            .forEach(
                (signature, name) -> {
                  if (SourceVersion.isKeyword(name)) {
                    report.addError("Can not use a Java keyword as method name: " + signature);
                  }
                });
      }
    }

    private class ComponentMethodValidator {
      private final XMethodElement method;
      private final XMethodType resolvedMethod;
      private final List parameterTypes;
      private final List parameters;
      private final XType returnType;

      ComponentMethodValidator(XMethodElement method) {
        this.method = method;
        this.resolvedMethod = method.asMemberOf(component.getType());
        this.parameterTypes = resolvedMethod.getParameterTypes();
        this.parameters = method.getParameters();
        this.returnType = resolvedMethod.getReturnType();
      }

      void validateMethod() {
        validateNoTypeVariables();

        // abstract methods are ones we have to implement, so they each need to be validated
        // first, check the return type. if it's a subcomponent, validate that method as
        // such.
        Optional subcomponentAnnotation = subcomponentAnnotation();
        if (subcomponentAnnotation.isPresent()) {
          validateSubcomponentFactoryMethod(subcomponentAnnotation.get());
        } else if (subcomponentCreatorAnnotation().isPresent()) {
          validateSubcomponentCreatorMethod();
        } else {
          // if it's not a subcomponent...
          switch (parameters.size()) {
            case 0:
              validateProvisionMethod();
              break;
            case 1:
              validateMembersInjectionMethod();
              break;
            default:
              reportInvalidMethod();
              break;
          }
        }
      }

      private void validateNoTypeVariables() {
        if (!resolvedMethod.getTypeVariableNames().isEmpty()) {
          report.addError("Component methods cannot have type variables", method);
        }
      }

      private Optional subcomponentAnnotation() {
        return checkForAnnotations(
            returnType,
            componentKind().legalSubcomponentKinds().stream()
                .map(ComponentKind::annotation)
                .collect(toImmutableSet()));
      }

      private Optional subcomponentCreatorAnnotation() {
        return checkForAnnotations(
            returnType,
            componentAnnotation().isProduction()
                ? intersection(subcomponentCreatorAnnotations(), productionCreatorAnnotations())
                : subcomponentCreatorAnnotations());
      }

      private void validateSubcomponentFactoryMethod(XAnnotation subcomponentAnnotation) {
        referencedSubcomponents.put(returnType.getTypeElement(), method);

        ImmutableSet legalModuleAnnotations =
            ComponentKind.forAnnotatedElement(returnType.getTypeElement())
                .get()
                .legalModuleKinds()
                .stream()
                .map(ModuleKind::annotation)
                .collect(toImmutableSet());
        ImmutableSet moduleTypes =
            ComponentAnnotation.componentAnnotation(subcomponentAnnotation).modules();

        // TODO(gak): This logic maybe/probably shouldn't live here as it requires us to traverse
        // subcomponents and their modules separately from how it is done in ComponentDescriptor and
        // ModuleDescriptor
        ImmutableSet transitiveModules = getTransitiveModules(moduleTypes);

        Set referencedModules = Sets.newHashSet();
        for (int i = 0; i < parameterTypes.size(); i++) {
          XExecutableParameterElement parameter = parameters.get(i);
          XType parameterType = parameterTypes.get(i);
          if (checkForAnnotations(parameterType, legalModuleAnnotations).isPresent()) {
            XTypeElement module = parameterType.getTypeElement();
            if (referencedModules.contains(module)) {
              report.addError(
                  String.format(
                      "A module may only occur once an an argument in a Subcomponent factory "
                          + "method, but %s was already passed.",
                      module.getQualifiedName()),
                  parameter);
            }
            if (!transitiveModules.contains(module)) {
              report.addError(
                  String.format(
                      "%s is present as an argument to the %s factory method, but is not one of the"
                          + " modules used to implement the subcomponent.",
                      module.getQualifiedName(), returnType.getTypeElement().getQualifiedName()),
                  method);
            }
            referencedModules.add(module);
          } else {
            report.addError(
                String.format(
                    "Subcomponent factory methods may only accept modules, but %s is not.",
                    parameterType),
                parameter);
          }
        }
      }

      /**
       * Returns the full set of modules transitively included from the given seed modules, which
       * includes all transitive {@link Module#includes} and all transitive super classes. If a
       * module is malformed and a type listed in {@link Module#includes} is not annotated with
       * {@link Module}, it is ignored.
       */
      private ImmutableSet getTransitiveModules(
          Collection seedModules) {
        Set processedElements = Sets.newLinkedHashSet();
        Queue moduleQueue = new ArrayDeque<>(seedModules);
        ImmutableSet.Builder moduleElements = ImmutableSet.builder();
        for (XTypeElement moduleElement : consumingIterable(moduleQueue)) {
          if (processedElements.add(moduleElement)) {
            moduleAnnotation(moduleElement)
                .ifPresent(
                    moduleAnnotation -> {
                      moduleElements.add(moduleElement);
                      moduleQueue.addAll(moduleAnnotation.includes());
                      moduleQueue.addAll(includesFromSuperclasses(moduleElement));
                    });
          }
        }
        return moduleElements.build();
      }

      /** Returns {@link Module#includes()} from all transitive super classes. */
      private ImmutableSet includesFromSuperclasses(XTypeElement element) {
        ImmutableSet.Builder builder = ImmutableSet.builder();
        XType superclass = element.getSuperType();
        while (superclass != null && !TypeName.OBJECT.equals(superclass.getTypeName())) {
          element = superclass.getTypeElement();
          moduleAnnotation(element)
              .ifPresent(moduleAnnotation -> builder.addAll(moduleAnnotation.includes()));
          superclass = element.getSuperType();
        }
        return builder.build();
      }

      private void validateSubcomponentCreatorMethod() {
        referencedSubcomponents.put(returnType.getTypeElement().getEnclosingTypeElement(), method);

        if (!parameters.isEmpty()) {
          report.addError(builderMethodRequiresNoArgs(), method);
        }

        XTypeElement creatorElement = returnType.getTypeElement();
        // TODO(sameb): The creator validator right now assumes the element is being compiled
        // in this pass, which isn't true here.  We should change error messages to spit out
        // this method as the subject and add the original subject to the message output.
        report.addSubreport(creatorValidator.validate(creatorElement));
      }

      private void validateProvisionMethod() {
        dependencyRequestValidator.validateDependencyRequest(report, method, returnType);
      }

      private void validateMembersInjectionMethod() {
        XType parameterType = getOnlyElement(parameterTypes);
        report.addSubreport(
            membersInjectionValidator.validateMembersInjectionMethod(method, parameterType));
        if (!(isVoid(returnType) || returnType.isSameType(parameterType))) {
          report.addError(
              "Members injection methods may only return the injected type or void.", method);
        }
      }

      private void reportInvalidMethod() {
        report.addError(
            "This method isn't a valid provision method, members injection method or "
                + "subcomponent factory method. Dagger cannot implement this method",
            method);
      }
    }

    private void validateNoConflictingEntryPoints() {
      // Collect entry point methods that are not overridden by others. If the "same" method is
      // inherited from more than one supertype, each will be in the multimap.
      SetMultimap entryPoints = HashMultimap.create();

      // TODO(b/201729320): There's a bug in auto-common's MoreElements#overrides(), b/201729320,
      // which prevents us from using XTypeElement#getAllMethods() here (since that method relies on
      // MoreElements#overrides() under the hood).
      //
      // There's two options here.
      //    1. Fix the bug in auto-common and update XProcessing's auto-common dependency
      //    2. Add a new method in XProcessing which relies on Elements#overrides(), which does not
      //       have this issue. However, this approach risks causing issues for EJC (Eclipse) users.
      methodsIn(elements.getAllMembers(toJavac(component))).stream()
          .map(method -> asMethod(toXProcessing(method, processingEnv)))
          .filter(method -> isEntryPoint(method, method.asMemberOf(component.getType())))
          .forEach(
              method -> addMethodUnlessOverridden(method, entryPoints.get(getSimpleName(method))));

      asMap(entryPoints).values().stream()
          .filter(methods -> distinctKeys(methods).size() > 1)
          .forEach(this::reportConflictingEntryPoints);
    }

    private void reportConflictingEntryPoints(Collection methods) {
      verify(
          methods.stream().map(XMethodElement::getEnclosingElement).distinct().count()
              == methods.size(),
          "expected each method to be declared on a different type: %s",
          methods);
      StringBuilder message = new StringBuilder("conflicting entry point declarations:");
      methodSignatureFormatter
          .typedFormatter(component.getType())
          .formatIndentedList(
              message,
              ImmutableList.sortedCopyOf(
                  comparing(method -> method.getEnclosingElement().getClassName().canonicalName()),
                  methods),
              1);
      report.addError(message.toString());
    }

    private void validateSubcomponentReferences() {
      Maps.filterValues(referencedSubcomponents.asMap(), methods -> methods.size() > 1)
          .forEach(
              (subcomponent, methods) ->
                  report.addError(
                      String.format(moreThanOneRefToSubcomponent(), subcomponent, methods),
                      component));
    }

    private void validateComponentDependencies() {
      for (XType type : componentAnnotation().dependencyTypes()) {
        if (!isDeclared(type)) {
          report.addError(type + " is not a valid component dependency type");
        } else if (moduleAnnotation(type.getTypeElement()).isPresent()) {
          report.addError(type + " is a module, which cannot be a component dependency");
        }
      }
    }

    private void validateReferencedModules() {
      report.addSubreport(
          moduleValidator.validateReferencedModules(
              component,
              componentAnnotation().annotation(),
              componentKind().legalModuleKinds(),
              new HashSet<>()));
    }

    private void validateSubcomponents() {
      // Make sure we validate any subcomponents we're referencing.
      referencedSubcomponents
          .keySet()
          .forEach(subcomponent -> report.addSubreport(validate(subcomponent)));
    }

    private ImmutableSet distinctKeys(Set methods) {
      return methods.stream()
          .map(this::dependencyRequest)
          .map(DependencyRequest::key)
          .collect(toImmutableSet());
    }

    private DependencyRequest dependencyRequest(XMethodElement method) {
      XMethodType methodType = method.asMemberOf(component.getType());
      return ComponentKind.forAnnotatedElement(component).get().isProducer()
          ? dependencyRequestFactory.forComponentProductionMethod(method, methodType)
          : dependencyRequestFactory.forComponentProvisionMethod(method, methodType);
    }
  }

  private static boolean isEntryPoint(XMethodElement method, XMethodType methodType) {
    return method.isAbstract()
        && method.getParameters().isEmpty()
        && !isVoid(methodType.getReturnType())
        && methodType.getTypeVariableNames().isEmpty();
  }

  private void addMethodUnlessOverridden(XMethodElement method, Set methods) {
    if (methods.stream().noneMatch(existingMethod -> overridesAsDeclared(existingMethod, method))) {
      methods.removeIf(existingMethod -> overridesAsDeclared(method, existingMethod));
      methods.add(method);
    }
  }

  /**
   * Returns {@code true} if {@code overrider} overrides {@code overridden} considered from within
   * the type that declares {@code overrider}.
   */
  // TODO(dpb): Does this break for ECJ?
  private boolean overridesAsDeclared(XMethodElement overrider, XMethodElement overridden) {
    return elements.overrides(
        toJavac(overrider),
        toJavac(overridden),
        toJavac(asTypeElement(overrider.getEnclosingElement())));
  }

  private static Optional checkForAnnotations(XType type, Set annotations) {
    return Optional.ofNullable(type.getTypeElement())
        .flatMap(typeElement -> getAnyAnnotation(typeElement, annotations));
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy