Skip to content

Commit b85adf7

Browse files
authored
Implement stabilityPatternToRegex (#110)
1 parent 30f8713 commit b85adf7

File tree

3 files changed

+90
-23
lines changed

3 files changed

+90
-23
lines changed

compose-stability-analyzer-idea/src/main/kotlin/com/skydoves/compose/stability/idea/settings/StabilityProjectSettingsState.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,7 @@ public class StabilityProjectSettingsState :
7171

7272
patterns.mapNotNull { pattern ->
7373
try {
74-
pattern
75-
.replace(".", "\\.")
76-
.replace("*", ".*")
77-
.toRegex()
74+
stabilityPatternToRegex(pattern)
7875
} catch (e: Exception) {
7976
null
8077
}

compose-stability-analyzer-idea/src/main/kotlin/com/skydoves/compose/stability/idea/settings/StabilitySettingsState.kt

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,7 @@ public class StabilitySettingsState : PersistentStateComponent<StabilitySettings
144144
.filter { it.isNotEmpty() && !it.startsWith("#") } // Support comments
145145
.map { pattern ->
146146
try {
147-
// Convert glob-style wildcards to regex
148-
pattern
149-
.replace(".", "\\.")
150-
.replace("*", ".*")
151-
.toRegex()
147+
stabilityPatternToRegex(pattern)
152148
} catch (e: Exception) {
153149
// If regex is invalid, treat as literal string
154150
Regex.escape(pattern).toRegex()
@@ -183,11 +179,7 @@ public class StabilitySettingsState : PersistentStateComponent<StabilitySettings
183179
// Convert patterns to regex
184180
patterns.mapNotNull { pattern ->
185181
try {
186-
// Convert glob-style wildcards to regex
187-
pattern
188-
.replace(".", "\\.")
189-
.replace("*", ".*")
190-
.toRegex()
182+
stabilityPatternToRegex(pattern)
191183
} catch (e: Exception) {
192184
null // Skip invalid patterns
193185
}
@@ -203,3 +195,43 @@ public class StabilitySettingsState : PersistentStateComponent<StabilitySettings
203195
}
204196
}
205197
}
198+
199+
/**
200+
* Converts a Compose stability configuration pattern to a [Regex].
201+
*
202+
* Follows the same semantics as the Compose compiler's stability configuration file:
203+
* - `**` matches any sequence of characters including dots (multi-segment wildcard)
204+
* - `*` matches any sequence of characters except dots (single-segment wildcard)
205+
* - `.` is treated as a literal dot
206+
* - All other characters are regex-escaped
207+
*
208+
* Examples:
209+
* - `com.datalayer.*` matches `com.datalayer.Foo` but NOT `com.datalayer.sub.Foo`
210+
* - `com.datalayer.**` matches `com.datalayer.Foo` AND `com.datalayer.sub.Foo`
211+
*/
212+
internal fun stabilityPatternToRegex(pattern: String): Regex {
213+
val regex = buildString {
214+
var i = 0
215+
while (i < pattern.length) {
216+
when {
217+
pattern[i] == '*' && i + 1 < pattern.length && pattern[i + 1] == '*' -> {
218+
append(".*")
219+
i += 2
220+
}
221+
pattern[i] == '*' -> {
222+
append("[^.]*")
223+
i++
224+
}
225+
pattern[i] == '.' -> {
226+
append("\\.")
227+
i++
228+
}
229+
else -> {
230+
append(Regex.escape(pattern[i].toString()))
231+
i++
232+
}
233+
}
234+
}
235+
}
236+
return regex.toRegex()
237+
}

compose-stability-analyzer-idea/src/test/kotlin/com/skydoves/compose/stability/idea/settings/StabilitySettingsStateTest.kt

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,9 @@ class StabilitySettingsStateTest {
114114
val patterns = settings.getIgnoredPatternsAsRegex()
115115
assertEquals(1, patterns.size)
116116

117+
// Single * matches only one package segment (no dots)
117118
assertTrue(patterns[0].matches("com.example.User"))
118-
assertTrue(patterns[0].matches("com.example.ui.Card"))
119+
assertFalse(patterns[0].matches("com.example.ui.Card"))
119120
assertFalse(patterns[0].matches("com.other.User"))
120121
}
121122

@@ -170,14 +171,31 @@ class StabilitySettingsStateTest {
170171
}
171172

172173
@Test
173-
fun testGetIgnoredPatternsAsRegex_handlesWildcards() {
174+
fun testGetIgnoredPatternsAsRegex_singleStarMatchesSingleSegment() {
174175
val settings = StabilitySettingsState()
175176
settings.ignoredTypePatterns = "com.example.*"
176177

177178
val patterns = settings.getIgnoredPatternsAsRegex()
178179
assertEquals(1, patterns.size)
179180

180-
// Wildcard should match multiple segments
181+
// Single * matches only one segment (no dots)
182+
assertTrue(patterns[0].matches("com.example.User"))
183+
assertFalse(patterns[0].matches("com.example.ui.UserCard"))
184+
assertFalse(patterns[0].matches("com.example.data.model.User"))
185+
186+
// Should not match different package
187+
assertFalse(patterns[0].matches("com.other.User"))
188+
}
189+
190+
@Test
191+
fun testGetIgnoredPatternsAsRegex_doubleStarMatchesMultipleSegments() {
192+
val settings = StabilitySettingsState()
193+
settings.ignoredTypePatterns = "com.example.**"
194+
195+
val patterns = settings.getIgnoredPatternsAsRegex()
196+
assertEquals(1, patterns.size)
197+
198+
// Double ** matches across package boundaries (including dots)
181199
assertTrue(patterns[0].matches("com.example.User"))
182200
assertTrue(patterns[0].matches("com.example.ui.UserCard"))
183201
assertTrue(patterns[0].matches("com.example.data.model.User"))
@@ -262,7 +280,7 @@ class StabilitySettingsStateTest {
262280
}
263281

264282
@Test
265-
fun testGetCustomStableTypesAsRegex_handlesInvalidRegex() {
283+
fun testGetCustomStableTypesAsRegex_handlesSpecialCharacters() {
266284
val configFile = tempFolder.newFile("stability-config.txt")
267285
configFile.writeText(
268286
"""
@@ -277,10 +295,11 @@ class StabilitySettingsStateTest {
277295

278296
val patterns = settings.getCustomStableTypesAsRegex()
279297

280-
// Should skip invalid patterns and only include valid ones
281-
assertEquals(2, patterns.size)
298+
// All patterns are valid (special characters are properly escaped)
299+
assertEquals(3, patterns.size)
282300
assertTrue(patterns[0].matches("com.example.ValidClass"))
283-
assertTrue(patterns[1].matches("com.example.AnotherValid"))
301+
assertTrue(patterns[1].matches("[invalid(regex"))
302+
assertTrue(patterns[2].matches("com.example.AnotherValid"))
284303
}
285304

286305
@Test
@@ -378,8 +397,8 @@ class StabilitySettingsStateTest {
378397
}
379398

380399
@Test
381-
fun testGetCustomStableTypesAsRegex_wildcardMatching() {
382-
val configFile = tempFolder.newFile("stability-config.txt")
400+
fun testGetCustomStableTypesAsRegex_singleStarWildcard() {
401+
val configFile = tempFolder.newFile("stability-config-single.txt")
383402
configFile.writeText("com.example.stable.*")
384403

385404
val settings = StabilitySettingsState()
@@ -388,8 +407,27 @@ class StabilitySettingsStateTest {
388407
val patterns = settings.getCustomStableTypesAsRegex()
389408
assertEquals(1, patterns.size)
390409

410+
// Single * matches only direct children
411+
assertTrue(patterns[0].matches("com.example.stable.User"))
412+
assertFalse(patterns[0].matches("com.example.stable.data.Repository"))
413+
assertFalse(patterns[0].matches("com.example.unstable.User"))
414+
}
415+
416+
@Test
417+
fun testGetCustomStableTypesAsRegex_doubleStarWildcard() {
418+
val configFile = tempFolder.newFile("stability-config-double.txt")
419+
configFile.writeText("com.example.stable.**")
420+
421+
val settings = StabilitySettingsState()
422+
settings.stabilityConfigurationPath = configFile.absolutePath
423+
424+
val patterns = settings.getCustomStableTypesAsRegex()
425+
assertEquals(1, patterns.size)
426+
427+
// Double ** matches across subpackages
391428
assertTrue(patterns[0].matches("com.example.stable.User"))
392429
assertTrue(patterns[0].matches("com.example.stable.data.Repository"))
430+
assertTrue(patterns[0].matches("com.example.stable.data.model.Entity"))
393431
assertFalse(patterns[0].matches("com.example.unstable.User"))
394432
}
395433
}

0 commit comments

Comments
 (0)