Skip to content

Commit 2740722

Browse files
Fix a bug with private superclass interceptors (#175)
We weren't publishing our added functions in class metadata, and that was causing private @InterceptTest properties to be missed. Adding it was made complicated by the fact that we started getting fake overrides, that we now need to remove. Co-authored-by: Jesse Wilson <jwilson@squareup.com>
1 parent 0348d04 commit 2740722

File tree

7 files changed

+148
-2
lines changed

7 files changed

+148
-2
lines changed

burst-gradle-plugin/src/test/kotlin/app/cash/burst/gradle/TestInterceptorGradlePluginTest.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,27 @@ class TestInterceptorGradlePluginTest {
131131
)
132132
}
133133
}
134+
135+
@Test
136+
fun abstractClassHasSuperClassThatIntercepts() {
137+
val tester = GradleTester("interceptorAcrossModules")
138+
tester.cleanAndBuild(":lib:test")
139+
140+
with(tester.readTestSuite("app.cash.burst.tests.BottomTest")) {
141+
assertThat(systemOut).isEqualTo(
142+
"""
143+
|> intercepting top
144+
| running bottom
145+
|< intercepted top
146+
|> intercepting top
147+
| running middle
148+
|< intercepted top
149+
|> intercepting top
150+
| running top
151+
|< intercepted top
152+
|
153+
""".trimMargin(),
154+
)
155+
}
156+
}
134157
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package app.cash.burst.tests
2+
3+
import kotlin.test.Test
4+
5+
class BottomTest : MiddleTest() {
6+
@Test
7+
fun testBottom() {
8+
println(" running bottom")
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package app.cash.burst.tests
2+
3+
import app.cash.burst.testlib.TopTest
4+
import kotlin.test.Test
5+
6+
abstract class MiddleTest : TopTest() {
7+
@Test
8+
fun testMiddle() {
9+
println(" running middle")
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package app.cash.burst.testlib
2+
3+
import app.cash.burst.InterceptTest
4+
import kotlin.test.Test
5+
6+
abstract class TopTest {
7+
@InterceptTest
8+
private val interceptor = BasicInterceptor("top")
9+
10+
@Test
11+
fun testTop() {
12+
println(" running top")
13+
}
14+
}

burst-kotlin-plugin-tests/src/test/kotlin/app/cash/burst/kotlin/TestInterceptorKotlinPluginTest.kt

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,4 +1535,70 @@ class TestInterceptorKotlinPluginTest {
15351535
"intercepted",
15361536
)
15371537
}
1538+
1539+
@Test
1540+
fun abstractClassInTheMiddle() {
1541+
val log = BurstTester(
1542+
packageName = "com.example",
1543+
).compileAndRun(
1544+
SourceFile.kotlin(
1545+
"Main.kt",
1546+
"""
1547+
package com.example
1548+
1549+
import app.cash.burst.InterceptTest
1550+
import app.cash.burst.TestFunction
1551+
import app.cash.burst.TestInterceptor
1552+
import kotlin.test.AfterTest
1553+
import kotlin.test.BeforeTest
1554+
import kotlin.test.Test
1555+
1556+
abstract class TopTest {
1557+
@InterceptTest
1558+
val interceptor = object : TestInterceptor {
1559+
override fun intercept(testFunction: TestFunction) {
1560+
log("intercepting ${'$'}{testFunction.className}.${'$'}{testFunction.functionName}")
1561+
testFunction()
1562+
}
1563+
}
1564+
1565+
@Test
1566+
fun testTop() {
1567+
log("top")
1568+
}
1569+
}
1570+
1571+
abstract class MiddleTest : TopTest() {
1572+
@Test
1573+
fun testMiddle() {
1574+
log("middle")
1575+
}
1576+
}
1577+
1578+
class BottomTest : MiddleTest() {
1579+
@Test
1580+
fun testBottom() {
1581+
log("bottom")
1582+
}
1583+
}
1584+
1585+
fun main(vararg args: String) {
1586+
val test = BottomTest()
1587+
test.testTop()
1588+
test.testMiddle()
1589+
test.testBottom()
1590+
}
1591+
""",
1592+
),
1593+
)
1594+
1595+
assertThat(log).containsExactly(
1596+
"intercepting TopTest.testTop",
1597+
"top",
1598+
"intercepting MiddleTest.testMiddle",
1599+
"middle",
1600+
"intercepting BottomTest.testBottom",
1601+
"bottom",
1602+
)
1603+
}
15381604
}

burst-kotlin-plugin/src/main/kotlin/app/cash/burst/kotlin/HierarchyInterceptorInjector.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
package app.cash.burst.kotlin
1919

2020
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
21+
import org.jetbrains.kotlin.ir.backend.js.utils.isDispatchReceiver
2122
import org.jetbrains.kotlin.ir.declarations.IrClass
2223
import org.jetbrains.kotlin.ir.declarations.IrProperty
2324
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
2425
import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI
26+
import org.jetbrains.kotlin.ir.types.classFqName
2527
import org.jetbrains.kotlin.ir.util.functions
2628
import org.jetbrains.kotlin.ir.util.isSubtypeOfClass
2729
import org.jetbrains.kotlin.ir.util.properties
@@ -47,7 +49,7 @@ internal class HierarchyInterceptorInjector(
4749
// If this class directly declares an intercept() function, return that. Otherwise, our injected
4850
// symbol would collide with that one.
4951
val existing = classDeclaration.interceptFunction
50-
if (existing != null) return existing
52+
if (existing != null && !existing.isFakeOverride) return existing
5153

5254
// Rewrite the superclass first!
5355
val superClass = classDeclaration.superClass
@@ -77,6 +79,7 @@ internal class HierarchyInterceptorInjector(
7779
originalParent = classDeclaration,
7880
interceptorProperties = interceptorProperties,
7981
superclassIntercept = superClassInterceptFunction,
82+
existingIntercept = existing,
8083
)
8184

8285
for (function in originalFunctions) {
@@ -103,7 +106,15 @@ internal class HierarchyInterceptorInjector(
103106

104107
/** The `intercept()` function declared by this class. */
105108
private val IrClass.interceptFunction: IrSimpleFunction?
106-
get() = functions.firstOrNull { burstApis.testInterceptorIntercept in it.overriddenSymbols }
109+
get() {
110+
val other = burstApis.testInterceptorIntercept.owner
111+
return functions.firstOrNull {
112+
it.name == other.name &&
113+
it.parameters.size == other.parameters.size &&
114+
it.parameters[0].isDispatchReceiver &&
115+
it.parameters[1].type.classFqName == other.parameters[1].type.classFqName
116+
}
117+
}
107118

108119
private fun unexpectedInterceptTest(property: IrProperty): Nothing {
109120
throw BurstCompilationException(

burst-kotlin-plugin/src/main/kotlin/app/cash/burst/kotlin/InterceptorInjector.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ internal class InterceptorInjector(
120120
private val originalParent: IrClass,
121121
private val interceptorProperties: List<IrProperty>,
122122
private val superclassIntercept: IrSimpleFunction?,
123+
private val existingIntercept: IrSimpleFunction?,
123124
) {
124125
private val packageName = originalParent.packageFqName?.asString() ?: ""
125126
private val className = generateSequence(originalParent) { it.parentClassOrNull }
@@ -157,6 +158,11 @@ internal class InterceptorInjector(
157158
fun defineIntercept(): IrSimpleFunction {
158159
check(interceptFunctionSymbol == null) { "already defined?!" }
159160

161+
// If there's already a fake override, remove it.
162+
if (existingIntercept != null) {
163+
originalParent.declarations.remove(existingIntercept)
164+
}
165+
160166
// TODO: don't add 'implements TestInterceptor' if it already does.
161167
originalParent.superTypes += burstApis.testInterceptorType
162168

@@ -295,6 +301,11 @@ internal class InterceptorInjector(
295301

296302
this.interceptFunctionSymbol = function.symbol
297303

304+
// Make this function visible to subclasses in other modules.
305+
if (existingIntercept == null) {
306+
pluginContext.metadataDeclarationRegistrar.registerFunctionAsMetadataVisible(function)
307+
}
308+
298309
return function
299310
}
300311

0 commit comments

Comments
 (0)