• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 @file:OptIn(ExperimentalProcessingApi::class)
18 
19 package dagger.hilt.android.processor.internal.viewmodel
20 
21 import androidx.room.compiler.processing.ExperimentalProcessingApi
22 import androidx.room.compiler.processing.XMethodElement
23 import androidx.room.compiler.processing.XProcessingEnv
24 import androidx.room.compiler.processing.XProcessingEnv.Companion.create
25 import androidx.room.compiler.processing.XType
26 import androidx.room.compiler.processing.XTypeElement
27 import androidx.room.compiler.processing.compat.XConverters.toXProcessing
28 import com.google.auto.service.AutoService
29 import com.google.common.graph.EndpointPair
30 import com.google.common.graph.ImmutableNetwork
31 import dagger.hilt.android.processor.internal.AndroidClassNames
32 import dagger.hilt.processor.internal.getQualifiedName
33 import dagger.hilt.processor.internal.hasAnnotation
34 import dagger.internal.codegen.xprocessing.XTypeElements
35 import dagger.spi.model.Binding
36 import dagger.spi.model.BindingGraph
37 import dagger.spi.model.BindingGraph.Edge
38 import dagger.spi.model.BindingGraph.Node
39 import dagger.spi.model.BindingGraphPlugin
40 import dagger.spi.model.BindingKind
41 import dagger.spi.model.DaggerProcessingEnv
42 import dagger.spi.model.DaggerType
43 import dagger.spi.model.DiagnosticReporter
44 import javax.tools.Diagnostic.Kind
45 
46 /** Plugin to validate users do not inject @HiltViewModel classes. */
47 @AutoService(BindingGraphPlugin::class)
48 class ViewModelValidationPlugin : BindingGraphPlugin {
49 
50   private lateinit var env: XProcessingEnv
51   private lateinit var daggerProcessingEnv: DaggerProcessingEnv
52 
53   override fun init(processingEnv: DaggerProcessingEnv, options: MutableMap<String, String>) {
54     daggerProcessingEnv = processingEnv
55   }
56 
57   override fun onProcessingRoundBegin() {
58     env = daggerProcessingEnv.toXProcessingEnv()
59   }
60 
61   override fun visitGraph(bindingGraph: BindingGraph, diagnosticReporter: DiagnosticReporter) {
62     if (bindingGraph.rootComponentNode().isSubcomponent()) {
63       // This check does not work with partial graphs since it needs to take into account the source
64       // component.
65       return
66     }
67 
68     val network: ImmutableNetwork<Node, Edge> = bindingGraph.network()
69     bindingGraph.dependencyEdges().forEach { edge ->
70       val pair: EndpointPair<Node> = network.incidentNodes(edge)
71       val target: Node = pair.target()
72       val source: Node = pair.source()
73       if (target !is Binding) {
74         return@forEach
75       }
76       if (isHiltViewModelBinding(target) && !isInternalHiltViewModelUsage(source)) {
77         diagnosticReporter.reportDependency(
78           Kind.ERROR,
79           edge,
80           "\nInjection of an @HiltViewModel class is prohibited since it does not create a " +
81             "ViewModel instance correctly.\nAccess the ViewModel via the Android APIs " +
82             "(e.g. ViewModelProvider) instead." +
83             "\nInjected ViewModel: ${target.key().type()}\n",
84         )
85       } else if (
86         isViewModelAssistedFactory(target) && !isInternalViewModelAssistedFactoryUsage(source)
87       ) {
88         diagnosticReporter.reportDependency(
89           Kind.ERROR,
90           edge,
91           "\nInjection of an assisted factory for Hilt ViewModel is prohibited since it " +
92             "can not be used to create a ViewModel instance correctly.\nAccess the ViewModel via " +
93             "the Android APIs (e.g. ViewModelProvider) instead." +
94             "\nInjected factory: ${target.key().type()}\n",
95         )
96       }
97     }
98   }
99 
100   private fun isHiltViewModelBinding(target: Binding): Boolean {
101     // Make sure this is from an @Inject constructor rather than an overridden binding like an
102     // @Provides and that the class is annotated with @HiltViewModel.
103     return target.kind() == BindingKind.INJECTION &&
104       target.key().type().hasAnnotation(AndroidClassNames.HILT_VIEW_MODEL)
105   }
106 
107   private fun isInternalHiltViewModelUsage(source: Node): Boolean {
108     // We expect @HiltViewModel classes to be bound into a map with an @Binds like
109     // @Binds
110     // @IntoMap
111     // @StringKey(...)
112     // @HiltViewModelMap
113     // abstract ViewModel bindViewModel(FooViewModel vm)
114     //
115     // So we check that it is a multibinding contribution with the internal qualifier.
116     // TODO(erichang): Should we check for even more things?
117     return source is Binding &&
118       source.key().qualifier().isPresent() &&
119       source.key().qualifier().get().getQualifiedName() ==
120         AndroidClassNames.HILT_VIEW_MODEL_MAP_QUALIFIER.canonicalName() &&
121       source.key().multibindingContributionIdentifier().isPresent()
122   }
123 
124   private fun isViewModelAssistedFactory(target: Binding): Boolean {
125     if (target.kind() != BindingKind.ASSISTED_FACTORY) return false
126     val factoryType = target.key().type()
127     return getAssistedInjectTypeElement(factoryType.toXType(env))
128       .hasAnnotation(AndroidClassNames.HILT_VIEW_MODEL)
129   }
130 
131   private fun getAssistedInjectTypeElement(factoryType: XType): XTypeElement =
132     // The factory method and the type element for its return type cannot be
133     // null as the BindingGraph won't be created if the
134     // @AssistedFactory-annotated class is invalid.
135     getAssistedFactoryMethods(factoryType.typeElement)
136       .single()
137       .asMemberOf(factoryType)
138       .returnType
139       .typeElement!!
140 
141   private fun getAssistedFactoryMethods(factory: XTypeElement?): List<XMethodElement> {
142     return XTypeElements.getAllNonPrivateInstanceMethods(factory)
143       .filter { it.isAbstract() }
144       .filter { !it.isJavaDefault() }
145   }
146 
147   private fun isInternalViewModelAssistedFactoryUsage(source: Node): Boolean {
148     // We expect the only usage of the assisted factory for a Hilt ViewModel is in the
149     // code we generate:
150     // @Binds
151     // @IntoMap
152     // @StringKey(...)
153     // @HiltViewModelAssistedMap
154     // public abstract Object bind(FooFactory factory);
155     return source is Binding &&
156       source.key().qualifier().isPresent() &&
157       source.key().qualifier().get().getQualifiedName() ==
158         AndroidClassNames.HILT_VIEW_MODEL_ASSISTED_FACTORY_MAP_QUALIFIER.canonicalName() &&
159       source.key().multibindingContributionIdentifier().isPresent()
160   }
161 }
162 
DaggerTypenull163 private fun DaggerType.toXType(processingEnv: XProcessingEnv): XType {
164   return when (backend()) {
165     DaggerProcessingEnv.Backend.JAVAC -> javac().toXProcessing(processingEnv)
166     DaggerProcessingEnv.Backend.KSP -> ksp().toXProcessing(processingEnv)
167     else -> error("Backend ${ backend() } not supported yet.")
168   }
169 }
170 
DaggerProcessingEnvnull171 private fun DaggerProcessingEnv.toXProcessingEnv(): XProcessingEnv {
172   return when (backend()) {
173     DaggerProcessingEnv.Backend.JAVAC -> create(javac())
174     DaggerProcessingEnv.Backend.KSP -> create(ksp(), resolver())
175     else -> error("Backend ${ backend() } not supported yet.")
176   }
177 }
178