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
52 override fun init(processingEnv: DaggerProcessingEnv, options: MutableMap<String, String>) {
53 env = processingEnv.toXProcessingEnv()
54 }
55
56 override fun visitGraph(bindingGraph: BindingGraph, diagnosticReporter: DiagnosticReporter) {
57 if (bindingGraph.rootComponentNode().isSubcomponent()) {
58 // This check does not work with partial graphs since it needs to take into account the source
59 // component.
60 return
61 }
62
63 val network: ImmutableNetwork<Node, Edge> = bindingGraph.network()
64 bindingGraph.dependencyEdges().forEach { edge ->
65 val pair: EndpointPair<Node> = network.incidentNodes(edge)
66 val target: Node = pair.target()
67 val source: Node = pair.source()
68 if (target !is Binding) {
69 return@forEach
70 }
71 if (isHiltViewModelBinding(target) && !isInternalHiltViewModelUsage(source)) {
72 diagnosticReporter.reportDependency(
73 Kind.ERROR,
74 edge,
75 "\nInjection of an @HiltViewModel class is prohibited since it does not create a " +
76 "ViewModel instance correctly.\nAccess the ViewModel via the Android APIs " +
77 "(e.g. ViewModelProvider) instead." +
78 "\nInjected ViewModel: ${target.key().type()}\n"
79 )
80 } else if (
81 isViewModelAssistedFactory(target) && !isInternalViewModelAssistedFactoryUsage(source)
82 ) {
83 diagnosticReporter.reportDependency(
84 Kind.ERROR,
85 edge,
86 "\nInjection of an assisted factory for Hilt ViewModel is prohibited since it " +
87 "can not be used to create a ViewModel instance correctly.\nAccess the ViewModel via " +
88 "the Android APIs (e.g. ViewModelProvider) instead." +
89 "\nInjected factory: ${target.key().type()}\n"
90 )
91 }
92 }
93 }
94
95 private fun isHiltViewModelBinding(target: Binding): Boolean {
96 // Make sure this is from an @Inject constructor rather than an overridden binding like an
97 // @Provides and that the class is annotated with @HiltViewModel.
98 return target.kind() == BindingKind.INJECTION &&
99 target.key().type().hasAnnotation(AndroidClassNames.HILT_VIEW_MODEL)
100 }
101
102 private fun isInternalHiltViewModelUsage(source: Node): Boolean {
103 // We expect @HiltViewModel classes to be bound into a map with an @Binds like
104 // @Binds
105 // @IntoMap
106 // @StringKey(...)
107 // @HiltViewModelMap
108 // abstract ViewModel bindViewModel(FooViewModel vm)
109 //
110 // So we check that it is a multibinding contribution with the internal qualifier.
111 // TODO(erichang): Should we check for even more things?
112 return source is Binding &&
113 source.key().qualifier().isPresent() &&
114 source.key().qualifier().get().getQualifiedName() ==
115 AndroidClassNames.HILT_VIEW_MODEL_MAP_QUALIFIER.canonicalName() &&
116 source.key().multibindingContributionIdentifier().isPresent()
117 }
118
119 private fun isViewModelAssistedFactory(target: Binding): Boolean {
120 if (target.kind() != BindingKind.ASSISTED_FACTORY) return false
121 val factoryType = target.key().type()
122 return getAssistedInjectTypeElement(factoryType.toXType(env))
123 .hasAnnotation(AndroidClassNames.HILT_VIEW_MODEL)
124 }
125
126 private fun getAssistedInjectTypeElement(factoryType: XType): XTypeElement =
127 // The factory method and the type element for its return type cannot be
128 // null as the BindingGraph won't be created if the
129 // @AssistedFactory-annotated class is invalid.
130 getAssistedFactoryMethods(factoryType.typeElement)
131 .single()
132 .asMemberOf(factoryType)
133 .returnType
134 .typeElement!!
135
136 private fun getAssistedFactoryMethods(factory: XTypeElement?): List<XMethodElement> {
137 return XTypeElements.getAllNonPrivateInstanceMethods(factory)
138 .filter { it.isAbstract() }
139 .filter { !it.isJavaDefault() }
140 }
141
142 private fun isInternalViewModelAssistedFactoryUsage(source: Node): Boolean {
143 // We expect the only usage of the assisted factory for a Hilt ViewModel is in the
144 // code we generate:
145 // @Binds
146 // @IntoMap
147 // @StringKey(...)
148 // @HiltViewModelAssistedMap
149 // public abstract Object bind(FooFactory factory);
150 return source is Binding &&
151 source.key().qualifier().isPresent() &&
152 source.key().qualifier().get().getQualifiedName() ==
153 AndroidClassNames.HILT_VIEW_MODEL_ASSISTED_FACTORY_MAP_QUALIFIER.canonicalName() &&
154 source.key().multibindingContributionIdentifier().isPresent()
155 }
156 }
157
DaggerTypenull158 private fun DaggerType.toXType(processingEnv: XProcessingEnv): XType {
159 return when (backend()) {
160 DaggerProcessingEnv.Backend.JAVAC -> javac().toXProcessing(processingEnv)
161 DaggerProcessingEnv.Backend.KSP -> ksp().toXProcessing(processingEnv)
162 else -> error("Backend ${ backend() } not supported yet.")
163 }
164 }
165
DaggerProcessingEnvnull166 private fun DaggerProcessingEnv.toXProcessingEnv(): XProcessingEnv {
167 return when (backend()) {
168 DaggerProcessingEnv.Backend.JAVAC -> create(javac())
169 DaggerProcessingEnv.Backend.KSP -> create(ksp(), resolver())
170 else -> error("Backend ${ backend() } not supported yet.")
171 }
172 }
173