1 /* <lambda>null2 * Copyright (C) 2020 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.android.processor.internal.viewmodel 18 19 import com.google.auto.common.MoreTypes.asElement 20 import com.google.auto.service.AutoService 21 import com.google.common.graph.EndpointPair 22 import com.google.common.graph.ImmutableNetwork 23 import com.squareup.javapoet.ClassName 24 import dagger.hilt.android.processor.internal.AndroidClassNames 25 import dagger.hilt.processor.internal.Processors.hasAnnotation 26 import dagger.model.Binding 27 import dagger.model.BindingGraph 28 import dagger.model.BindingGraph.Edge 29 import dagger.model.BindingGraph.Node 30 import dagger.model.BindingKind 31 import dagger.spi.BindingGraphPlugin 32 import dagger.spi.DiagnosticReporter 33 import javax.tools.Diagnostic.Kind 34 35 /** 36 * Plugin to validate users do not inject @HiltViewModel classes. 37 */ 38 @AutoService(BindingGraphPlugin::class) 39 class ViewModelValidationPlugin : BindingGraphPlugin { 40 41 override fun visitGraph(bindingGraph: BindingGraph, diagnosticReporter: DiagnosticReporter) { 42 if (bindingGraph.rootComponentNode().isSubcomponent()) { 43 // This check does not work with partial graphs since it needs to take into account the source 44 // component. 45 return 46 } 47 48 val network: ImmutableNetwork<Node, Edge> = bindingGraph.network() 49 bindingGraph.dependencyEdges().forEach { edge -> 50 val pair: EndpointPair<Node> = network.incidentNodes(edge) 51 val target: Node = pair.target() 52 val source: Node = pair.source() 53 if (target is Binding && 54 isHiltViewModelBinding(target) && 55 !isInternalHiltViewModelUsage(source) 56 ) { 57 diagnosticReporter.reportDependency( 58 Kind.ERROR, 59 edge, 60 "\nInjection of an @HiltViewModel class is prohibited since it does not create a " + 61 "ViewModel instance correctly.\nAccess the ViewModel via the Android APIs " + 62 "(e.g. ViewModelProvider) instead." + 63 "\nInjected ViewModel: ${target.key().type()}\n" 64 ) 65 } 66 } 67 } 68 69 private fun isHiltViewModelBinding(target: Binding): Boolean { 70 // Make sure this is from an @Inject constructor rather than an overridden binding like an 71 // @Provides and that the class is annotated with @HiltViewModel. 72 return target.kind() == BindingKind.INJECTION && 73 hasAnnotation(asElement(target.key().type()), AndroidClassNames.HILT_VIEW_MODEL) 74 } 75 76 private fun isInternalHiltViewModelUsage(source: Node): Boolean { 77 // We expect @HiltViewModel classes to be bound into a map with an @Binds like 78 // @Binds 79 // @IntoMap 80 // @StringKey(...) 81 // @HiltViewModelMap 82 // abstract ViewModel bindViewModel(FooViewModel vm) 83 // 84 // So we check that it is a multibinding contribution with the internal qualifier. 85 // TODO(erichang): Should we check for even more things? 86 return source is Binding && 87 source.key().qualifier().isPresent() && 88 ClassName.get(source.key().qualifier().get().getAnnotationType()) == 89 AndroidClassNames.HILT_VIEW_MODEL_MAP_QUALIFIER && 90 source.key().multibindingContributionIdentifier().isPresent() 91 } 92 } 93