From 5eab2dd0b37cb13a4612ccf8e6363b0526d375d6 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 18 Aug 2026 16:15:46 -0400 Subject: [PATCH] android: the error enum has to be flat, or the bindings don't compile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fifth run cleared ktlint and detekt and failed compiling the GENERATED Kotlin: 'message' hides member of supertype 'Throwable' and needs an 'override' modifier My design, surfacing one layer down. CoreError's variants carried a `message` field, and uniffi turns an error enum into exception classes extending Throwable — which already has `message`. `#[uniffi(flat_error)]` is the right fix rather than renaming the field. Renaming would dodge the collision and leave `e.message` null on the Kotlin side, so every call site would have to know which variant it caught just to read the text. Flat passes the Display string to the Throwable constructor, where Kotlin expects it, and costs nothing that matters: each variant is still its own subclass, so `catch (e: CoreException.NotLinked)` still works and a `when` is still exhaustive. Only the fields stop crossing, and for every variant that has one the field IS the Display string. Confirmed by generating the bindings and reading them: sealed class CoreException(message: String): kotlin.Exception(message) { class NotLinked(message: String) : CoreException(message) class Store(message: String) : CoreException(message) class Network(message: String) : CoreException(message) } That check is worth keeping. thoughtsync-ffi already builds a HOST .so as part of the workspace, and `--library` mode reads metadata straight out of it — so the exact Kotlin the Android lane will compile can be generated and inspected here, with no Android toolchain involved. It also let me verify the app's call sites against the real generated API rather than against my assumptions about uniffi's naming: ThoughtSync(dataDir), createNote(draft), listNotes(query), Note.displayTitle, and NoteDraft/NoteQuery's parameter names all match. Co-Authored-By: Claude Opus 5 (1M context) --- android/ffi/src/lib.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/android/ffi/src/lib.rs b/android/ffi/src/lib.rs index 9022c31..0e2e682 100644 --- a/android/ffi/src/lib.rs +++ b/android/ffi/src/lib.rs @@ -58,6 +58,21 @@ uniffi::setup_scaffolding!(); /// working exactly as intended — and the UI's response is to offer linking, not to /// show an error. #[derive(Debug, thiserror::Error, uniffi::Error)] +// FLAT, so the Kotlin side gets the message on `Throwable` where it belongs. +// +// Without this, uniffi generates an exception subclass with a `message` PROPERTY +// per variant — which collides with `Throwable.message` and fails to compile: +// "'message' hides member of supertype 'Throwable' and needs an 'override' +// modifier". Renaming the field would dodge the collision but leave +// `e.message` null in Kotlin, so every call site would have to know the variant +// just to read the text. +// +// Flat keeps what actually matters: each variant is still its own Kotlin +// subclass, so `catch (e: CoreException.NotLinked)` still works and a `when` is +// still exhaustive. Only the FIELDS stop crossing, and the Display string — +// which is the field, for every variant that has one — comes through as the +// exception message. +#[uniffi(flat_error)] pub enum CoreError { /// No server is linked. Not a fault; the app is local-first and this is its /// resting state.