blob: a8e57dc9daaea7fe4e1adc8b19dc0b096f15ec87 [file] [log] [blame]
/*
* Copyright (C) 2020 The Dagger Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dagger.hilt.android.processor.internal.viewmodel
import com.google.auto.common.MoreTypes.asElement
import com.google.auto.service.AutoService
import com.google.common.graph.EndpointPair
import com.google.common.graph.ImmutableNetwork
import com.squareup.javapoet.ClassName
import dagger.hilt.android.processor.internal.AndroidClassNames
import dagger.hilt.processor.internal.Processors.hasAnnotation
import dagger.model.Binding
import dagger.model.BindingGraph
import dagger.model.BindingGraph.Edge
import dagger.model.BindingGraph.Node
import dagger.model.BindingKind
import dagger.spi.BindingGraphPlugin
import dagger.spi.DiagnosticReporter
import javax.tools.Diagnostic.Kind
/**
* Plugin to validate users do not inject @HiltViewModel classes.
*/
@AutoService(BindingGraphPlugin::class)
class ViewModelValidationPlugin : BindingGraphPlugin {
override fun visitGraph(bindingGraph: BindingGraph, diagnosticReporter: DiagnosticReporter) {
if (bindingGraph.rootComponentNode().isSubcomponent()) {
// This check does not work with partial graphs since it needs to take into account the source
// component.
return
}
val network: ImmutableNetwork<Node, Edge> = bindingGraph.network()
bindingGraph.dependencyEdges().forEach { edge ->
val pair: EndpointPair<Node> = network.incidentNodes(edge)
val target: Node = pair.target()
val source: Node = pair.source()
if (target is Binding &&
isHiltViewModelBinding(target) &&
!isInternalHiltViewModelUsage(source)
) {
diagnosticReporter.reportDependency(
Kind.ERROR,
edge,
"\nInjection of an @HiltViewModel class is prohibited since it does not create a " +
"ViewModel instance correctly.\nAccess the ViewModel via the Android APIs " +
"(e.g. ViewModelProvider) instead." +
"\nInjected ViewModel: ${target.key().type()}\n"
)
}
}
}
private fun isHiltViewModelBinding(target: Binding): Boolean {
// Make sure this is from an @Inject constructor rather than an overridden binding like an
// @Provides and that the class is annotated with @HiltViewModel.
return target.kind() == BindingKind.INJECTION &&
hasAnnotation(asElement(target.key().type()), AndroidClassNames.HILT_VIEW_MODEL)
}
private fun isInternalHiltViewModelUsage(source: Node): Boolean {
// We expect @HiltViewModel classes to be bound into a map with an @Binds like
// @Binds
// @IntoMap
// @StringKey(...)
// @HiltViewModelMap
// abstract ViewModel bindViewModel(FooViewModel vm)
//
// So we check that it is a multibinding contribution with the internal qualifier.
// TODO(erichang): Should we check for even more things?
return source is Binding &&
source.key().qualifier().isPresent() &&
ClassName.get(source.key().qualifier().get().getAnnotationType()) ==
AndroidClassNames.HILT_VIEW_MODEL_MAP_QUALIFIER &&
source.key().multibindingContributionIdentifier().isPresent()
}
}