1 /* 2 * Copyright (C) 2018 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.bindinggraphvalidation; 18 19 import static dagger.internal.codegen.base.Formatter.INDENT; 20 import static dagger.internal.codegen.base.Scopes.getReadableSource; 21 import static dagger.internal.codegen.xprocessing.XElements.asExecutable; 22 import static dagger.internal.codegen.xprocessing.XElements.closestEnclosingTypeElement; 23 import static dagger.spi.model.BindingKind.INJECTION; 24 import static java.util.stream.Collectors.joining; 25 import static javax.tools.Diagnostic.Kind.ERROR; 26 27 import com.google.common.collect.ImmutableSetMultimap; 28 import com.google.common.collect.Multimaps; 29 import dagger.internal.codegen.base.Scopes; 30 import dagger.internal.codegen.binding.MethodSignatureFormatter; 31 import dagger.internal.codegen.compileroption.CompilerOptions; 32 import dagger.internal.codegen.validation.DiagnosticMessageGenerator; 33 import dagger.spi.model.Binding; 34 import dagger.spi.model.BindingGraph; 35 import dagger.spi.model.BindingGraph.ComponentNode; 36 import dagger.spi.model.BindingGraphPlugin; 37 import dagger.spi.model.DiagnosticReporter; 38 import java.util.Optional; 39 import java.util.Set; 40 import javax.inject.Inject; 41 import javax.tools.Diagnostic; 42 43 /** 44 * Reports an error for any component that uses bindings with scopes that are not assigned to the 45 * component. 46 */ 47 final class IncompatiblyScopedBindingsValidator implements BindingGraphPlugin { 48 49 private final MethodSignatureFormatter methodSignatureFormatter; 50 private final CompilerOptions compilerOptions; 51 private final DiagnosticMessageGenerator.Factory diagnosticMessageGeneratorFactory; 52 53 @Inject IncompatiblyScopedBindingsValidator( MethodSignatureFormatter methodSignatureFormatter, CompilerOptions compilerOptions, DiagnosticMessageGenerator.Factory diagnosticMessageGeneratorFactory)54 IncompatiblyScopedBindingsValidator( 55 MethodSignatureFormatter methodSignatureFormatter, 56 CompilerOptions compilerOptions, 57 DiagnosticMessageGenerator.Factory diagnosticMessageGeneratorFactory) { 58 this.methodSignatureFormatter = methodSignatureFormatter; 59 this.compilerOptions = compilerOptions; 60 this.diagnosticMessageGeneratorFactory = diagnosticMessageGeneratorFactory; 61 } 62 63 @Override pluginName()64 public String pluginName() { 65 return "Dagger/IncompatiblyScopedBindings"; 66 } 67 68 @Override visitGraph(BindingGraph bindingGraph, DiagnosticReporter diagnosticReporter)69 public void visitGraph(BindingGraph bindingGraph, DiagnosticReporter diagnosticReporter) { 70 DiagnosticMessageGenerator diagnosticMessageGenerator = 71 diagnosticMessageGeneratorFactory.create(bindingGraph); 72 ImmutableSetMultimap.Builder<ComponentNode, dagger.spi.model.Binding> incompatibleBindings = 73 ImmutableSetMultimap.builder(); 74 for (dagger.spi.model.Binding binding : bindingGraph.bindings()) { 75 binding 76 .scope() 77 .filter(scope -> !scope.isReusable()) 78 .ifPresent( 79 scope -> { 80 ComponentNode componentNode = 81 bindingGraph.componentNode(binding.componentPath()).get(); 82 if (!componentNode.scopes().contains(scope)) { 83 // @Inject bindings in module or subcomponent binding graphs will appear at the 84 // properly scoped ancestor component, so ignore them here. 85 if (binding.kind().equals(INJECTION) 86 && (bindingGraph.rootComponentNode().isSubcomponent() 87 || !bindingGraph.rootComponentNode().isRealComponent())) { 88 return; 89 } 90 incompatibleBindings.put(componentNode, binding); 91 } 92 }); 93 } 94 Multimaps.asMap(incompatibleBindings.build()) 95 .forEach((componentNode, bindings) -> 96 report(componentNode, bindings, diagnosticReporter, diagnosticMessageGenerator)); 97 } 98 report( ComponentNode componentNode, Set<Binding> bindings, DiagnosticReporter diagnosticReporter, DiagnosticMessageGenerator diagnosticMessageGenerator)99 private void report( 100 ComponentNode componentNode, 101 Set<Binding> bindings, 102 DiagnosticReporter diagnosticReporter, 103 DiagnosticMessageGenerator diagnosticMessageGenerator) { 104 Diagnostic.Kind diagnosticKind = ERROR; 105 StringBuilder message = 106 new StringBuilder( 107 componentNode.componentPath().currentComponent().className().canonicalName()); 108 109 if (!componentNode.isRealComponent()) { 110 // If the "component" is really a module, it will have no scopes attached. We want to report 111 // if there is more than one scope in that component. 112 if (bindings.stream().map(Binding::scope).map(Optional::get).distinct().count() <= 1) { 113 return; 114 } 115 message.append(" contains bindings with different scopes:"); 116 diagnosticKind = compilerOptions.moduleHasDifferentScopesDiagnosticKind(); 117 } else if (componentNode.scopes().isEmpty()) { 118 message.append(" (unscoped) may not reference scoped bindings:"); 119 } else { 120 message 121 .append(" scoped with ") 122 .append( 123 componentNode.scopes().stream().map(Scopes::getReadableSource).collect(joining(" "))) 124 .append(" may not reference bindings with different scopes:"); 125 } 126 127 // TODO(ronshapiro): Should we group by scope? 128 for (Binding binding : bindings) { 129 message.append('\n').append(INDENT); 130 131 // TODO(dpb): Use BindingDeclarationFormatter. 132 // But that doesn't print scopes for @Inject-constructed types. 133 switch (binding.kind()) { 134 case DELEGATE: 135 case PROVISION: 136 message.append( 137 methodSignatureFormatter.format( 138 asExecutable(binding.bindingElement().get().xprocessing()))); 139 break; 140 141 case INJECTION: 142 message 143 .append(getReadableSource(binding.scope().get())) 144 .append(" class ") 145 .append( 146 closestEnclosingTypeElement(binding.bindingElement().get().xprocessing()) 147 .getQualifiedName()) 148 .append(diagnosticMessageGenerator.getMessage(binding)); 149 150 break; 151 152 default: 153 throw new AssertionError(binding); 154 } 155 156 message.append('\n'); 157 } 158 diagnosticReporter.reportComponent(diagnosticKind, componentNode, message.toString()); 159 } 160 } 161