Skip to content

Commit d2c7fbe

Browse files
matejdroskydoves
andauthored
Stability file (#105)
* Share compareStability between impl and tests * Add stability configuration file support to Gradle checker * List all parameters in new composable error * Fix invalid KDoc * Fix getCustomStableTypesAsRegex ignoring other files if one is missing * Fix formatting * Update formatting Co-authored-by: Jaewoong Eum <skydoves2@gmail.com> * Simplify createEntry use * Extract getCustomStableTypesAsRegex to a separate function this will allow it to be unit tested * Add basic getCustomStableTypesAsRegex tests * Improve wildcard handling * Add a test for binary file * Handle invalid characters in patterns * Replace custom parser with a copy of the Google's official one * Properly support generic class matching * Fix code formatting * Add path sensitivity * Remove unused import --------- Co-authored-by: Jaewoong Eum <skydoves2@gmail.com>
1 parent 0e37c31 commit d2c7fbe

File tree

10 files changed

+1100
-216
lines changed

10 files changed

+1100
-216
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,11 @@ composeStabilityAnalyzer {
836836

837837
// Allow the check to run, even if the baseline does not exist (default: false)
838838
allowMissingBaseline.set(false)
839+
840+
// Add stability configuration file
841+
// Matches compose's identical property
842+
// (see https://developer.android.com/develop/ui/compose/performance/stability/fix#configuration-file)
843+
stabilityConfigurationFiles.add(rootProject.layout.projectDirectory.file("stability_config.conf"))
839844
}
840845
}
841846
```

stability-gradle/api/stability-gradle.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public abstract class com/skydoves/compose/stability/gradle/StabilityCheckTask :
3030
public abstract fun getIgnoredPackages ()Lorg/gradle/api/provider/ListProperty;
3131
public abstract fun getProjectName ()Lorg/gradle/api/provider/Property;
3232
public abstract fun getQuietCheck ()Lorg/gradle/api/provider/Property;
33+
public abstract fun getStabilityConfigurationFiles ()Lorg/gradle/api/provider/ListProperty;
3334
public abstract fun getStabilityFileSuffix ()Lorg/gradle/api/provider/Property;
3435
public abstract fun getStabilityInputFiles ()Lorg/gradle/api/file/ConfigurableFileCollection;
3536
public abstract fun getStabilityReferenceFiles ()Lorg/gradle/api/file/ConfigurableFileCollection;
@@ -58,5 +59,6 @@ public abstract class com/skydoves/compose/stability/gradle/StabilityValidationC
5859
public final fun getIncludeTests ()Lorg/gradle/api/provider/Property;
5960
public final fun getOutputDir ()Lorg/gradle/api/file/DirectoryProperty;
6061
public final fun getQuietCheck ()Lorg/gradle/api/provider/Property;
62+
public final fun getStabilityConfigurationFiles ()Lorg/gradle/api/provider/ListProperty;
6163
}
6264

stability-gradle/src/main/kotlin/com/skydoves/compose/stability/gradle/StabilityAnalyzerExtension.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.skydoves.compose.stability.gradle
1818
import org.gradle.api.Action
1919
import org.gradle.api.file.DirectoryProperty
2020
import org.gradle.api.file.ProjectLayout
21+
import org.gradle.api.file.RegularFile
2122
import org.gradle.api.model.ObjectFactory
2223
import org.gradle.api.provider.ListProperty
2324
import org.gradle.api.provider.Property
@@ -180,4 +181,16 @@ public abstract class StabilityValidationConfig @Inject constructor(
180181
*/
181182
public val allowMissingBaseline: Property<Boolean> =
182183
objects.property(Boolean::class.javaObjectType).convention(false)
184+
185+
/**
186+
* List of paths to stability configuration files.
187+
*
188+
* For more information, see this link:
189+
* - [AndroidX stability configuration file](https://developer.android.com/develop/ui/compose/performance/stability/fix#configuration-file)
190+
*
191+
* Default: empty
192+
*/
193+
public val stabilityConfigurationFiles: ListProperty<RegularFile> = objects
194+
.listProperty(RegularFile::class.java)
195+
.convention(emptyList())
183196
}

stability-gradle/src/main/kotlin/com/skydoves/compose/stability/gradle/StabilityAnalyzerGradlePlugin.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ public class StabilityAnalyzerGradlePlugin : KotlinCompilerPluginSupportPlugin {
114114
quietCheck.set(extension.stabilityValidation.quietCheck)
115115
ignoreNonRegressiveChanges.set(extension.stabilityValidation.ignoreNonRegressiveChanges)
116116
allowMissingBaseline.set(extension.stabilityValidation.allowMissingBaseline)
117+
stabilityConfigurationFiles.set(extension.stabilityValidation.stabilityConfigurationFiles)
117118
}
118119

119120
// Make check task depend on stabilityCheck if enabled (only if check task exists)
@@ -180,6 +181,7 @@ public class StabilityAnalyzerGradlePlugin : KotlinCompilerPluginSupportPlugin {
180181
stabilityFileSuffix.set(variant.name)
181182
ignoreNonRegressiveChanges.set(extension.stabilityValidation.ignoreNonRegressiveChanges)
182183
allowMissingBaseline.set(extension.stabilityValidation.allowMissingBaseline)
184+
stabilityConfigurationFiles.set(extension.stabilityValidation.stabilityConfigurationFiles)
183185
}
184186

185187
aggregateDumpTask.configure {

stability-gradle/src/main/kotlin/com/skydoves/compose/stability/gradle/StabilityCheckTask.kt

Lines changed: 39 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@ package com.skydoves.compose.stability.gradle
1818
import org.gradle.api.DefaultTask
1919
import org.gradle.api.GradleException
2020
import org.gradle.api.file.ConfigurableFileCollection
21+
import org.gradle.api.file.RegularFile
2122
import org.gradle.api.provider.ListProperty
2223
import org.gradle.api.provider.Property
2324
import org.gradle.api.tasks.Input
2425
import org.gradle.api.tasks.InputFiles
2526
import org.gradle.api.tasks.Optional
27+
import org.gradle.api.tasks.PathSensitive
28+
import org.gradle.api.tasks.PathSensitivity
2629
import org.gradle.api.tasks.TaskAction
2730

2831
/**
@@ -36,12 +39,14 @@ public abstract class StabilityCheckTask : DefaultTask() {
3639
* Input file containing current stability information from compiler.
3740
*/
3841
@get:InputFiles
42+
@get:PathSensitive(PathSensitivity.RELATIVE)
3943
public abstract val stabilityInputFiles: ConfigurableFileCollection
4044

4145
/**
4246
* Directory containing the reference stability file.
4347
*/
4448
@get:InputFiles
49+
@get:PathSensitive(PathSensitivity.RELATIVE)
4550
public abstract val stabilityReferenceFiles: ConfigurableFileCollection
4651

4752
/**
@@ -90,6 +95,10 @@ public abstract class StabilityCheckTask : DefaultTask() {
9095
@get:Input
9196
public abstract val allowMissingBaseline: Property<Boolean>
9297

98+
@get:InputFiles
99+
@get:PathSensitive(PathSensitivity.RELATIVE)
100+
public abstract val stabilityConfigurationFiles: ListProperty<RegularFile>
101+
93102
init {
94103
group = "verification"
95104
description = "Check composable stability against reference file"
@@ -142,8 +151,25 @@ public abstract class StabilityCheckTask : DefaultTask() {
142151

143152
val currentStability = parseStabilityFromCompiler(inputFile)
144153
val referenceStability = parseStabilityFile(referenceFile)
154+
155+
val stabilityConfigurationMatchers = stabilityConfigurationFiles.get()
156+
.flatMap { configRegularFile ->
157+
val file = configRegularFile.asFile
158+
159+
if (!file.exists()) {
160+
return@flatMap emptyList()
161+
}
162+
163+
StabilityConfigParser.fromFile(file.path).stableTypeMatchers
164+
}
165+
145166
val differences =
146-
compareStability(currentStability, referenceStability, ignoreNonRegressiveChanges.get())
167+
compareStability(
168+
currentStability,
169+
referenceStability,
170+
ignoreNonRegressiveChanges.get(),
171+
stabilityConfigurationMatchers,
172+
)
147173

148174
if (differences.isNotEmpty()) {
149175
val message = buildString {
@@ -465,82 +491,6 @@ public abstract class StabilityCheckTask : DefaultTask() {
465491

466492
return entries
467493
}
468-
469-
private fun compareStability(
470-
current: Map<String, StabilityEntry>,
471-
reference: Map<String, StabilityEntry>,
472-
ignoreNonRegressiveChanges: Boolean = false,
473-
474-
): List<StabilityDifference> {
475-
val differences = mutableListOf<StabilityDifference>()
476-
477-
// Check for new functions
478-
current.keys.subtract(reference.keys).forEach { functionName ->
479-
if (!ignoreNonRegressiveChanges || !current.getValue(functionName).skippable) {
480-
differences.add(StabilityDifference.NewFunction(functionName))
481-
}
482-
}
483-
484-
// Check for removed functions
485-
if (!ignoreNonRegressiveChanges) {
486-
reference.keys.subtract(current.keys).forEach { functionName ->
487-
differences.add(StabilityDifference.RemovedFunction(functionName))
488-
}
489-
}
490-
491-
// Check for changed stability
492-
current.keys.intersect(reference.keys).forEach { functionName ->
493-
val currentEntry = current[functionName]!!
494-
val referenceEntry = reference[functionName]!!
495-
496-
// Check skippability change
497-
if (currentEntry.skippable != referenceEntry.skippable &&
498-
(!ignoreNonRegressiveChanges || !currentEntry.skippable)
499-
) {
500-
differences.add(
501-
StabilityDifference.SkippabilityChanged(
502-
functionName,
503-
referenceEntry.skippable,
504-
currentEntry.skippable,
505-
),
506-
)
507-
}
508-
509-
// Check if parameter count changed
510-
if (currentEntry.parameters.size != referenceEntry.parameters.size) {
511-
if (
512-
!ignoreNonRegressiveChanges ||
513-
currentEntry.parameters.any { it.stability != "STABLE" }
514-
) {
515-
differences.add(
516-
StabilityDifference.ParameterCountChanged(
517-
functionName,
518-
referenceEntry.parameters.size,
519-
currentEntry.parameters.size,
520-
),
521-
)
522-
}
523-
} else {
524-
// Check parameter stability changes (only if count is the same)
525-
currentEntry.parameters.zip(referenceEntry.parameters).forEach { (current, ref) ->
526-
if (current.stability != ref.stability &&
527-
(!ignoreNonRegressiveChanges || current.stability != "STABLE")
528-
) {
529-
differences.add(
530-
StabilityDifference.ParameterStabilityChanged(
531-
functionName,
532-
current.name,
533-
ref.stability,
534-
current.stability,
535-
),
536-
)
537-
}
538-
}
539-
}
540-
}
541-
542-
return differences
543-
}
544494
}
545495

546496
/**
@@ -549,8 +499,18 @@ public abstract class StabilityCheckTask : DefaultTask() {
549499
internal sealed class StabilityDifference {
550500
public abstract fun format(): String
551501

552-
public data class NewFunction(val name: String) : StabilityDifference() {
553-
override fun format(): String = "+ $name (new composable)"
502+
public data class NewFunction(
503+
val name: String,
504+
val parameters: List<ParameterInfo>,
505+
) : StabilityDifference() {
506+
override fun format(): String {
507+
return if (parameters.isEmpty()) {
508+
"+ $name (new composable)"
509+
} else {
510+
"+ $name (new composable):\n" +
511+
parameters.joinToString("\n") { " ${it.name}: ${it.stability}" }
512+
}
513+
}
554514
}
555515

556516
public data class RemovedFunction(val name: String) : StabilityDifference() {
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Designed and developed by 2025 skydoves (Jaewoong Eum)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.skydoves.compose.stability.gradle
17+
18+
internal fun compareStability(
19+
current: Map<String, StabilityEntry>,
20+
reference: Map<String, StabilityEntry>,
21+
ignoreNonRegressiveChanges: Boolean = false,
22+
forceStableTypes: List<FqNameMatcher> = emptyList(),
23+
24+
): List<StabilityDifference> {
25+
val differences = mutableListOf<StabilityDifference>()
26+
27+
// Check for new functions
28+
current.keys.subtract(reference.keys).forEach { functionName ->
29+
if (!ignoreNonRegressiveChanges || !current.getValue(functionName).isStable(forceStableTypes)) {
30+
val parametersWithFixedStability = current.getValue(functionName).parameters
31+
.map { parameter ->
32+
parameter.copy(
33+
stability = if (parameter.isStable(forceStableTypes)) {
34+
"STABLE"
35+
} else {
36+
parameter.stability
37+
},
38+
)
39+
}
40+
41+
differences.add(StabilityDifference.NewFunction(functionName, parametersWithFixedStability))
42+
}
43+
}
44+
45+
// Check for removed functions
46+
if (!ignoreNonRegressiveChanges) {
47+
reference.keys.subtract(current.keys).forEach { functionName ->
48+
differences.add(StabilityDifference.RemovedFunction(functionName))
49+
}
50+
}
51+
52+
// Check for changed stability
53+
current.keys.intersect(reference.keys).forEach { functionName ->
54+
val currentEntry = current[functionName]!!
55+
val referenceEntry = reference[functionName]!!
56+
57+
// Check skippability change
58+
if (currentEntry.isStable(forceStableTypes) != referenceEntry.isStable(forceStableTypes) &&
59+
(!ignoreNonRegressiveChanges || !currentEntry.isStable(forceStableTypes))
60+
) {
61+
differences.add(
62+
StabilityDifference.SkippabilityChanged(
63+
functionName,
64+
referenceEntry.isStable(forceStableTypes),
65+
currentEntry.isStable(forceStableTypes),
66+
),
67+
)
68+
}
69+
70+
// Check if parameter count changed
71+
if (currentEntry.parameters.size != referenceEntry.parameters.size) {
72+
if (
73+
!ignoreNonRegressiveChanges ||
74+
currentEntry.parameters.any { !it.isStable(forceStableTypes) }
75+
) {
76+
differences.add(
77+
StabilityDifference.ParameterCountChanged(
78+
functionName,
79+
referenceEntry.parameters.size,
80+
currentEntry.parameters.size,
81+
),
82+
)
83+
}
84+
} else {
85+
// Check parameter stability changes (only if count is the same)
86+
currentEntry.parameters.zip(referenceEntry.parameters).forEach { (current, ref) ->
87+
if (current.stability != ref.stability &&
88+
(!ignoreNonRegressiveChanges || !current.isStable(forceStableTypes))
89+
) {
90+
differences.add(
91+
StabilityDifference.ParameterStabilityChanged(
92+
functionName,
93+
current.name,
94+
ref.stability,
95+
current.stability,
96+
),
97+
)
98+
}
99+
}
100+
}
101+
}
102+
103+
return differences
104+
}
105+
106+
private fun StabilityEntry.isStable(forceStableTypes: List<FqNameMatcher>): Boolean {
107+
return skippable ||
108+
(parameters.isNotEmpty() && parameters.all { it.isStable(forceStableTypes) })
109+
}
110+
111+
private fun ParameterInfo.isStable(forceStableTypes: List<FqNameMatcher>): Boolean {
112+
return stability == "STABLE" || forceStableTypes.any { it.matches(type) }
113+
}

0 commit comments

Comments
 (0)