Skip to content
Merged
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
14 changes: 7 additions & 7 deletions app/stability/app.stability
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ 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<com.skydoves.myapplication.models.UnstableUser>, func2: kotlin.coroutines.SuspendFunction0<com.skydoves.myapplication.models.UnstableUser>, content: @[Composable] @[ExtensionFunctionType] androidx.compose.runtime.internal.ComposableFunction1<androidx.compose.foundation.layout.ColumnScope, kotlin.Unit>): 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<androidx.compose.runtime.Composer, kotlin.Int, com.skydoves.myapplication.models.UnstableUser>, func2: kotlin.coroutines.SuspendFunction0<com.skydoves.myapplication.models.UnstableUser>, content: @[ExtensionFunctionType] kotlin.Function3<androidx.compose.foundation.layout.ColumnScope, androidx.compose.runtime.Composer, kotlin.Int, kotlin.Unit>): kotlin.Unit
skippable: false
restartable: true
params:
- modifier: STABLE (marked @Stable or @Immutable)
- 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
Expand Down Expand Up @@ -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<com.skydoves.myapplication.models.StableUser>, 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<com.skydoves.myapplication.models.StableUser>, elevation: androidx.compose.material3.CardElevation?, unstableUser: com.skydoves.myapplication.models.UnstableUser): kotlin.Unit
skippable: false
restartable: true
params:
Expand All @@ -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<kotlin.String>, items2: kotlin.collections.MutableList<kotlin.String>, 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<kotlin.String>, items2: kotlin.collections.MutableList<kotlin.String>, items3: com.skydoves.myapplication.models.UnstableUser, modifier: androidx.compose.ui.Modifier?): kotlin.Unit
skippable: false
restartable: true
params:
Expand Down Expand Up @@ -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>): kotlin.Unit
public fun com.skydoves.myapplication.TrackedCounterDisplay(count: kotlin.Int, content: kotlin.Function2<androidx.compose.runtime.Composer, kotlin.Int, kotlin.Unit>): 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -508,16 +497,39 @@ 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,
"Interface type - actual implementation could be mutable",
)
}

// 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
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -271,32 +271,48 @@ 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,
reason = "Interface type - actual implementation could be mutable",
)
}

// 19. Abstract classes - cannot determine (RUNTIME)
// 18. Abstract classes - cannot determine (RUNTIME)
if (classSymbol.modality == KaSymbolModality.ABSTRACT) {
return KtStability.Runtime(
className = fqName ?: simpleName,
reason = "Abstract class - actual implementation could be mutable",
)
}

// 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
}
}
}
}

/**
Expand Down Expand Up @@ -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
}

/**
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
4 changes: 2 additions & 2 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion stability-compiler/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.<anonymous>.Bar"
val filteredComposables = composables.filter { !it.qualifiedName.contains("<anonymous>") }

// Don't create file if there are no entries
if (composables.isEmpty()) {
if (filteredComposables.isEmpty()) {
return
}

Expand All @@ -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()}\",")
Expand Down Expand Up @@ -78,7 +83,7 @@ public class StabilityInfoCollector(
}

appendLine(" ]")
if (index < composables.size - 1) {
if (index < filteredComposables.size - 1) {
appendLine(" },")
} else {
appendLine(" }")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
}

/**
Expand Down Expand Up @@ -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.") &&
Expand Down Expand Up @@ -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",
Expand Down
7 changes: 6 additions & 1 deletion stability-gradle/api/stability-gradle.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> ()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 {
Expand Down
Loading