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