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

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

There is a newer version: 2.52
Show newest version
/*
 * Copyright (C) 2015 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.base.Functions.constant;
import static com.google.common.base.Predicates.and;
import static com.google.common.base.Predicates.in;
import static com.google.common.base.Predicates.not;
import static dagger.internal.codegen.base.Scopes.getReadableSource;
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet;
import static dagger.internal.codegen.xprocessing.XElements.getSimpleName;

import androidx.room.compiler.processing.XExecutableParameterElement;
import androidx.room.compiler.processing.XTypeElement;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import dagger.internal.codegen.base.ModuleKind;
import dagger.internal.codegen.binding.ComponentDescriptor;
import dagger.internal.codegen.binding.ComponentDescriptor.ComponentMethodDescriptor;
import dagger.internal.codegen.binding.InjectionAnnotations;
import dagger.internal.codegen.binding.ModuleDescriptor;
import dagger.internal.codegen.compileroption.CompilerOptions;
import dagger.internal.codegen.model.Scope;
import java.util.Collection;
import java.util.Formatter;
import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;

/** Validates the relationships between parent components and subcomponents. */
final class ComponentHierarchyValidator {
  private static final Joiner COMMA_SEPARATED_JOINER = Joiner.on(", ");

  private final CompilerOptions compilerOptions;
  private final InjectionAnnotations injectionAnnotations;

  @Inject
  ComponentHierarchyValidator(
      CompilerOptions compilerOptions, InjectionAnnotations injectionAnnotations) {
    this.compilerOptions = compilerOptions;
    this.injectionAnnotations = injectionAnnotations;
  }

  ValidationReport validate(ComponentDescriptor componentDescriptor) {
    ValidationReport.Builder report = ValidationReport.about(componentDescriptor.typeElement());
    validateSubcomponentMethods(
        report,
        componentDescriptor,
        Maps.toMap(componentDescriptor.moduleTypes(), constant(componentDescriptor.typeElement())));
    validateRepeatedScopedDeclarations(report, componentDescriptor, LinkedHashMultimap.create());

    if (compilerOptions.scopeCycleValidationType().diagnosticKind().isPresent()) {
      validateScopeHierarchy(
          report, componentDescriptor, LinkedHashMultimap.create());
    }
    validateProductionModuleUniqueness(report, componentDescriptor, LinkedHashMultimap.create());
    return report.build();
  }

  private void validateSubcomponentMethods(
      ValidationReport.Builder report,
      ComponentDescriptor componentDescriptor,
      ImmutableMap existingModuleToOwners) {
    componentDescriptor
        .childComponentsDeclaredByFactoryMethods()
        .forEach(
            (method, childComponent) -> {
              if (childComponent.hasCreator()) {
                report.addError(
                    "Components may not have factory methods for subcomponents that define a "
                        + "builder.",
                    method.methodElement());
              } else {
                validateFactoryMethodParameters(report, method, existingModuleToOwners);
              }

              validateSubcomponentMethods(
                  report,
                  childComponent,
                  new ImmutableMap.Builder()
                      .putAll(existingModuleToOwners)
                      .putAll(
                          Maps.toMap(
                              Sets.difference(
                                  childComponent.moduleTypes(), existingModuleToOwners.keySet()),
                              constant(childComponent.typeElement())))
                      .build());
            });
  }

  private void validateFactoryMethodParameters(
      ValidationReport.Builder report,
      ComponentMethodDescriptor subcomponentMethodDescriptor,
      ImmutableMap existingModuleToOwners) {
    for (XExecutableParameterElement factoryMethodParameter :
        subcomponentMethodDescriptor.methodElement().getParameters()) {
      XTypeElement moduleType = factoryMethodParameter.getType().getTypeElement();
      if (existingModuleToOwners.containsKey(moduleType)) {
        /* Factory method tries to pass a module that is already present in the parent.
         * This is an error. */
        report.addError(
            String.format(
                "%s is present in %s. A subcomponent cannot use an instance of a "
                    + "module that differs from its parent.",
                getSimpleName(moduleType),
                existingModuleToOwners.get(moduleType).getQualifiedName()),
            factoryMethodParameter);
      }
    }
  }

