• 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 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