1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
//! Kotlin source emitter for the AAR module (JNI mode).
//!
//! Produces a pure-Kotlin JNI layout with no bundled Java facade:
//!
//! - `<Module>Bridge.kt` — a Kotlin `object` with `external fun` JNI
//! declarations and `init { System.loadLibrary("<crate>_jni") }`.
//! - `DefaultClient.kt` — coroutine-friendly client class holding a `Long`
//! handle when the API has methodful types.
//! - Data classes, enums, and error types are emitted as `.kt` files via the
//! kotlin backend's pub helpers.
//!
//! `KotlinFfiStyle::Jni` is forced by the parent backend (`lib.rs`) before
//! this module is called, so `config.kotlin_ffi_style()` will always return
//! `KotlinFfiStyle::Jni` here.
mod module_facade;
mod trait_interfaces;
pub use trait_interfaces::format_method_signature;
use std::collections::BTreeSet;
use std::path::Path;
use crate::backends::kotlin::{
emit_enum_pub, emit_error_type_pub, emit_jni_bridge_object, emit_jni_client_class,
emit_type_pub_with_defaults_sealed_and_constructible,
};
use crate::backends::kotlin_android::naming::kotlin_package;
use crate::backends::kotlin_android::template_env;
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::ApiSurface;
use crate::core::jni::bridge_class_name;
/// Emit all Kotlin source files for the AAR module.
///
/// `kotlin_source_dir` is the resolved Kotlin source destination —
/// `<project_root>/src/main/kotlin/<dotted_package_as_path>/` in the Gradle
/// Android source-set layout.
pub fn emit(api: &ApiSurface, config: &ResolvedCrateConfig, kotlin_source_dir: &Path) -> Vec<GeneratedFile> {
let package = kotlin_package(config);
let mut files = Vec::new();
// Collect type aliases that should be treated as excluded.
//
// Two sources contribute:
// 1. `kotlin_android.exclude_types` (explicit user override).
// 2. Trait-bridge `type_alias` values whose `param_name` is listed in
// `kotlin_android.exclude_functions`. When the user opts out of the
// bridge function (e.g. because the JNI bridge has no trait-handle
// implementation yet), the bridge's type alias is still unresolvable
// in Kotlin code — its corresponding `options_field` on the bridge's
// `options_type` would emit a dangling reference like
// `val visitor: VisitorHandle?`. Treat the alias as excluded so the
// field is dropped along with the function.
let kotlin_android_excluded_function_names: std::collections::HashSet<&str> = config
.kotlin_android
.as_ref()
.map(|c| c.exclude_functions.iter().map(String::as_str).collect())
.unwrap_or_default();
let mut effective_excluded_types: std::collections::HashSet<String> = config
.kotlin_android
.as_ref()
.map(|c| c.exclude_types.iter().cloned().collect())
.unwrap_or_default();
for bridge in &config.trait_bridges {
if bridge.exclude_languages.iter().any(|l| l == "kotlin_android") {
if let Some(alias) = &bridge.type_alias {
effective_excluded_types.insert(alias.clone());
}
}
if let Some(name) = bridge.param_name.as_deref() {
if kotlin_android_excluded_function_names.contains(name) {
if let Some(alias) = &bridge.type_alias {
effective_excluded_types.insert(alias.clone());
}
}
}
}
// Mirror the FFI backend's `contains('<')` filter for workspace-declared opaque types
// with generic-parameter rust_paths — the FFI backend skips `_new`/`_free` symbols for
// them, so Kotlin Android JNI external-fun declarations against those symbols would
// throw `UnsatisfiedLinkError` at runtime.
for (name, path) in &config.opaque_types {
if path.contains('<') {
effective_excluded_types.insert(name.clone());
}
}
// Build an `enum_name → default_variant` map so the data-class emitter
// can synthesise constructor defaults for Named enum fields (e.g.
// `headingStyle: HeadingStyle = HeadingStyle.ATX`). The Jackson Kotlin
// module rejects deserialization of partial JSON when a
// non-nullable field has no default — every non-optional Named enum
// field needs one to round-trip.
//
// Only true Kotlin enums are included here. Tagged/untagged enums (with
// `serde_tag` or `serde_untagged` set) are emitted as sealed classes in
// Kotlin and are handled differently (variant names are PascalCase, not
// SCREAMING_SNAKE_CASE).
//
// Enums without a declared `#[default]` variant map to an empty string;
// the emitter treats this as "no synthesisable default" and falls
// through to the type-based path (null for optional fields, no default
// for required ones). The mere presence of the entry distinguishes
// true enums from data-class struct types and sealed classes.
let enum_defaults: std::collections::HashMap<String, String> = api
.enums
.iter()
.filter(|en| en.serde_tag.is_none() && !en.serde_untagged && en.variants.iter().all(|v| v.fields.is_empty()))
.map(|en| {
let default_variant = en
.variants
.iter()
.find(|v| v.is_default)
.map(|v| v.name.clone())
.unwrap_or_default();
(en.name.clone(), default_variant)
})
.collect();
// Build the set of Kotlin sealed-class names — Rust enums emitted as
// sealed classes because they are tagged (`#[serde(tag = "...")]`) or
// untagged (`#[serde(untagged)]`). Data-class fields whose declared
// type references one of these names need a `@field:JsonSerialize(\`as\` = ...)`
// (or `contentAs` for collections) annotation so Jackson routes through
// the sealed class's custom serializer (which emits the discriminator)
// instead of the runtime variant's default POJO serializer.
let sealed_class_names: std::collections::HashSet<String> = api
.enums
.iter()
.filter(|en| en.serde_tag.is_some() || en.serde_untagged)
.map(|en| en.name.clone())
.collect();
// Non-enum, non-trait, non-opaque data class types whose Rust source has a
// `Default` impl (has_default = true). For fields whose declared type
// references one of these names, the emitter can safely synthesize
// `= Name()` as the constructor default — preventing Jackson's Kotlin
// module from raising `MissingKotlinParameterException` when the wire
// JSON omits the nested struct (the common shape of partial-update
// payloads in test fixtures).
let default_constructible_types: std::collections::HashSet<String> = api
.types
.iter()
.filter(|t| !t.is_trait && !t.is_opaque && t.has_default)
.map(|t| t.name.clone())
.collect();
// Bridge object: external fun declarations + System.loadLibrary init block.
let mut bridge_file = emit_jni_bridge_object(api, config);
bridge_file.path = kotlin_source_dir.join(bridge_file.path.file_name().expect("bridge file must have a filename"));
files.push(bridge_file);
// BridgeException: a RuntimeException subclass thrown by the JNI shim when
// a native call fails. The Rust ERROR_CLASS constant references this class
// as `<package>/<BridgeName>Exception`. Without this file the JVM raises
// NoClassDefFoundError on the first JNI call that needs to propagate an error.
let bridge = bridge_class_name(&config.name);
let exception_class = format!("{bridge}Exception");
let exception_content = format!(
"// Generated by alef. Do not edit by hand.\n\n\
package {package}\n\n\
class {exception_class}(message: String?, cause: Throwable?) : RuntimeException(message, cause) {{\n\
{}\n\
}}\n",
" constructor(message: String?) : this(message, null)"
);
files.push(GeneratedFile {
path: kotlin_source_dir.join(format!("{exception_class}.kt")),
content: exception_content,
generated_header: false,
});
// DefaultClient.kt — only emitted when the API has methodful opaque types.
if let Some(mut client_file) = emit_jni_client_class(api, config, Some(&package)) {
client_file.path = kotlin_source_dir.join("DefaultClient.kt");
files.push(client_file);
}
// Data classes, enums, and error types as pure Kotlin.
for ty in &api.types {
if ty.is_opaque || ty.is_trait || ty.binding_excluded {
continue;
}
// Skip whole types whose name is in the effective exclude set
// (e.g. trait-bridge `VisitorHandle` when the bridge function is
// excluded — the alias has no Kotlin representation).
if effective_excluded_types.contains(&ty.name) {
continue;
}
let mut imports: BTreeSet<String> = BTreeSet::new();
let mut body = String::new();
// Drop fields whose type references an excluded alias so the data
// class definition does not emit a dangling reference. The bridge
// function is already filtered out of the module facade, so its
// companion field cannot be set by callers — defaulting it to
// `Default::default()` Rust-side preserves runtime correctness.
let needs_field_filter = ty
.fields
.iter()
.any(|f| effective_excluded_types.iter().any(|name| f.ty.references_named(name)));
if needs_field_filter {
let mut filtered = ty.clone();
filtered
.fields
.retain(|f| !effective_excluded_types.iter().any(|name| f.ty.references_named(name)));
emit_type_pub_with_defaults_sealed_and_constructible(
&filtered,
&mut body,
&mut imports,
&enum_defaults,
&sealed_class_names,
&default_constructible_types,
);
} else {
emit_type_pub_with_defaults_sealed_and_constructible(
ty,
&mut body,
&mut imports,
&enum_defaults,
&sealed_class_names,
&default_constructible_types,
);
}
if body.trim().is_empty() {
continue;
}
let content = assemble_kt_content(&package, &imports, &body);
files.push(GeneratedFile {
path: kotlin_source_dir.join(format!("{}.kt", ty.name)),
content,
generated_header: false,
});
}
for en in &api.enums {
if en.binding_excluded {
continue;
}
let mut body = String::new();
emit_enum_pub(en, &mut body, &package);
if body.trim().is_empty() {
continue;
}
let content = assemble_kt_content(&package, &BTreeSet::new(), &body);
files.push(GeneratedFile {
path: kotlin_source_dir.join(format!("{}.kt", en.name)),
content,
generated_header: false,
});
}
for error in &api.errors {
let mut imports: BTreeSet<String> = BTreeSet::new();
let mut body = String::new();
emit_error_type_pub(error, &mut body, &mut imports);
if body.trim().is_empty() {
continue;
}
let content = assemble_kt_content(&package, &imports, &body);
files.push(GeneratedFile {
path: kotlin_source_dir.join(format!("{}.kt", error.name)),
content,
generated_header: false,
});
}
trait_interfaces::emit_trait_interfaces(api, config, kotlin_source_dir, &package, &mut files);
// Emit the free-function facade object (Module.kt) when visible functions exist.
module_facade::emit_module_kt(api, config, kotlin_source_dir, &package, &mut files);
files
}
/// Assemble a complete `.kt` file from package, imports, and body.
pub(super) fn assemble_kt_content(package: &str, imports: &BTreeSet<String>, body: &str) -> String {
// File-level suppression annotations silence ktlint / detekt rules that are
// inherently violated by generated code (trailing commas, annotation spacing,
// when-entry bracing, etc.) and cannot be trivially fixed without a full
// reformatter post-processing step.
let suppressions = vec![
"ktlint:standard:trailing-comma-on-call-site",
"ktlint:standard:trailing-comma-on-declaration-site",
"ktlint:standard:spacing-between-declarations-with-comments",
"ktlint:standard:spacing-between-declarations-with-annotations",
"ktlint:standard:when-entry-bracing",
"ktlint:standard:blank-line-between-when-conditions",
"ktlint:standard:blank-line-before-declaration",
"ktlint:standard:chain-method-continuation",
"ktlint:standard:annotation",
"ktlint:standard:max-line-length",
"ktlint:standard:no-semi",
"ktlint:standard:statement-wrapping",
"MaxLineLength",
"TooManyFunctions",
"FunctionParameterNaming",
"LongParameterList",
"CyclomaticComplexMethod",
"LongMethod",
"MagicNumber",
// Jackson deserializer for heterogeneous-default sealed enums nests
// when-blocks past detekt's NestedBlockDepth threshold (introduced in
// the deserializer added by 2bdbb0db8). Generated code; restructuring
// would obscure the readNode → match → readNode flow.
"NestedBlockDepth",
// Untagged-enum (serde) deserializers emit one `if (node.isFoo) return ...`
// per variant. detekt's `ReturnCount` default limit (2) flags 3+ variants.
// Generated code; collapsing to a single return would require a different
// dispatch shape than the consistent one-branch-per-variant emission.
"ReturnCount",
// Instance method stubs for bridged methods throw UnsupportedOperationException
// without using their parameters, triggering detekt's UnusedParameter rule.
// Suppressed here since these stubs are generated graceful error messages.
"UnusedParameter",
];
let imports = imports.iter().cloned().collect::<Vec<_>>();
template_env::render(
"kt_file.jinja",
minijinja::context! {
package => package,
imports => imports,
suppressions => suppressions,
body => body,
},
)
}