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