Skip to content

Commit a0e9a5c

Browse files
authored
Merge pull request #12 from skydoves/fix/recursive-analysis
Prevent recursive detection for the complicated type structures
2 parents 02e1c63 + e15767d commit a0e9a5c

File tree

3 files changed

+65
-3
lines changed

3 files changed

+65
-3
lines changed

compose-stability-analyzer-idea/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ All notable changes to the IntelliJ IDEA plugin will be documented in this file.
66

77
### Fixed
88
- Fixed stability analysis for Compose shape types (RoundedCornerShape, CircleShape, etc.) to correctly show as STABLE instead of RUNTIME
9+
- Fixed StackOverflowError when analyzing recursive or self-referential types (Issue #11)
910
- Improved consistency between IDEA plugin and compiler plugin stability inference
1011
- Added Compose Foundation shapes to known stable types list
12+
- Added cycle detection to prevent infinite recursion in type stability analysis
1113

1214
### Improved
1315
- Enhanced accuracy of stability analysis to match compiler plugin behavior
16+
- Better handling of complex function type aliases and deeply nested generic types
1417

1518
---
1619

compose-stability-analyzer-idea/src/main/kotlin/com/skydoves/compose/stability/idea/k2/KtStabilityInferencer.kt

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,45 @@ internal class KtStabilityInferencer {
5858
private val settings: StabilitySettingsState
5959
get() = StabilitySettingsState.getInstance()
6060

61+
// Cycle detection for recursive types
62+
private val analyzingTypes = ThreadLocal.withInitial { mutableSetOf<String>() }
63+
6164
/**
6265
* Analyzes a Kotlin type to determine its stability.
6366
* Main entry point for K2-based stability analysis.
6467
*/
6568
context(KaSession)
6669
internal fun ktStabilityOf(type: KaType): KtStability {
67-
// Get the original type string BEFORE stripping nullability (preserves annotations)
68-
val originalTypeString = type.render(position = org.jetbrains.kotlin.types.Variance.INVARIANT)
70+
val originalTypeString = try {
71+
type.render(position = org.jetbrains.kotlin.types.Variance.INVARIANT)
72+
} catch (e: StackOverflowError) {
73+
return KtStability.Runtime(
74+
className = "Unknown",
75+
reason = "Unable to render type due to complexity",
76+
)
77+
}
78+
79+
val currentlyAnalyzing = analyzingTypes.get()
80+
if (originalTypeString in currentlyAnalyzing) {
81+
return KtStability.Runtime(
82+
className = originalTypeString,
83+
reason = StabilityConstants.Messages.CIRCULAR_REFERENCE,
84+
)
85+
}
86+
87+
currentlyAnalyzing.add(originalTypeString)
88+
try {
89+
return ktStabilityOfInternal(type, originalTypeString)
90+
} finally {
91+
currentlyAnalyzing.remove(originalTypeString)
92+
}
93+
}
6994

95+
/**
96+
* Internal implementation separated for proper cleanup.
97+
*/
98+
context(KaSession)
99+
private fun ktStabilityOfInternal(type: KaType, originalTypeString: String): KtStability {
70100
// 1. Nullable types - MUST be checked first to strip nullability
71101
// Use KaTypeNullability enum for compatibility with Android Studio AI-243
72102
val nonNullableType = if (type.isMarkedNullable) {

stability-compiler/src/main/kotlin/com/skydoves/compose/stability/compiler/lower/StabilityAnalyzerTransformer.kt

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ public class StabilityAnalyzerTransformer(
5555
private val irBuilder = RecompositionIrBuilder(context)
5656
private var irBuilderInitialized = false
5757

58+
// Cycle detection for recursive types
59+
private val analyzingTypes = ThreadLocal.withInitial { mutableSetOf<String>() }
60+
5861
override fun visitFunctionNew(declaration: IrFunction): IrStatement {
5962
val functionName = declaration.name.asString()
6063
val fqName = declaration.kotlinFqName.asString()
@@ -248,8 +251,34 @@ public class StabilityAnalyzerTransformer(
248251
}
249252

250253
val classSymbol = type.classOrNull
251-
val fqName = type.classFqName?.asString()
254+
val fqName = try {
255+
type.classFqName?.asString()
256+
} catch (e: StackOverflowError) {
257+
return ParameterStability.RUNTIME
258+
}
259+
260+
val typeId = fqName ?: classSymbol?.owner?.name?.asString() ?: type.render()
261+
val currentlyAnalyzing = analyzingTypes.get()
262+
if (typeId in currentlyAnalyzing) {
263+
return ParameterStability.RUNTIME
264+
}
252265

266+
currentlyAnalyzing.add(typeId)
267+
try {
268+
return analyzeTypeStabilityInternal(type, classSymbol, fqName)
269+
} finally {
270+
currentlyAnalyzing.remove(typeId)
271+
}
272+
}
273+
274+
/**
275+
* Internal implementation separated for proper cleanup.
276+
*/
277+
private fun analyzeTypeStabilityInternal(
278+
type: IrType,
279+
classSymbol: org.jetbrains.kotlin.ir.symbols.IrClassSymbol?,
280+
fqName: String?,
281+
): ParameterStability {
253282
// 2b. Type parameters (T, E, K, V in generics) - RUNTIME
254283
// If we can't resolve to a class, it's likely a type parameter
255284
if (classSymbol == null) {

0 commit comments

Comments
 (0)