1 /* 2 * Copyright (C) 2019 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.aggregateddeps; 18 19 import static com.google.common.base.Preconditions.checkNotNull; 20 import static com.google.common.base.Preconditions.checkState; 21 import static com.google.common.collect.Iterables.getOnlyElement; 22 import static dagger.hilt.processor.internal.aggregateddeps.AggregatedDepsGenerator.AGGREGATING_PACKAGE; 23 import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList; 24 import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet; 25 26 import com.google.auto.value.AutoValue; 27 import com.google.common.collect.HashMultimap; 28 import com.google.common.collect.ImmutableList; 29 import com.google.common.collect.ImmutableMap; 30 import com.google.common.collect.ImmutableSet; 31 import com.google.common.collect.ImmutableSetMultimap; 32 import com.google.common.collect.SetMultimap; 33 import com.squareup.javapoet.ClassName; 34 import dagger.hilt.processor.internal.AnnotationValues; 35 import dagger.hilt.processor.internal.BadInputException; 36 import dagger.hilt.processor.internal.ClassNames; 37 import dagger.hilt.processor.internal.ComponentDescriptor; 38 import dagger.hilt.processor.internal.ProcessorErrors; 39 import dagger.hilt.processor.internal.Processors; 40 import dagger.hilt.processor.internal.aggregateddeps.ComponentDependencies.AggregatedDepMetadata.DependencyType; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.Optional; 44 import javax.lang.model.element.AnnotationMirror; 45 import javax.lang.model.element.AnnotationValue; 46 import javax.lang.model.element.Element; 47 import javax.lang.model.element.ElementKind; 48 import javax.lang.model.element.PackageElement; 49 import javax.lang.model.element.TypeElement; 50 import javax.lang.model.util.Elements; 51 52 /** Represents information needed to create a component (i.e. modules, entry points, etc) */ 53 @AutoValue 54 public abstract class ComponentDependencies { builder()55 private static Builder builder() { 56 return new AutoValue_ComponentDependencies.Builder(); 57 } 58 59 /** Returns the modules for a component, without any filtering. */ modules()60 public abstract Dependencies modules(); 61 62 /** Returns the entry points associated with the given a component. */ entryPoints()63 public abstract Dependencies entryPoints(); 64 65 /** Returns the component entry point associated with the given a component. */ componentEntryPoints()66 public abstract Dependencies componentEntryPoints(); 67 68 @AutoValue.Builder 69 abstract static class Builder { modulesBuilder()70 abstract Dependencies.Builder modulesBuilder(); 71 entryPointsBuilder()72 abstract Dependencies.Builder entryPointsBuilder(); 73 componentEntryPointsBuilder()74 abstract Dependencies.Builder componentEntryPointsBuilder(); 75 autoBuild()76 abstract ComponentDependencies autoBuild(); 77 build(Elements elements)78 ComponentDependencies build(Elements elements) { 79 validateModules(modulesBuilder().build(), elements); 80 return autoBuild(); 81 } 82 } 83 84 /** A key used for grouping a test dependency by both its component and test name. */ 85 @AutoValue 86 abstract static class TestDepKey { of(ClassName component, ClassName test)87 static TestDepKey of(ClassName component, ClassName test) { 88 return new AutoValue_ComponentDependencies_TestDepKey(component, test); 89 } 90 91 /** Returns the name of the component this dependency should be installed in. */ component()92 abstract ClassName component(); 93 94 /** Returns the name of the test that this dependency should be installed in. */ test()95 abstract ClassName test(); 96 } 97 98 /** 99 * Holds a set of component dependencies, e.g. modules or entry points. 100 * 101 * <p>This class handles separating dependencies into global and test dependencies. Global 102 * dependencies are installed with every test, where test dependencies are only installed with the 103 * specified test. The total set of dependencies includes all global + test dependencies. 104 */ 105 @AutoValue 106 public abstract static class Dependencies { builder()107 static Builder builder() { 108 return new AutoValue_ComponentDependencies_Dependencies.Builder(); 109 } 110 111 /** Returns the global deps keyed by component. */ globalDeps()112 abstract ImmutableSetMultimap<ClassName, TypeElement> globalDeps(); 113 114 /** Returns the global test deps keyed by component. */ globalTestDeps()115 abstract ImmutableSetMultimap<ClassName, TypeElement> globalTestDeps(); 116 117 /** Returns the test deps keyed by component and test. */ testDeps()118 abstract ImmutableSetMultimap<TestDepKey, TypeElement> testDeps(); 119 120 /** Returns the uninstalled test deps keyed by test. */ uninstalledTestDeps()121 abstract ImmutableSetMultimap<ClassName, TypeElement> uninstalledTestDeps(); 122 123 /** Returns the global uninstalled test deps. */ globalUninstalledTestDeps()124 abstract ImmutableSet<TypeElement> globalUninstalledTestDeps(); 125 126 /** Returns the dependencies to be installed in the given component for the given root. */ get(ClassName component, ClassName root, boolean isTestRoot)127 public ImmutableSet<TypeElement> get(ClassName component, ClassName root, boolean isTestRoot) { 128 if (!isTestRoot) { 129 return globalDeps().get(component); 130 } 131 132 ImmutableSet<TypeElement> uninstalledTestDepsForRoot = uninstalledTestDeps().get(root); 133 return ImmutableSet.<TypeElement>builder() 134 .addAll( 135 globalDeps().get(component).stream() 136 .filter(dep -> !uninstalledTestDepsForRoot.contains(dep)) 137 .filter(dep -> !globalUninstalledTestDeps().contains(dep)) 138 .collect(toImmutableSet())) 139 .addAll(globalTestDeps().get(component)) 140 .addAll(testDeps().get(TestDepKey.of(component, root))) 141 .build(); 142 } 143 144 @AutoValue.Builder 145 abstract static class Builder { globalDepsBuilder()146 abstract ImmutableSetMultimap.Builder<ClassName, TypeElement> globalDepsBuilder(); 147 globalTestDepsBuilder()148 abstract ImmutableSetMultimap.Builder<ClassName, TypeElement> globalTestDepsBuilder(); 149 testDepsBuilder()150 abstract ImmutableSetMultimap.Builder<TestDepKey, TypeElement> testDepsBuilder(); 151 uninstalledTestDepsBuilder()152 abstract ImmutableSetMultimap.Builder<ClassName, TypeElement> uninstalledTestDepsBuilder(); 153 globalUninstalledTestDepsBuilder()154 abstract ImmutableSet.Builder<TypeElement> globalUninstalledTestDepsBuilder(); 155 build()156 abstract Dependencies build(); 157 } 158 } 159 160 /** 161 * Pulls the component dependencies from the {@code packageName}. 162 * 163 * <p>Dependency files are generated by the {@link AggregatedDepsProcessor}, and have the form: 164 * 165 * <pre>{@code 166 * {@literal @}AggregatedDeps( 167 * components = { 168 * "foo.FooComponent", 169 * "bar.BarComponent" 170 * }, 171 * modules = "baz.BazModule" 172 * ) 173 * 174 * }</pre> 175 */ from( ImmutableSet<ComponentDescriptor> descriptors, Elements elements)176 public static ComponentDependencies from( 177 ImmutableSet<ComponentDescriptor> descriptors, Elements elements) { 178 Map<String, ComponentDescriptor> descriptorLookup = descriptorLookupMap(descriptors); 179 ImmutableList<AggregatedDepMetadata> metadatas = 180 getAggregatedDeps(elements).stream() 181 .map(deps -> AggregatedDepMetadata.create(deps, descriptorLookup, elements)) 182 .collect(toImmutableList()); 183 184 ComponentDependencies.Builder componentDependencies = ComponentDependencies.builder(); 185 for (AggregatedDepMetadata metadata : metadatas) { 186 Dependencies.Builder builder = null; 187 switch (metadata.dependencyType()) { 188 case MODULE: 189 builder = componentDependencies.modulesBuilder(); 190 break; 191 case ENTRY_POINT: 192 builder = componentDependencies.entryPointsBuilder(); 193 break; 194 case COMPONENT_ENTRY_POINT: 195 builder = componentDependencies.componentEntryPointsBuilder(); 196 break; 197 } 198 199 for (ComponentDescriptor componentDescriptor : metadata.componentDescriptors()) { 200 ClassName component = componentDescriptor.component(); 201 if (metadata.testElement().isPresent()) { 202 // In this case the @InstallIn or @TestInstallIn applies to only the given test root. 203 ClassName test = ClassName.get(metadata.testElement().get()); 204 builder.testDepsBuilder().put(TestDepKey.of(component, test), metadata.dependency()); 205 builder.uninstalledTestDepsBuilder().putAll(test, metadata.replacedDependencies()); 206 } else { 207 // In this case the @InstallIn or @TestInstallIn applies to all roots 208 if (!metadata.replacedDependencies().isEmpty()) { 209 // If there are replacedDependencies() it means this is a @TestInstallIn 210 builder.globalTestDepsBuilder().put(component, metadata.dependency()); 211 builder.globalUninstalledTestDepsBuilder().addAll(metadata.replacedDependencies()); 212 } else { 213 builder.globalDepsBuilder().put(component, metadata.dependency()); 214 } 215 } 216 } 217 } 218 219 // Collect all @UninstallModules. 220 // TODO(b/176438516): Filter @UninstallModules at the root. 221 metadatas.stream() 222 .filter(metadata -> metadata.testElement().isPresent()) 223 .map(metadata -> metadata.testElement().get()) 224 .distinct() 225 .filter(testElement -> Processors.hasAnnotation(testElement, ClassNames.IGNORE_MODULES)) 226 .forEach( 227 testElement -> 228 componentDependencies 229 .modulesBuilder() 230 .uninstalledTestDepsBuilder() 231 .putAll( 232 ClassName.get(testElement), getUninstalledModules(testElement, elements))); 233 234 return componentDependencies.build(elements); 235 } 236 descriptorLookupMap( ImmutableSet<ComponentDescriptor> descriptors)237 private static ImmutableMap<String, ComponentDescriptor> descriptorLookupMap( 238 ImmutableSet<ComponentDescriptor> descriptors) { 239 ImmutableMap.Builder<String, ComponentDescriptor> builder = ImmutableMap.builder(); 240 for (ComponentDescriptor descriptor : descriptors) { 241 // This is a temporary hack to map the old ApplicationComponent to the new SingletonComponent. 242 // Technically, this is only needed for backwards compatibility with libraries using the old 243 // processor since new processors should convert to the new SingletonComponent when generating 244 // the metadata class. 245 if (descriptor.component().equals(ClassNames.SINGLETON_COMPONENT)) { 246 builder.put("dagger.hilt.android.components.ApplicationComponent", descriptor); 247 } 248 builder.put(descriptor.component().toString(), descriptor); 249 } 250 return builder.build(); 251 } 252 253 // Validate that the @UninstallModules doesn't contain any test modules. validateModules(Dependencies moduleDeps, Elements elements)254 private static Dependencies validateModules(Dependencies moduleDeps, Elements elements) { 255 SetMultimap<ClassName, TypeElement> invalidTestModules = HashMultimap.create(); 256 moduleDeps.testDeps().entries().stream() 257 .filter( 258 e -> moduleDeps.uninstalledTestDeps().containsEntry(e.getKey().test(), e.getValue())) 259 .forEach(e -> invalidTestModules.put(e.getKey().test(), e.getValue())); 260 261 // Currently we don't have a good way to throw an error for all tests, so we sort (to keep the 262 // error reporting order stable) and then choose the first test. 263 // TODO(bcorso): Consider using ProcessorErrorHandler directly to report all errors at once? 264 Optional<ClassName> invalidTest = 265 invalidTestModules.keySet().stream() 266 .min((test1, test2) -> test1.toString().compareTo(test2.toString())); 267 if (invalidTest.isPresent()) { 268 throw new BadInputException( 269 String.format( 270 "@UninstallModules on test, %s, should not containing test modules, " 271 + "but found: %s", 272 invalidTest.get(), 273 invalidTestModules.get(invalidTest.get()).stream() 274 // Sort modules to keep stable error messages. 275 .sorted((test1, test2) -> test1.toString().compareTo(test2.toString())) 276 .collect(toImmutableList())), 277 elements.getTypeElement(invalidTest.get().toString())); 278 } 279 return moduleDeps; 280 } 281 getUninstalledModules( TypeElement testElement, Elements elements)282 private static ImmutableSet<TypeElement> getUninstalledModules( 283 TypeElement testElement, Elements elements) { 284 ImmutableList<TypeElement> userUninstallModules = 285 Processors.getAnnotationClassValues( 286 elements, 287 Processors.getAnnotationMirror(testElement, ClassNames.IGNORE_MODULES), 288 "value"); 289 290 // For pkg-private modules, find the generated wrapper class and uninstall that instead. 291 return userUninstallModules.stream() 292 .map(uninstallModule -> getPublicDependency(uninstallModule, elements)) 293 .collect(toImmutableSet()); 294 } 295 296 /** Returns the public Hilt wrapper module, or the module itself if its already public. */ getPublicDependency(TypeElement dependency, Elements elements)297 private static TypeElement getPublicDependency(TypeElement dependency, Elements elements) { 298 return PkgPrivateMetadata.of(elements, dependency, ClassNames.MODULE) 299 .map(metadata -> elements.getTypeElement(metadata.generatedClassName().toString())) 300 .orElse(dependency); 301 } 302 303 /** Returns the top-level elements of the aggregated deps package. */ getAggregatedDeps(Elements elements)304 private static ImmutableList<AnnotationMirror> getAggregatedDeps(Elements elements) { 305 PackageElement packageElement = elements.getPackageElement(AGGREGATING_PACKAGE); 306 checkState( 307 packageElement != null, 308 "Couldn't find package %s. Did you mark your @Module classes with @InstallIn annotations?", 309 AGGREGATING_PACKAGE); 310 311 List<? extends Element> aggregatedDepsElements = packageElement.getEnclosedElements(); 312 checkState( 313 !aggregatedDepsElements.isEmpty(), 314 "No dependencies found. Did you mark your @Module classes with @InstallIn annotations?"); 315 316 ImmutableList.Builder<AnnotationMirror> builder = ImmutableList.builder(); 317 for (Element element : aggregatedDepsElements) { 318 ProcessorErrors.checkState( 319 element.getKind() == ElementKind.CLASS, 320 element, 321 "Only classes may be in package %s. Did you add custom code in the package?", 322 AGGREGATING_PACKAGE); 323 324 AnnotationMirror aggregatedDeps = 325 Processors.getAnnotationMirror(element, ClassNames.AGGREGATED_DEPS); 326 ProcessorErrors.checkState( 327 aggregatedDeps != null, 328 element, 329 "Classes in package %s must be annotated with @AggregatedDeps: %s. Found: %s.", 330 AGGREGATING_PACKAGE, 331 element.getSimpleName(), 332 element.getAnnotationMirrors()); 333 334 builder.add(aggregatedDeps); 335 } 336 return builder.build(); 337 } 338 339 @AutoValue 340 abstract static class AggregatedDepMetadata { create( AnnotationMirror aggregatedDeps, Map<String, ComponentDescriptor> descriptorLookup, Elements elements)341 static AggregatedDepMetadata create( 342 AnnotationMirror aggregatedDeps, 343 Map<String, ComponentDescriptor> descriptorLookup, 344 Elements elements) { 345 ImmutableMap<String, AnnotationValue> aggregatedDepsValues = 346 Processors.getAnnotationValues(elements, aggregatedDeps); 347 348 return new AutoValue_ComponentDependencies_AggregatedDepMetadata( 349 getTestElement(aggregatedDepsValues.get("test"), elements), 350 getComponents(aggregatedDepsValues.get("components"), descriptorLookup), 351 getDependencyType( 352 aggregatedDepsValues.get("modules"), 353 aggregatedDepsValues.get("entryPoints"), 354 aggregatedDepsValues.get("componentEntryPoints")), 355 getDependency( 356 aggregatedDepsValues.get("modules"), 357 aggregatedDepsValues.get("entryPoints"), 358 aggregatedDepsValues.get("componentEntryPoints"), 359 elements), 360 getReplacedDependencies(aggregatedDepsValues.get("replaces"), elements)); 361 } 362 363 enum DependencyType { 364 MODULE, 365 ENTRY_POINT, 366 COMPONENT_ENTRY_POINT 367 } 368 testElement()369 abstract Optional<TypeElement> testElement(); 370 componentDescriptors()371 abstract ImmutableList<ComponentDescriptor> componentDescriptors(); 372 dependencyType()373 abstract DependencyType dependencyType(); 374 dependency()375 abstract TypeElement dependency(); 376 replacedDependencies()377 abstract ImmutableSet<TypeElement> replacedDependencies(); 378 getTestElement( AnnotationValue testValue, Elements elements)379 private static Optional<TypeElement> getTestElement( 380 AnnotationValue testValue, Elements elements) { 381 checkNotNull(testValue); 382 String test = AnnotationValues.getString(testValue); 383 return test.isEmpty() ? Optional.empty() : Optional.of(elements.getTypeElement(test)); 384 } 385 getComponents( AnnotationValue componentsValue, Map<String, ComponentDescriptor> descriptorLookup)386 private static ImmutableList<ComponentDescriptor> getComponents( 387 AnnotationValue componentsValue, Map<String, ComponentDescriptor> descriptorLookup) { 388 checkNotNull(componentsValue); 389 ImmutableList<String> componentNames = 390 AnnotationValues.getAnnotationValues(componentsValue).stream() 391 .map(AnnotationValues::getString) 392 .collect(toImmutableList()); 393 394 checkState(!componentNames.isEmpty()); 395 ImmutableList.Builder<ComponentDescriptor> components = ImmutableList.builder(); 396 for (String componentName : componentNames) { 397 checkState( 398 descriptorLookup.containsKey(componentName), 399 "%s is not a valid Component. Did you add or remove code in package %s?", 400 componentName, 401 AGGREGATING_PACKAGE); 402 components.add(descriptorLookup.get(componentName)); 403 } 404 return components.build(); 405 } 406 getDependencyType( AnnotationValue modulesValue, AnnotationValue entryPointsValue, AnnotationValue componentEntryPointsValue)407 private static DependencyType getDependencyType( 408 AnnotationValue modulesValue, 409 AnnotationValue entryPointsValue, 410 AnnotationValue componentEntryPointsValue) { 411 checkNotNull(modulesValue); 412 checkNotNull(entryPointsValue); 413 checkNotNull(componentEntryPointsValue); 414 415 ImmutableSet.Builder<DependencyType> dependencyTypes = ImmutableSet.builder(); 416 if (!AnnotationValues.getAnnotationValues(modulesValue).isEmpty()) { 417 dependencyTypes.add(DependencyType.MODULE); 418 } 419 if (!AnnotationValues.getAnnotationValues(entryPointsValue).isEmpty()) { 420 dependencyTypes.add(DependencyType.ENTRY_POINT); 421 } 422 if (!AnnotationValues.getAnnotationValues(componentEntryPointsValue).isEmpty()) { 423 dependencyTypes.add(DependencyType.COMPONENT_ENTRY_POINT); 424 } 425 return getOnlyElement(dependencyTypes.build()); 426 } 427 getDependency( AnnotationValue modulesValue, AnnotationValue entryPointsValue, AnnotationValue componentEntryPointsValue, Elements elements)428 private static TypeElement getDependency( 429 AnnotationValue modulesValue, 430 AnnotationValue entryPointsValue, 431 AnnotationValue componentEntryPointsValue, 432 Elements elements) { 433 checkNotNull(modulesValue); 434 checkNotNull(entryPointsValue); 435 checkNotNull(componentEntryPointsValue); 436 437 return elements.getTypeElement( 438 AnnotationValues.getString( 439 getOnlyElement( 440 ImmutableList.<AnnotationValue>builder() 441 .addAll(AnnotationValues.getAnnotationValues(modulesValue)) 442 .addAll(AnnotationValues.getAnnotationValues(entryPointsValue)) 443 .addAll(AnnotationValues.getAnnotationValues(componentEntryPointsValue)) 444 .build()))); 445 } 446 getReplacedDependencies( AnnotationValue replacedDependenciesValue, Elements elements)447 private static ImmutableSet<TypeElement> getReplacedDependencies( 448 AnnotationValue replacedDependenciesValue, Elements elements) { 449 // Allow null values to support libraries using a Hilt version before @TestInstallIn was added 450 return replacedDependenciesValue == null 451 ? ImmutableSet.of() 452 : AnnotationValues.getAnnotationValues(replacedDependenciesValue).stream() 453 .map(AnnotationValues::getString) 454 .map(elements::getTypeElement) 455 .map(replacedDep -> getPublicDependency(replacedDep, elements)) 456 .collect(toImmutableSet()); 457 } 458 } 459 } 460