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