Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
@@ -0,0 +1,59 @@
data class Wrapper {
val value: ToBeStable
field = value
get

constructor(value: ToBeStable) /* primary */ {
super/*Any*/()
/* <init>() */

}

operator fun component1(): ToBeStable {
return <this>.#value
}

fun copy(value: ToBeStable = <this>.#value): Wrapper {
return Wrapper(value = value)
}

override operator fun equals(other: Any?): Boolean {
when {
EQEQEQ(arg0 = <this>, arg1 = other) -> return true
}
when {
other !is Wrapper -> return false
}
val tmp_0: Wrapper = other /*as Wrapper */
when {
EQEQ(arg0 = <this>.#value, arg1 = tmp_0.#value).not() -> return false
}
return true
}

override fun hashCode(): Int {
return <this>.#value.hashCode()
}

override fun toString(): String {
return "Wrapper(" + "value=" + <this>.#value + ")"
}

}

interface ToBeStable {
}

@Composable
fun ComposableToBeSkippable(wrapper: Wrapper) {
val _tracker: RecompositionTracker = rememberRecompositionTracker(composableName = "ComposableToBeSkippable", tag = "", threshold = 2, fqName = "ComposableToBeSkippable", isAutoTraced = true)
_tracker.trackParameter(name = "wrapper", type = "<root>.Wrapper", value = wrapper, isStable = true)
val _startTime: Long = nanoTime()
try // COMPOSITE {
println(message = wrapper.toString())
// }
finally // COMPOSITE {
_tracker.recordDuration(startTimeNanos = _startTime)
_tracker.logIfThresholdMet()
// }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// DUMP_KT_IR
// ENABLE_TRACE_ALL
// STABILITY_CONFIGURATION_FILES: compiler-tests/src/test/data/dump/ir/stability_config.conf
Comment thread
coderabbitai[bot] marked this conversation as resolved.

import androidx.compose.runtime.Composable

interface ToBeStable

data class Wrapper(val value: ToBeStable)

// The following line in StabilityConfigurationFile.fir.kt.txt confirms the stability_config.conf was taken into account:
// _tracker.trackParameter(name = "wrapper", type = "<root>.Wrapper", value = wrapper, isStable = true)
// The test will fail if isStable = false (meaning ToBeStable was not marked as stable, hence Wrapper is also not stable).
@Composable
fun ComposableToBeSkippable(wrapper: Wrapper) {
println(wrapper.toString())
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ToBeStable
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ public void testSimpleDataClass() {
run("SimpleDataClass.kt");
}

@Test
@TestMetadata("StabilityConfigurationFile.kt")
public void testStabilityConfigurationFile() {
run("StabilityConfigurationFile.kt");
}

@Test
@TestMetadata("TraceAllInjection.kt")
public void testTraceAllInjection() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import org.jetbrains.kotlin.test.directives.model.DirectivesContainer
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.EnvironmentConfigurator
import org.jetbrains.kotlin.test.services.TestServices
import java.io.File

/**
* Test configurator that registers the Stability Analyzer compiler plugin
Expand All @@ -53,12 +54,17 @@ class StabilityTestConfigurator(testServices: TestServices) :

val traceAll = StabilityTestDirectives.ENABLE_TRACE_ALL in module.directives

val stabilityConfigurationFiles = module
.directives[StabilityTestDirectives.STABILITY_CONFIGURATION_FILES]
.map { File(it) }

// Register IR generation extension for backend transformations
IrGenerationExtension.registerExtension(
StabilityAnalyzerIrGenerationExtension(
stabilityOutputDir = "",
projectDependencies = "",
traceAll = traceAll,
stabilityConfigurationFiles = stabilityConfigurationFiles,
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,7 @@ object StabilityTestDirectives : SimpleDirectivesContainer() {
val ENABLE_TRACE_ALL by directive(
"Enables trace-all auto-instrumentation (composeStabilityAnalyzer.traceAll) for this test",
)
val STABILITY_CONFIGURATION_FILES by stringDirective(
"Specifies stability configuration file(s) for this test",
)
}
24 changes: 24 additions & 0 deletions docs/gradle-plugin/stability-configuration-files.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Stability configuration files

You can provide stability configuration files to tell compiler plugin which types should be treated as stable, even if the compiler marks them as unstable. This uses the same format as the [Compose compiler's stability configuration file](https://developer.android.com/develop/ui/compose/performance/stability/fix#configuration-file), so you can reuse the same file for both the compiler and stability validation.

```kotlin
composeStabilityAnalyzer {
stabilityConfigurationFiles.add(
rootProject.layout.projectDirectory.file("stability_config.conf")
)
}
```

The configuration file contains fully-qualified type names, one per line. Lines starting with `//` are treated as comments. Wildcard patterns are supported: `*` matches a single package segment and `**` matches across package boundaries.

```
// stability_config.conf
com.google.firebase.auth.FirebaseUser
com.example.generated.*
com.example.models.**
```

When these files are configured, the compiler plugin treats any parameter type matching these patterns as stable.

This is particularly useful when your project already uses a stability configuration file for the Compose compiler. By pointing `stabilityConfigurationFiles` to the same file, the stability validation respects the same overrides, keeping the two in sync.
5 changes: 5 additions & 0 deletions docs/gradle-plugin/stability-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ composeStabilityAnalyzer {
// Add stability configuration file
// Matches compose's identical property
// (see https://developer.android.com/develop/ui/compose/performance/stability/fix#configuration-file)
// DEPRECATED: use stabilityConfigurationFiles in top-level composeStabilityAnalyzer block
stabilityConfigurationFiles.add(
rootProject.layout.projectDirectory.file("stability_config.conf")
)
Expand Down Expand Up @@ -184,6 +185,10 @@ composeStabilityAnalyzer {

### `stabilityConfigurationFiles`

!!! warning "DEPRECATED"

This option has been deprecated in favor of setting `stabilityConfigurationFiles` in top-level `composeStabilityAnalyzer` block. New option wires stability configuration files directly into compiler plugin to support marking nested class members as stable. See [Stability configuration files](stability-configuration-files.md) for how to use new option.

You can provide stability configuration files to tell `stabilityCheck` which types should be treated as stable, even if the compiler marks them as unstable. This uses the same format as the [Compose compiler's stability configuration file](https://developer.android.com/develop/ui/compose/performance/stability/fix#configuration-file), so you can reuse the same file for both the compiler and stability validation.

```kotlin
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ nav:
- 'Custom Logger': gradle-plugin/custom-logger.md
- 'Stability Validation': gradle-plugin/stability-validation.md
- 'CI/CD Integration': gradle-plugin/ci-cd.md
- 'Stability configuration files': gradle-plugin/stability-configuration-files.md
- 'Stability Concepts': stability-concepts.md
- 'Kotlin Version Map': version-map.md
- 'Sponsor': sponsor.md
Expand Down
17 changes: 13 additions & 4 deletions stability-compiler/api/stability-compiler.api
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ public final class com/skydoves/compose/stability/compiler/ComposableStabilityIn
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class com/skydoves/compose/stability/compiler/FqNameMatcher {
public fun <init> (Ljava/lang/String;)V
public fun equals (Ljava/lang/Object;)Z
public fun hashCode ()I
public final fun matches (Ljava/lang/String;)Z
}

public final class com/skydoves/compose/stability/compiler/ParameterStabilityInfo {
public static final field Companion Lcom/skydoves/compose/stability/compiler/ParameterStabilityInfo$Companion;
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
Expand Down Expand Up @@ -92,6 +99,7 @@ public final class com/skydoves/compose/stability/compiler/StabilityAnalyzerComm
public final class com/skydoves/compose/stability/compiler/StabilityAnalyzerCommandLineProcessor$Companion {
public final fun getOPTION_ENABLED ()Lorg/jetbrains/kotlin/compiler/plugin/CliOption;
public final fun getOPTION_PROJECT_DEPENDENCIES ()Lorg/jetbrains/kotlin/compiler/plugin/CliOption;
public final fun getOPTION_STABILITY_CONFIGURATION_FILE ()Lorg/jetbrains/kotlin/compiler/plugin/CliOption;
public final fun getOPTION_STABILITY_OUTPUT_DIR ()Lorg/jetbrains/kotlin/compiler/plugin/CliOption;
public final fun getOPTION_TRACE_ALL ()Lorg/jetbrains/kotlin/compiler/plugin/CliOption;
public final fun getOPTION_TRACE_ALL_THRESHOLD ()Lorg/jetbrains/kotlin/compiler/plugin/CliOption;
Expand All @@ -101,6 +109,7 @@ public final class com/skydoves/compose/stability/compiler/StabilityAnalyzerConf
public static final field INSTANCE Lcom/skydoves/compose/stability/compiler/StabilityAnalyzerConfigurationKeys;
public final fun getKEY_ENABLED ()Lorg/jetbrains/kotlin/config/CompilerConfigurationKey;
public final fun getKEY_PROJECT_DEPENDENCIES ()Lorg/jetbrains/kotlin/config/CompilerConfigurationKey;
public final fun getKEY_STABILITY_CONFIGURATION_FILES ()Lorg/jetbrains/kotlin/config/CompilerConfigurationKey;
public final fun getKEY_STABILITY_OUTPUT_DIR ()Lorg/jetbrains/kotlin/config/CompilerConfigurationKey;
public final fun getKEY_TRACE_ALL ()Lorg/jetbrains/kotlin/config/CompilerConfigurationKey;
public final fun getKEY_TRACE_ALL_THRESHOLD ()Lorg/jetbrains/kotlin/config/CompilerConfigurationKey;
Expand All @@ -111,8 +120,8 @@ public final class com/skydoves/compose/stability/compiler/StabilityAnalyzerFirE
}

public final class com/skydoves/compose/stability/compiler/StabilityAnalyzerIrGenerationExtension : org/jetbrains/kotlin/backend/common/extensions/IrGenerationExtension {
public fun <init> (Ljava/lang/String;Ljava/lang/String;ZI)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;ZILjava/util/List;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZILjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun generate (Lorg/jetbrains/kotlin/ir/declarations/IrModuleFragment;Lorg/jetbrains/kotlin/backend/common/extensions/IrPluginContext;)V
public fun getPlatformIntrinsicExtension (Lorg/jetbrains/kotlin/backend/common/LoweringContext;)Lorg/jetbrains/kotlin/backend/common/extensions/IrIntrinsicExtension;
public fun getShouldAlsoBeAppliedInKaptStubGenerationMode ()Z
Expand Down Expand Up @@ -204,8 +213,8 @@ public final class com/skydoves/compose/stability/compiler/lower/RecompositionIr

public final class com/skydoves/compose/stability/compiler/lower/StabilityAnalyzerTransformer : org/jetbrains/kotlin/backend/common/IrElementTransformerVoidWithContext {
public static final field Companion Lcom/skydoves/compose/stability/compiler/lower/StabilityAnalyzerTransformer$Companion;
public fun <init> (Lorg/jetbrains/kotlin/backend/common/extensions/IrPluginContext;Lcom/skydoves/compose/stability/compiler/StabilityInfoCollector;Ljava/util/List;ZI)V
public synthetic fun <init> (Lorg/jetbrains/kotlin/backend/common/extensions/IrPluginContext;Lcom/skydoves/compose/stability/compiler/StabilityInfoCollector;Ljava/util/List;ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Lorg/jetbrains/kotlin/backend/common/extensions/IrPluginContext;Lcom/skydoves/compose/stability/compiler/StabilityInfoCollector;Ljava/util/List;ZILjava/util/List;)V
public synthetic fun <init> (Lorg/jetbrains/kotlin/backend/common/extensions/IrPluginContext;Lcom/skydoves/compose/stability/compiler/StabilityInfoCollector;Ljava/util/List;ZILjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun visitFunctionNew (Lorg/jetbrains/kotlin/ir/declarations/IrFunction;)Lorg/jetbrains/kotlin/ir/IrStatement;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.CompilerConfigurationKey
import java.io.File

public object StabilityAnalyzerConfigurationKeys {
public val KEY_ENABLED: CompilerConfigurationKey<Boolean> =
Expand All @@ -37,6 +38,9 @@ public object StabilityAnalyzerConfigurationKeys {

public val KEY_TRACE_ALL_THRESHOLD: CompilerConfigurationKey<Int> =
CompilerConfigurationKey<Int>("traceAllThreshold")

public val KEY_STABILITY_CONFIGURATION_FILES: CompilerConfigurationKey<List<File>> =
CompilerConfigurationKey<List<File>>("stabilityConfigurationFiles")
}

@OptIn(ExperimentalCompilerApi::class)
Expand Down Expand Up @@ -79,6 +83,14 @@ public class StabilityAnalyzerCommandLineProcessor : CommandLineProcessor {
description = "Recomposition count threshold for auto-traced composables",
required = false,
)

public val OPTION_STABILITY_CONFIGURATION_FILE: CliOption = CliOption(
optionName = "stabilityConfigurationFile",
valueDescription = "<path>",
description = "Stability configuration file to take into account when analyzing stability",
required = false,
allowMultipleOccurrences = true,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed to pass multiple files

)
}

override val pluginId: String = PLUGIN_ID
Expand All @@ -89,6 +101,7 @@ public class StabilityAnalyzerCommandLineProcessor : CommandLineProcessor {
OPTION_PROJECT_DEPENDENCIES,
OPTION_TRACE_ALL,
OPTION_TRACE_ALL_THRESHOLD,
OPTION_STABILITY_CONFIGURATION_FILE,
)

override fun processOption(
Expand Down Expand Up @@ -121,6 +134,11 @@ public class StabilityAnalyzerCommandLineProcessor : CommandLineProcessor {
StabilityAnalyzerConfigurationKeys.KEY_TRACE_ALL_THRESHOLD,
value.toIntOrNull() ?: 2,
)

OPTION_STABILITY_CONFIGURATION_FILE -> configuration.appendList(
StabilityAnalyzerConfigurationKeys.KEY_STABILITY_CONFIGURATION_FILES,
File(value),
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class StabilityAnalyzerIrGenerationExtension(
private val projectDependencies: String,
private val traceAll: Boolean = false,
private val traceAllThreshold: Int = 2,
private val stabilityConfigurationFiles: List<File> = emptyList(),
) : IrGenerationExtension {

override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
Expand Down Expand Up @@ -56,13 +57,20 @@ public class StabilityAnalyzerIrGenerationExtension(
emptyList()
}

// Construct matchers out of stability configuration files
val stabilityConfigurationMatchers = stabilityConfigurationFiles.flatMap { file ->
if (!file.exists()) return@flatMap emptyList()
StabilityConfigParser.fromFile(file.absolutePath).stableTypeMatchers
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// Create and run the stability analyzer transformer
val transformer = StabilityAnalyzerTransformer(
pluginContext = pluginContext,
stabilityCollector = collector,
projectDependencies = dependencyModules,
traceAll = traceAll,
traceAllThreshold = traceAllThreshold,
stabilityConfigurationMatchers = stabilityConfigurationMatchers,
)

moduleFragment.transformChildrenVoid(transformer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ public class StabilityAnalyzerPluginRegistrar : CompilerPluginRegistrar() {
2,
)

val stabilityConfigurationFiles = configuration.getList(
StabilityAnalyzerConfigurationKeys.KEY_STABILITY_CONFIGURATION_FILES,
)

// Register FIR extensions for frontend analysis (K2).
// Kotlin 2.4.0 (KT-83341) moved K2 extension registration off the IntelliJ
// ProjectExtensionDescriptor mechanism; use the ExtensionStorage-scoped helpers so the
Expand All @@ -71,6 +75,7 @@ public class StabilityAnalyzerPluginRegistrar : CompilerPluginRegistrar() {
projectDependencies = projectDependencies,
traceAll = traceAll,
traceAllThreshold = traceAllThreshold,
stabilityConfigurationFiles = stabilityConfigurationFiles,
),
)
}
Expand Down
Loading