1 /* 2 * Copyright (C) 2015 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.internal.codegen.validation; 18 19 import static com.google.common.base.Functions.constant; 20 import static com.google.common.base.Predicates.and; 21 import static com.google.common.base.Predicates.in; 22 import static com.google.common.base.Predicates.not; 23 import static dagger.internal.codegen.base.Scopes.getReadableSource; 24 import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet; 25 import static dagger.internal.codegen.xprocessing.XElements.getSimpleName; 26 27 import androidx.room.compiler.processing.XExecutableParameterElement; 28 import androidx.room.compiler.processing.XTypeElement; 29 import com.google.common.base.Joiner; 30 import com.google.common.base.Predicate; 31 import com.google.common.collect.ImmutableMap; 32 import com.google.common.collect.ImmutableSet; 33 import com.google.common.collect.ImmutableSetMultimap; 34 import com.google.common.collect.Iterables; 35 import com.google.common.collect.LinkedHashMultimap; 36 import com.google.common.collect.Maps; 37 import com.google.common.collect.Multimaps; 38 import com.google.common.collect.SetMultimap; 39 import com.google.common.collect.Sets; 40 import dagger.internal.codegen.base.ModuleKind; 41 import dagger.internal.codegen.binding.ComponentDescriptor; 42 import dagger.internal.codegen.binding.ComponentDescriptor.ComponentMethodDescriptor; 43 import dagger.internal.codegen.binding.InjectionAnnotations; 44 import dagger.internal.codegen.binding.ModuleDescriptor; 45 import dagger.internal.codegen.compileroption.CompilerOptions; 46 import dagger.internal.codegen.model.Scope; 47 import java.util.Collection; 48 import java.util.Formatter; 49 import java.util.Map; 50 import java.util.Optional; 51 import javax.inject.Inject; 52 53 /** Validates the relationships between parent components and subcomponents. */ 54 final class ComponentHierarchyValidator { 55 private static final Joiner COMMA_SEPARATED_JOINER = Joiner.on(", "); 56 57 private final CompilerOptions compilerOptions; 58 private final InjectionAnnotations injectionAnnotations; 59 60 @Inject ComponentHierarchyValidator( CompilerOptions compilerOptions, InjectionAnnotations injectionAnnotations)61 ComponentHierarchyValidator( 62 CompilerOptions compilerOptions, InjectionAnnotations injectionAnnotations) { 63 this.compilerOptions = compilerOptions; 64 this.injectionAnnotations = injectionAnnotations; 65 } 66 validate(ComponentDescriptor componentDescriptor)67 ValidationReport validate(ComponentDescriptor componentDescriptor) { 68 ValidationReport.Builder report = ValidationReport.about(componentDescriptor.typeElement()); 69 validateSubcomponentMethods( 70 report, 71 componentDescriptor, 72 Maps.toMap(componentDescriptor.moduleTypes(), constant(componentDescriptor.typeElement()))); 73 validateRepeatedScopedDeclarations(report, componentDescriptor, LinkedHashMultimap.create()); 74 75 if (compilerOptions.scopeCycleValidationType().diagnosticKind().isPresent()) { 76 validateScopeHierarchy( 77 report, componentDescriptor, LinkedHashMultimap.<ComponentDescriptor, Scope>create()); 78 } 79 validateProductionModuleUniqueness(report, componentDescriptor, LinkedHashMultimap.create()); 80 return report.build(); 81 } 82 validateSubcomponentMethods( ValidationReport.Builder report, ComponentDescriptor componentDescriptor, ImmutableMap<XTypeElement, XTypeElement> existingModuleToOwners)83 private void validateSubcomponentMethods( 84 ValidationReport.Builder report, 85 ComponentDescriptor componentDescriptor, 86 ImmutableMap<XTypeElement, XTypeElement> existingModuleToOwners) { 87 componentDescriptor 88 .childComponentsDeclaredByFactoryMethods() 89 .forEach( 90 (method, childComponent) -> { 91 if (childComponent.hasCreator()) { 92 report.addError( 93 "Components may not have factory methods for subcomponents that define a " 94 + "builder.", 95 method.methodElement()); 96 } else { 97 validateFactoryMethodParameters(report, method, existingModuleToOwners); 98 } 99 100 validateSubcomponentMethods( 101 report, 102 childComponent, 103 new ImmutableMap.Builder<XTypeElement, XTypeElement>() 104 .putAll(existingModuleToOwners) 105 .putAll( 106 Maps.toMap( 107 Sets.difference( 108 childComponent.moduleTypes(), existingModuleToOwners.keySet()), 109 constant(childComponent.typeElement()))) 110 .build()); 111 }); 112 } 113 validateFactoryMethodParameters( ValidationReport.Builder report, ComponentMethodDescriptor subcomponentMethodDescriptor, ImmutableMap<XTypeElement, XTypeElement> existingModuleToOwners)114 private void validateFactoryMethodParameters( 115 ValidationReport.Builder report, 116 ComponentMethodDescriptor subcomponentMethodDescriptor, 117 ImmutableMap<XTypeElement, XTypeElement> existingModuleToOwners) { 118 for (XExecutableParameterElement factoryMethodParameter : 119 subcomponentMethodDescriptor.methodElement().getParameters()) { 120 XTypeElement moduleType = factoryMethodParameter.getType().getTypeElement(); 121 if (existingModuleToOwners.containsKey(moduleType)) { 122 /* Factory method tries to pass a module that is already present in the parent. 123 * This is an error. */ 124 report.addError( 125 String.format( 126 "%s is present in %s. A subcomponent cannot use an instance of a " 127 + "module that differs from its parent.", 128 getSimpleName(moduleType), 129 existingModuleToOwners.get(moduleType).getQualifiedName()), 130 factoryMethodParameter); 131 } 132 } 133 } 134 135 /** 136 * Checks that components do not have any scopes that are also applied on any of their ancestors. 137 */ validateScopeHierarchy( ValidationReport.Builder report, ComponentDescriptor subject, SetMultimap<ComponentDescriptor, Scope> scopesByComponent)138 private void validateScopeHierarchy( 139 ValidationReport.Builder report, 140 ComponentDescriptor subject, 141 SetMultimap<ComponentDescriptor, Scope> scopesByComponent) { 142 scopesByComponent.putAll(subject, subject.scopes()); 143 144 for (ComponentDescriptor childComponent : subject.childComponents()) { 145 validateScopeHierarchy(report, childComponent, scopesByComponent); 146 } 147 148 scopesByComponent.removeAll(subject); 149 150 Predicate<Scope> subjectScopes = 151 subject.isProduction() 152 // TODO(beder): validate that @ProductionScope is only applied on production components 153 ? and(in(subject.scopes()), not(Scope::isProductionScope)) 154 : in(subject.scopes()); 155 SetMultimap<ComponentDescriptor, Scope> overlappingScopes = 156 Multimaps.filterValues(scopesByComponent, subjectScopes); 157 if (!overlappingScopes.isEmpty()) { 158 StringBuilder error = 159 new StringBuilder() 160 .append(subject.typeElement().getQualifiedName()) 161 .append(" has conflicting scopes:"); 162 for (Map.Entry<ComponentDescriptor, Scope> entry : overlappingScopes.entries()) { 163 Scope scope = entry.getValue(); 164 error 165 .append("\n ") 166 .append(entry.getKey().typeElement().getQualifiedName()) 167 .append(" also has ") 168 .append(getReadableSource(scope)); 169 } 170 report.addItem( 171 error.toString(), 172 compilerOptions.scopeCycleValidationType().diagnosticKind().get(), 173 subject.typeElement()); 174 } 175 } 176 validateProductionModuleUniqueness( ValidationReport.Builder report, ComponentDescriptor componentDescriptor, SetMultimap<ComponentDescriptor, ModuleDescriptor> producerModulesByComponent)177 private void validateProductionModuleUniqueness( 178 ValidationReport.Builder report, 179 ComponentDescriptor componentDescriptor, 180 SetMultimap<ComponentDescriptor, ModuleDescriptor> producerModulesByComponent) { 181 ImmutableSet<ModuleDescriptor> producerModules = 182 componentDescriptor.modules().stream() 183 .filter(module -> module.kind().equals(ModuleKind.PRODUCER_MODULE)) 184 .collect(toImmutableSet()); 185 186 producerModulesByComponent.putAll(componentDescriptor, producerModules); 187 for (ComponentDescriptor childComponent : componentDescriptor.childComponents()) { 188 validateProductionModuleUniqueness(report, childComponent, producerModulesByComponent); 189 } 190 producerModulesByComponent.removeAll(componentDescriptor); 191 192 193 SetMultimap<ComponentDescriptor, ModuleDescriptor> repeatedModules = 194 Multimaps.filterValues(producerModulesByComponent, producerModules::contains); 195 if (repeatedModules.isEmpty()) { 196 return; 197 } 198 199 StringBuilder error = new StringBuilder(); 200 Formatter formatter = new Formatter(error); 201 202 formatter.format( 203 "%s repeats @ProducerModules:", componentDescriptor.typeElement().getQualifiedName()); 204 205 for (Map.Entry<ComponentDescriptor, Collection<ModuleDescriptor>> entry : 206 repeatedModules.asMap().entrySet()) { 207 formatter.format("\n %s also installs: ", entry.getKey().typeElement().getQualifiedName()); 208 COMMA_SEPARATED_JOINER 209 .appendTo( 210 error, 211 Iterables.transform(entry.getValue(), m -> m.moduleElement().getQualifiedName())); 212 } 213 214 report.addError(error.toString()); 215 } 216 validateRepeatedScopedDeclarations( ValidationReport.Builder report, ComponentDescriptor component, SetMultimap<ComponentDescriptor, ModuleDescriptor> modulesWithScopes)217 private void validateRepeatedScopedDeclarations( 218 ValidationReport.Builder report, 219 ComponentDescriptor component, 220 // TODO(ronshapiro): optimize ModuleDescriptor.hashCode()/equals. Otherwise this could be 221 // quite costly 222 SetMultimap<ComponentDescriptor, ModuleDescriptor> modulesWithScopes) { 223 ImmutableSet<ModuleDescriptor> modules = 224 component.modules().stream().filter(this::hasScopedDeclarations).collect(toImmutableSet()); 225 modulesWithScopes.putAll(component, modules); 226 for (ComponentDescriptor childComponent : component.childComponents()) { 227 validateRepeatedScopedDeclarations(report, childComponent, modulesWithScopes); 228 } 229 modulesWithScopes.removeAll(component); 230 231 SetMultimap<ComponentDescriptor, ModuleDescriptor> repeatedModules = 232 Multimaps.filterValues(modulesWithScopes, modules::contains); 233 if (repeatedModules.isEmpty()) { 234 return; 235 } 236 237 report.addError( 238 repeatedModulesWithScopeError(component, ImmutableSetMultimap.copyOf(repeatedModules))); 239 } 240 hasScopedDeclarations(ModuleDescriptor module)241 private boolean hasScopedDeclarations(ModuleDescriptor module) { 242 return !moduleScopes(module).isEmpty(); 243 } 244 repeatedModulesWithScopeError( ComponentDescriptor component, ImmutableSetMultimap<ComponentDescriptor, ModuleDescriptor> repeatedModules)245 private String repeatedModulesWithScopeError( 246 ComponentDescriptor component, 247 ImmutableSetMultimap<ComponentDescriptor, ModuleDescriptor> repeatedModules) { 248 StringBuilder error = 249 new StringBuilder() 250 .append(component.typeElement().getQualifiedName()) 251 .append(" repeats modules with scoped bindings or declarations:"); 252 253 repeatedModules 254 .asMap() 255 .forEach( 256 (conflictingComponent, conflictingModules) -> { 257 error 258 .append("\n - ") 259 .append(conflictingComponent.typeElement().getQualifiedName()) 260 .append(" also includes:"); 261 for (ModuleDescriptor conflictingModule : conflictingModules) { 262 error 263 .append("\n - ") 264 .append(conflictingModule.moduleElement().getQualifiedName()) 265 .append(" with scopes: ") 266 .append(COMMA_SEPARATED_JOINER.join(moduleScopes(conflictingModule))); 267 } 268 }); 269 return error.toString(); 270 } 271 moduleScopes(ModuleDescriptor module)272 private ImmutableSet<Scope> moduleScopes(ModuleDescriptor module) { 273 return module.allBindingDeclarations().stream() 274 .map(declaration -> injectionAnnotations.getScope(declaration.bindingElement().get())) 275 .filter(scope -> scope.isPresent() && !scope.get().isReusable()) 276 .map(Optional::get) 277 .collect(toImmutableSet()); 278 } 279 } 280