From 9b851dd0e39f7d2e5f006d6f2c06ed38e289fdaa Mon Sep 17 00:00:00 2001 From: Ben Lee Date: Fri, 26 Jun 2026 15:01:13 -0700 Subject: [PATCH 1/2] Use platform classloader for lint invokers --- src/cli/AndroidLintCliInvoker.kt | 7 ++++++- src/cli/AndroidLintCliInvokerCache.kt | 13 +++++++++++-- tests/src/cli/AndroidLintCliInvokerCacheTest.kt | 10 +++++----- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/cli/AndroidLintCliInvoker.kt b/src/cli/AndroidLintCliInvoker.kt index 2d2ceaf..642bb5a 100644 --- a/src/cli/AndroidLintCliInvoker.kt +++ b/src/cli/AndroidLintCliInvoker.kt @@ -85,7 +85,12 @@ class AndroidLintCliInvoker( const val ERRNO_INTERNAL_CONTINUE = 100 fun createUsingJars( - parentClassloader: ClassLoader = this::class.java.classLoader, + // Parent on the platform classloader (JDK classes only) so lint loads its entire runtime — + // crucially kotlin-stdlib AND kotlin-reflect — from its own deploy jar. Parenting on the + // worker's classloader instead lets the worker's kotlin-stdlib shadow lint's, and since the + // worker has no kotlin-reflect, Kotlin reflection inside lint detectors (e.g. the partial + // analysis "fx" checks) silently fails. This mirrors AGP's AndroidLintWorkAction. + parentClassloader: ClassLoader = ClassLoader.getPlatformClassLoader(), vararg jars: Path, ): AndroidLintCliInvoker { require(jars.isNotEmpty()) { diff --git a/src/cli/AndroidLintCliInvokerCache.kt b/src/cli/AndroidLintCliInvokerCache.kt index e5cda61..8f3ea7a 100644 --- a/src/cli/AndroidLintCliInvokerCache.kt +++ b/src/cli/AndroidLintCliInvokerCache.kt @@ -15,7 +15,12 @@ import java.nio.file.Path * while a request against the old jar is still running, so the old classloader is only disposed * once its last lease is released. */ -internal class AndroidLintCliInvokerCache { +internal class AndroidLintCliInvokerCache( + // The classloader lint's deploy jar is parented on. Defaults to the platform classloader so lint + // is fully isolated from the worker's classpath. Tests override this to resolve stub lint classes + // from the test classpath. + private val parentClassloader: ClassLoader = ClassLoader.getPlatformClassLoader(), +) { private class Entry( val key: String, val invoker: AndroidLintCliInvoker, @@ -44,7 +49,11 @@ internal class AndroidLintCliInvokerCache { } current = null } - val invoker = AndroidLintCliInvoker.createUsingJars(jars = jars.toTypedArray()) + val invoker = + AndroidLintCliInvoker.createUsingJars( + parentClassloader = parentClassloader, + jars = jars.toTypedArray(), + ) createdCount += 1 current = Entry(key = key, invoker = invoker, activeLeases = 1) return invoker diff --git a/tests/src/cli/AndroidLintCliInvokerCacheTest.kt b/tests/src/cli/AndroidLintCliInvokerCacheTest.kt index b97c319..fc20d57 100644 --- a/tests/src/cli/AndroidLintCliInvokerCacheTest.kt +++ b/tests/src/cli/AndroidLintCliInvokerCacheTest.kt @@ -30,7 +30,7 @@ class AndroidLintCliInvokerCacheTest { @Test fun `acquiring twice with an unchanged jar reuses the invoker`() { - val cache = AndroidLintCliInvokerCache() + val cache = AndroidLintCliInvokerCache(parentClassloader = javaClass.classLoader) val jar = writeJar("lint.jar", entryCount = 1) val first = cache.acquire(listOf(jar)) @@ -44,7 +44,7 @@ class AndroidLintCliInvokerCacheTest { @Test fun `a changed jar evicts the cached invoker and disposes it`() { - val cache = AndroidLintCliInvokerCache() + val cache = AndroidLintCliInvokerCache(parentClassloader = javaClass.classLoader) val jar = writeJar("lint.jar", entryCount = 1) val first = cache.acquire(listOf(jar)) @@ -62,7 +62,7 @@ class AndroidLintCliInvokerCacheTest { @Test fun `eviction defers disposal until the last lease is released`() { - val cache = AndroidLintCliInvokerCache() + val cache = AndroidLintCliInvokerCache(parentClassloader = javaClass.classLoader) val jar = writeJar("lint.jar", entryCount = 1) val first = cache.acquire(listOf(jar)) @@ -83,7 +83,7 @@ class AndroidLintCliInvokerCacheTest { @Test fun `invoke creates a fresh Main per call and applies checkDependencies per call`() { - val cache = AndroidLintCliInvokerCache() + val cache = AndroidLintCliInvokerCache(parentClassloader = javaClass.classLoader) val jar = writeJar("lint.jar", entryCount = 1) val invoker = cache.acquire(listOf(jar)) @@ -103,7 +103,7 @@ class AndroidLintCliInvokerCacheTest { @Test fun `concurrent invocations share one classloader with independent Main instances`() { - val cache = AndroidLintCliInvokerCache() + val cache = AndroidLintCliInvokerCache(parentClassloader = javaClass.classLoader) val jar = writeJar("lint.jar", entryCount = 1) // Both runs must be in flight at the same time before either completes. Main.runBarrier = CyclicBarrier(2) From 6e14e08ce20673a4a21a304ffdd73e4ab1e0e945 Mon Sep 17 00:00:00 2001 From: Ben Lee Date: Fri, 26 Jun 2026 17:58:42 -0700 Subject: [PATCH 2/2] more --- tests/src/cli/AndroidLintActionTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/src/cli/AndroidLintActionTest.kt b/tests/src/cli/AndroidLintActionTest.kt index b75181c..b60b2a6 100644 --- a/tests/src/cli/AndroidLintActionTest.kt +++ b/tests/src/cli/AndroidLintActionTest.kt @@ -26,7 +26,7 @@ class AndroidLintActionTest { @Test fun `sequential work requests reuse a single cached lint classloader`() { - val cache = AndroidLintCliInvokerCache() + val cache = AndroidLintCliInvokerCache(parentClassloader = javaClass.classLoader) val executor = AndroidLintAction.AndroidLintExecutor(cache) val jar = writeStubLintJar() @@ -43,7 +43,7 @@ class AndroidLintActionTest { fun `lint issue exit codes do not fail the work request or evict the cache`() { // By design the action always succeeds and produces a report; enforcement happens in the // android_lint_test validator. A lint exit code must not kill or degrade the worker. - val cache = AndroidLintCliInvokerCache() + val cache = AndroidLintCliInvokerCache(parentClassloader = javaClass.classLoader) val executor = AndroidLintAction.AndroidLintExecutor(cache) val jar = writeStubLintJar() @@ -61,7 +61,7 @@ class AndroidLintActionTest { @Test fun `lint runs with an invocation-scoped user home`() { - val cache = AndroidLintCliInvokerCache() + val cache = AndroidLintCliInvokerCache(parentClassloader = javaClass.classLoader) val executor = AndroidLintAction.AndroidLintExecutor(cache) val jar = writeStubLintJar() @@ -75,7 +75,7 @@ class AndroidLintActionTest { @Test fun `lint receives sdk home from android home`() { - val cache = AndroidLintCliInvokerCache() + val cache = AndroidLintCliInvokerCache(parentClassloader = javaClass.classLoader) val executor = AndroidLintAction.AndroidLintExecutor(cache) val jar = writeStubLintJar()