diff --git a/deeplinkdispatch-processor/src/main/java/com/airbnb/deeplinkdispatch/DeepLinkProcessor.kt b/deeplinkdispatch-processor/src/main/java/com/airbnb/deeplinkdispatch/DeepLinkProcessor.kt index 792df3ee..16f90ab7 100644 --- a/deeplinkdispatch-processor/src/main/java/com/airbnb/deeplinkdispatch/DeepLinkProcessor.kt +++ b/deeplinkdispatch-processor/src/main/java/com/airbnb/deeplinkdispatch/DeepLinkProcessor.kt @@ -344,7 +344,15 @@ class DeepLinkProcessor( private fun validClassElement(classElement: XTypeElement) = classElement.isActivity() || classElement.isHandler() private fun verifyMethod(methodElement: XMethodElement) { - if (!methodElement.isStatic()) { + // XProcessing's isStatic() for KSP relies on annotation type resolution to detect + // @JvmStatic. This resolution can intermittently fail during incremental compilation + // with KSP2's Analysis API, causing isStatic() to return false for methods that are + // actually @JvmStatic inside a Kotlin object. As a workaround, we also accept methods + // whose enclosing type is a Kotlin object or companion object, since all methods in + // these types are effectively static in the JVM bytecode. + val enclosingElement = methodElement.enclosingElement + val isInKotlinObject = enclosingElement is XTypeElement && enclosingElement.isKotlinObject() + if (!methodElement.isStatic() && !isInKotlinObject) { throw DeepLinkProcessorException( element = methodElement, errorMessage = "Only static methods can be annotated with @${DEEP_LINK_CLASS.simpleName}", diff --git a/deeplinkdispatch-processor/src/test/java/com/airbnb/deeplinkdispatch/DeepLinkProcessorKspTest.kt b/deeplinkdispatch-processor/src/test/java/com/airbnb/deeplinkdispatch/DeepLinkProcessorKspTest.kt index 0b95ead3..25b13f01 100644 --- a/deeplinkdispatch-processor/src/test/java/com/airbnb/deeplinkdispatch/DeepLinkProcessorKspTest.kt +++ b/deeplinkdispatch-processor/src/test/java/com/airbnb/deeplinkdispatch/DeepLinkProcessorKspTest.kt @@ -1449,4 +1449,74 @@ class DeepLinkProcessorKspTest : BaseDeepLinkProcessorTest() { ) } } + + /** + * Verifies that methods inside a Kotlin `object` with @JvmStatic compile successfully. + * This exercises the isInKotlinObject fallback in verifyMethod(), which works around + * intermittent KSP annotation resolution failures where isStatic() returns false + * for @JvmStatic methods in object declarations. + */ + @Test + fun testMethodInKotlinObjectWithJvmStaticCompiles() { + val kotlinObjectSource = + Source.KotlinSource( + "com/example/SampleDeepLinks.kt", + """ + package com.example + import android.content.Context + import android.content.Intent + import com.airbnb.deeplinkdispatch.DeepLink + + object SampleDeepLinks { + @DeepLink("airbnb://example.com/deepLink") + @JvmStatic + fun intentForDeepLink(context: Context): Intent = Intent() + } + """, + ) + val results = + listOf( + compileIncremental( + sourceFiles = listOf(kotlinObjectSource, module, fakeBaseDeeplinkDelegateJava), + useKsp = true, + ), + ) + results.forEach { result -> + assertThat(result.result.exitCode) + .isEqualTo(KotlinCompilation.ExitCode.OK) + } + } + + /** + * Verifies that a non-static method (no @JvmStatic) in a plain class still fails. + */ + @Test + fun testNonStaticMethodInClassStillFails() { + val nonStaticSource = + Source.KotlinSource( + "com/example/SampleActivity.kt", + """ + package com.example + import android.content.Context + import android.content.Intent + import com.airbnb.deeplinkdispatch.DeepLink + + class SampleActivity : android.app.Activity() { + @DeepLink("airbnb://example.com/deepLink") + fun intentFromNoStatic(context: Context): Intent = Intent() + } + """, + ) + val results = + listOf( + compileIncremental( + sourceFiles = listOf(nonStaticSource, module, fakeBaseDeeplinkDelegateJava), + useKsp = true, + ), + ) + assertCompileError( + results = results, + errorMessage = "Only static methods can be annotated", + ) + } }