dagger.hilt.processor.internal.root.ir.ComponentTreeDepsIrCreator.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of hilt-android-compiler Show documentation
Show all versions of hilt-android-compiler Show documentation
A fast dependency injector for Android and Java.
/*
* Copyright (C) 2021 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.ir
import com.squareup.javapoet.ClassName
// Produces ComponentTreeDepsIr for a set of aggregated deps and roots to process.
class ComponentTreeDepsIrCreator private constructor(
private val isSharedTestComponentsEnabled: Boolean,
private val aggregatedRoots: Set,
private val defineComponentDeps: Set,
private val aliasOfDeps: Set,
private val aggregatedDeps: Set,
private val aggregatedUninstallModulesDeps: Set,
private val aggregatedEarlyEntryPointDeps: Set,
) {
private fun prodComponents(): Set {
// There should only be one prod root in a given build.
val aggregatedRoot = aggregatedRoots.single()
return setOf(
ComponentTreeDepsIr(
name = ComponentTreeDepsNameGenerator().generate(aggregatedRoot.root),
rootDeps = setOf(aggregatedRoot.fqName),
defineComponentDeps = defineComponentDeps.map { it.fqName }.toSet(),
aliasOfDeps = aliasOfDeps.map { it.fqName }.toSet(),
aggregatedDeps =
// @AggregatedDeps with non-empty replaces are from @TestInstallIn and should not be
// installed in production components
aggregatedDeps.filter { it.replaces.isEmpty() }.map { it.fqName }.toSet(),
uninstallModulesDeps = emptySet(),
earlyEntryPointDeps = emptySet(),
)
)
}
private fun testComponents(): Set {
val rootsUsingSharedComponent = rootsUsingSharedComponent(aggregatedRoots)
val aggregatedRootsByRoot = aggregatedRoots.associateBy { it.root }
val aggregatedDepsByRoot = aggregatedDepsByRoot(
aggregatedRoots = aggregatedRoots,
rootsUsingSharedComponent = rootsUsingSharedComponent,
hasEarlyEntryPoints = aggregatedEarlyEntryPointDeps.isNotEmpty()
)
val uninstallModuleDepsByRoot =
aggregatedUninstallModulesDeps.associate { it.test to it.fqName }
return mutableSetOf().apply {
aggregatedDepsByRoot.keys.forEach { root ->
val isDefaultRoot = root == DEFAULT_ROOT_CLASS_NAME
val isEarlyEntryPointRoot = isDefaultRoot && aggregatedEarlyEntryPointDeps.isNotEmpty()
// We want to base the generated name on the user written root rather than a generated root.
val rootName = if (isDefaultRoot) {
DEFAULT_ROOT_CLASS_NAME
} else {
aggregatedRootsByRoot.getValue(root).originatingRoot
}
val componentNameGenerator =
if (isSharedTestComponentsEnabled) {
ComponentTreeDepsNameGenerator(
destinationPackage = "dagger.hilt.android.internal.testing.root",
otherRootNames = aggregatedDepsByRoot.keys
)
} else {
ComponentTreeDepsNameGenerator()
}
add(
ComponentTreeDepsIr(
name = componentNameGenerator.generate(rootName),
rootDeps =
// Non-default component: the root
// Shared component: all roots sharing the component
// EarlyEntryPoint component: empty
if (isDefaultRoot) {
rootsUsingSharedComponent.map { aggregatedRootsByRoot.getValue(it).fqName }.toSet()
} else {
setOf(aggregatedRootsByRoot.getValue(root).fqName)
},
defineComponentDeps = defineComponentDeps.map { it.fqName }.toSet(),
aliasOfDeps = aliasOfDeps.map { it.fqName }.toSet(),
aggregatedDeps = aggregatedDepsByRoot.getOrElse(root) { emptySet() },
uninstallModulesDeps = uninstallModuleDepsByRoot[root]?.let { setOf(it) } ?: emptySet(),
earlyEntryPointDeps =
if (isEarlyEntryPointRoot) {
aggregatedEarlyEntryPointDeps.map { it.fqName }.toSet()
} else {
emptySet()
}
)
)
}
}
}
private fun rootsUsingSharedComponent(roots: Set): Set {
if (!isSharedTestComponentsEnabled) {
return emptySet()
}
val hasLocalModuleDependencies: Set = mutableSetOf().apply {
addAll(aggregatedDeps.filter { it.module != null }.mapNotNull { it.test })
addAll(aggregatedUninstallModulesDeps.map { it.test })
}
return roots
.filter { it.isTestRoot && it.allowsSharingComponent }
.map { it.root }
.filter { !hasLocalModuleDependencies.contains(it) }
.toSet()
}
private fun aggregatedDepsByRoot(
aggregatedRoots: Set,
rootsUsingSharedComponent: Set,
hasEarlyEntryPoints: Boolean
): Map> {
val testDepsByRoot = aggregatedDeps
.filter { it.test != null }
.groupBy(keySelector = { it.test }, valueTransform = { it.fqName })
val globalModules = aggregatedDeps
.filter { it.test == null && it.module != null }
.map { it.fqName }
val globalEntryPointsByComponent = aggregatedDeps
.filter { it.test == null && it.module == null }
.groupBy(keySelector = { it.test }, valueTransform = { it.fqName })
val result = mutableMapOf>()
aggregatedRoots.forEach { aggregatedRoot ->
if (!rootsUsingSharedComponent.contains(aggregatedRoot.root)) {
result.getOrPut(aggregatedRoot.root) { linkedSetOf() }.apply {
addAll(globalModules)
addAll(globalEntryPointsByComponent.values.flatten())
addAll(testDepsByRoot.getOrElse(aggregatedRoot.root) { emptyList() })
}
}
}
// Add the Default/EarlyEntryPoint root if necessary.
if (rootsUsingSharedComponent.isNotEmpty()) {
result.getOrPut(DEFAULT_ROOT_CLASS_NAME) { linkedSetOf() }.apply {
addAll(globalModules)
addAll(globalEntryPointsByComponent.values.flatten())
addAll(rootsUsingSharedComponent.flatMap { testDepsByRoot.getOrElse(it) { emptyList() } })
}
} else if (hasEarlyEntryPoints) {
result.getOrPut(DEFAULT_ROOT_CLASS_NAME) { linkedSetOf() }.apply {
addAll(globalModules)
addAll(
globalEntryPointsByComponent.entries
.filterNot { (component, _) -> component == SINGLETON_COMPONENT_CLASS_NAME }
.flatMap { (_, entryPoints) -> entryPoints }
)
}
}
return result
}
/**
* Generates a component name for a tree that will be based off the given root after mapping it to
* the [destinationPackage] and disambiguating from [otherRootNames].
*/
private class ComponentTreeDepsNameGenerator(
private val destinationPackage: String? = null,
private val otherRootNames: Collection = emptySet()
) {
private val simpleNameMap: Map by lazy {
mutableMapOf().apply {
otherRootNames.groupBy { it.enclosedName() }.values.forEach { conflictingRootNames ->
if (conflictingRootNames.size == 1) {
// If there's only 1 root there's nothing to disambiguate so return the simple name.
put(conflictingRootNames.first(), conflictingRootNames.first().enclosedName())
} else {
// There are conflicting simple names, so disambiguate them with a unique prefix.
// We keep them small to fix https://github.com/google/dagger/issues/421.
// Sorted in order to guarantee determinism if this is invoked by different processors.
val usedNames = mutableSetOf()
conflictingRootNames.sorted().forEach { rootClassName ->
val basePrefix = rootClassName.let { className ->
val containerName = className.enclosingClassName()?.enclosedName() ?: ""
if (containerName.isNotEmpty() && containerName[0].isUpperCase()) {
// If parent element looks like a class, use its initials as a prefix.
containerName.filterNot { it.isLowerCase() }
} else {
// Not in a normally named class. Prefix with the initials of the elements
// leading here.
className.toString().split('.').dropLast(1)
.joinToString(separator = "") { "${it.first()}" }
}
}
var uniqueName = basePrefix
var differentiator = 2
while (!usedNames.add(uniqueName)) {
uniqueName = basePrefix + differentiator++
}
put(rootClassName, "${uniqueName}_${rootClassName.enclosedName()}")
}
}
}
}
}
fun generate(rootName: ClassName): ClassName =
ClassName.get(
destinationPackage ?: rootName.packageName(),
if (otherRootNames.isEmpty()) {
rootName.enclosedName()
} else {
simpleNameMap.getValue(rootName)
}
).append("_ComponentTreeDeps")
private fun ClassName.enclosedName() = simpleNames().joinToString(separator = "_")
private fun ClassName.append(suffix: String) = peerClass(simpleName() + suffix)
}
companion object {
@JvmStatic
fun components(
isTest: Boolean,
isSharedTestComponentsEnabled: Boolean,
aggregatedRoots: Set,
defineComponentDeps: Set,
aliasOfDeps: Set,
aggregatedDeps: Set,
aggregatedUninstallModulesDeps: Set,
aggregatedEarlyEntryPointDeps: Set,
) = ComponentTreeDepsIrCreator(
isSharedTestComponentsEnabled,
// TODO(bcorso): Consider creating a common interface for fqName so that we can sort these
// using a shared method rather than repeating the sorting logic.
aggregatedRoots.toList().sortedBy { it.fqName.canonicalName() }.toSet(),
defineComponentDeps.toList().sortedBy { it.fqName.canonicalName() }.toSet(),
aliasOfDeps.toList().sortedBy { it.fqName.canonicalName() }.toSet(),
aggregatedDeps.toList().sortedBy { it.fqName.canonicalName() }.toSet(),
aggregatedUninstallModulesDeps.toList().sortedBy { it.fqName.canonicalName() }.toSet(),
aggregatedEarlyEntryPointDeps.toList().sortedBy { it.fqName.canonicalName() }.toSet()
).let { producer ->
if (isTest) {
producer.testComponents()
} else {
producer.prodComponents()
}
}
val DEFAULT_ROOT_CLASS_NAME: ClassName =
ClassName.get("dagger.hilt.android.internal.testing.root", "Default")
val SINGLETON_COMPONENT_CLASS_NAME: ClassName =
ClassName.get("dagger.hilt.components", "SingletonComponent")
}
}