1 /* <lambda>null2 * Copyright (C) 2021 The Dagger Authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package dagger.hilt.processor.internal.root.ir 18 19 import com.squareup.javapoet.ClassName 20 21 // Produces ComponentTreeDepsIr for a set of aggregated deps and roots to process. 22 class ComponentTreeDepsIrCreator private constructor( 23 private val isSharedTestComponentsEnabled: Boolean, 24 private val aggregatedRoots: Set<AggregatedRootIr>, 25 private val defineComponentDeps: Set<DefineComponentClassesIr>, 26 private val aliasOfDeps: Set<AliasOfPropagatedDataIr>, 27 private val aggregatedDeps: Set<AggregatedDepsIr>, 28 private val aggregatedUninstallModulesDeps: Set<AggregatedUninstallModulesIr>, 29 private val aggregatedEarlyEntryPointDeps: Set<AggregatedEarlyEntryPointIr>, 30 ) { 31 private fun prodComponents(): Set<ComponentTreeDepsIr> { 32 // There should only be one prod root in a given build. 33 val aggregatedRoot = aggregatedRoots.single() 34 return setOf( 35 ComponentTreeDepsIr( 36 name = ComponentTreeDepsNameGenerator().generate(aggregatedRoot.root), 37 rootDeps = setOf(aggregatedRoot.fqName), 38 defineComponentDeps = defineComponentDeps.map { it.fqName }.toSet(), 39 aliasOfDeps = aliasOfDeps.map { it.fqName }.toSet(), 40 aggregatedDeps = 41 // @AggregatedDeps with non-empty replaces are from @TestInstallIn and should not be 42 // installed in production components 43 aggregatedDeps.filter { it.replaces.isEmpty() }.map { it.fqName }.toSet(), 44 uninstallModulesDeps = emptySet(), 45 earlyEntryPointDeps = emptySet(), 46 ) 47 ) 48 } 49 50 private fun testComponents(): Set<ComponentTreeDepsIr> { 51 val rootsUsingSharedComponent = rootsUsingSharedComponent(aggregatedRoots) 52 val aggregatedRootsByRoot = aggregatedRoots.associateBy { it.root } 53 val aggregatedDepsByRoot = aggregatedDepsByRoot( 54 aggregatedRoots = aggregatedRoots, 55 rootsUsingSharedComponent = rootsUsingSharedComponent, 56 hasEarlyEntryPoints = aggregatedEarlyEntryPointDeps.isNotEmpty() 57 ) 58 val uninstallModuleDepsByRoot = 59 aggregatedUninstallModulesDeps.associate { it.test to it.fqName } 60 return mutableSetOf<ComponentTreeDepsIr>().apply { 61 aggregatedDepsByRoot.keys.forEach { root -> 62 val isDefaultRoot = root == DEFAULT_ROOT_CLASS_NAME 63 val isEarlyEntryPointRoot = isDefaultRoot && aggregatedEarlyEntryPointDeps.isNotEmpty() 64 // We want to base the generated name on the user written root rather than a generated root. 65 val rootName = if (isDefaultRoot) { 66 DEFAULT_ROOT_CLASS_NAME 67 } else { 68 aggregatedRootsByRoot.getValue(root).originatingRoot 69 } 70 val componentNameGenerator = 71 if (isSharedTestComponentsEnabled) { 72 ComponentTreeDepsNameGenerator( 73 destinationPackage = "dagger.hilt.android.internal.testing.root", 74 otherRootNames = aggregatedDepsByRoot.keys 75 ) 76 } else { 77 ComponentTreeDepsNameGenerator() 78 } 79 add( 80 ComponentTreeDepsIr( 81 name = componentNameGenerator.generate(rootName), 82 rootDeps = 83 // Non-default component: the root 84 // Shared component: all roots sharing the component 85 // EarlyEntryPoint component: empty 86 if (isDefaultRoot) { 87 rootsUsingSharedComponent.map { aggregatedRootsByRoot.getValue(it).fqName }.toSet() 88 } else { 89 setOf(aggregatedRootsByRoot.getValue(root).fqName) 90 }, 91 defineComponentDeps = defineComponentDeps.map { it.fqName }.toSet(), 92 aliasOfDeps = aliasOfDeps.map { it.fqName }.toSet(), 93 aggregatedDeps = aggregatedDepsByRoot.getOrElse(root) { emptySet() }, 94 uninstallModulesDeps = uninstallModuleDepsByRoot[root]?.let { setOf(it) } ?: emptySet(), 95 earlyEntryPointDeps = 96 if (isEarlyEntryPointRoot) { 97 aggregatedEarlyEntryPointDeps.map { it.fqName }.toSet() 98 } else { 99 emptySet() 100 } 101 ) 102 ) 103 } 104 } 105 } 106 107 private fun rootsUsingSharedComponent(roots: Set<AggregatedRootIr>): Set<ClassName> { 108 if (!isSharedTestComponentsEnabled) { 109 return emptySet() 110 } 111 val hasLocalModuleDependencies: Set<ClassName> = mutableSetOf<ClassName>().apply { 112 addAll(aggregatedDeps.filter { it.module != null }.mapNotNull { it.test }) 113 addAll(aggregatedUninstallModulesDeps.map { it.test }) 114 } 115 return roots 116 .filter { it.isTestRoot && it.allowsSharingComponent } 117 .map { it.root } 118 .filter { !hasLocalModuleDependencies.contains(it) } 119 .toSet() 120 } 121 122 private fun aggregatedDepsByRoot( 123 aggregatedRoots: Set<AggregatedRootIr>, 124 rootsUsingSharedComponent: Set<ClassName>, 125 hasEarlyEntryPoints: Boolean 126 ): Map<ClassName, Set<ClassName>> { 127 val testDepsByRoot = aggregatedDeps 128 .filter { it.test != null } 129 .groupBy(keySelector = { it.test }, valueTransform = { it.fqName }) 130 val globalModules = aggregatedDeps 131 .filter { it.test == null && it.module != null } 132 .map { it.fqName } 133 val globalEntryPointsByComponent = aggregatedDeps 134 .filter { it.test == null && it.module == null } 135 .groupBy(keySelector = { it.test }, valueTransform = { it.fqName }) 136 val result = mutableMapOf<ClassName, LinkedHashSet<ClassName>>() 137 aggregatedRoots.forEach { aggregatedRoot -> 138 if (!rootsUsingSharedComponent.contains(aggregatedRoot.root)) { 139 result.getOrPut(aggregatedRoot.root) { linkedSetOf() }.apply { 140 addAll(globalModules) 141 addAll(globalEntryPointsByComponent.values.flatten()) 142 addAll(testDepsByRoot.getOrElse(aggregatedRoot.root) { emptyList() }) 143 } 144 } 145 } 146 // Add the Default/EarlyEntryPoint root if necessary. 147 if (rootsUsingSharedComponent.isNotEmpty()) { 148 result.getOrPut(DEFAULT_ROOT_CLASS_NAME) { linkedSetOf() }.apply { 149 addAll(globalModules) 150 addAll(globalEntryPointsByComponent.values.flatten()) 151 addAll(rootsUsingSharedComponent.flatMap { testDepsByRoot.getOrElse(it) { emptyList() } }) 152 } 153 } else if (hasEarlyEntryPoints) { 154 result.getOrPut(DEFAULT_ROOT_CLASS_NAME) { linkedSetOf() }.apply { 155 addAll(globalModules) 156 addAll( 157 globalEntryPointsByComponent.entries 158 .filterNot { (component, _) -> component == SINGLETON_COMPONENT_CLASS_NAME } 159 .flatMap { (_, entryPoints) -> entryPoints } 160 ) 161 } 162 } 163 return result 164 } 165 166 /** 167 * Generates a component name for a tree that will be based off the given root after mapping it to 168 * the [destinationPackage] and disambiguating from [otherRootNames]. 169 */ 170 private class ComponentTreeDepsNameGenerator( 171 private val destinationPackage: String? = null, 172 private val otherRootNames: Collection<ClassName> = emptySet() 173 ) { 174 private val simpleNameMap: Map<ClassName, String> by lazy { 175 mutableMapOf<ClassName, String>().apply { 176 otherRootNames.groupBy { it.enclosedName() }.values.forEach { conflictingRootNames -> 177 if (conflictingRootNames.size == 1) { 178 // If there's only 1 root there's nothing to disambiguate so return the simple name. 179 put(conflictingRootNames.first(), conflictingRootNames.first().enclosedName()) 180 } else { 181 // There are conflicting simple names, so disambiguate them with a unique prefix. 182 // We keep them small to fix https://github.com/google/dagger/issues/421. 183 // Sorted in order to guarantee determinism if this is invoked by different processors. 184 val usedNames = mutableSetOf<String>() 185 conflictingRootNames.sorted().forEach { rootClassName -> 186 val basePrefix = rootClassName.let { className -> 187 val containerName = className.enclosingClassName()?.enclosedName() ?: "" 188 if (containerName.isNotEmpty() && containerName[0].isUpperCase()) { 189 // If parent element looks like a class, use its initials as a prefix. 190 containerName.filterNot { it.isLowerCase() } 191 } else { 192 // Not in a normally named class. Prefix with the initials of the elements 193 // leading here. 194 className.toString().split('.').dropLast(1) 195 .joinToString(separator = "") { "${it.first()}" } 196 } 197 } 198 var uniqueName = basePrefix 199 var differentiator = 2 200 while (!usedNames.add(uniqueName)) { 201 uniqueName = basePrefix + differentiator++ 202 } 203 put(rootClassName, "${uniqueName}_${rootClassName.enclosedName()}") 204 } 205 } 206 } 207 } 208 } 209 210 fun generate(rootName: ClassName): ClassName = 211 ClassName.get( 212 destinationPackage ?: rootName.packageName(), 213 if (otherRootNames.isEmpty()) { 214 rootName.enclosedName() 215 } else { 216 simpleNameMap.getValue(rootName) 217 } 218 ).append("_ComponentTreeDeps") 219 220 private fun ClassName.enclosedName() = simpleNames().joinToString(separator = "_") 221 222 private fun ClassName.append(suffix: String) = peerClass(simpleName() + suffix) 223 } 224 225 companion object { 226 227 @JvmStatic 228 fun components( 229 isTest: Boolean, 230 isSharedTestComponentsEnabled: Boolean, 231 aggregatedRoots: Set<AggregatedRootIr>, 232 defineComponentDeps: Set<DefineComponentClassesIr>, 233 aliasOfDeps: Set<AliasOfPropagatedDataIr>, 234 aggregatedDeps: Set<AggregatedDepsIr>, 235 aggregatedUninstallModulesDeps: Set<AggregatedUninstallModulesIr>, 236 aggregatedEarlyEntryPointDeps: Set<AggregatedEarlyEntryPointIr>, 237 ) = ComponentTreeDepsIrCreator( 238 isSharedTestComponentsEnabled, 239 // TODO(bcorso): Consider creating a common interface for fqName so that we can sort these 240 // using a shared method rather than repeating the sorting logic. 241 aggregatedRoots.toList().sortedBy { it.fqName.canonicalName() }.toSet(), 242 defineComponentDeps.toList().sortedBy { it.fqName.canonicalName() }.toSet(), 243 aliasOfDeps.toList().sortedBy { it.fqName.canonicalName() }.toSet(), 244 aggregatedDeps.toList().sortedBy { it.fqName.canonicalName() }.toSet(), 245 aggregatedUninstallModulesDeps.toList().sortedBy { it.fqName.canonicalName() }.toSet(), 246 aggregatedEarlyEntryPointDeps.toList().sortedBy { it.fqName.canonicalName() }.toSet() 247 ).let { producer -> 248 if (isTest) { 249 producer.testComponents() 250 } else { 251 producer.prodComponents() 252 } 253 } 254 255 val DEFAULT_ROOT_CLASS_NAME: ClassName = 256 ClassName.get("dagger.hilt.android.internal.testing.root", "Default") 257 val SINGLETON_COMPONENT_CLASS_NAME: ClassName = 258 ClassName.get("dagger.hilt.components", "SingletonComponent") 259 } 260 } 261