android: the import ktlint and detekt cannot see
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m31s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m6s
Desktop (Tauri) / Update manifest (push) Successful in 5s
Android / Kotlin + Rust (debug APK) (push) Successful in 7m26s

`750d11d` failed CI at `compileDebugKotlin` with `Unresolved reference 'Build'`.
`defaultDeviceName()` reads `android.os.Build`, and the import was lost when
`SyncPairing.kt` was split out of `SyncScreen.kt`. One line to fix.

The interesting part is that ktlint and detekt had both passed it, locally and
in CI. Neither resolves symbols — they parse — so a file that cannot compile is
indistinguishable to them from one that can. A clean analyzer run is not
evidence the code builds, and on this repo `compileDebugKotlin` is the only
gate that type-checks at all, since there is no Android SDK on the workstation.

So: `android/tools/check-symbols.py`, covering that one blind spot. It flags any
capitalised identifier that is neither imported, declared in the same package, a
type parameter, nor implicitly available. Not a type checker and not pretending
to be — a pre-push filter for the single mistake that survives every other local
gate, erring toward false positives.

Verified against a known-bad tree rather than trusted on a green: deleting the
`Build` import from a copy makes it fail with the same two references the Kotlin
compiler reported. That step is not ceremony. An earlier attempt at this check
stripped line comments with `re.S`, where `//.*` eats each file from its first
comment to EOF — it examined almost nothing and reported everything clean.

ci-requirements.md now documents all three Kotlin checks, and its claim that no
workflow consumes the Android image yet is gone; the lane has been running since
step 5.
This commit is contained in:
2026-08-19 15:51:12 -04:00
parent 750d11d32e
commit 65d8f5f9c6
3 changed files with 171 additions and 1 deletions
+43 -1
View File
@@ -187,7 +187,49 @@ The Rust pin is in LOCKSTEP with `ci-tauri` and `ci-tauri-win`. All three build
mismatched Rust minor across the lanes would mean divergent resolution for no
reason. Bump the three together or not at all.
No workflow consumes it yet; the lane arrives with the app skeleton (M12 step 5).
## Checking the Kotlin lane before pushing
Same authorisation and same reasoning as the Rust section below — analyzers, run
in the CI image, with the workflow's exact arguments. From `android/`:
```
IMG=git.fabledsword.com/bvandeusen/ci-rust-android:1.97
DOCK="docker run --rm --user $(id -u):$(id -g) -e HOME=/tmp -v $PWD:/w -w /w"
$DOCK $IMG ktlint "app/src/main/**/*.kt"
$DOCK $IMG detekt --build-upon-default-config --config config/detekt.yml \
--input app/src/main/java
```
`HOME=/tmp` because both tools want a writable home for their caches and
`--user` has taken the image's away.
**Neither of these can see a missing import.** They parse Kotlin without
resolving symbols, so a file that cannot possibly compile passes both. That is
not a gap to work around — it is what these tools are — but it means a clean
local run says nothing about whether the code builds. It cost a red CI run on
`750d11d`, where `android.os.Build` was lost in a file split and both analyzers
were happy.
So there is a third local check, covering exactly that one blind spot:
```
python3 android/tools/check-symbols.py
```
It flags any capitalised identifier that is neither imported, declared in the
same package, a type parameter, nor implicitly available. Not a type checker —
`compileDebugKotlin` in CI remains the only real one, and it is also the ONLY
lane that type-checks at all, since there is no Android SDK on the workstation.
Run all three before a push that touches Kotlin.
A caution worth keeping, because it bit twice: a checker of this shape is itself
easy to get vacuously right. The first version stripped line comments with
`re.sub(r'//.*', src, flags=re.S)`, and DOTALL makes `//.*` swallow each file
from its first comment to EOF — so it reported everything clean by examining
almost nothing. **Test a checker against a known-bad tree before trusting a
green from it**; this one is verified by deleting the `Build` import from a copy
of the source and confirming it fails.
## Checking the Rust lane before pushing