  /**
   * Checks that components do not have any scopes that are also applied on any of their ancestors.
   */
  private void validateScopeHierarchy(
      ValidationReport.Builder report,
      ComponentDescriptor subject,
      SetMultimap scopesByComponent) {
    scopesByComponent.putAll(subject, subject.scopes());

    for (ComponentDescriptor childComponent : subject.childComponents()) {
      validateScopeHierarchy(report, childComponent, scopesByComponent);
    }

    scopesByComponent.removeAll(subject);

    Predicate subjectScopes =
        subject.isProduction()
            // TODO(beder): validate that @ProductionScope is only applied on production components
            ? and(in(subject.scopes()), not(Scope::isProductionScope))
            : in(subject.scopes());
    SetMultimap overlappingScopes =
        Multimaps.filterValues(scopesByComponent, subjectScopes);
    if (!overlappingScopes.isEmpty()) {
      StringBuilder error =
          new StringBuilder()
              .append(subject.typeElement().getQualifiedName())
              .append(" has conflicting scopes:");
      for (Map.Entry entry : overlappingScopes.entries()) {
        Scope scope = entry.getValue();
        error
            .append("\n  ")
            .append(entry.getKey().typeElement().getQualifiedName())
            .append(" also has ")
            .append(getReadableSource(scope));
      }
      report.addItem(
          error.toString(),
          compilerOptions.scopeCycleValidationType().diagnosticKind().get(),
          subject.typeElement());
    }
  }

  private void validateProductionModuleUniqueness(
      ValidationReport.Builder report,
      ComponentDescriptor componentDescriptor,
      SetMultimap producerModulesByComponent) {
    ImmutableSet producerModules =
        componentDescriptor.modules().stream()
            .filter(module -> module.kind().equals(ModuleKind.PRODUCER_MODULE))
            .collect(toImmutableSet());

    producerModulesByComponent.putAll(componentDescriptor, producerModules);
    for (ComponentDescriptor childComponent : componentDescriptor.childComponents()) {
      validateProductionModuleUniqueness(report, childComponent, producerModulesByComponent);
    }
    producerModulesByComponent.removeAll(componentDescriptor);


    SetMultimap repeatedModules =
        Multimaps.filterValues(producerModulesByComponent, producerModules::contains);
    if (repeatedModules.isEmpty()) {
      return;
    }

    StringBuilder error = new StringBuilder();
    Formatter formatter = new Formatter(error);

    formatter.format(
        "%s repeats @ProducerModules:", componentDescriptor.typeElement().getQualifiedName());

    for (Map.Entry> entry :
        repeatedModules.asMap().entrySet()) {
      formatter.format("\n  %s also installs: ", entry.getKey().typeElement().getQualifiedName());
      COMMA_SEPARATED_JOINER
          .appendTo(
              error,
              Iterables.transform(entry.getValue(), m -> m.moduleElement().getQualifiedName()));
    }

    report.addError(error.toString());
  }

  private void validateRepeatedScopedDeclarations(
      ValidationReport.Builder report,
      ComponentDescriptor component,
      // TODO(ronshapiro): optimize ModuleDescriptor.hashCode()/equals. Otherwise this could be
      // quite costly
      SetMultimap modulesWithScopes) {
    ImmutableSet modules =
        component.modules().stream().filter(this::hasScopedDeclarations).collect(toImmutableSet());
    modulesWithScopes.putAll(component, modules);
    for (ComponentDescriptor childComponent : component.childComponents()) {
      validateRepeatedScopedDeclarations(report, childComponent, modulesWithScopes);
    }
    modulesWithScopes.removeAll(component);

    SetMultimap repeatedModules =
        Multimaps.filterValues(modulesWithScopes, modules::contains);
    if (repeatedModules.isEmpty()) {
      return;
    }

    report.addError(
        repeatedModulesWithScopeError(component, ImmutableSetMultimap.copyOf(repeatedModules)));
  }

  private boolean hasScopedDeclarations(ModuleDescriptor module) {
    return !moduleScopes(module).isEmpty();
  }

  private String repeatedModulesWithScopeError(
      ComponentDescriptor component,
      ImmutableSetMultimap repeatedModules) {
    StringBuilder error =
        new StringBuilder()
            .append(component.typeElement().getQualifiedName())
            .append(" repeats modules with scoped bindings or declarations:");

    repeatedModules
        .asMap()
        .forEach(
            (conflictingComponent, conflictingModules) -> {
              error
                  .append("\n  - ")
                  .append(conflictingComponent.typeElement().getQualifiedName())
                  .append(" also includes:");
              for (ModuleDescriptor conflictingModule : conflictingModules) {
                error
                    .append("\n    - ")
                    .append(conflictingModule.moduleElement().getQualifiedName())
                    .append(" with scopes: ")
                    .append(COMMA_SEPARATED_JOINER.join(moduleScopes(conflictingModule)));
              }
            });
    return error.toString();
  }

  private ImmutableSet moduleScopes(ModuleDescriptor module) {
    return module.allBindingDeclarations().stream()
        .map(declaration -> injectionAnnotations.getScope(declaration.bindingElement().get()))
        .filter(scope -> scope.isPresent() && !scope.get().isReusable())
        .map(Optional::get)
        .collect(toImmutableSet());
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy