Skip to content

Reject default arguments on composable contract members - #85

Merged
AhmadKharfan merged 1 commit into
developfrom
feat/api-module-guard
Jul 30, 2026
Merged

Reject default arguments on composable contract members#85
AhmadKharfan merged 1 commit into
developfrom
feat/api-module-guard

Conversation

@AhmadKharfan

Copy link
Copy Markdown
Owner

Why

Splitting GitPanelApi into a contract module shipped a crash:

java.lang.AbstractMethodError: abstract method
  "void ...feature.git.api.GitPanelApi.Panel(..., androidx.compose.runtime.Composer, int)"
  at ...GitPanelApi$ComposeDefaultImpls.Panel$default(GitPanelApi.kt:21)

The cause was default argument values on a @Composable interface member. The Compose compiler emits a ComposeDefaultImpls.<name>$default bridge that calls the abstract method through a synthesised signature; while the interface and its implementation shared a module that resolved, and once they were split it did not.

It compiled, passed detekt, and passed all 714 unit tests. It was only found by opening the panel on an emulator.

That was one contract module. The plan is to give every feature one, and both reference architectures this is modelled on put @Composable entry points in exactly those modules — so this is the shape the codebase is about to adopt seven more times. A rule the build enforces is worth more than a note in a review checklist.

What changed

verifyModuleBoundaries now also scans the Kotlin sources of every :*:api module and fails on a @Composable interface member declared with default argument values, pointing at the file and line and explaining the failure mode.

The detection lives in ApiContractRules.kt as pure Kotlin with no Gradle types, matching how the dependency rules are written, so it is directly unit testable.

Behavior

No behavior change. One additional check in an existing verification task, already wired into CI.

Tests

Nine unit tests. Three cover detection, including the exact multi-line declaration that shipped the crash. The other six pin the cases that must not be reported, since a source-level check earns its keep only if it is quiet:

  • a non-@Composable interface member with defaults — those do not go through the bridge and are safe, e.g. ActiveBuildRepository.clear(buildId: String? = null)
  • a top-level @Composable with defaults, which is ordinary and correct
  • a data class default value, e.g. BuildClientMeta
  • an implementing class's overrides
  • comparison operators, so >= inside a signature is not read as a default

The guard was verified against the real bug, not just against fixtures: reintroducing onOpenHistory: () -> Unit = {} into GitPanelApi makes the build fail with the new message, and removing it makes it pass again.

Verification

  • ./gradlew -p build-logic test — 9 tests, 0 failures
  • ./gradlew test detekt verifyModuleBoundaries :app:assembleDebug — BUILD SUCCESSFUL
  • Configuration cache: entry stored then reused across consecutive runs
  • Boundary report: 0 baseline entries, 0 violations

@AhmadKharfan
AhmadKharfan merged commit 7315500 into develop Jul 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant