• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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