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