/* * Copyright (C) 2020 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.hilt.processor.internal.root; import static com.google.common.base.Suppliers.memoize; import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet; import static javax.lang.model.element.Modifier.ABSTRACT; import static javax.lang.model.element.Modifier.PRIVATE; import static javax.lang.model.element.Modifier.STATIC; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSetMultimap; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.TypeName; import dagger.hilt.processor.internal.ClassNames; import dagger.hilt.processor.internal.ComponentDescriptor; import dagger.hilt.processor.internal.ComponentTree; import dagger.hilt.processor.internal.KotlinMetadataUtils; import dagger.hilt.processor.internal.Processors; import dagger.hilt.processor.internal.aggregateddeps.ComponentDependencies; import dagger.hilt.processor.internal.aliasof.AliasOfs; import dagger.internal.codegen.kotlin.KotlinMetadataUtil; import java.util.List; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.util.ElementFilter; import javax.lang.model.util.Elements; import javax.tools.Diagnostic; /** Contains metadata about the given hilt root. */ public final class RootMetadata { private static final ClassName APPLICATION_CONTEXT_MODULE = ClassName.get("dagger.hilt.android.internal.modules", "ApplicationContextModule"); static RootMetadata create( Root root, ComponentTree componentTree, ComponentDependencies deps, ProcessingEnvironment env) { RootMetadata metadata = new RootMetadata(root, componentTree, deps, env); metadata.validate(); return metadata; } static RootMetadata copyWithNewTree( RootMetadata other, ComponentTree componentTree) { return create(other.root, componentTree, other.deps, other.env); } private final Root root; private final ProcessingEnvironment env; private final Elements elements; private final ComponentTree componentTree; private final ComponentDependencies deps; private final Supplier> scopesByComponent = memoize(this::getScopesByComponentUncached); private final Supplier testRootMetadata = memoize(this::testRootMetadataUncached); private RootMetadata( Root root, ComponentTree componentTree, ComponentDependencies deps, ProcessingEnvironment env) { this.root = root; this.env = env; this.elements = env.getElementUtils(); this.componentTree = componentTree; this.deps = deps; } public Root root() { return root; } public ComponentTree componentTree() { return componentTree; } public ComponentDependencies deps() { return deps; } public ImmutableSet modules(ClassName componentName) { return deps.modules().get(componentName, root.classname(), root.isTestRoot()); } public ImmutableSet entryPoints(ClassName componentName) { return ImmutableSet.builder() .addAll(getUserDefinedEntryPoints(componentName)) .add(componentName) .build(); } public ImmutableSet scopes(ClassName componentName) { return scopesByComponent.get().get(componentName); } /** * Returns all modules in the given component that do not have accessible default constructors. * Note that a non-static module nested in an outer class is considered to have no default * constructors, since an instance of the outer class is needed to construct the module. This also * filters out framework modules directly referenced by the codegen, since those are already known * about and are specifically handled in the codegen. */ public ImmutableSet modulesThatDaggerCannotConstruct(ClassName componentName) { return modules(componentName).stream() .filter(module -> !daggerCanConstruct(module)) .filter(module -> !APPLICATION_CONTEXT_MODULE.equals(ClassName.get(module))) .collect(toImmutableSet()); } public TestRootMetadata testRootMetadata() { return testRootMetadata.get(); } public boolean waitForBindValue() { return false; } private TestRootMetadata testRootMetadataUncached() { return TestRootMetadata.of(env, root().element()); } /** * Validates that the {@link RootType} annotation is compatible with its {@link TypeElement} and * {@link ComponentDependencies}. */ private void validate() { // Only test modules in the application component can be missing default constructor for (ComponentDescriptor componentDescriptor : componentTree.getComponentDescriptors()) { ClassName componentName = componentDescriptor.component(); for (TypeElement extraModule : modulesThatDaggerCannotConstruct(componentName)) { if (root.type().isTestRoot() && !componentName.equals(ClassNames.SINGLETON_COMPONENT)) { env.getMessager() .printMessage( Diagnostic.Kind.ERROR, "[Hilt] All test modules (unless installed in ApplicationComponent) must use " + "static provision methods or have a visible, no-arg constructor. Found: " + extraModule.getQualifiedName(), root.element()); } else if (!root.type().isTestRoot()) { env.getMessager() .printMessage( Diagnostic.Kind.ERROR, "[Hilt] All modules must be static and use static provision methods or have a " + "visible, no-arg constructor. Found: " + extraModule.getQualifiedName(), root.element()); } } } } private ImmutableSet getUserDefinedEntryPoints(ClassName componentName) { ImmutableSet.Builder entryPointSet = ImmutableSet.builder(); entryPointSet.add(ClassNames.GENERATED_COMPONENT); for (TypeElement element : deps.entryPoints().get(componentName, root.classname(), root.isTestRoot())) { entryPointSet.add(ClassName.get(element)); } return entryPointSet.build(); } private ImmutableSetMultimap getScopesByComponentUncached() { ImmutableSetMultimap.Builder builder = ImmutableSetMultimap.builder(); ImmutableSet defineComponentScopes = componentTree.getComponentDescriptors().stream() .flatMap(descriptor -> descriptor.scopes().stream()) .collect(toImmutableSet()); AliasOfs aliasOfs = new AliasOfs(env, defineComponentScopes); for (ComponentDescriptor componentDescriptor : componentTree.getComponentDescriptors()) { for (ClassName scope : componentDescriptor.scopes()) { builder.put(componentDescriptor.component(), scope); builder.putAll(componentDescriptor.component(), aliasOfs.getAliasesFor(scope)); } } return builder.build(); } private static boolean daggerCanConstruct(TypeElement type) { KotlinMetadataUtil metadataUtil = KotlinMetadataUtils.getMetadataUtil(); boolean isKotlinObject = metadataUtil.isObjectClass(type) || metadataUtil.isCompanionObjectClass(type); if (isKotlinObject) { // Treat Kotlin object modules as if Dagger can construct them (it technically can't, but it // doesn't need to as it can use them since all their provision methods are static). return true; } return !isInnerClass(type) && !hasNonDaggerAbstractMethod(type) && (hasOnlyStaticProvides(type) || hasVisibleEmptyConstructor(type)); } private static boolean isInnerClass(TypeElement type) { return type.getNestingKind().isNested() && !type.getModifiers().contains(STATIC); } private static boolean hasNonDaggerAbstractMethod(TypeElement type) { // TODO(erichang): Actually this isn't really supported b/28989613 return ElementFilter.methodsIn(type.getEnclosedElements()).stream() .filter(method -> method.getModifiers().contains(ABSTRACT)) .anyMatch(method -> !Processors.hasDaggerAbstractMethodAnnotation(method)); } private static boolean hasOnlyStaticProvides(TypeElement type) { // TODO(erichang): Check for @Produces too when we have a producers story return ElementFilter.methodsIn(type.getEnclosedElements()).stream() .filter(method -> Processors.hasAnnotation(method, ClassNames.PROVIDES)) .allMatch(method -> method.getModifiers().contains(STATIC)); } private static boolean hasVisibleEmptyConstructor(TypeElement type) { List constructors = ElementFilter.constructorsIn(type.getEnclosedElements()); return constructors.isEmpty() || constructors.stream() .filter(constructor -> constructor.getParameters().isEmpty()) .anyMatch(constructor -> !constructor.getModifiers().contains(PRIVATE)); } }