Skip to content

fix(gradle): resolve generated classes under AGP built-in Kotlin#9403

Open
michnovka wants to merge 1 commit into
detekt:mainfrom
michnovka:fix/builtin-kotlin-generated-classpath
Open

fix(gradle): resolve generated classes under AGP built-in Kotlin#9403
michnovka wants to merge 1 commit into
detekt:mainfrom
michnovka:fix/builtin-kotlin-generated-classpath

Conversation

@michnovka

@michnovka michnovka commented Jun 14, 2026

Copy link
Copy Markdown

Fixes #9402

Problem

Under AGP built-in Kotlin, the type-resolution tasks (detektMain / detektDebug and the matching detektBaseline* tasks) run with an incomplete classpath: the variant's compiled generated classes (BuildConfig, and other javac-compiled generated Java like view binding) are not on the analysis classpath. So detekt can't resolve references to those generated types, logs compiler errors found during analysis, and emits false positives from type-resolution rules such as UnusedPrivateFunction and RedundantSuspendModifier. This has been the case since built-in Kotlin support landed in #9100.

The only workaround is keeping the legacy kotlin-android plugin plus android.builtInKotlin=false / android.newDsl=false, all of which are removed in AGP 10.

Fix

Put each Android variant's compiled classes onto the detekt and baseline task classpaths via Artifacts.forScope(PROJECT).use(task).toGet(ScopedArtifact.CLASSES, ...), folded into the existing classpath. AGP only exposes the compiled generated classes through ScopedArtifact.CLASSES, and its toGet requires ListProperty sinks on the consuming task, so the two sinks (generatedClassesJars / generatedClassesDirs) are kept internal. This also establishes the compile dependency so the generated classes exist when detekt runs. JVM and KMP compilations are unaffected.

Tests

  • New functional tests assert detektMain and detektBaselineMain resolve generated BuildConfig.
  • Strengthen the existing built-in-Kotlin "javac intermediates" (view binding) test, which previously only asserted doesNotContain("UnreachableCode"), a check that passes even when types are unresolved.

Related

@detekt-ci

detekt-ci commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator
Warnings
⚠️ It looks like this PR contains functional changes without a corresponding test.

Generated by 🚫 dangerJS against c74d382

@michnovka

Copy link
Copy Markdown
Author

The tests are in src/functionalTest (DetektAndroidBuiltInKotlinSpec) — Danger only matches src/test. Gradle task wiring like this can only be covered with a real Gradle build, same as the surrounding built-in-Kotlin tests.

@codecov

codecov Bot commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.72%. Comparing base (fd5683e) to head (c74d382).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #9403      +/-   ##
============================================
- Coverage     84.72%   84.72%   -0.01%     
- Complexity     4486     4492       +6     
============================================
  Files           571      571              
  Lines         12423    12407      -16     
  Branches       2767     2767              
============================================
- Hits          10526    10512      -14     
+ Misses          696      694       -2     
  Partials       1201     1201              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@BraisGabin

Copy link
Copy Markdown
Member

Thanks to track and fix this! Does this also work for ViewBinding?

For context: I wrote some tests too for similar cases (you can read context at #9331). No need to use those tests, I'm just linking for context.

@michnovka

Copy link
Copy Markdown
Author

Yep, ViewBinding works too. Its generated class ends up on the classpath the same way BuildConfig/R do.

It's actually already covered: the existing javac intermediates on classpath test uses ActivitySampleBinding. I tightened its assertion to also check for compiler errors found during analysis (before it only looked for UnreachableCode, which passed even when the binding wasn't resolved), so now it's red without this change and green with it.

Thanks for the #9331 pointer, I'll have a look.

@michnovka

Copy link
Copy Markdown
Author

@BraisGabin had a look at #9331, thanks for the pointer.

They line up pretty nicely. This PR puts the generated classes on the analysis classpath, so it covers the AGP generated sources (BuildConfig, R, ViewBinding). #9331 runs the compiler plugins during analysis, which is what Parcelize and serialization need, and which this approach can't really do anyway since the source declaration shadows the compiled binary.

So the BuildConfig and ViewBinding cases in your CompilerPluginsTests should already pass with this PR. You could probably drop those two from #9331 and just keep the Parcelize one. One heads up: we both touch Detekt, DetektCreateBaselineTask and SharedTasks, so whichever lands first the other is a quick rebase.

@3flex

3flex commented Jun 15, 2026

Copy link
Copy Markdown
Member

Is there no way to achieve this without adding new parameters to the Detekt task?

We shouldn't need any parameters to configure the detekt compilation task that doesn't exist on the Kotlin compilation task.

Under AGP built-in Kotlin, detekt's type-resolution tasks (detektMain/detektDebug
and the matching detektBaseline* tasks) run with an incomplete classpath: the
variant's compiled generated classes (BuildConfig and other javac-compiled
generated Java such as view binding) are not on the analysis classpath. detekt
cannot resolve references to those generated types, logs "compiler errors found
during analysis", and emits false positives from type-resolution rules such as
UnusedPrivateFunction and RedundantSuspendModifier. This has been the case since
built-in Kotlin support landed in detekt#9100.

Put each Android variant's compiled classes onto the detekt and baseline task
classpaths via Artifacts.forScope(PROJECT).use(task).toGet(ScopedArtifact.CLASSES,
...), folded into the existing classpath. AGP only exposes the compiled generated
classes through ScopedArtifact.CLASSES, and its toGet requires ListProperty sinks
on the consuming task, so the two sinks (generatedClassesJars/generatedClassesDirs)
are kept internal. This also establishes the compile dependency so the generated
classes exist when detekt runs. JVM and KMP compilations are unaffected.

Add functional tests asserting detektMain and detektBaselineMain resolve generated
BuildConfig, and strengthen the existing built-in-Kotlin "javac intermediates"
(view binding) test, which previously only asserted doesNotContain("UnreachableCode"),
a check that passes even when types are unresolved.
@michnovka michnovka force-pushed the fix/builtin-kotlin-generated-classpath branch from 7c86847 to c74d382 Compare June 15, 2026 08:05
@michnovka

Copy link
Copy Markdown
Author

Yeah, I tried but couldn't get around it. AGP only exposes the compiled generated classes through ScopedArtifact.CLASSES, and the only read API for that (toGet) needs the ListProperty sinks on the task. BuildConfig comes in via the Kotlin task's javaSources, which isn't part of the public kotlin-gradle-plugin-api detekt builds against, so there's nothing existing to reuse.

I've made both props internal so they're out of the public task API now, just wiring. Does that work for you?

@3flex

3flex commented Jun 15, 2026

Copy link
Copy Markdown
Member

I've made both props internal so they're out of the public task API now, just wiring. Does that work for you?

Thanks, I'd prefer that for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

4 participants