Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ private constructor(val viewModelElement: XTypeElement, val assistedFactory: XTy
.getAsType(ASSISTED_FACTORY_VALUE)
val assistedFactory = assistedFactoryType.typeElement!!

Processors.checkNotErrorType(assistedFactoryType)

if (assistedFactoryType.asTypeName() != XTypeName.ANY_OBJECT) {
ProcessorErrors.checkState(
isAssistedInjectFeatureEnabled,
Expand Down
2 changes: 0 additions & 2 deletions hilt-compiler/main/java/dagger/hilt/processor/internal/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@ java_library(
"//third_party/java/guava/base",
"//third_party/java/guava/collect",
"//third_party/java/javapoet",
"//third_party/kotlin/kotlin:kotlin_stdlib",
"//third_party/kotlin/kotlin_symbol_processing:symbol-processing-api",
],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,5 +502,11 @@ public static boolean hasJavaPackagePrivateVisibility(XHasModifiers element) {
&& !element.isPublic();
}

public static void checkNotErrorType(XType type) {
if (type.isError()) {
throw new ErrorTypeException("bad type", type.getTypeElement());
}
}

private Processors() {}
}
12 changes: 12 additions & 0 deletions javatests/dagger/hilt/android/hiltviewmodel/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dagger.hilt.android.hiltviewmodel">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31"/>
<application
android:name="dagger.hilt.android.testing.HiltTestApplication"
android:theme="@style/Theme.AppCompat.Light">
<activity
android:name="dagger.hilt.android.hiltviewmodel.KspGeneratedAssistedFactoryTest$TestActivity"
android:exported="false" />
</application>
</manifest>
63 changes: 63 additions & 0 deletions javatests/dagger/hilt/android/hiltviewmodel/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Copyright (C) 2026 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.

load("@rules_android//rules:rules.bzl", "android_local_test")
load("//third_party/kotlin/build_extensions:rules.bzl", "kt_android_library")

package(default_visibility = ["//:src"])

kt_android_library(
name = "KspGeneratedAssistedFactoryTest",
testonly = True,
srcs = ["KspGeneratedAssistedFactoryTest.kt"],
javacopts = [
"-Adagger.hilt.enableAssistedInjectViewModels=true",
"-Adagger.useKspInFunctionalTests=enabled",
],
plugins = [
"//javatests/dagger/hilt/android/hiltviewmodel/processor:plugin",
],
deps = [
"//hilt-android-testing/main/java/dagger/hilt/android/testing:hilt_android_rule",
"//hilt-android-testing/main/java/dagger/hilt/android/testing:hilt_android_test",
"//hilt-android/main/java/dagger/hilt/android/lifecycle:hilt_view_model",
"//hilt-android/main/java/dagger/hilt/android/lifecycle:hilt_view_model_extensions",
"//javatests/dagger/hilt/android/hiltviewmodel/processor:annotation",
"//third_party/android/androidx_test/core",
"//third_party/android/androidx_test/ext/junit",
"//third_party/java/androidx/activity",
"//third_party/java/androidx/appcompat",
"//third_party/java/androidx/fragment",
"//third_party/java/androidx/lifecycle/viewmodel",
"//third_party/java/androidx/lifecycle/viewmodel_savedstate",
"//third_party/java/dagger",
"//third_party/java/dagger/hilt:android_entry_point",
"//third_party/java/dagger/hilt:view_model",
"//third_party/java/jsr305_annotations",
"//third_party/java/jsr330_inject",
"//third_party/java/junit",
"//third_party/java/robolectric",
"//third_party/java/truth",
],
)

android_local_test(
name = "KspGeneratedAssistedFactoryTest_android_local_test",
manifest = "AndroidManifest.xml",
test_class = "dagger.hilt.android.hiltviewmodel.KspGeneratedAssistedFactoryTest",
deps = [
":KspGeneratedAssistedFactoryTest",
"//hilt-android/main/java/dagger/hilt/android/lifecycle:hilt_view_model_extensions",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright (C) 2026 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.hiltviewmodel

import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import androidx.activity.viewModels
import androidx.lifecycle.ViewModel
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.hiltviewmodel.processor.GenerateAssistedFactory
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.lifecycle.withCreationCallback
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
import javax.annotation.Nullable
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
@Config(application = HiltTestApplication::class)
class KspGeneratedAssistedFactoryTest {
@get:Rule val rule = HiltAndroidRule(this)

@GenerateAssistedFactory
@HiltViewModel(assistedFactory = MyViewModelFactory::class)
class MyViewModel @AssistedInject constructor(@Assisted val id: String) : ViewModel() {}

@AndroidEntryPoint(FragmentActivity::class)
class TestActivity : Hilt_KspGeneratedAssistedFactoryTest_TestActivity() {
val vm: MyViewModel by
viewModels(
extrasProducer = {
this.defaultViewModelCreationExtras.withCreationCallback { factory: MyViewModelFactory ->
factory.create("test")
}
}
)

override fun onCreate(@Nullable savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}

@Test
fun hiltViewModelWithGeneratedAssistedFactory_compilesSuccessfully() {
rule.inject()
ActivityScenario.launch(TestActivity::class.java).use { scenario ->
scenario.onActivity { activity -> assertThat(activity.vm.id).isEqualTo("test") }
}
}
}
52 changes: 52 additions & 0 deletions javatests/dagger/hilt/android/hiltviewmodel/processor/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright (C) 2026 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.

load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_codegen_plugin", "kt_codegen_processor", "kt_codegen_processor_type", "kt_jvm_library")

package(default_visibility = ["//:src"])

kt_jvm_library(
name = "annotation",
srcs = ["GenerateAssistedFactory.kt"],
)

kt_jvm_library(
name = "processor_lib",
srcs = [
"FactoryGeneratorProcessor.kt",
"FactoryGeneratorProcessorProvider.kt",
],
deps = [
"//third_party/java/auto:service",
"//third_party/kotlin/kotlin_symbol_processing:symbol-processing-api",
],
)

kt_codegen_processor(
name = "processor",
generates_api = 1,
output_intents = [
"kotlin",
],
processor_class = "dagger.hilt.android.hiltviewmodel.processor.FactoryGeneratorProcessorProvider",
processor_type = kt_codegen_processor_type.KSP,
)

kt_codegen_plugin(
name = "plugin",
processors = [":processor"],
deps = [
":processor_lib",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright (C) 2026 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.hiltviewmodel.processor

import com.google.devtools.ksp.processing.Dependencies
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration

/**
* Minimal KSP processor that generates an @AssistedFactory interface for each ViewModel annotated
* with @GenerateAssistedFactory.
*
* For a ViewModel like:
*
* @GenerateAssistedFactory
* @HiltViewModel(assistedFactory = MyViewModelFactory::class) class MyViewModel @AssistedInject
* constructor(
* @Assisted val id: String ) : ViewModel()
*
* It generates:
*
* @AssistedFactory interface MyViewModelFactory { fun create(id: String): MyViewModel }
*/
class FactoryGeneratorProcessor(private val environment: SymbolProcessorEnvironment) :
SymbolProcessor {

private val generated = mutableSetOf<String>()

override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols =
resolver
.getSymbolsWithAnnotation(
"dagger.hilt.android.hiltviewmodel.processor.GenerateAssistedFactory"
)
.filterIsInstance<KSClassDeclaration>()
.toList()

for (viewModel in symbols) {
val qn = viewModel.qualifiedName!!.asString()
if (qn in generated) continue

val source = viewModel.containingFile!!
val pkg = viewModel.packageName.asString()
val simpleName = viewModel.simpleName.asString()
val relativeClassName = if (qn.startsWith("$pkg.")) qn.substring(pkg.length + 1) else qn

val assistedParams =
viewModel.primaryConstructor?.parameters?.filter { param ->
param.annotations.any { it.shortName.asString() == "Assisted" }
} ?: emptyList()

val paramsText =
assistedParams.joinToString(", ") { param ->
val name = param.name!!.asString()
val type = param.type.resolve().declaration.simpleName.asString()
"$name: $type"
}

val file =
environment.codeGenerator.createNewFile(
Dependencies(aggregating = false, sources = arrayOf(source)),
pkg,
"${simpleName}Factory",
"kt",
)

file.use {
it.write(
"""
package $pkg

import dagger.assisted.AssistedFactory

@AssistedFactory
interface ${simpleName}Factory {
fun create($paramsText): $relativeClassName
}
"""
.trimIndent()
.toByteArray()
)
}

generated += qn
environment.logger.warn("[factory-generator] Generated ${simpleName}Factory")
}

return emptyList()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2026 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.hiltviewmodel.processor

import com.google.auto.service.AutoService
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider

@AutoService(SymbolProcessorProvider::class)
class FactoryGeneratorProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor =
FactoryGeneratorProcessor(environment)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright (C) 2026 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.hiltviewmodel.processor

@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.CLASS)
annotation class GenerateAssistedFactory
Loading