diff --git a/app/stability/app.stability b/app/stability/app.stability index 9644d5e..80bc861 100644 --- a/app/stability/app.stability +++ b/app/stability/app.stability @@ -13,7 +13,7 @@ public fun com.skydoves.myapplication.ActionButton(text: kotlin.String, onClick: - onClick: STABLE (function type) @Composable -public fun com.skydoves.myapplication.Card(modifier: androidx.compose.ui.Modifier, shape: androidx.compose.ui.graphics.Shape, colors: androidx.compose.material3.CardColors, elevation: androidx.compose.material3.CardElevation, func: @[Composable] androidx.compose.runtime.internal.ComposableFunction0, func2: kotlin.coroutines.SuspendFunction0, content: @[Composable] @[ExtensionFunctionType] androidx.compose.runtime.internal.ComposableFunction1): kotlin.Unit +public fun com.skydoves.myapplication.Card(modifier: androidx.compose.ui.Modifier?, shape: androidx.compose.ui.graphics.Shape?, colors: androidx.compose.material3.CardColors?, elevation: androidx.compose.material3.CardElevation?, func: kotlin.Function2, func2: kotlin.coroutines.SuspendFunction0, content: @[ExtensionFunctionType] kotlin.Function3): kotlin.Unit skippable: false restartable: true params: @@ -21,9 +21,9 @@ public fun com.skydoves.myapplication.Card(modifier: androidx.compose.ui.Modifie - shape: STABLE (marked @Stable or @Immutable) - colors: STABLE (marked @Stable or @Immutable) - elevation: STABLE (marked @Stable or @Immutable) - - func: STABLE (composable function type) + - func: STABLE (function type) - func2: RUNTIME (requires runtime check) - - content: STABLE (composable function type) + - content: STABLE (function type) @Composable public fun com.skydoves.myapplication.CorrectUsage(): kotlin.Unit @@ -62,7 +62,7 @@ public fun com.skydoves.myapplication.GenericDisplay(item: T of com.skydoves.mya - displayText: STABLE (function type) @Composable -public fun com.skydoves.myapplication.Icon(title: kotlin.String, painter: androidx.compose.ui.graphics.painter.Painter, users: kotlin.collections.List, elevation: androidx.compose.material3.CardElevation, unstableUser: com.skydoves.myapplication.models.UnstableUser): kotlin.Unit +public fun com.skydoves.myapplication.Icon(title: kotlin.String, painter: androidx.compose.ui.graphics.painter.Painter, users: kotlin.collections.List, elevation: androidx.compose.material3.CardElevation?, unstableUser: com.skydoves.myapplication.models.UnstableUser): kotlin.Unit skippable: false restartable: true params: @@ -86,7 +86,7 @@ public fun com.skydoves.myapplication.MainScreen(): kotlin.Unit params: @Composable -public fun com.skydoves.myapplication.MixedStabilityDisplay(title: kotlin.String, count: kotlin.Int, items: kotlin.collections.List, items2: kotlin.collections.MutableList, items3: com.skydoves.myapplication.models.UnstableUser, modifier: androidx.compose.ui.Modifier): kotlin.Unit +public fun com.skydoves.myapplication.MixedStabilityDisplay(title: kotlin.String, count: kotlin.Int, items: kotlin.collections.List, items2: kotlin.collections.MutableList, items3: com.skydoves.myapplication.models.UnstableUser, modifier: androidx.compose.ui.Modifier?): kotlin.Unit skippable: false restartable: true params: @@ -197,12 +197,12 @@ public fun com.skydoves.myapplication.TrackedActionButton(text: kotlin.String, o - onClick: STABLE (function type) @Composable -public fun com.skydoves.myapplication.TrackedCounterDisplay(count: kotlin.Int, content: @[Composable] androidx.compose.runtime.internal.ComposableFunction0): kotlin.Unit +public fun com.skydoves.myapplication.TrackedCounterDisplay(count: kotlin.Int, content: kotlin.Function2): kotlin.Unit skippable: true restartable: true params: - count: STABLE (primitive type) - - content: STABLE (composable function type) + - content: STABLE (function type) @Composable public fun com.skydoves.myapplication.TrackedMixedParameters(title: kotlin.String, count: kotlin.Int, user: com.skydoves.myapplication.models.StableUser): kotlin.Unit diff --git a/compose-stability-analyzer-idea/src/main/kotlin/com/skydoves/compose/stability/idea/StabilityAnalysisConstants.kt b/compose-stability-analyzer-idea/src/main/kotlin/com/skydoves/compose/stability/idea/StabilityAnalysisConstants.kt index fd44e9d..dbf3de3 100644 --- a/compose-stability-analyzer-idea/src/main/kotlin/com/skydoves/compose/stability/idea/StabilityAnalysisConstants.kt +++ b/compose-stability-analyzer-idea/src/main/kotlin/com/skydoves/compose/stability/idea/StabilityAnalysisConstants.kt @@ -84,6 +84,15 @@ internal object StabilityAnalysisConstants { "androidx.compose.ui.unit.DpSize", "androidx.compose.ui.unit.Constraints", + // Compose Foundation shapes + "androidx.compose.foundation.shape.RoundedCornerShape", + "androidx.compose.foundation.shape.CircleShape", + "androidx.compose.foundation.shape.CutCornerShape", + "androidx.compose.foundation.shape.CornerBasedShape", + "androidx.compose.foundation.shape.AbsoluteRoundedCornerShape", + "androidx.compose.foundation.shape.AbsoluteCutCornerShape", + "androidx.compose.ui.graphics.RectangleShape", + // Compose text value classes "androidx.compose.ui.text.style.TextAlign", "androidx.compose.ui.text.style.TextDirection", diff --git a/compose-stability-analyzer-idea/src/main/kotlin/com/skydoves/compose/stability/idea/StabilityAnalyzer.kt b/compose-stability-analyzer-idea/src/main/kotlin/com/skydoves/compose/stability/idea/StabilityAnalyzer.kt index ea0f7c2..b2bb215 100644 --- a/compose-stability-analyzer-idea/src/main/kotlin/com/skydoves/compose/stability/idea/StabilityAnalyzer.kt +++ b/compose-stability-analyzer-idea/src/main/kotlin/com/skydoves/compose/stability/idea/StabilityAnalyzer.kt @@ -487,18 +487,7 @@ internal object StabilityAnalyzer { } } - // 5. Check for @StabilityInferred (from separate compilation) - val hasStabilityInferred = resolved.annotationEntries.any { annotation -> - annotation.shortName?.asString() == "StabilityInferred" - } - if (hasStabilityInferred) { - return StabilityResult( - ParameterStability.RUNTIME, - "Annotated with @StabilityInferred (from separate compilation)", - ) - } - - // 6. Check if it's a kotlinx immutable collection (always stable) + // 5. Check if it's a kotlinx immutable collection (always stable) if (fqName != null && fqName.startsWith("kotlinx.collections.immutable.")) { if (fqName.contains("Immutable") || fqName.contains("Persistent")) { return StabilityResult( @@ -508,7 +497,7 @@ internal object StabilityAnalyzer { } } - // 7. If it's an interface, return RUNTIME (cannot determine) + // 6. If it's an interface, return RUNTIME (cannot determine) if (resolved.isInterface()) { return StabilityResult( ParameterStability.RUNTIME, @@ -516,8 +505,31 @@ internal object StabilityAnalyzer { ) } - // 8. Analyze class properties (both data classes and normal classes) - return analyzeClassPropertiesViaPsiWithReason(resolved, className) + // 7. Analyze class properties first (both data classes and normal classes) + // This check MUST come before @StabilityInferred to properly detect definitive cases + val propertyStability = analyzeClassPropertiesViaPsiWithReason(resolved, className) + + // If property analysis gives a definitive answer (STABLE or UNSTABLE), return it + // Only fall back to @StabilityInferred for uncertain (RUNTIME) cases + if (propertyStability != null && + propertyStability.stability != ParameterStability.RUNTIME + ) { + return propertyStability + } + + // 8. Check for @StabilityInferred (from separate compilation) + val hasStabilityInferred = resolved.annotationEntries.any { annotation -> + annotation.shortName?.asString() == "StabilityInferred" + } + if (hasStabilityInferred) { + return StabilityResult( + ParameterStability.RUNTIME, + "Annotated with @StabilityInferred (from separate compilation)", + ) + } + + // Return property stability or null if no definitive answer + return propertyStability } } } diff --git a/compose-stability-analyzer-idea/src/main/kotlin/com/skydoves/compose/stability/idea/k2/KtStabilityInferencer.kt b/compose-stability-analyzer-idea/src/main/kotlin/com/skydoves/compose/stability/idea/k2/KtStabilityInferencer.kt index 9ee2afc..78af612 100644 --- a/compose-stability-analyzer-idea/src/main/kotlin/com/skydoves/compose/stability/idea/k2/KtStabilityInferencer.kt +++ b/compose-stability-analyzer-idea/src/main/kotlin/com/skydoves/compose/stability/idea/k2/KtStabilityInferencer.kt @@ -48,10 +48,10 @@ import org.jetbrains.kotlin.analysis.api.types.KaTypeNullability * 14. Standard collections (RUNTIME) * 15. Value classes * 16. Enums - * 17. @StabilityInferred (RUNTIME) - * 18. Interfaces (RUNTIME) - * 19. Abstract classes (RUNTIME) - * 20. Regular classes (with superclass stability checking) + * 17. Interfaces (RUNTIME) + * 18. Abstract classes (RUNTIME) + * 19. Regular classes - property analysis (returns STABLE/UNSTABLE if definitive) + * 20. @StabilityInferred (RUNTIME - only for uncertain cases) */ internal class KtStabilityInferencer { @@ -271,15 +271,7 @@ internal class KtStabilityInferencer { return KtStability.Certain(stable = true, reason = StabilityConstants.Messages.ENUM_STABLE) } - // 17. Check for @StabilityInferred annotation (runtime check) - if (classSymbol.hasStabilityInferredAnnotation()) { - return KtStability.Runtime( - className = fqName ?: simpleName, - reason = "Annotated with @StabilityInferred (from separate compilation)", - ) - } - - // 18. Interfaces - cannot determine (RUNTIME) + // 17. Interfaces - cannot determine (RUNTIME) if (classSymbol.classKind == KaClassKind.INTERFACE) { return KtStability.Runtime( className = fqName ?: simpleName, @@ -287,7 +279,7 @@ internal class KtStabilityInferencer { ) } - // 19. Abstract classes - cannot determine (RUNTIME) + // 18. Abstract classes - cannot determine (RUNTIME) if (classSymbol.modality == KaSymbolModality.ABSTRACT) { return KtStability.Runtime( className = fqName ?: simpleName, @@ -295,8 +287,32 @@ internal class KtStabilityInferencer { ) } - // 20. Regular classes (including data classes) - analyze properties - return analyzeClassProperties(classSymbol, currentlyAnalyzing) + // 19. Regular classes - analyze properties first before checking @StabilityInferred + val propertyStability = analyzeClassProperties(classSymbol, currentlyAnalyzing) + + return when { + propertyStability is KtStability.Certain -> propertyStability + else -> { + // 20. Check @StabilityInferred: parameters=0 means stable, else runtime + val stabilityInferredParams = classSymbol.getStabilityInferredParameters() + when { + stabilityInferredParams != null -> { + if (stabilityInferredParams == 0) { + KtStability.Certain( + stable = true, + reason = "Annotated with @StabilityInferred(parameters=0)", + ) + } else { + KtStability.Runtime( + className = fqName ?: simpleName, + reason = "Annotated with @StabilityInferred(parameters=$stabilityInferredParams)", + ) + } + } + else -> propertyStability + } + } + } } /** @@ -494,14 +510,12 @@ internal class KtStabilityInferencer { } /** - * Check if a class has @StabilityInferred annotation. + * TODO: Read @StabilityInferred parameters field using K2 Analysis API. + * Returns null (conservative RUNTIME) until reliable API is found. */ context(KaSession) - private fun KaClassSymbol.hasStabilityInferredAnnotation(): Boolean { - return annotations.any { annotation -> - val fqName = annotation.classId?.asSingleFqName()?.asString() - fqName == "androidx.compose.runtime.internal.StabilityInferred" - } + private fun KaClassSymbol.getStabilityInferredParameters(): Int? { + return null } /** diff --git a/gradle.properties b/gradle.properties index 802de3e..06c2a1e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -51,7 +51,7 @@ kotlin.mpp.androidGradlePluginCompatibility.nowarn=true # Maven publishing GROUP=com.github.skydoves -VERSION_NAME=0.4.0 +VERSION_NAME=0.4.1 POM_URL=https://github.com/skydoves/compose-stability-analyzer/ POM_SCM_URL=https://github.com/skydoves/compose-stability-analyzer/ diff --git a/settings.gradle.kts b/settings.gradle.kts index 3a9b869..2f5b996 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,20 +1,20 @@ pluginManagement { repositories { + mavenLocal() gradlePluginPortal() google() mavenCentral() - mavenLocal() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.PREFER_PROJECT) repositories { + mavenLocal() google() mavenCentral() maven { url = uri("https://plugins.gradle.org/m2/") } maven { url = uri("https://cache-redirector.jetbrains.com/intellij-dependencies") } - mavenLocal() } } rootProject.name = "compose-stability-analyzer" diff --git a/stability-compiler/build.gradle.kts b/stability-compiler/build.gradle.kts index 229e4eb..3dab9c6 100644 --- a/stability-compiler/build.gradle.kts +++ b/stability-compiler/build.gradle.kts @@ -29,7 +29,7 @@ kotlin { dependencies { compileOnly(libs.kotlin.stdlib) compileOnly(libs.kotlin.compiler.embeddable) - api(project(":stability-runtime")) + implementation(project(":stability-runtime")) testImplementation(kotlin("test")) testImplementation(kotlin("test-junit")) diff --git a/stability-compiler/src/main/kotlin/com/skydoves/compose/stability/compiler/StabilityInfoCollector.kt b/stability-compiler/src/main/kotlin/com/skydoves/compose/stability/compiler/StabilityInfoCollector.kt index 9758081..832ae21 100644 --- a/stability-compiler/src/main/kotlin/com/skydoves/compose/stability/compiler/StabilityInfoCollector.kt +++ b/stability-compiler/src/main/kotlin/com/skydoves/compose/stability/compiler/StabilityInfoCollector.kt @@ -37,10 +37,15 @@ public class StabilityInfoCollector( /** * Export collected stability information to JSON file. * Does nothing if no composables were collected. + * Filters out anonymous composables (compiler-generated lambda functions). */ public fun export() { + // Filter out anonymous composables (compiler-generated functions) + // Check the qualified name to catch cases like "Foo..Bar" + val filteredComposables = composables.filter { !it.qualifiedName.contains("") } + // Don't create file if there are no entries - if (composables.isEmpty()) { + if (filteredComposables.isEmpty()) { return } @@ -50,7 +55,7 @@ public class StabilityInfoCollector( appendLine("{") appendLine(" \"composables\": [") - composables.sortedBy { it.qualifiedName }.forEachIndexed { index, info -> + filteredComposables.sortedBy { it.qualifiedName }.forEachIndexed { index, info -> appendLine(" {") appendLine(" \"qualifiedName\": \"${info.qualifiedName.escapeJson()}\",") appendLine(" \"simpleName\": \"${info.simpleName.escapeJson()}\",") @@ -78,7 +83,7 @@ public class StabilityInfoCollector( } appendLine(" ]") - if (index < composables.size - 1) { + if (index < filteredComposables.size - 1) { appendLine(" },") } else { appendLine(" }") diff --git a/stability-compiler/src/main/kotlin/com/skydoves/compose/stability/compiler/lower/StabilityAnalyzerTransformer.kt b/stability-compiler/src/main/kotlin/com/skydoves/compose/stability/compiler/lower/StabilityAnalyzerTransformer.kt index bfa0347..26dd533 100644 --- a/stability-compiler/src/main/kotlin/com/skydoves/compose/stability/compiler/lower/StabilityAnalyzerTransformer.kt +++ b/stability-compiler/src/main/kotlin/com/skydoves/compose/stability/compiler/lower/StabilityAnalyzerTransformer.kt @@ -310,23 +310,32 @@ public class StabilityAnalyzerTransformer( return ParameterStability.STABLE } - // 14. Check for @StabilityInferred annotation (runtime check) - if (type.hasStabilityInferredAnnotation()) { - return ParameterStability.RUNTIME - } - - // 15. Interfaces - cannot determine (RUNTIME) + // 14. Interfaces - cannot determine (RUNTIME) if (clazz.isInterfaceIr()) { return ParameterStability.RUNTIME } - // 16. Abstract classes - cannot determine (RUNTIME) + // 15. Abstract classes - cannot determine (RUNTIME) if (clazz.modality == org.jetbrains.kotlin.descriptors.Modality.ABSTRACT) { return ParameterStability.RUNTIME } - // 17. Regular classes (including data classes) - analyze properties - return analyzeClassProperties(clazz) + // 16. Regular classes - analyze properties first before checking @StabilityInferred + val propertyStability = analyzeClassProperties(clazz) + + when (propertyStability) { + ParameterStability.STABLE -> return ParameterStability.STABLE + ParameterStability.UNSTABLE -> return ParameterStability.UNSTABLE + ParameterStability.RUNTIME -> { + // 17. Check @StabilityInferred: parameters=0 means stable, else runtime + val stabilityInferredParams = type.getStabilityInferredParameters() + return if (stabilityInferredParams == 0) { + ParameterStability.STABLE + } else { + ParameterStability.RUNTIME + } + } + } } /** @@ -395,6 +404,14 @@ public class StabilityAnalyzerTransformer( return clazz.hasAnnotation(stabilityInferredFqName) } + /** + * TODO: Read @StabilityInferred parameters field without deprecated IR APIs. + * Returns null (conservative RUNTIME) until stable API is available. + */ + private fun IrType.getStabilityInferredParameters(): Int? { + return null + } + private fun IrType.isCollection(): Boolean { val className = this.classFqName?.asString() ?: return false return className.startsWith("kotlin.collections.") && @@ -548,6 +565,15 @@ public class StabilityAnalyzerTransformer( "androidx.compose.ui.unit.DpSize", "androidx.compose.ui.unit.Constraints", + // Compose Foundation shapes + "androidx.compose.foundation.shape.RoundedCornerShape", + "androidx.compose.foundation.shape.CircleShape", + "androidx.compose.foundation.shape.CutCornerShape", + "androidx.compose.foundation.shape.CornerBasedShape", + "androidx.compose.foundation.shape.AbsoluteRoundedCornerShape", + "androidx.compose.foundation.shape.AbsoluteCutCornerShape", + "androidx.compose.ui.graphics.RectangleShape", + // Compose text value classes "androidx.compose.ui.text.style.TextAlign", "androidx.compose.ui.text.style.TextDirection", diff --git a/stability-gradle/api/stability-gradle.api b/stability-gradle/api/stability-gradle.api index 186225b..83ccc5a 100644 --- a/stability-gradle/api/stability-gradle.api +++ b/stability-gradle/api/stability-gradle.api @@ -5,11 +5,16 @@ public abstract class com/skydoves/compose/stability/gradle/StabilityAnalyzerExt public final fun stabilityValidation (Lorg/gradle/api/Action;)V } -public final class com/skydoves/compose/stability/gradle/StabilityAnalyzerGradlePlugin : org/gradle/api/Plugin { +public final class com/skydoves/compose/stability/gradle/StabilityAnalyzerGradlePlugin : org/jetbrains/kotlin/gradle/plugin/KotlinCompilerPluginSupportPlugin { public static final field Companion Lcom/skydoves/compose/stability/gradle/StabilityAnalyzerGradlePlugin$Companion; public fun ()V public synthetic fun apply (Ljava/lang/Object;)V public fun apply (Lorg/gradle/api/Project;)V + public fun applyToCompilation (Lorg/jetbrains/kotlin/gradle/plugin/KotlinCompilation;)Lorg/gradle/api/provider/Provider; + public fun getCompilerPluginId ()Ljava/lang/String; + public fun getPluginArtifact ()Lorg/jetbrains/kotlin/gradle/plugin/SubpluginArtifact; + public fun getPluginArtifactForNative ()Lorg/jetbrains/kotlin/gradle/plugin/SubpluginArtifact; + public fun isApplicable (Lorg/jetbrains/kotlin/gradle/plugin/KotlinCompilation;)Z } public final class com/skydoves/compose/stability/gradle/StabilityAnalyzerGradlePlugin$Companion { diff --git a/stability-gradle/src/main/kotlin/com/skydoves/compose/stability/gradle/StabilityAnalyzerGradlePlugin.kt b/stability-gradle/src/main/kotlin/com/skydoves/compose/stability/gradle/StabilityAnalyzerGradlePlugin.kt index 1dd2ed0..3b0f9eb 100644 --- a/stability-gradle/src/main/kotlin/com/skydoves/compose/stability/gradle/StabilityAnalyzerGradlePlugin.kt +++ b/stability-gradle/src/main/kotlin/com/skydoves/compose/stability/gradle/StabilityAnalyzerGradlePlugin.kt @@ -15,38 +15,61 @@ */ package com.skydoves.compose.stability.gradle -import org.gradle.api.Plugin import org.gradle.api.Project -import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension -import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.gradle.api.provider.Provider +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet.Companion.COMMON_MAIN_SOURCE_SET_NAME +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetContainer +import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact +import org.jetbrains.kotlin.gradle.plugin.SubpluginOption /** * Gradle plugin for Compose Stability Analyzer. * Automatically configures the Kotlin compiler plugin for stability analysis. + * + * This plugin follows the KotlinCompilerPluginSupportPlugin pattern for proper + * integration with the Kotlin Gradle Plugin. */ -public class StabilityAnalyzerGradlePlugin : Plugin { +public class StabilityAnalyzerGradlePlugin : KotlinCompilerPluginSupportPlugin { public companion object { + // Plugin IDs + private const val COMPILER_PLUGIN_ID = "com.skydoves.compose.stability.compiler" + private const val MULTIPLATFORM_PLUGIN_ID = "org.jetbrains.kotlin.multiplatform" + + // Artifact coordinates + private const val GROUP_ID = "com.github.skydoves" + private const val COMPILER_ARTIFACT_ID = "compose-stability-compiler" + private const val RUNTIME_ARTIFACT_ID = "compose-stability-runtime" + // This version should match the version in gradle.properties // Update this when bumping the library version - private const val VERSION = "0.4.0" + private const val VERSION = "0.4.1" + + // Compiler option keys + private const val OPTION_ENABLED = "enabled" + private const val OPTION_STABILITY_OUTPUT_DIR = "stabilityOutputDir" } - override fun apply(project: Project) { - val extension = project.extensions.create( + override fun apply(target: Project) { + // Create extension for user configuration + val extension = target.extensions.create( "composeStabilityAnalyzer", StabilityAnalyzerExtension::class.java, - project.layout, + target.layout, ) + // Add runtime to compiler plugin classpath for all compilations + addRuntimeToCompilerClasspath(target) + // Register stability dump task - val stabilityDumpTask = project.tasks.register( + val stabilityDumpTask = target.tasks.register( "stabilityDump", StabilityDumpTask::class.java, ) { stabilityInputFile.set( - project.layout.buildDirectory.file("stability/stability-info.json"), + target.layout.buildDirectory.file("stability/stability-info.json"), ) outputDir.set(extension.stabilityValidation.outputDir) ignoredPackages.set(extension.stabilityValidation.ignoredPackages) @@ -54,12 +77,12 @@ public class StabilityAnalyzerGradlePlugin : Plugin { } // Register stability check task - val stabilityCheckTask = project.tasks.register( + val stabilityCheckTask = target.tasks.register( "stabilityCheck", StabilityCheckTask::class.java, ) { stabilityInputFile.set( - project.layout.buildDirectory.file("stability/stability-info.json"), + target.layout.buildDirectory.file("stability/stability-info.json"), ) stabilityDir.set(extension.stabilityValidation.outputDir) ignoredPackages.set(extension.stabilityValidation.ignoredPackages) @@ -67,152 +90,141 @@ public class StabilityAnalyzerGradlePlugin : Plugin { } // Make check task depend on stabilityCheck if enabled - project.tasks.named("check") { + target.tasks.named("check") { dependsOn(stabilityCheckTask) } // Configure after project evaluation - project.afterEvaluate { - val ignoredProjects = extension.stabilityValidation.ignoredProjects.get() - val projectName = project.name - val isIgnored = ignoredProjects.contains(projectName) + target.afterEvaluate { + configureTaskDependencies(target, extension, stabilityDumpTask, stabilityCheckTask) + addRuntimeDependency(target) + } + } - if (isIgnored) { - return@afterEvaluate - } + override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean { + val project = kotlinCompilation.target.project + val extension = project.extensions.findByType(StabilityAnalyzerExtension::class.java) + ?: return false - configureKotlinCompilerPlugin(project, extension) - configureTaskDependencies(project, stabilityDumpTask, stabilityCheckTask) + // Check if project is ignored + val ignoredProjects = extension.stabilityValidation.ignoredProjects.get() + if (ignoredProjects.contains(project.name)) { + return false } - // Add runtime dependency - use project dependency if available, otherwise use Maven coordinates - val rootProject = project.rootProject - val runtimeProject = rootProject.findProject(":stability-runtime") - val lintProject = rootProject.findProject(":stability-lint") - - if (runtimeProject != null) { - project.dependencies.add("implementation", runtimeProject) - if (lintProject != null) { - project.dependencies.add("lintChecks", lintProject) - } - } else { - val dep = "com.github.skydoves:compose-stability-runtime:$VERSION" - project.dependencies.add("implementation", dep) + // Check if this is a test compilation + val includeTests = extension.stabilityValidation.includeTests.get() + if (!includeTests && isTestCompilation(kotlinCompilation)) { + return false } + + return true } - private fun configureKotlinCompilerPlugin( - project: Project, - extension: StabilityAnalyzerExtension, - ) { - val kotlinExtension = project.extensions.findByType(KotlinJvmProjectExtension::class.java) - ?: project.extensions.findByType(KotlinAndroidProjectExtension::class.java) - ?: project.extensions.findByType(KotlinMultiplatformExtension::class.java) + override fun getCompilerPluginId(): String = COMPILER_PLUGIN_ID - if (kotlinExtension == null) { - project.logger.warn( - "Kotlin plugin not found. Compose Stability Analyzer will not be configured.", - ) - return - } + override fun getPluginArtifact(): SubpluginArtifact { + return SubpluginArtifact( + groupId = GROUP_ID, + artifactId = COMPILER_ARTIFACT_ID, + version = VERSION, + ) + } - // Apply the compiler plugin - use project dependency if available, otherwise use Maven coordinates - val rootProject = project.rootProject - val compilerProject = rootProject.findProject(":stability-compiler") - if (compilerProject != null) { - project.dependencies.add("kotlinCompilerPluginClasspath", compilerProject) - } else { - project.dependencies.add( - "kotlinCompilerPluginClasspath", - "com.github.skydoves:compose-stability-compiler:$VERSION", - ) - } + override fun getPluginArtifactForNative(): SubpluginArtifact { + return SubpluginArtifact( + groupId = GROUP_ID, + artifactId = COMPILER_ARTIFACT_ID, + version = VERSION, + ) + } - val includeTests = extension.stabilityValidation.includeTests.get() + override fun applyToCompilation( + kotlinCompilation: KotlinCompilation<*>, + ): Provider> { + val project = kotlinCompilation.target.project + val extension = project.extensions.getByType(StabilityAnalyzerExtension::class.java) - // Configure compiler arguments - when (kotlinExtension) { - is KotlinJvmProjectExtension -> { - kotlinExtension.target.compilations.configureEach { - if (includeTests || !isTestCompilation(name)) { - compileTaskProvider.configure { - compilerOptions.freeCompilerArgs.addAll( - buildCompilerArguments(project, extension), - ) - } - } - } - } + return project.provider { + listOf( + SubpluginOption( + key = OPTION_ENABLED, + value = extension.enabled.get().toString(), + ), + SubpluginOption( + key = OPTION_STABILITY_OUTPUT_DIR, + value = project.layout.buildDirectory.dir("stability").get().asFile.absolutePath, + ), + ) + } + } - is KotlinAndroidProjectExtension -> { - kotlinExtension.target.compilations.configureEach { - if (includeTests || !isTestCompilation(name)) { - compileTaskProvider.configure { - compilerOptions.freeCompilerArgs.addAll( - buildCompilerArguments(project, extension), - ) - } - } - } - } + /** + * Add runtime to compiler plugin classpath. + * This ensures the compiler plugin can access runtime classes during compilation. + */ + private fun addRuntimeToCompilerClasspath(project: Project) { + project.afterEvaluate { + val runtimeProject = getRuntimeProject(project) + val runtimeDependency = runtimeProject + ?: "$GROUP_ID:$RUNTIME_ARTIFACT_ID:$VERSION" - is KotlinMultiplatformExtension -> { - kotlinExtension.targets.configureEach { - compilations.configureEach { - if (includeTests || !isTestCompilation(name)) { - compileTaskProvider.configure { - compilerOptions.freeCompilerArgs.addAll( - buildCompilerArguments(project, extension), - ) - } - } - } + // Add runtime to all compiler plugin classpath configurations (not general compiler classpath) + project.configurations.configureEach { + if (name.contains("CompilerPluginClasspath", ignoreCase = true)) { + project.dependencies.add(name, runtimeDependency) } } } } /** - * Check if a compilation is a test compilation based on its name. - * Test compilations typically have names like "test", "testDebug", "testRelease", - * "debugUnitTest", "debugAndroidTest", etc. + * Add runtime dependency to the project. + * For multiplatform projects, adds to commonMain sourceSet. + * For JVM/Android projects, adds to implementation configuration. */ - private fun isTestCompilation(compilationName: String): Boolean { - val lowerName = compilationName.lowercase() - return lowerName.contains("test") || - lowerName.contains("androidtest") || - lowerName.contains("unittest") - } + private fun addRuntimeDependency(project: Project) { + val runtimeProject = getRuntimeProject(project) + val lintProject = getLintProject(project) - private fun buildCompilerArguments( - project: Project, - extension: StabilityAnalyzerExtension, - ): List { - val pluginId = "com.skydoves.compose.stability.compiler" - val args = mutableListOf() + val runtimeDependency = runtimeProject + ?: "$GROUP_ID:$RUNTIME_ARTIFACT_ID:$VERSION" - args.add("-P") - args.add("plugin:$pluginId:enabled=${extension.enabled.get()}") - - val stabilityOutputDir = - project.layout.buildDirectory.dir("stability").get().asFile.absolutePath - args.add("-P") - args.add("plugin:$pluginId:stabilityOutputDir=$stabilityOutputDir") + // Check if this is a multiplatform project + if (project.plugins.hasPlugin(MULTIPLATFORM_PLUGIN_ID)) { + // For multiplatform projects, add dependency to commonMain sourceSet + val kotlinExtension = project.extensions.getByName("kotlin") as KotlinSourceSetContainer + val commonMain = kotlinExtension.sourceSets.getByName(COMMON_MAIN_SOURCE_SET_NAME) + commonMain.dependencies { + implementation(runtimeDependency) + } + } else { + // For JVM/Android projects, add to implementation configuration + project.dependencies.add("implementation", runtimeDependency) - return args + // Lint is only supported for Android projects + if (lintProject != null) { + project.dependencies.add("lintChecks", lintProject) + } + } } + /** + * Configure task dependencies for stability dump and check tasks. + */ private fun configureTaskDependencies( project: Project, + extension: StabilityAnalyzerExtension, stabilityDumpTask: org.gradle.api.tasks.TaskProvider, stabilityCheckTask: org.gradle.api.tasks.TaskProvider, ) { - val extension = project.extensions.getByType(StabilityAnalyzerExtension::class.java) val includeTests = extension.stabilityValidation.includeTests.get() project.tasks.matching { task -> val isKotlinCompile = task.name.startsWith("compile") && task.name.contains("Kotlin") - val isTestTask = isTestCompilation(task.name) + val isTestTask = task.name.lowercase().let { + it.contains("test") || it.contains("androidtest") || it.contains("unittest") + } // Include task if it's a Kotlin compile task and either: // 1. includeTests is true, OR @@ -223,4 +235,35 @@ public class StabilityAnalyzerGradlePlugin : Plugin { stabilityCheckTask.get().dependsOn(this) } } + + /** + * Check if a compilation is a test compilation. + */ + private fun isTestCompilation(compilation: KotlinCompilation<*>): Boolean { + val compilationName = compilation.name.lowercase() + return compilationName.contains("test") || + compilationName.contains("androidtest") || + compilationName.contains("unittest") + } + + /** + * Get the compiler plugin project if available. + */ + private fun getCompilerProject(): Project? { + return null // Will be resolved from current project's rootProject in actual usage + } + + /** + * Get the runtime project if available. + */ + private fun getRuntimeProject(project: Project): Project? { + return project.rootProject.findProject(":stability-runtime") + } + + /** + * Get the lint project if available. + */ + private fun getLintProject(project: Project): Project? { + return project.rootProject.findProject(":stability-lint") + } } diff --git a/stability-gradle/src/main/kotlin/com/skydoves/compose/stability/gradle/StabilityCheckTask.kt b/stability-gradle/src/main/kotlin/com/skydoves/compose/stability/gradle/StabilityCheckTask.kt index 69502d3..67ac4d8 100644 --- a/stability-gradle/src/main/kotlin/com/skydoves/compose/stability/gradle/StabilityCheckTask.kt +++ b/stability-gradle/src/main/kotlin/com/skydoves/compose/stability/gradle/StabilityCheckTask.kt @@ -137,7 +137,8 @@ public abstract class StabilityCheckTask : DefaultTask() { val obj = content.substring(objStart, objEnd + 1) val entry = parseComposableObject(obj) - if (entry != null) { + // Filter out compiler-generated anonymous composables + if (entry != null && !entry.qualifiedName.contains("")) { entries[entry.qualifiedName] = entry } diff --git a/stability-gradle/src/main/kotlin/com/skydoves/compose/stability/gradle/StabilityDumpTask.kt b/stability-gradle/src/main/kotlin/com/skydoves/compose/stability/gradle/StabilityDumpTask.kt index 65a747f..2e0f807 100644 --- a/stability-gradle/src/main/kotlin/com/skydoves/compose/stability/gradle/StabilityDumpTask.kt +++ b/stability-gradle/src/main/kotlin/com/skydoves/compose/stability/gradle/StabilityDumpTask.kt @@ -253,7 +253,9 @@ public abstract class StabilityDumpTask : DefaultTask() { val packageName = entry.qualifiedName.substringBeforeLast('.', "") val className = entry.qualifiedName.substringAfterLast('.') - !ignoredPackages.any { packageName.startsWith(it) } && + // Filter out compiler-generated anonymous composables + !entry.qualifiedName.contains("") && + !ignoredPackages.any { packageName.startsWith(it) } && !ignoredClasses.contains(className) } }