diff --git a/CHANGELOG.md b/CHANGELOG.md index 833369e..612258c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,47 @@ All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +The next release is **breaking**, so it needs a `0.2.0` version bump: under +SemVer a `0.x` crate signals incompatibility by raising the minor. Several +changes since `0.1.0` alter the public model, this one included. + +### Added + +- Generation-time validation: `KtFile::validate` / `validate_with`, + `Diagnostic`, `Check`, `Severity` and `ValidationPolicy`, run by + `merge_files` and `write_files`. `merge_files_with` / `write_files_with` + expose per-check severities and return surviving warnings. +- Kotlin identifier utilities: `is_valid_kotlin_ident`, `mangle_kotlin_ident`, + `escape_kotlin_ident`, `is_escaped_kotlin_ident`, `is_writable_kotlin_ident`, + `is_kotlin_hard_keyword`, `KOTLIN_HARD_KEYWORDS`, plus the package-path + equivalents. +- Extension functions: `receiver` on `KtFun` and `KtFunSig`, with + `KtType::render_receiver` for the parentheses a function-type receiver needs. +- `open` and `sealed` classes, named companion objects, and `KtFunSig` for + abstract members. +- `KOTLIN_BANNER` and `merged_file_path` are exported; both were previously + `pub` inside private modules and unreachable. +- Two runnable examples, `showcase` and `invalid`, pinned by golden files. + +### Changed — breaking + +- `KtClassKind` variants carry their own data, so constructor parameters, + enum entries and the class modifier live on the kind rather than on + `KtClass`. `Plain`/`Abstract` became `Class { modifier }`, and `Companion` + and `ValueInline` are gone. +- A companion object is `KtCompanion`, reached only through + `KtClass::companion`; `KtClass::companion_object()` is removed. +- Supertypes are `KtSupertypes { superclass, interfaces }`; + `KtClass::supertype()` is replaced by `extends` and `implements`. +- `KtFunInterface::method` is a `KtFunSig`, which cannot carry a body. +- `external` is a `KtBody` variant set by `KtFun::external()`, not a modifier + string. +- `KtFun` and `KtFunSig` gained a `receiver` field. Callers using the builders + are unaffected; a caller constructing either with a **struct literal** must + add it. + ## 0.1.0 Initial release. diff --git a/README.md b/README.md index 0f818aa..a0cf1bc 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ cargo run --example invalid # a broken model, and what the validator says `showcase` is a small but complete generator — it builds `KtFile` fragments, merges them so each package collapses to one file, and renders classes, objects, enums, data and value classes, sealed interfaces, `fun interface`s, -type aliases, properties with accessors and delegates, `external` natives, and -raw blocks. `invalid` shows the two ways a mistake surfaces: as a diagnostic +type aliases, extension functions, properties with accessors and delegates, +`external` natives, and raw blocks. `invalid` shows the two ways a mistake surfaces: as a diagnostic from the validator, or as a builder that refuses to construct the value at all. To accept an intended change to either output: diff --git a/examples/invalid.rs b/examples/invalid.rs index ae18be1..3a4387d 100644 --- a/examples/invalid.rs +++ b/examples/invalid.rs @@ -96,6 +96,24 @@ fn broken_declarations() -> KtFile { .returns(KtType::long()) .body(KtCode::new()), ) + // Extensions are keyed on their receiver, so these two collide while + // the same pair on different receivers would not. + .decl( + KtFun::new("asRaw") + .receiver(KtType::cls("io.example.Codec")) + .body(KtCode::new()), + ) + .decl( + KtFun::new("asRaw") + .receiver(KtType::cls("io.example.Codec")) + .body(KtCode::new()), + ) + // ...as here: same name, different receiver, no diagnostic. + .decl( + KtFun::new("asRaw") + .receiver(KtType::cls("io.example.Other")) + .body(KtCode::new()), + ) // Names that are not legal Kotlin identifiers. .decl( KtClass::class_("My-Class") diff --git a/examples/showcase.rs b/examples/showcase.rs index 7402a43..ae9d468 100644 --- a/examples/showcase.rs +++ b/examples/showcase.rs @@ -247,6 +247,23 @@ fn session_fragment() -> KtFile { .line("return acc"), ), ) + // The interface's member extension, implemented. + .member( + KtFun::new("label") + .vis(KtVis::Public) + .modifier("override") + .receiver(KtType::cls("Sample")) + .returns(KtType::string()) + .expr_body(KtCode::new().line("\"${keyExpr}@${describe()}\"")), + ) + // A member extension of the class itself. + .member( + KtFun::new("toSample") + .vis(KtVis::Public) + .receiver(KtType::byte_array()) + .returns(KtType::cls("Sample")) + .expr_body(KtCode::new().line("Sample(describe(), this)")), + ) // A nested class — its own scope, so its members may reuse names. .member( KtClass::class_("Config") @@ -270,7 +287,15 @@ fn session_fragment() -> KtFile { // `KtFunSig` is exactly that. let describable = KtClass::interface_("Describable") .vis(KtVis::Public) - .member(KtFunSig::new("describe").returns(KtType::string())); + .member(KtFunSig::new("describe").returns(KtType::string())) + // A member extension: abstract here, supplied by the implementor. + // `KtFunSig` carries a receiver too, so a signature does not quietly + // become a plain member. + .member( + KtFunSig::new("label") + .receiver(KtType::cls("Sample")) + .returns(KtType::string()), + ); // A `fun interface` (SAM) — its single method cannot carry a body. let handler = KtFunInterface::new( @@ -283,6 +308,47 @@ fn session_fragment() -> KtFile { .type_param("out R") .kdoc("Invoked from the native thread for each reply."); + // Top-level extension functions. The receiver is a type, not part of the + // name, so it resolves through the import set and the name stays a plain + // identifier the validator can check as one. + let summary = KtFun::new("summary") + .vis(KtVis::Public) + .receiver(KtType::cls("Sample")) + .returns(KtType::string()) + .expr_body(KtCode::new().line("\"$keyExpr (${payload.size} bytes)\"")); + + // Generics render before the receiver, which renders before the name. + let map_replies = KtFun::new("mapValues") + .vis(KtVis::Public) + .generic("R") + .receiver(KtType::generic("List", [KtType::cls("Reply")])) + .param(KtParam::new( + "transform", + KtType::lambda( + [("sample".to_string(), KtType::cls("Sample"))], + KtType::var_r(), + ), + )) + .returns(KtType::generic("List", [KtType::var_r()])) + .expr_body( + KtCode::new().line("filterIsInstance().map { transform(it.sample) }"), + ); + + // An extension on a *function type*. The receiver needs parentheses here + // or the `.` would bind to the return type instead. + let as_raw = KtFun::new("asRaw") + .vis(KtVis::Internal) + .receiver(KtType::lambda( + [("sample".to_string(), KtType::cls("Sample"))], + KtType::unit(), + )) + .returns(KtType::cls("io.example.api.internal.RawSink")) + // The proxy adapts a typed callback to the raw one the natives call, + // so it has to narrow `Reply` to the `Sample` the receiver takes. + .expr_body( + KtCode::new().line("RawSink { raw -> if (raw is Reply.Value) this(raw.sample) }"), + ); + KtFile::new("io.example.api") // FQNs named only from raw body text, which the model cannot see. .imports(["io.example.api.internal.JNINative".to_string()]) @@ -290,6 +356,9 @@ fn session_fragment() -> KtFile { .decl(describable) .decl(handler) .decl(session) + .decl(summary) + .decl(map_replies) + .decl(as_raw) .decl(KtDecl::TypeAlias { vis: KtVis::Public, name: "SampleList".to_string(), @@ -343,7 +412,14 @@ fn natives_fragment() -> KtFile { ), }; + let raw_sink = KtFunInterface::new( + "RawSink", + KtFunSig::new("accept").param(KtParam::new("reply", KtType::cls("io.example.api.Reply"))), + ) + .vis(KtVis::Internal); + KtFile::new("io.example.api.internal") + .decl(raw_sink) .decl(natives) .decl(loader) // An FQN referenced only from raw text the model cannot see. diff --git a/src/model.rs b/src/model.rs index 8c01eac..80bbd95 100644 --- a/src/model.rs +++ b/src/model.rs @@ -148,6 +148,10 @@ pub struct KtFunSig { pub kdoc: Option, /// Generic type-variable names: `["R"]` → `fun …`. pub generics: Vec, + /// Extension receiver: `Some(Foo)` → `fun Foo.name(…)`. A separate field + /// rather than part of `name`, so `name` stays a plain identifier that can + /// be checked as one. + pub receiver: Option, pub params: Vec, pub ret: Option, } @@ -160,6 +164,7 @@ impl KtFunSig { annotations: Vec::new(), kdoc: None, generics: Vec::new(), + receiver: None, params: Vec::new(), ret: None, } @@ -168,6 +173,11 @@ impl KtFunSig { self.vis = v; self } + /// Make this an extension function on `ty`: `fun Foo.name(…)`. + pub fn receiver(mut self, ty: KtType) -> Self { + self.receiver = Some(ty); + self + } pub fn annotation(mut self, a: impl Into) -> Self { self.annotations.push(a.into()); self @@ -200,6 +210,7 @@ impl From for KtFun { annotations: s.annotations, kdoc: s.kdoc, generics: s.generics, + receiver: s.receiver, params: s.params, ret: s.ret, body: KtBody::None, @@ -826,6 +837,9 @@ pub struct KtFun { pub kdoc: Option, /// Generic type-variable names: `["R"]` → `fun …`. pub generics: Vec, + /// Extension receiver: `Some(Foo)` → `fun Foo.name(…)`. See + /// [`KtFunSig::receiver`]. + pub receiver: Option, pub params: Vec, pub ret: Option, pub body: KtBody, @@ -840,6 +854,7 @@ impl KtFun { annotations: Vec::new(), kdoc: None, generics: Vec::new(), + receiver: None, params: Vec::new(), ret: None, body: KtBody::None, @@ -850,6 +865,11 @@ impl KtFun { self.vis = v; self } + /// Make this an extension function on `ty`: `fun Foo.name(…)`. + pub fn receiver(mut self, ty: KtType) -> Self { + self.receiver = Some(ty); + self + } /// Add a modifier keyword (`override`, `inline`, `operator`, …). /// /// # Panics @@ -903,9 +923,9 @@ impl KtFun { self } - /// This function's signature: same name, generics, parameters and return - /// type, with the body and modifiers dropped. What a concrete member looks - /// like as an interface abstract. + /// This function's signature: same name, generics, receiver, parameters and + /// return type, with the body and modifiers dropped. What a concrete member + /// looks like as an interface abstract. pub fn signature(&self) -> KtFunSig { KtFunSig { name: self.name.clone(), @@ -913,6 +933,7 @@ impl KtFun { annotations: self.annotations.clone(), kdoc: self.kdoc.clone(), generics: self.generics.clone(), + receiver: self.receiver.clone(), params: self.params.clone(), ret: self.ret.clone(), } diff --git a/src/render.rs b/src/render.rs index 4c0f368..fe803e6 100644 --- a/src/render.rs +++ b/src/render.rs @@ -434,6 +434,8 @@ struct SigView<'a> { /// `external` renders ahead of the other modifiers; it lives on the body. external: bool, generics: &'a [String], + /// Extension receiver, rendered as `Recv.` before the name. + receiver: Option<&'a KtType>, name: &'a str, params: &'a [KtParam], ret: Option<&'a KtType>, @@ -448,6 +450,7 @@ impl<'a> From<&'a KtFun> for SigView<'a> { modifiers: &f.modifiers, external: matches!(f.body, KtBody::External), generics: &f.generics, + receiver: f.receiver.as_ref(), name: &f.name, params: &f.params, ret: f.ret.as_ref(), @@ -464,6 +467,7 @@ impl<'a> From<&'a KtFunSig> for SigView<'a> { modifiers: &[], external: false, generics: &f.generics, + receiver: f.receiver.as_ref(), name: &f.name, params: &f.params, ret: f.ret.as_ref(), @@ -494,6 +498,12 @@ fn render_fun_signature(f: &SigView<'_>, level: usize, imports: &mut ImportSet, if !f.generics.is_empty() { out.push_str(&format!("<{}> ", f.generics.join(", "))); } + if let Some(recv) = f.receiver { + // Receiver position, not ordinary type position: a function type needs + // parentheses here (see `KtType::render_receiver`). + out.push_str(&recv.render_receiver(imports)); + out.push('.'); + } out.push_str(f.name); let ps: Vec = f .params diff --git a/src/tests.rs b/src/tests.rs index e7f42f1..30408d5 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -462,6 +462,42 @@ fn same_package_types_need_no_import() { assert!(src.contains("a: Local"), "{src}"); } +#[test] +fn an_extension_receiver_renders_before_the_name() { + // Generics first, then the receiver, then the bare name — and the receiver + // goes through the import set like any other type. + let f = KtFun::new("asRaw") + .generic("R") + .receiver(KtType::generic("io.other.Cb", [KtType::var_("R")])) + .returns(KtType::cls("io.p.CbRaw")) + .expr_body(KtCode::new().line("CbRaw { }")); + let src = render::render_one(&f.into(), "io.p"); + assert!(src.contains("fun Cb.asRaw(): CbRaw"), "{src}"); + assert!(src.contains("import io.other.Cb"), "{src}"); +} + +#[test] +fn an_extension_functions_name_is_still_a_plain_identifier() { + // The point of the field: the receiver is not smuggled into `name`, so the + // identifier check sees `asRaw` and passes. + let file = KtFile::new("io.p").decl( + KtFun::new("asRaw") + .receiver(KtType::cls("io.p.Cb")) + .expr_body(KtCode::new().line("CbRaw { }")), + ); + assert_eq!(file.validate(), vec![]); +} + +#[test] +fn signature_keeps_the_extension_receiver() { + // `signature()` drops the body and modifiers; losing the receiver too would + // silently turn an extension into a member. + let f = KtFun::new("asRaw") + .receiver(KtType::cls("io.p.Cb")) + .body(KtCode::new()); + assert!(f.signature().receiver.is_some()); +} + #[test] fn type_construction_covers_metadata_shapes() { let mut imp = ImportSet::new("p"); @@ -1180,6 +1216,57 @@ fn functions_with_identical_parameter_types_collide() { assert!(diags[0].message.contains("`send(Int)`"), "{diags:#?}"); } +#[test] +fn extensions_on_different_receivers_are_not_duplicates() { + // Kotlin dispatches an extension on its receiver, so these are two + // declarations. Keying the overload on the name alone reported them as a + // redeclaration. + let f = KtFile::new("io.p") + .decl( + KtFun::new("asRaw") + .receiver(KtType::cls("io.p.Cb")) + .body(KtCode::new()), + ) + .decl( + KtFun::new("asRaw") + .receiver(KtType::cls("io.p.Other")) + .body(KtCode::new()), + ); + assert_eq!(f.validate(), vec![]); +} + +#[test] +fn two_extensions_on_the_same_receiver_are_duplicates() { + let f = KtFile::new("io.p") + .decl( + KtFun::new("asRaw") + .receiver(KtType::cls("io.p.Cb")) + .body(KtCode::new()), + ) + .decl( + KtFun::new("asRaw") + .receiver(KtType::cls("io.p.Cb")) + .body(KtCode::new()), + ); + let diags = f.validate(); + assert_eq!(diags.len(), 1, "{diags:#?}"); + assert_eq!(diags[0].check, Check::DuplicateFunction); + assert!(diags[0].message.contains("`io.p.Cb.asRaw()`"), "{diags:#?}"); +} + +#[test] +fn an_extension_does_not_collide_with_a_member_of_the_same_name() { + // `fun Cb.asRaw()` and `fun asRaw()` are different declarations. + let f = KtFile::new("io.p") + .decl( + KtFun::new("asRaw") + .receiver(KtType::cls("io.p.Cb")) + .body(KtCode::new()), + ) + .decl(KtFun::new("asRaw").body(KtCode::new())); + assert_eq!(f.validate(), vec![]); +} + #[test] fn duplicates_inside_a_class_body_are_found() { // Where nearly all generated functions actually live — and where the old @@ -1619,3 +1706,77 @@ fn merging_carries_a_banner_override_into_the_merged_file() { .expect("merge"); assert_eq!(merged[0].banner.as_deref(), Some("// first")); } + +#[test] +fn a_function_type_receiver_is_parenthesized() { + // `fun (Int) -> String.ext()` parses the `.` against the return type and + // does not compile; the receiver needs its own parentheses. + let f = KtFun::new("asRaw") + .receiver(KtType::lambda( + [("value".to_string(), KtType::int())], + KtType::string(), + )) + .body(KtCode::new()); + let src = render::render_one(&f.into(), "io.p"); + assert!( + src.contains("fun ((value: Int) -> String).asRaw()"), + "{src}" + ); +} + +#[test] +fn a_nullable_function_type_receiver_is_not_double_parenthesized() { + // `KtType::render` already wraps a nullable function type. + let f = KtFun::new("asRaw") + .receiver( + KtType::lambda([("value".to_string(), KtType::int())], KtType::string()).nullable(), + ) + .body(KtCode::new()); + let src = render::render_one(&f.into(), "io.p"); + assert!( + src.contains("fun ((value: Int) -> String)?.asRaw()"), + "{src}" + ); +} + +#[test] +fn a_named_receiver_is_rendered_unchanged() { + let mut imports = ImportSet::new("io.p"); + assert_eq!( + KtType::cls("io.other.Cb").render_receiver(&mut imports), + "Cb" + ); + assert_eq!( + KtType::generic("List", [KtType::int()]).render_receiver(&mut imports), + "List" + ); + assert_eq!( + KtType::cls("io.other.Cb") + .nullable() + .render_receiver(&mut imports), + "Cb?" + ); +} + +#[test] +fn a_duplicate_on_a_function_type_receiver_reads_as_kotlin_syntax() { + // The diagnostic parenthesizes the receiver on the same rule the renderer + // uses, so what it prints matches what would be emitted. + let ext = || { + KtFun::new("asRaw") + .receiver(KtType::lambda( + [("value".to_string(), KtType::int())], + KtType::unit(), + )) + .body(KtCode::new()) + }; + let f = KtFile::new("io.p").decl(ext()).decl(ext()); + let diags = f.validate(); + assert_eq!(diags.len(), 1, "{diags:#?}"); + assert!( + diags[0] + .message + .contains("`((value: Int) -> Unit).asRaw()`"), + "{diags:#?}" + ); +} diff --git a/src/types.rs b/src/types.rs index c4d3f54..8aab4e7 100644 --- a/src/types.rs +++ b/src/types.rs @@ -122,6 +122,42 @@ impl KtType { } } + /// Render in **extension-receiver** position — `fun .name()`. + /// + /// A function type needs parentheses there, or the `.` binds to its return + /// type instead: `fun ((Int) -> String).ext()`, never + /// `fun (Int) -> String.ext()`. A nullable one is already parenthesized by + /// [`Self::render`], so it is left alone. + /// + /// ``` + /// use kotlin_codegen::{ImportSet, KtType}; + /// let mut imports = ImportSet::new("io.p"); + /// let f = KtType::lambda([("x".to_string(), KtType::int())], KtType::string()); + /// assert_eq!(f.render_receiver(&mut imports), "((x: Int) -> String)"); + /// assert_eq!(KtType::string().render_receiver(&mut imports), "String"); + /// ``` + pub fn render_receiver(&self, imports: &mut ImportSet) -> String { + let rendered = self.render(imports); + if self.needs_receiver_parens() { + format!("({rendered})") + } else { + rendered + } + } + + /// Whether writing this type in receiver position needs parentheses added + /// around it. True only for a non-nullable function type — a nullable one + /// is already parenthesized by [`Self::render`] and by its `Display`. + pub(crate) fn needs_receiver_parens(&self) -> bool { + matches!( + self, + KtType::Function { + nullable: false, + .. + } + ) + } + /// Render to Kotlin source, registering imports in `imports`. pub fn render(&self, imports: &mut ImportSet) -> String { match self { diff --git a/src/validate.rs b/src/validate.rs index 498868a..ed832ed 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -28,7 +28,7 @@ use super::{ ident::{is_valid_kotlin_package, is_writable_kotlin_ident}, model::{ KtBody, KtClass, KtClassKind, KtClassModifier, KtCompanion, KtCtorParam, KtDecl, KtFile, - KtParam, + KtFun, KtParam, }, slot::KtPropertyValue, }; @@ -304,11 +304,12 @@ impl KtFile { /// Keeping types and values apart is what allows `class Foo` and `val Foo` to /// coexist, which Kotlin permits and this check used to reject. /// -/// **Limitation.** There is no type resolver here, so parameter types are -/// compared *as written*: `io.p.Foo` and `Foo` are different keys even when -/// they name the same type, and `fun f(x: T)` does not collide with -/// `fun f(x: R)`. The check therefore misses some real duplicates. It is a -/// net, not a proof — and a net that never catches a fish it shouldn't. +/// **Limitation.** There is no type resolver here, so a function's parameter +/// types — and its extension receiver — are compared *as written*: `io.p.Foo` +/// and `Foo` are different keys even when they name the same type, and +/// `fun f(x: T)` does not collide with `fun f(x: R)`. The check +/// therefore misses some real duplicates. It is a net, not a proof — and a net +/// that never catches a fish it shouldn't. fn check_scope<'a>( decls: &'a [KtDecl], ctor_params: &'a [KtCtorParam], @@ -318,7 +319,7 @@ fn check_scope<'a>( ) { let mut types: BTreeSet<&str> = BTreeSet::new(); let mut values: BTreeSet<&str> = BTreeSet::new(); - let mut funs: BTreeSet<(&str, String)> = BTreeSet::new(); + let mut funs: BTreeSet = BTreeSet::new(); let mut raws: BTreeSet<&str> = BTreeSet::new(); // `val`/`var` constructor parameters are properties of the class, so they @@ -360,12 +361,12 @@ fn check_scope<'a>( if f.name.is_empty() { continue; } - let sig = param_signature(&f.params); - if !funs.insert((&f.name, sig.clone())) { + let sig = fun_signature(f); + if !funs.insert(sig.clone()) { d.push( Check::DuplicateFunction, scope, - format!("duplicate function `{}({sig})`", f.name), + format!("duplicate function `{sig}`"), ); } } @@ -394,6 +395,22 @@ fn check_scope<'a>( } } +/// A function's overload identity, as it reads in a diagnostic: +/// `f(Int)`, or `Foo.f(Int)` for an extension function. +/// +/// The receiver is part of the identity because Kotlin dispatches an extension +/// on it — `Foo.f()` and `Bar.f()` are two declarations in one package, not a +/// redeclaration. It is parenthesized on the same rule the renderer uses, so +/// the diagnostic reads as the syntax it is describing. +fn fun_signature(f: &KtFun) -> String { + let params = param_signature(&f.params); + match &f.receiver { + Some(r) if r.needs_receiver_parens() => format!("({r}).{}({params})", f.name), + Some(r) => format!("{r}.{}({params})", f.name), + None => format!("{}({params})", f.name), + } +} + /// A function's parameter types as written, which is the only signature the /// model can offer — see the note on [`check_scope`]. fn param_signature(params: &[KtParam]) -> String { diff --git a/tests/golden/invalid.txt b/tests/golden/invalid.txt index 3082d35..168409b 100644 --- a/tests/golden/invalid.txt +++ b/tests/golden/invalid.txt @@ -6,6 +6,7 @@ error [invalid-identifier] in `io.example.broken/My-Class/object`: parameter nam error [duplicate-type] in `io.example.broken`: duplicate type `Session` error [duplicate-type] in `io.example.broken`: duplicate type `Describable` error [duplicate-function] in `io.example.broken`: duplicate function `send(Int)` +error [duplicate-function] in `io.example.broken`: duplicate function `io.example.Codec.asRaw()` error [duplicate-value] in `io.example.broken/Holder`: duplicate value `id` error [duplicate-type] in `io.example.broken/Outer`: companion object `Factory` collides with another type of that name error [duplicate-raw] in `io.example.broken`: duplicate raw block `__loader` @@ -17,7 +18,7 @@ error [import-collision] in `io.example.broken`: import simple-name collision: ` error [invalid-package] in `io..example.object`: `io..example.object` is not a valid Kotlin package path === merging refuses to produce anything === -16 Kotlin validation error(s): +17 Kotlin validation error(s): error [invalid-package] in `io..example.object`: `io..example.object` is not a valid Kotlin package path error [invalid-identifier] in `io.example.broken`: class name `My-Class` is not a valid Kotlin identifier error [invalid-identifier] in `io.example.broken/My-Class`: constructor parameter name `2fast` is not a valid Kotlin identifier @@ -26,6 +27,7 @@ error [invalid-package] in `io..example.object`: `io..example.object` is not a v error [duplicate-type] in `io.example.broken`: duplicate type `Session` error [duplicate-type] in `io.example.broken`: duplicate type `Describable` error [duplicate-function] in `io.example.broken`: duplicate function `send(Int)` + error [duplicate-function] in `io.example.broken`: duplicate function `io.example.Codec.asRaw()` error [duplicate-value] in `io.example.broken/Holder`: duplicate value `id` error [duplicate-type] in `io.example.broken/Outer`: companion object `Factory` collides with another type of that name error [duplicate-raw] in `io.example.broken`: duplicate raw block `__loader` @@ -36,12 +38,13 @@ error [invalid-package] in `io..example.object`: `io..example.object` is not a v error [import-collision] in `io.example.broken`: import simple-name collision: `io.example.a.Codec` and `io.example.b.Codec` === the same model under `warn_all()` === -merged 2 file(s) with 16 warning(s); generation continues +merged 2 file(s) with 17 warning(s); generation continues === one check downgraded, another switched off === error [duplicate-type] in `io.example.broken`: duplicate type `Session` error [duplicate-type] in `io.example.broken`: duplicate type `Describable` warning [duplicate-function] in `io.example.broken`: duplicate function `send(Int)` +warning [duplicate-function] in `io.example.broken`: duplicate function `io.example.Codec.asRaw()` error [duplicate-value] in `io.example.broken/Holder`: duplicate value `id` error [duplicate-type] in `io.example.broken/Outer`: companion object `Factory` collides with another type of that name error [duplicate-raw] in `io.example.broken`: duplicate raw block `__loader` diff --git a/tests/golden/showcase.txt b/tests/golden/showcase.txt index 63136f3..c516a0f 100644 --- a/tests/golden/showcase.txt +++ b/tests/golden/showcase.txt @@ -19,6 +19,7 @@ public fun describeAll(items: List): String package io.example.api import io.example.api.internal.JNINative +import io.example.api.internal.RawSink import io.example.internal.Guard /** @@ -72,6 +73,8 @@ public abstract class NativeHandle(initialPtr: Long) : AutoCloseable { public interface Describable { fun describe(): String + + fun Sample.label(): String } /** Invoked from the native thread for each reply. */ @@ -114,6 +117,10 @@ public open class Session(initialPtr: Long) : NativeHandle(initialPtr), Describa return acc } + public override fun Sample.label(): String = "${keyExpr}@${describe()}" + + public fun ByteArray.toSample(): Sample = Sample(describe(), this) + public class Config { val describe = "config" } @@ -124,6 +131,12 @@ public open class Session(initialPtr: Long) : NativeHandle(initialPtr), Describa } } +public fun Sample.summary(): String = "$keyExpr (${payload.size} bytes)" + +public fun List.mapValues(transform: (sample: Sample) -> R): List = filterIsInstance().map { transform(it.sample) } + +internal fun ((sample: Sample) -> Unit).asRaw(): RawSink = RawSink { raw -> if (raw is Reply.Value) this(raw.sample) } + public typealias SampleList = List --- kotlin/io/example/api/internal.kt --- @@ -132,6 +145,10 @@ package io.example.api.internal import io.example.api.Reply +internal fun interface RawSink { + fun accept(reply: Reply) +} + /** One-to-one with the exported Rust symbols. */ internal object JNINative { internal external fun sessionOpen(config: String): Long