diff --git a/AGENTS.md b/AGENTS.md index 3efa8527..6e4bb25a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,8 @@ src/ ├── context.rs # GlobalContext (scopes, registry, externals) ├── external_map.rs # `--external Foo=::path::Bar` resolution ├── parse/ # .d.ts → IR -│ ├── docs.rs # JSDoc extraction (incl. @throws) +│ ├── annotate.rs # `--annotate` side files: layer @rust/@throws by name +│ ├── docs.rs # JSDoc extraction (incl. @throws, @rust) │ ├── members.rs # interface / class member converters │ ├── merge.rs # var + interface merging │ ├── classify.rs # interface classification (class-like vs dictionary) diff --git a/CONVENTIONS.md b/CONVENTIONS.md index dfd5f90d..5b2b5f18 100644 --- a/CONVENTIONS.md +++ b/CONVENTIONS.md @@ -37,6 +37,8 @@ in sync with the snapshot fixtures (`tests/fixtures/*.d.ts` paired with * [`toString()` is `Display`](#tostring-is-display) * [`Promise` returns become `async fn`](#promiset-returns-become-async-fn) * [`@throws` JSDoc → typed error](#throws-jsdoc--typed-error) +* [`@rust` JSDoc → Rust scalar](#rust-jsdoc--rust-scalar) +* [`--annotate` side files](#--annotate-side-files) * [Subtyping LUB across unions](#subtyping-lub-across-unions) * [Module declarations and namespace nesting](#module-declarations-and-namespace-nesting) * [Type aliases and `export { X as Y }`](#type-aliases-and-export--x-as-y-) @@ -3389,6 +3391,132 @@ impl CompressionStream { setter taking a named union alias is wrapped like any binding (`set_relative_to(&self, val: impl Into)`). +## `@rust` JSDoc → Rust scalar + +TS `number` lowers to `f64`, which is wrong-shaped for most of the +numbers a platform API actually carries (`status: number` is a `u16`, +`port: number` a `u16`, `limit: number` a `u32`). A `@rust` tag forces +the Rust scalar; wasm-bindgen converts the JS number at the boundary: + +```ts +interface Response { + /** @rust u16 */ + readonly status: number; + /** + * @rust status: u16 + * @rust u32 + */ + withStatus(status: number, reason?: string): number; + /** @rust u32 */ + count(): Promise; +} +``` + +```rust +pub fn status(this: &Response) -> u16; +pub fn with_status(this: &Response, status: u16) -> u32; +pub async fn count(this: &Response) -> Result; +``` + +* `@rust ` applies to the member's own type: a property (getter, + setter and builder method alike) or a callable's return. +* `@rust : ` applies to the named parameter of a method, + constructor or function; several tags may be stacked. +* The scalar replaces the leaf through `| null` / `| undefined` and an + optional declaration (read as `Nullable` / `Option`), `T[]` + (`Vec` / `&[u8]`) and an async return's `Promise<…>` + (`Result`). +* A generic handle's argument must be a JS type, so inside one the + tagged leaf keeps the JS number it narrows: `Number`, or `BigInt` for + `u64`/`i64`. A `Promise` getter or argument is + `Promise`, `number[][]` is `Vec>` and + `(number | null)[]` is `Vec>`; the tag still + applies where the leaf reaches a Rust position. +* Accepted scalars: `u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 usize`. + wasm-bindgen crosses `u64`/`i64` as a JS `BigInt`, so they apply only + to a `bigint` leaf; every other scalar applies only to a `number` + leaf. A `@rust u64` on a `number` would make the JS glue reject the + value at runtime, so it is refused: sizes beyond `u32` stay `f64` + (exact to 2^53). An unknown scalar, an incompatible leaf, or a + parameter name that does not exist is an error diagnostic and the + tag is ignored. + +Why a JSDoc tag rather than a branded alias (`type u16 = number`): the +alias form needs the upstream declaration to *use* the alias, which an +upstream file never does, so annotations would still have to be +layered on — and a tag reads as what it is, a Rust-side refinement, +while a `u16` alias in `.d.ts` suggests a TS-level distinction that +does not exist. + +## `--annotate` side files + +Upstream declaration files (`@cloudflare/workers-types`, +`lib.dom.d.ts`) carry no `@rust` or `@throws` tags, and editing a +vendored copy does not survive an upgrade. `--annotate` takes side +`.d.ts` files that re-declare **only the members they tag**; the tags +are applied to the input's declarations by declaration name and member +name, and the side file is otherwise not emitted: + +```ts +// annotations.d.ts +interface Response { + /** @rust u16 */ + readonly status: number; + /** @throws {TypeError} if the body has already been used */ + clone(): Response; +} +interface Console { /** @throws {never} */ log(...data: any[]): void; } +declare abstract class Cache { /** @rust ttl: u32 */ put(url: string, ttl?: number): Promise; } +``` + +```sh +ts-gen --input node_modules/@cloudflare/workers-types/index.d.ts \ + --annotate annotations.d.ts … +``` + +* `@rust` applies as if written on the input member; a property's tag + reaches its setter too. Parameters match by name. +* `@throws` replaces the input member's failure mode. Getters are + infallible, so on a property it contributes only its doc. +* A side member applies to every overload of the input method or + function, and the `## Errors` section its `@throws` renders is + appended to the input's doc once. The rest of the side doc is not + carried over, so a member copied from upstream with its description + does not repeat it. Overloads may differ in return + type (`delete(key): Promise`, `delete(keys: string[]): + Promise`), so a return `@rust` applies to the overloads that + can carry it. +* A declaration, member or parameter named in the side file that does + not exist in the input fails the run, so a typo cannot silently miss + a binding; so does an invalid `@rust` tag, or one whose scalar the + input's type cannot carry. Side members without tags are ignored, + whatever their description. +* Only classes, interfaces and functions can be annotated: a + `declare namespace`, `declare module`, variable, or alias that sets no + type-parameter default (see below) in the side file fails the run + rather than being skipped, as does a bare name + that the input declares more than once (globally and inside a + module), since names are matched without scope. +* A side declaration's type-parameter defaults replace the input's, by + parameter name; a default for a parameter the input lacks fails the + run. The output changes wherever a default is read: a class or + interface default becomes the emitted Rust default, and an alias's + expansion takes it. The `annotate` fixture, modelled on + workers-types, declares `Request` + and `RequestInfo = Request | string`, where `Cf` defaults to a union of the incoming and + outgoing shapes (`CfProperties = Incoming | Outgoing`), which erases. + The side file's `interface Request` + makes the emitted type `Request`, so a bare `Request` reads `cf()` as + `Incoming`. Its `type RequestInfo = …` + (whose target is ignored) makes the expansion `Request`, which is now `Request`'s defaults and so collapses to the + bare name: `fetch(input: &Request)`. +* The side file may use `interface` for a class and need not repeat + type parameters or parameter types other than tagged ones; only names + matter. + ## Subtyping LUB across unions `TypeRef::Union` resolution applies a Least Upper Bound across its members diff --git a/README.md b/README.md index 9a9aa68d..9a5af421 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,15 @@ ts-gen --input node_modules/@cloudflare/workers-types/index.d.ts --output src/bi --export Request --export Response --export KVNamespace --export cloudflare:workers ``` +Layer `@rust` / `@throws` tags onto a large declaration file from a side +file (see `CONVENTIONS.md`, "`--annotate` side files"): + +```sh +ts-gen --input node_modules/@cloudflare/workers-types/index.d.ts \ + --output src/bindings.rs \ + --annotate types/annotations.d.ts +``` + Opt generic imports into wasm-bindgen's experimental per-monomorphization codegen: diff --git a/integration-tests/tests/rust_tags.rs b/integration-tests/tests/rust_tags.rs new file mode 100644 index 00000000..4fb864f4 --- /dev/null +++ b/integration-tests/tests/rust_tags.rs @@ -0,0 +1,36 @@ +#![cfg(target_arch = "wasm32")] + +use js_sys::{Array, BigInt, Function, JsNullable, Nullable, Number, Promise, Undefined}; + +use ts_gen_integration_tests::rust_tags::*; + +// This fixture has no backing JavaScript module, so it provides compile-only +// coverage that every `@rust` scalar has a wasm-bindgen ABI, including a +// tagged leaf inside a generic handle, which stays a JS `Number`. +#[allow(dead_code)] +async fn rust_tag_signatures_compile(response: &Response, callback: &Function Undefined>) { + let _: u16 = response.status(); + let _: Nullable = response.content_length(); + let _: u32 = response.with_status(200); + let _: u32 = response.with_status_with_reason(200, "OK"); + let _: u32 = response.count().await.unwrap(); + let _: Nullable = response.maybe_count().await.unwrap(); + response.upload_part(1); + let _: Vec = response.bytes(); + response.listen(&[80, 443]); + let _: u64 = response.offset(); + + let closed: Promise = response.closed(); + let _: Number = response.closed().await.unwrap(); + let _: BigInt = response.total().await.unwrap(); + response.when_ready(&closed); + let matrix: Vec> = response.matrix(); + response.set_matrix(&matrix); + let codes: Vec> = response.codes(); + response.set_codes(&codes); + + let init = ResponseInit::builder().status(204).build(); + let _: Option = init.status(); + let _: i32 = set_timeout(callback); + let _: i32 = set_timeout_with_ms(callback, 10u32); +} diff --git a/integration-tests/tests/rust_tags_mono.rs b/integration-tests/tests/rust_tags_mono.rs new file mode 100644 index 00000000..456f41d1 --- /dev/null +++ b/integration-tests/tests/rust_tags_mono.rs @@ -0,0 +1,41 @@ +//! @ts-gen --experimental-generic-mono + +#![cfg(target_arch = "wasm32")] + +use js_sys::{Array, BigInt, Function, JsNullable, Nullable, Number, Promise, Undefined}; + +use ts_gen_integration_tests::rust_tags_mono::*; + +// This fixture has no backing JavaScript module, so it provides compile-only +// coverage that every `@rust` scalar has a wasm-bindgen ABI, including a +// tagged leaf inside a generic handle, which stays a JS `Number`. +#[allow(dead_code)] +async fn rust_tag_mono_signatures_compile( + response: &Response, + callback: &Function Undefined>, +) { + let _: u16 = response.status(); + let _: Nullable = response.content_length(); + let _: u32 = response.with_status(200); + let _: u32 = response.with_status_with_reason(200, "OK"); + let _: u32 = response.count().await.unwrap(); + let _: Nullable = response.maybe_count().await.unwrap(); + response.upload_part(1); + let _: Vec = response.bytes(); + response.listen(&[80, 443]); + let _: u64 = response.offset(); + + let closed: Promise = response.closed(); + let _: Number = response.closed().await.unwrap(); + let _: BigInt = response.total().await.unwrap(); + response.when_ready(&closed); + let matrix: Vec> = response.matrix(); + response.set_matrix(&matrix); + let codes: Vec> = response.codes(); + response.set_codes(&codes); + + let init = ResponseInit::builder().status(204).build(); + let _: Option = init.status(); + let _: i32 = set_timeout(callback); + let _: i32 = set_timeout_with_ms(callback, 10u32); +} diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index c692eaf7..13a43eea 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -535,6 +535,7 @@ fn dynamic_union_variant_name(ty: &TypeRef) -> String { match ty { T::Boolean | T::BooleanLiteral(_) => "Bool".to_string(), T::Number | T::NumberLiteral(_) => "F64".to_string(), + T::RustScalar(s) => s.as_str().to_uppercase(), T::String | T::StringLiteral(_) => "String".to_string(), T::BigInt => "BigInt".to_string(), T::Void | T::Undefined => "Undefined".to_string(), diff --git a/src/codegen/signatures.rs b/src/codegen/signatures.rs index e2fb38e9..ca8e00a9 100644 --- a/src/codegen/signatures.rs +++ b/src/codegen/signatures.rs @@ -387,6 +387,7 @@ pub fn collect_type_params( | TypeRef::ArrayBufferView | TypeRef::PropertyKey | TypeRef::TypedSlice(..) + | TypeRef::RustScalar(_) | TypeRef::StringLiteral(_) | TypeRef::NumberLiteral(_) | TypeRef::BooleanLiteral(_) diff --git a/src/codegen/surface.rs b/src/codegen/surface.rs index aa09731a..009aacbb 100644 --- a/src/codegen/surface.rs +++ b/src/codegen/surface.rs @@ -797,6 +797,7 @@ fn type_refs(ty: &TypeRef, scope: ScopeId, out: &mut Vec<(String, ScopeId)>) { | TypeRef::ArrayBufferView | TypeRef::PropertyKey | TypeRef::TypedSlice(..) + | TypeRef::RustScalar(_) | TypeRef::StringLiteral(_) | TypeRef::NumberLiteral(_) | TypeRef::BooleanLiteral(_) diff --git a/src/codegen/typemap.rs b/src/codegen/typemap.rs index 6f3fe2e1..c969927c 100644 --- a/src/codegen/typemap.rs +++ b/src/codegen/typemap.rs @@ -438,6 +438,7 @@ fn write_type_ref_key(buf: &mut String, ty: &TypeRef) { TypeRef::ArrayBufferView => buf.push_str("ArrayBufferView"), TypeRef::PropertyKey => buf.push_str("PropertyKey"), TypeRef::TypedSlice(e, _) => write!(buf, "slice:{}", e.typed_array()).unwrap(), + TypeRef::RustScalar(s) => buf.push_str(s.as_str()), TypeRef::StringLiteral(s) => write!(buf, "lit:{s:?}").unwrap(), TypeRef::NumberLiteral(n) => write!(buf, "lit:{n}").unwrap(), TypeRef::BooleanLiteral(b) => write!(buf, "lit:{b}").unwrap(), @@ -1371,6 +1372,7 @@ impl<'a> CodegenContext<'a> { | TypeRef::ArrayBufferView | TypeRef::PropertyKey | TypeRef::TypedSlice(..) + | TypeRef::RustScalar(_) | TypeRef::StringLiteral(_) | TypeRef::NumberLiteral(_) | TypeRef::BooleanLiteral(_) @@ -2034,6 +2036,11 @@ pub fn to_syn_type( match ty { TypeRef::Boolean | TypeRef::BooleanLiteral(_) => return quote! { Boolean }, TypeRef::Number | TypeRef::NumberLiteral(_) => return quote! { Number }, + // A generic argument must be a JS type, so a `@rust` scalar + // inside a handle (`Promise<…>` that is not an async return, + // `Array<…>`, `JsNullable<…>`) keeps the JS number it narrows. + TypeRef::RustScalar(s) if s.is_bigint() => return quote! { BigInt }, + TypeRef::RustScalar(_) => return quote! { Number }, TypeRef::String | TypeRef::StringLiteral(_) => return quote! { JsString }, TypeRef::Void | TypeRef::Undefined => return quote! { Undefined }, _ => {} @@ -2086,6 +2093,11 @@ pub fn to_syn_type( TypeRef::Unknown => maybe_ref(quote! { JsValue }, borrow), TypeRef::Object => maybe_ref(quote! { Object }, borrow), TypeRef::Symbol => maybe_ref(quote! { Symbol }, borrow), + // Always by value; wasm-bindgen converts the JS number. + TypeRef::RustScalar(s) => { + let ident = syn::Ident::new(s.as_str(), proc_macro2::Span::call_site()); + quote! { #ident } + } // === TS-only synthetic === // `ArrayBufferView` is a TS-only union alias (typed-array @@ -3233,6 +3245,7 @@ fn is_mono_native_leaf(ty: &TypeRef, ctx: &CodegenContext<'_>, scope: ScopeId) - | TypeRef::BooleanLiteral(_) | TypeRef::Number | TypeRef::NumberLiteral(_) + | TypeRef::RustScalar(_) | TypeRef::String | TypeRef::StringLiteral(_) ) || ctx.enum_kind(ty, scope).is_some() @@ -4036,8 +4049,8 @@ fn is_mono_type_param(ty: &TypeRef, ctx: Option<&CodegenContext<'_>>, scope: Sco } /// Whether a top-level `T[]` element keeps its Rust-native form -/// (`String` / `f64`) inside the slice / `Vec`, rather than its handle -/// form. Enums count as the primitive they carry. +/// (`String` / `f64` / a `@rust` scalar) inside the slice / `Vec`, rather +/// than its handle form. Enums count as the primitive they carry. fn is_native_array_element( elem: &TypeRef, ctx: Option<&CodegenContext<'_>>, @@ -4049,6 +4062,7 @@ fn is_native_array_element( | TypeRef::StringLiteral(_) | TypeRef::Number | TypeRef::NumberLiteral(_) + | TypeRef::RustScalar(_) | TypeRef::Boolean | TypeRef::BooleanLiteral(_) ) @@ -4160,7 +4174,8 @@ pub(crate) fn is_mono_string_list_return( ) } -/// Whether a top-level `T[]` element lowers to `f64`. +/// Whether a top-level `T[]` element is a JS number: `f64`, or the scalar +/// a `@rust` tag forces. fn is_numeric_array_element( elem: &TypeRef, ctx: Option<&CodegenContext<'_>>, @@ -4168,7 +4183,7 @@ fn is_numeric_array_element( ) -> bool { matches!( native_array_leaf(elem, ctx, scope).as_ref(), - TypeRef::Number | TypeRef::NumberLiteral(_) + TypeRef::Number | TypeRef::NumberLiteral(_) | TypeRef::RustScalar(_) ) } diff --git a/src/ir.rs b/src/ir.rs index b2f44a72..095016bc 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -270,6 +270,11 @@ pub enum TypeRef { /// flattening, never parsed. TypedSlice(TypedArrayElement, SharedMemory), + /// A Rust scalar forced by a `@rust ` JSDoc tag. TS `number` + /// is `f64`; declarations that know better (`status: number` is a + /// `u16`) say so here, and wasm-bindgen converts at the boundary. + RustScalar(RustScalar), + // === Syntactic constructs === /// `T[]` — the syntactic array shortcut. Distinct from /// `Array` (which is a [`TypeRef::Reference`] to the global @@ -458,6 +463,73 @@ impl TypedArrayElement { } } +/// The scalar set a `@rust` tag may name. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RustScalar { + U8, + U16, + U32, + U64, + I8, + I16, + I32, + I64, + F32, + F64, + Usize, +} + +impl RustScalar { + pub fn parse(name: &str) -> Option { + Some(match name { + "u8" => Self::U8, + "u16" => Self::U16, + "u32" => Self::U32, + "u64" => Self::U64, + "i8" => Self::I8, + "i16" => Self::I16, + "i32" => Self::I32, + "i64" => Self::I64, + "f32" => Self::F32, + "f64" => Self::F64, + "usize" => Self::Usize, + _ => return None, + }) + } + + /// Whether wasm-bindgen crosses this scalar as a JS `BigInt`. + pub fn is_bigint(self) -> bool { + matches!(self, Self::U64 | Self::I64) + } + + /// Whether a TS leaf type can cross the boundary as this scalar. + /// wasm-bindgen passes `u64`/`i64` as `BigInt`, so they need a + /// `bigint`; every other scalar is a JS `number`. + pub fn accepts(self, leaf: &TypeRef) -> bool { + if self.is_bigint() { + matches!(leaf, TypeRef::BigInt) + } else { + matches!(leaf, TypeRef::Number) + } + } + + pub fn as_str(self) -> &'static str { + match self { + Self::U8 => "u8", + Self::U16 => "u16", + Self::U32 => "u32", + Self::U64 => "u64", + Self::I8 => "i8", + Self::I16 => "i16", + Self::I32 => "i32", + Self::I64 => "i64", + Self::F32 => "f32", + Self::F64 => "f64", + Self::Usize => "usize", + } + } +} + impl TypeRef { /// The union of `members` in normal form (see [`Union`]). A single /// surviving member is returned bare, so this is also how a union is @@ -532,6 +604,43 @@ impl TypeRef { matches!(self, TypeRef::Null | TypeRef::Undefined) } + /// Replace the scalar leaf of `self` (through `| null` / `| undefined`, + /// `Promise`, and `T[]`) with `scalar`, i.e. what a `@rust` tag applies + /// to. `None` when the leaf cannot carry `scalar` (see + /// [`RustScalar::accepts`]). + pub fn with_rust_scalar(&self, scalar: RustScalar) -> Option { + if let Some((core, n)) = self.split_empties() { + return Some(core.with_rust_scalar(scalar)?.with_empties(n)); + } + Some(match self { + TypeRef::Array(inner) => TypeRef::Array(Box::new(inner.with_rust_scalar(scalar)?)), + TypeRef::Reference { + segments, + generic_args, + } if segments.len() == 1 && segments[0] == "Promise" && generic_args.len() == 1 => { + TypeRef::generic("Promise", vec![generic_args[0].with_rust_scalar(scalar)?]) + } + leaf if scalar.accepts(leaf) => TypeRef::RustScalar(scalar), + _ => return None, + }) + } + + /// Whether a `@rust` scalar appears anywhere in `self`. + pub fn has_rust_scalar(&self) -> bool { + match self { + TypeRef::RustScalar(_) => true, + TypeRef::Array(inner) => inner.has_rust_scalar(), + TypeRef::Reference { generic_args, .. } => { + generic_args.iter().any(TypeRef::has_rust_scalar) + } + TypeRef::Union(u) => u.members().iter().any(TypeRef::has_rust_scalar), + TypeRef::Intersection(items) | TypeRef::Tuple(items) => { + items.iter().any(TypeRef::has_rust_scalar) + } + _ => false, + } + } + /// Construct a single-segment, non-generic reference: `Foo`. /// Convenience for the common case of a bare ident. pub fn ident(name: impl Into) -> Self { @@ -655,6 +764,7 @@ impl TypeRef { TypeRef::PropertyKey => "PropertyKey".into(), TypeRef::TypedSlice(e, _) if e.is_bigint() => "bigint[]".into(), TypeRef::TypedSlice(..) => "number[]".into(), + TypeRef::RustScalar(s) => s.as_str().into(), TypeRef::Array(inner) => { // Wrap composite inner types in parens so e.g. // `(A | B)[]` doesn't render as the wrong-precedence diff --git a/src/main.rs b/src/main.rs index 7ee21d0d..25c9f5cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,6 +41,19 @@ struct Cli { #[arg(short = 'E', long = "export", num_args = 1..)] export: Vec, + /// Side `.d.ts` files whose `@rust` and `@throws` JSDoc tags are + /// layered onto the input declarations, matched by declaration and + /// member name. + /// + /// Upstream declaration files carry no such tags and editing a + /// vendored copy does not survive an upgrade. A side file re-declares + /// only the members it annotates (`interface Response { /** @rust u16 + /// */ readonly status: number; }`); the tags are applied to the input's + /// declarations and the side file itself is not emitted. A name that + /// matches nothing is an error. + #[arg(long = "annotate", num_args = 1..)] + annotate: Vec, + /// JS module specifier (required when .d.ts has top-level exports). /// This module is the library the output binds, so it is emitted at /// the top level rather than in a `pub mod`. @@ -155,6 +168,24 @@ fn main() -> Result<()> { gctx.external_map.add_mappings(mapping); } + if !cli.annotate.is_empty() { + let files = resolve_input_files(&cli.annotate)?; + let (annotations, agctx) = ts_gen::parse::parse_dts_files(&files, cli.lib_name.as_deref())?; + agctx.diagnostics.emit(); + if agctx.diagnostics.has_errors() { + let files: Vec = cli + .annotate + .iter() + .map(|p| p.display().to_string()) + .collect(); + bail!("--annotate: invalid tags in {}", files.join(", ")); + } + let errors = ts_gen::parse::annotate::apply(&mut gctx, &module, &annotations, &agctx); + if !errors.is_empty() { + bail!("--annotate:\n {}", errors.join("\n ")); + } + } + gctx.diagnostics.emit(); if cli.verbose { diff --git a/src/parse/annotate.rs b/src/parse/annotate.rs new file mode 100644 index 00000000..04f6c250 --- /dev/null +++ b/src/parse/annotate.rs @@ -0,0 +1,654 @@ +//! `--annotate`: layer `@rust` and `@throws` tags from a side `.d.ts` +//! onto declarations of the main input, matched by declaration and +//! member name. Upstream declaration files carry no such tags, and +//! editing a vendored copy does not survive an upgrade; a side file that +//! re-declares only the annotated members does. + +use crate::context::{GlobalContext, TypeId}; +use crate::ir::{ + FunctionDecl, Member, Module, Param, RustScalar, Throws, TypeKind, TypeParam, TypeRef, +}; + +/// Apply every tag found in `annotations` to the matching declarations +/// of `module`. Returns one message per declaration, member or +/// parameter named in the side file that `module` lacks. +pub fn apply( + gctx: &mut GlobalContext, + module: &Module, + annotations: &Module, + agctx: &GlobalContext, +) -> Vec { + let mut errors = Vec::new(); + for &aid in &annotations.types { + let adecl = agctx.get_type(aid); + let (name, members, type_params) = match &adecl.kind { + TypeKind::Class(c) => (&c.name, &c.members, &c.type_params), + TypeKind::Interface(i) => (&i.name, &i.members, &i.type_params), + TypeKind::Function(f) => { + apply_function( + gctx, + module, + errors_section(adecl.doc.as_deref()), + f, + &mut errors, + ); + continue; + } + // An alias carries no members; only its type-parameter + // defaults can be overridden (`RequestInfo<…, Cf = …>`). + // This is its only lookup, so it reports a missing or + // ambiguous name itself (`apply_members` does for the rest). + TypeKind::TypeAlias(a) if a.type_params.iter().any(|tp| tp.default.is_some()) => { + match find_decl(gctx, module, &a.name) { + Ok(_) => apply_type_param_defaults( + gctx, + module, + &a.name, + &a.type_params, + &mut errors, + ), + Err(e) => errors.push(e), + } + continue; + } + // The side file is a curated list: a declaration this pass + // cannot carry tags for must not be silently skipped. + other => { + let name = crate::parse::declaration_name(other).unwrap_or_default(); + errors.push(format!( + "`{name}`: only classes, interfaces and functions can be annotated \ + (namespaces, modules, aliases and variables are not scoped yet)" + )); + continue; + } + }; + apply_members(gctx, module, name, members, &mut errors); + apply_type_param_defaults(gctx, module, name, type_params, &mut errors); + } + errors +} + +/// Every side declaration of a function applies to every input +/// declaration of it, as side method overloads do in `apply_members`. +fn apply_function( + gctx: &mut GlobalContext, + module: &Module, + doc: Option<&str>, + f: &FunctionDecl, + errors: &mut Vec, +) { + let side: Vec> = std::iter::once(Overlay { + own: scalar_leaf(&f.return_type), + params: &f.params, + throws: &f.throws, + doc, + }) + .chain(f.overloads.iter().map(|o| Overlay { + own: scalar_leaf(&o.return_type), + params: &o.params, + throws: &o.throws, + doc: errors_section(o.doc.as_deref()), + })) + .filter(|o| !o.is_empty()) + .collect(); + if side.is_empty() { + return; + } + let id = match find_decl(gctx, module, &f.js_name) { + Ok(id) => id, + Err(e) => { + errors.push(e); + return; + } + }; + let target = gctx.get_type_mut(id); + let TypeKind::Function(func) = &mut target.kind else { + errors.push(format!("`{}` is not a function", f.js_name)); + return; + }; + for (i, mut overlay) in side.into_iter().enumerate() { + if i > 0 { + overlay.doc = None; + } + let (mut found, mut mismatched) = (Vec::new(), Vec::new()); + let (mut returns_ok, mut return_mismatches) = (false, Vec::new()); + let targets = std::iter::once(( + &mut func.return_type, + &mut func.params, + &mut func.throws, + &mut target.doc, + )) + .chain( + func.overloads + .iter_mut() + .map(|o| (&mut o.return_type, &mut o.params, &mut o.throws, &mut o.doc)), + ); + for (ret, params, throws, doc) in targets { + match overlay.apply_return(ret) { + Some(m) => return_mismatches.push(m), + None => returns_ok = true, + } + overlay.apply_params(params, &mut found, &mut mismatched); + overlay.apply_throws(throws); + overlay.apply_doc(doc); + } + for p in overlay.missing_params(&found) { + errors.push(format!("`{}` has no parameter `{p}`", f.js_name)); + } + if overlay.own.is_some() && !returns_ok { + mismatched.extend(return_mismatches); + } + mismatched.dedup(); + for m in mismatched { + errors.push(format!("`{}`: {m}", f.js_name)); + } + } +} + +/// A side declaration's type-parameter defaults replace the input's, by +/// parameter name. The input's default is a TypeScript choice that may +/// erase (`RequestInfo<…, Cf = CfProperties<…>>`, a union of the incoming +/// and outgoing shapes); the side file picks the one an alias expands +/// with (`Cf = IncomingRequestCfProperties`). Only +/// parameters that declare a default in the side file are touched. +fn apply_type_param_defaults( + gctx: &mut GlobalContext, + module: &Module, + name: &str, + side: &[TypeParam], + errors: &mut Vec, +) { + if side.iter().all(|tp| tp.default.is_none()) { + return; + } + let Ok(id) = find_decl(gctx, module, name) else { + return; + }; + let targets: &mut Vec = match &mut gctx.get_type_mut(id).kind { + TypeKind::Class(c) => &mut c.type_params, + TypeKind::Interface(i) => &mut i.type_params, + TypeKind::DiscriminatedUnion(d) => &mut d.type_params, + TypeKind::TypeAlias(a) => &mut a.type_params, + _ => return, + }; + for tp in side.iter().filter(|tp| tp.default.is_some()) { + match targets.iter_mut().find(|t| t.name == tp.name) { + Some(target) => target.default = tp.default.clone(), + None => errors.push(format!("`{name}` has no type parameter `{}`", tp.name)), + } + } +} + +/// The input declaration a side name targets. A name declared more than +/// once (globally and in a module) cannot be targeted by bare name. +fn find_decl(gctx: &GlobalContext, module: &Module, name: &str) -> Result { + let matches: Vec = module + .types + .iter() + .copied() + .filter(|&id| { + let decl = gctx.get_type(id); + match &decl.kind { + TypeKind::Class(c) => c.name == name, + TypeKind::Interface(i) => i.name == name, + TypeKind::DiscriminatedUnion(d) => d.name == name, + TypeKind::TypeAlias(a) => a.name == name, + TypeKind::Function(f) => f.js_name == name, + _ => false, + } + }) + .collect(); + match matches.as_slice() { + [] => Err(format!("no declaration named `{name}`")), + [id] => Ok(*id), + _ => Err(format!( + "`{name}` names {} declarations; a side file cannot target one by bare name", + matches.len() + )), + } +} + +/// The tags one side-file member carries onto its targets. +struct Overlay<'a> { + /// `@rust `: the property type or callable return. + own: Option, + /// Parameters, of which only `@rust`-tagged ones are applied. + params: &'a [Param], + throws: &'a Throws, + /// The `## Errors` section `@throws` renders in the side doc, appended + /// to the target's doc. The side description is not carried over: a + /// member copied from upstream would repeat the input's. + doc: Option<&'a str>, +} + +impl Overlay<'_> { + fn is_empty(&self) -> bool { + self.own.is_none() + && self + .params + .iter() + .all(|p| scalar_leaf(&p.type_ref).is_none()) + && matches!(self.throws, Throws::None) + && self.doc.is_none_or(|d| d.trim().is_empty()) + } + + /// The mismatch message when the input's leaf cannot carry the + /// scalar; the side file's own type does not vouch for it. + fn apply_return(&self, ty: &mut TypeRef) -> Option { + let scalar = self.own?; + match ty.with_rust_scalar(scalar) { + Some(forced) => { + *ty = forced; + None + } + None => Some(mismatch(scalar, "type", ty)), + } + } + + /// Names that matched are pushed to `found`; a tag is only an error + /// when no overload of the group carries the parameter. + fn apply_params( + &self, + targets: &mut [Param], + found: &mut Vec, + mismatched: &mut Vec, + ) { + for ap in self.params { + let Some(scalar) = scalar_leaf(&ap.type_ref) else { + continue; + }; + if let Some(tp) = targets.iter_mut().find(|tp| tp.name == ap.name) { + match tp.type_ref.with_rust_scalar(scalar) { + Some(forced) => tp.type_ref = forced, + None => mismatched.push(mismatch( + scalar, + &format!("parameter `{}`", ap.name), + &tp.type_ref, + )), + } + if !found.contains(&ap.name) { + found.push(ap.name.clone()); + } + } + } + } + + fn apply_throws(&self, throws: &mut Throws) { + if !matches!(self.throws, Throws::None) { + *throws = self.throws.clone(); + } + } + + fn apply_doc(&self, doc: &mut Option) { + let Some(extra) = self.doc.map(str::trim).filter(|d| !d.is_empty()) else { + return; + }; + *doc = Some(match doc.take().filter(|d| !d.trim().is_empty()) { + Some(d) => format!("{}\n\n{extra}", d.trim_end()), + None => extra.to_string(), + }); + } + + fn missing_params(&self, found: &[String]) -> Vec { + self.params + .iter() + .filter(|p| scalar_leaf(&p.type_ref).is_some() && !found.contains(&p.name)) + .map(|p| p.name.clone()) + .collect() + } +} + +/// Which target members a side-file member addresses. +#[derive(PartialEq)] +enum Key<'a> { + Property(&'a str), + StaticProperty(&'a str), + Method(&'a str), + StaticMethod(&'a str), + Constructor, +} + +impl Key<'_> { + fn describe(&self) -> String { + match self { + Key::Property(n) => format!("property `{n}`"), + Key::StaticProperty(n) => format!("static property `{n}`"), + Key::Method(n) => format!("method `{n}`"), + Key::StaticMethod(n) => format!("static method `{n}`"), + Key::Constructor => "constructor".to_string(), + } + } +} + +/// Key and overlay of a side-file member; setters and index signatures +/// carry nothing (a property's tags sit on its getter). +fn side_member(m: &Member) -> Option<(Key<'_>, Overlay<'_>)> { + Some(match m { + // Getters are infallible, so a side getter's `@throws` only + // contributes its rendered doc, as it would written on the input. + Member::Getter(g) => ( + Key::Property(&g.js_name), + Overlay { + own: scalar_leaf(&g.type_ref), + params: &[], + throws: &Throws::None, + doc: errors_section(g.doc.as_deref()), + }, + ), + Member::StaticGetter(g) => ( + Key::StaticProperty(&g.js_name), + Overlay { + own: scalar_leaf(&g.type_ref), + params: &[], + throws: &Throws::None, + doc: errors_section(g.doc.as_deref()), + }, + ), + Member::Method(m) => ( + Key::Method(&m.js_name), + Overlay { + own: scalar_leaf(&m.return_type), + params: &m.params, + throws: &m.throws, + doc: errors_section(m.doc.as_deref()), + }, + ), + Member::StaticMethod(m) => ( + Key::StaticMethod(&m.js_name), + Overlay { + own: scalar_leaf(&m.return_type), + params: &m.params, + throws: &m.throws, + doc: errors_section(m.doc.as_deref()), + }, + ), + Member::Constructor(c) => ( + Key::Constructor, + Overlay { + own: None, + params: &c.params, + throws: &c.throws, + doc: errors_section(c.doc.as_deref()), + }, + ), + Member::Setter(_) | Member::StaticSetter(_) | Member::IndexSignature(_) => return None, + }) +} + +fn apply_members( + gctx: &mut GlobalContext, + module: &Module, + name: &str, + annotated: &[Member], + errors: &mut Vec, +) { + let id = match find_decl(gctx, module, name) { + Ok(id) => id, + Err(e) => { + errors.push(e); + return; + } + }; + let members: &mut Vec = match &mut gctx.get_type_mut(id).kind { + TypeKind::Class(c) => &mut c.members, + TypeKind::Interface(i) => &mut i.members, + TypeKind::DiscriminatedUnion(d) => &mut d.members, + _ => { + errors.push(format!("`{name}` is not a class or interface")); + return; + } + }; + // Each side overload applies to every target overload; its doc is + // appended once per member. + let mut seen: Vec> = Vec::new(); + for (key, mut overlay) in annotated.iter().filter_map(side_member) { + if overlay.is_empty() { + continue; + } + if seen.contains(&key) { + overlay.doc = None; + } + let mut hit = false; + let (mut found, mut mismatched) = (Vec::new(), Vec::new()); + // Overloads may differ in return type (`delete(key): boolean`, + // `delete(keys): number`); a return tag applies to those that can + // carry it and is an error only when none can. + let (mut returns_ok, mut return_mismatches) = (false, Vec::new()); + let mut on_return = |m: Option| match m { + Some(m) => return_mismatches.push(m), + None => returns_ok = true, + }; + for m in members.iter_mut() { + match (m, &key) { + (Member::Getter(t), Key::Property(n)) if t.js_name == *n => { + hit = true; + mismatched.extend(overlay.apply_return(&mut t.type_ref)); + overlay.apply_doc(&mut t.doc); + } + (Member::Setter(t), Key::Property(n)) if t.js_name == *n => { + mismatched.extend(overlay.apply_return(&mut t.type_ref)); + } + (Member::StaticGetter(t), Key::StaticProperty(n)) if t.js_name == *n => { + hit = true; + mismatched.extend(overlay.apply_return(&mut t.type_ref)); + overlay.apply_doc(&mut t.doc); + } + (Member::StaticSetter(t), Key::StaticProperty(n)) if t.js_name == *n => { + mismatched.extend(overlay.apply_return(&mut t.type_ref)); + } + (Member::Method(t), Key::Method(n)) if t.js_name == *n => { + hit = true; + on_return(overlay.apply_return(&mut t.return_type)); + overlay.apply_params(&mut t.params, &mut found, &mut mismatched); + overlay.apply_throws(&mut t.throws); + overlay.apply_doc(&mut t.doc); + } + (Member::StaticMethod(t), Key::StaticMethod(n)) if t.js_name == *n => { + hit = true; + on_return(overlay.apply_return(&mut t.return_type)); + overlay.apply_params(&mut t.params, &mut found, &mut mismatched); + overlay.apply_throws(&mut t.throws); + overlay.apply_doc(&mut t.doc); + } + (Member::Constructor(t), Key::Constructor) => { + hit = true; + overlay.apply_params(&mut t.params, &mut found, &mut mismatched); + overlay.apply_throws(&mut t.throws); + overlay.apply_doc(&mut t.doc); + } + _ => {} + } + } + if !hit { + errors.push(format!("`{name}` has no {}", key.describe())); + } + for p in overlay.missing_params(&found) { + errors.push(format!( + "`{name}` {} has no parameter `{p}`", + key.describe() + )); + } + if overlay.own.is_some() && !returns_ok { + mismatched.extend(return_mismatches); + } + mismatched.dedup(); + for m in mismatched { + errors.push(format!("`{name}` {}: {m}", key.describe())); + } + if !seen.contains(&key) { + seen.push(key); + } + } +} + +/// The rendered `## Errors` section of a side doc, up to the next +/// section; `None` without one. +fn errors_section(doc: Option<&str>) -> Option<&str> { + let doc = doc?; + let start = if doc.starts_with("## Errors") { + 0 + } else { + doc.find("\n## Errors")? + 1 + }; + let rest = &doc[start..]; + let end = rest.find("\n## ").unwrap_or(rest.len()); + Some(rest[..end].trim_end()) +} + +fn mismatch(scalar: RustScalar, what: &str, ty: &TypeRef) -> String { + format!( + "{what} `{}` cannot cross as `@rust {}` (u64/i64 need `bigint`, other scalars `number`)", + ty.format_ts(), + scalar.as_str() + ) +} + +/// The `@rust` scalar an annotated type carries, if any. +fn scalar_leaf(ty: &TypeRef) -> Option { + match ty { + TypeRef::RustScalar(s) => Some(*s), + TypeRef::Array(inner) => scalar_leaf(inner), + TypeRef::Union(u) => u.members().iter().find_map(scalar_leaf), + TypeRef::Reference { generic_args, .. } => generic_args.iter().find_map(scalar_leaf), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::apply; + + fn errors(input: &str, side: &str) -> Vec { + let (module, mut gctx) = crate::parse::parse_single_source(input, None).unwrap(); + let (annotations, agctx) = crate::parse::parse_single_source(side, None).unwrap(); + assert!( + !agctx.diagnostics.has_errors(), + "side file did not parse cleanly" + ); + apply(&mut gctx, &module, &annotations, &agctx) + } + + const INPUT: &str = " + interface Response { readonly status: number; redirect(url: string, status?: number): Response; } + declare function fetch(url: string): Promise; + "; + + #[test] + fn clean_side_file_applies() { + let side = "interface Response { /** @rust u16 */ readonly status: number; }"; + assert!(errors(INPUT, side).is_empty()); + } + + #[test] + fn missing_declaration() { + let side = "interface Nope { /** @rust u16 */ readonly status: number; }"; + assert_eq!(errors(INPUT, side), ["no declaration named `Nope`"]); + } + + #[test] + fn missing_alias() { + let input = " + interface Incoming { readonly colo: string; } + interface Request { readonly cf?: Cf; } + type RequestInfo = Request | string; + "; + let side = "type RequestInfoo = Request | string;"; + assert_eq!(errors(input, side), ["no declaration named `RequestInfoo`"]); + let ambiguous = " + type RequestInfo = Cf | string; + declare module \"m\" { type RequestInfo = Cf | string; } + "; + let side = "type RequestInfo = Cf | string;"; + let errs = errors(ambiguous, side); + assert_eq!(errs.len(), 1); + assert!(errs[0].contains("names 2 declarations"), "{errs:?}"); + } + + #[test] + fn errors_section_only() { + use super::errors_section; + assert_eq!(errors_section(Some("Creates a clone.")), None); + assert_eq!(errors_section(None), None); + let doc = + "Creates a clone.\n\n## Errors\n\n* `TypeError` — used\n\n## Example\n\n```js\nx\n```"; + assert_eq!( + errors_section(Some(doc)), + Some("## Errors\n\n* `TypeError` — used") + ); + let doc = "## Errors\n\n* `TypeError` — used"; + assert_eq!(errors_section(Some(doc)), Some(doc)); + } + + #[test] + fn missing_member() { + let side = "interface Response { /** @rust u16 */ readonly code: number; }"; + assert_eq!(errors(INPUT, side), ["`Response` has no property `code`"]); + } + + #[test] + fn missing_parameter() { + let side = "interface Response { /** @rust code: u16 */ redirect(url: string, code?: number): Response; }"; + assert_eq!( + errors(INPUT, side), + ["`Response` method `redirect` has no parameter `code`"] + ); + } + + #[test] + fn scalar_mismatch() { + // The side file types `url` as a number so its own tag is valid; + // the input's `string` cannot carry it. + let side = "/** @rust url: u32 */ declare function fetch(url: number): Promise;"; + let errs = errors(INPUT, side); + assert_eq!(errs.len(), 1); + assert!(errs[0].contains("cannot cross as `@rust u32`"), "{errs:?}"); + } + + #[test] + fn unsupported_declaration_kind() { + let side = "declare namespace NS { interface Inner { /** @rust u16 */ a: number; } }"; + let errs = errors(INPUT, side); + assert_eq!(errs.len(), 1); + assert!(errs[0].starts_with("`NS`: only classes"), "{errs:?}"); + } + + #[test] + fn type_param_default_replaces_input() { + let input = " + interface Incoming { readonly colo: string; } + interface Outgoing { readonly cacheTtl: number; } + type Cf = Incoming | Outgoing; + interface Request { readonly cf?: Props; } + "; + let side = "interface Request {}"; + let (module, mut gctx) = crate::parse::parse_single_source(input, None).unwrap(); + let (annotations, agctx) = crate::parse::parse_single_source(side, None).unwrap(); + assert!(apply(&mut gctx, &module, &annotations, &agctx).is_empty()); + let id = module.types.iter().copied().find(|&id| { + matches!(&gctx.get_type(id).kind, crate::ir::TypeKind::Interface(i) if i.name == "Request") + }).unwrap(); + let crate::ir::TypeKind::Interface(req) = &gctx.get_type(id).kind else { + unreachable!() + }; + assert_eq!( + req.type_params[1].default.as_ref().unwrap().format_ts(), + "Incoming" + ); + let bad = "interface Request {}"; + let (annotations, agctx) = crate::parse::parse_single_source(bad, None).unwrap(); + let errs = apply(&mut gctx, &module, &annotations, &agctx); + assert_eq!(errs, ["`Request` has no type parameter `Nope`"]); + } + + #[test] + fn ambiguous_name() { + let input = " + interface Thing { readonly a: number; } + declare module \"m\" { interface Thing { readonly a: number; } } + "; + let side = "interface Thing { /** @rust u16 */ readonly a: number; }"; + let errs = errors(input, side); + assert_eq!(errs.len(), 1); + assert!(errs[0].contains("names 2 declarations"), "{errs:?}"); + } +} diff --git a/src/parse/docs.rs b/src/parse/docs.rs index b67acc8b..267e142e 100644 --- a/src/parse/docs.rs +++ b/src/parse/docs.rs @@ -48,6 +48,15 @@ pub struct JsDocInfo { /// body (outside any `@param`) — the property-doc form, applied to the /// setter's value argument. pub slice_mode: Option, + + /// `@rust ` — Rust scalar type for the member's own type (a + /// property's type, a callable's return). See + /// [`crate::ir::RustScalar`] for the accepted set. + pub rust_type: Option, + + /// `@rust : ` — Rust scalar type for one named + /// parameter of the callable. + pub rust_params: Vec<(String, String)>, } impl JsDocInfo { @@ -203,6 +212,15 @@ fn convert_jsdoc_tags(lines: &[&str]) -> (String, JsDocInfo) { .or_else(|| line.strip_prefix("@return ")) { returns = Some(rest.to_string()); + } else if let Some(rest) = line.strip_prefix("@rust ") { + // `@rust u16` / `@rust status: u16`. Consumed, not rendered. + let rest = rest.trim(); + match rest.split_once(':') { + Some((param, ty)) => info + .rust_params + .push((param.trim().to_string(), ty.trim().to_string())), + None => info.rust_type = Some(rest.to_string()), + } } else if let Some(rest) = line.strip_prefix("@throws ") { // Capture both the structured type info (for `info.throws`) and a // human-readable line for the Errors section in the rendered doc. diff --git a/src/parse/erased_args.rs b/src/parse/erased_args.rs index b7ed8cde..0bfcc4d1 100644 --- a/src/parse/erased_args.rs +++ b/src/parse/erased_args.rs @@ -219,6 +219,7 @@ impl Pass<'_> { ), // Leaves: nothing nested to visit. TypeRef::Reference { .. } + | TypeRef::RustScalar(_) | TypeRef::Boolean | TypeRef::Number | TypeRef::String diff --git a/src/parse/first_pass/converters.rs b/src/parse/first_pass/converters.rs index 7057dc32..61048493 100644 --- a/src/parse/first_pass/converters.rs +++ b/src/parse/first_pass/converters.rs @@ -166,7 +166,7 @@ pub fn interface_from_signatures( pub fn convert_function_decl( func: &ast::Function<'_>, - throws: ir::Throws, + info: &crate::parse::docs::JsDocInfo, parent_scope: ScopeId, ctx: &mut ParseCtx<'_, '_>, ) -> Option { @@ -177,12 +177,19 @@ pub fn convert_function_decl( let mut type_params = convert_type_params(func.type_parameters.as_ref(), ctx.diag); let body_scope = create_body_scope(&mut type_params, parent_scope, ctx); - let params = convert_formal_params_scoped(&func.params, body_scope, ctx); - let return_type = func + let mut params = convert_formal_params_scoped(&func.params, body_scope, ctx); + let mut return_type = func .return_type .as_ref() .map(|rt| convert_ts_type_scoped(&rt.type_annotation, body_scope, ctx)) .unwrap_or(ir::TypeRef::Void); + crate::parse::members::apply_rust_tags( + info, + Some(&mut return_type), + &mut params, + &js_name, + ctx, + ); Some(ir::FunctionDecl { name: rust_name, @@ -191,7 +198,7 @@ pub fn convert_function_decl( params, return_type, overloads: vec![], - throws, + throws: info.throws(), async_modifier: func.r#async, body_scope, }) diff --git a/src/parse/first_pass/populate.rs b/src/parse/first_pass/populate.rs index 199931a7..d6c1ba8a 100644 --- a/src/parse/first_pass/populate.rs +++ b/src/parse/first_pass/populate.rs @@ -320,7 +320,7 @@ impl<'a, 'docs> PopulateCtx<'a, 'docs> { None => (None, Default::default()), }; if let Some(mut decl) = - convert_function_decl(func, jsinfo.throws(), scope, &mut self.parse_ctx()) + convert_function_decl(func, &jsinfo, scope, &mut self.parse_ctx()) { crate::parse::members::apply_slice_hints( &mut decl.params, @@ -595,9 +595,7 @@ impl<'a, 'docs> PopulateCtx<'a, 'docs> { declarations: &mut Vec, ) { let (doc, info) = self.lookup_callable_doc(dcx.export_span_start, func.span.start); - if let Some(mut d) = - convert_function_decl(func, info.throws(), dcx.scope, &mut self.parse_ctx()) - { + if let Some(mut d) = convert_function_decl(func, &info, dcx.scope, &mut self.parse_ctx()) { crate::parse::members::apply_slice_hints(&mut d.params, &info, &d.js_name, self.diag); declarations.push(dcx.decl(ir::TypeKind::Function(d), doc)); } diff --git a/src/parse/members.rs b/src/parse/members.rs index c066cbe3..b2b094f9 100644 --- a/src/parse/members.rs +++ b/src/parse/members.rs @@ -70,6 +70,66 @@ pub(crate) fn apply_slice_hints( } } +/// Apply `@rust ` to the member's own type and `@rust : +/// ` to the named parameters. Unknown scalars, incompatible +/// leaves and unknown parameter names are error diagnostics (fatal in +/// an `--annotate` side file), not silent no-ops. +pub(crate) fn apply_rust_tags( + info: &JsDocInfo, + own: Option<&mut TypeRef>, + params: &mut [Param], + member: &str, + ctx: &mut ParseCtx<'_, '_>, +) { + use crate::ir::RustScalar; + if let Some(name) = &info.rust_type { + match (RustScalar::parse(name), own) { + (Some(scalar), Some(ty)) => match ty.with_rust_scalar(scalar) { + Some(forced) => *ty = forced, + None => ctx.diag.error(incompatible(name, member, ty)), + }, + (Some(_), None) => ctx.diag.error(format!( + "`@rust {name}` on `{member}` has no type to apply to" + )), + (None, _) => ctx + .diag + .error(format!("`@rust {name}` on `{member}`: unknown scalar type")), + } + } + for (param, name) in &info.rust_params { + let Some(scalar) = RustScalar::parse(name) else { + ctx.diag.error(format!( + "`@rust {param}: {name}` on `{member}`: unknown scalar type" + )); + continue; + }; + // IR parameter names are already snake_case; tags use the TS name. + let ir_name = to_snake_case(param); + match params.iter_mut().find(|p| p.name == ir_name) { + Some(p) => match p.type_ref.with_rust_scalar(scalar) { + Some(forced) => p.type_ref = forced, + None => ctx.diag.error(incompatible( + name, + &format!("{member}({param})"), + &p.type_ref, + )), + }, + None => ctx.diag.error(format!( + "`@rust {param}: {name}` on `{member}`: no such parameter" + )), + } + } +} + +/// Diagnostic for a `@rust` scalar whose target type cannot carry it. +pub(crate) fn incompatible(scalar: &str, member: &str, ty: &TypeRef) -> String { + format!( + "`@rust {scalar}` on `{member}`: `{}` cannot cross as `{scalar}` (u64/i64 need \ + `bigint`, other scalars `number`)", + ty.format_ts() + ) +} + /// Push the given type parameters into a fresh child of `parent_scope` /// and return the new scope. Used by every member converter that /// declares its own `` — methods, free functions, hoisted @@ -801,11 +861,12 @@ fn convert_property_signature( let (doc, info) = split_info(ctx.docs.info_for_span(prop.span.start)); let slice_mode = slice_hint(info.slice_mode.as_deref(), &js_name, ctx.diag); - let type_ref = prop + let mut type_ref = prop .type_annotation .as_ref() .map(|ann| convert_ts_type_scoped(&ann.type_annotation, parent_scope, ctx)) .unwrap_or(TypeRef::Any); + apply_rust_tags(&info, Some(&mut type_ref), &mut [], &js_name, ctx); let mut members = vec![Member::Getter(GetterMember { scope: parent_scope, @@ -869,6 +930,7 @@ fn convert_method_signature( (Some(rt), _) => convert_ts_type_scoped(&rt.type_annotation, body_scope, ctx), (None, _) => TypeRef::Void, }; + apply_rust_tags(&info, Some(&mut return_type), &mut params, &js_name, ctx); // Hoist top-level `Iterable` / `AsyncIterable` returns into // synthesized wrapper interfaces with a `[Symbol.iterator]` method. @@ -989,6 +1051,7 @@ fn convert_class_method( (Some(rt), _) => convert_ts_type_scoped(&rt.type_annotation, body_scope, ctx), (None, _) => TypeRef::Void, }; + apply_rust_tags(&info, Some(&mut return_type), &mut params, &js_name, ctx); // Same iterable hoisting as `convert_method_signature`. Skip for // constructors — they don't have a meaningful return type. @@ -1101,11 +1164,12 @@ fn convert_class_property( let (doc, info) = split_info(ctx.docs.info_for_span(prop.span.start)); let slice_mode = slice_hint(info.slice_mode.as_deref(), &js_name, ctx.diag); - let type_ref = prop + let mut type_ref = prop .type_annotation .as_ref() .map(|ann| convert_ts_type_scoped(&ann.type_annotation, parent_scope, ctx)) .unwrap_or(TypeRef::Any); + apply_rust_tags(&info, Some(&mut type_ref), &mut [], &js_name, ctx); if prop.r#static { let mut members = vec![Member::StaticGetter(StaticGetterMember { diff --git a/src/parse/merge.rs b/src/parse/merge.rs index ebd3c45a..e9510d2c 100644 --- a/src/parse/merge.rs +++ b/src/parse/merge.rs @@ -73,11 +73,12 @@ pub fn convert_construct_signature( let mut type_params = crate::parse::types::convert_type_params(ctor.type_parameters.as_ref(), ctx.diag); let scope = crate::parse::members::create_body_scope(&mut type_params, parent_scope, ctx); - let params = convert_formal_params_scoped(&ctor.params, scope, ctx); + let mut params = convert_formal_params_scoped(&ctor.params, scope, ctx); let (doc, info) = match ctx.docs.info_for_span(ctor.span.start) { Some((d, i)) => (Some(d), i), None => (None, crate::parse::docs::JsDocInfo::default()), }; + crate::parse::members::apply_rust_tags(&info, None, &mut params, "constructor", ctx); ConstructorMember { scope, type_params, diff --git a/src/parse/mod.rs b/src/parse/mod.rs index b1879055..5c7ed10c 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -1,5 +1,6 @@ //! Parser orchestration: parse `.d.ts` files into IR. +pub mod annotate; pub mod classify; pub mod ctx; pub mod docs; @@ -1271,7 +1272,7 @@ fn is_value(kind: &crate::ir::TypeKind) -> bool { } /// Extract the name from a declaration kind, if it has one. -fn declaration_name(kind: &crate::ir::TypeKind) -> Option { +pub(crate) fn declaration_name(kind: &crate::ir::TypeKind) -> Option { use crate::ir::TypeKind; match kind { TypeKind::Class(c) => Some(c.name.clone()), diff --git a/tests/fixtures/annotate.d.ts b/tests/fixtures/annotate.d.ts new file mode 100644 index 00000000..53194c24 --- /dev/null +++ b/tests/fixtures/annotate.d.ts @@ -0,0 +1,59 @@ +//! @ts-gen --annotate tests/fixtures/side/annotate.d.ts + +// `--annotate` layers the `@rust` and `@throws` tags of a side file onto +// these declarations by name; the side file's own declarations are not +// emitted and untagged members there are ignored. + +declare abstract class Response { + readonly status: number; + readonly ok: boolean; + /** Creates a clone of the response. */ + clone(): Response; +} + +interface ResponseInit { + status?: number; + statusText?: string; +} + +declare abstract class Cache { + put(url: string, ttl?: number): Promise; + put(request: Response, ttl?: number): Promise; +} + +interface Storage { + delete(key: string): Promise; + delete(keys: string[]): Promise; +} + +interface Console { + log(...data: any[]): void; +} + +declare const console: Console; + +declare function setTimeout(callback: () => void, ms?: number): number; + +declare function setInterval(callback: () => void): number; +declare function setInterval(callback: () => void, ms: number): number; + +declare function size(key: string): number; +declare function size(keys: string[], n: number): number; + +// A side declaration's type-parameter defaults replace the input's. The +// side interface's default makes the emitted `Request`, so a bare `Request` reads `cf()` as `Incoming` rather than +// the erased union; the side alias's default expands `fetch`'s argument to +// `Request`, which collapses to `&Request`. +interface Incoming { + readonly colo: string; +} +interface Outgoing { + readonly cacheTtl: number; +} +type CfProperties = Incoming | Outgoing; +interface Request { + readonly cf?: Cf; +} +type RequestInfo = Request | string; +declare function fetch(input: RequestInfo): Promise; diff --git a/tests/fixtures/rust-tags.d.ts b/tests/fixtures/rust-tags.d.ts new file mode 100644 index 00000000..559a45c7 --- /dev/null +++ b/tests/fixtures/rust-tags.d.ts @@ -0,0 +1,62 @@ +// `@rust ` forces the Rust scalar for a property or a callable's +// return; `@rust : ` for one parameter. The scalar +// replaces the leaf through `?`, `| null`, `T[]` and an async return's +// `Promise<…>`; wasm-bindgen converts the JS number at the boundary. u64/i64 cross +// as BigInt, so they apply to `bigint` only; on a `number` (or any +// other type) the tag is an error diagnostic. + +interface Response { + /** @rust u16 */ + readonly status: number; + /** @rust u32 */ + readonly contentLength: number | null; + /** + * @rust status: u16 + * @rust u32 + */ + withStatus(status: number, reason?: string): number; + /** @rust u32 */ + count(): Promise; + /** @rust u32 */ + maybeCount(): Promise; + /** @rust partNumber: u16 */ + uploadPart(partNumber: number): void; + /** @rust u8 */ + bytes(): number[]; + /** @rust ports: u16 */ + listen(ports: number[]): void; + /** @rust u64 */ + readonly offset: bigint; + /** @rust u64 */ + readonly size: number; + /** @rust u16 */ + readonly statusText: string; + // Inside a generic handle the leaf must stay a JS type, so the tag + // narrows nothing there: a `Promise` that is not an async + // return, a nested array, an array of nullables. + /** @rust u32 */ + readonly closed: Promise; + /** @rust u64 */ + readonly total: Promise; + /** @rust ready: u32 */ + whenReady(ready: Promise): void; + /** @rust u8 */ + matrix(): number[][]; + /** @rust rows: u8 */ + setMatrix(rows: number[][]): void; + /** @rust u16 */ + codes(): (number | null)[]; + /** @rust codes: u16 */ + setCodes(codes: (number | null)[]): void; +} + +interface ResponseInit { + /** @rust u16 */ + status?: number; +} + +/** + * @rust ms: u32 + * @rust i32 + */ +declare function setTimeout(callback: () => void, ms?: number): number; diff --git a/tests/fixtures/side/annotate.d.ts b/tests/fixtures/side/annotate.d.ts new file mode 100644 index 00000000..fd74e691 --- /dev/null +++ b/tests/fixtures/side/annotate.d.ts @@ -0,0 +1,56 @@ +// Side file for `annotate.d.ts`: re-declares only the members it tags. + +// Only the `## Errors` section of a side doc is carried over: `clone`, +// copied from upstream, keeps one description, and `ok`, with no tag, +// is ignored. +interface Response { + /** @rust u16 */ + readonly status: number; + /** A note that is not carried over. */ + readonly ok: boolean; + /** + * Creates a clone of the response. + * @throws {TypeError} if the body has already been used + */ + clone(): Response; +} + +interface ResponseInit { + /** @rust u16 */ + status?: number; +} + +declare abstract class Cache { + /** @rust ttl: u32 */ + put(url: string, ttl?: number): Promise; +} + +// A return tag applies to the overloads whose return can carry it. +interface Storage { + /** @rust u32 */ + delete(keys: string[]): Promise; +} + +interface Console { + /** @throws {never} */ + log(...data: any[]): void; +} + +/** @rust i32 */ +declare function setTimeout(callback: () => void, ms?: number): number; + +// A side function applies to every input overload, and each side +// overload applies. +/** + * @rust i32 + * @throws {never} + */ +declare function setInterval(callback: () => void, ms?: number): number; + +/** @rust u32 */ +declare function size(key: string): number; +/** @rust n: u32 */ +declare function size(keys: string[], n: number): number; + +interface Request {} +type RequestInfo = Request | string; diff --git a/tests/snapshots/annotate/default.rs b/tests/snapshots/annotate/default.rs new file mode 100644 index 00000000..a580b00c --- /dev/null +++ b/tests/snapshots/annotate/default.rs @@ -0,0 +1,294 @@ +// Generated by ts-gen. Do not edit. + +#[allow(unused_imports)] +use js_sys::*; +#[allow(unused_imports)] +use wasm_bindgen::prelude::*; +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Response; + #[wasm_bindgen(method, getter)] + pub fn status(this: &Response) -> u16; + #[wasm_bindgen(method, getter)] + pub fn ok(this: &Response) -> bool; + #[doc = " Creates a clone of the response."] + #[doc = ""] + #[doc = " ## Errors"] + #[doc = ""] + #[doc = " * `TypeError` — if the body has already been used"] + #[wasm_bindgen(method)] + pub fn clone(this: &Response) -> Response; + #[doc = " Creates a clone of the response."] + #[doc = ""] + #[doc = " ## Errors"] + #[doc = ""] + #[doc = " * `TypeError` — if the body has already been used"] + #[wasm_bindgen(method, catch, js_name = "clone")] + pub fn try_clone(this: &Response) -> Result; +} +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object , is_type_of = JsValue :: is_object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type ResponseInit; + #[wasm_bindgen(method, getter)] + pub fn status(this: &ResponseInit) -> Option; + #[wasm_bindgen(method, setter)] + pub fn set_status(this: &ResponseInit, val: u16); + #[wasm_bindgen(method, getter, js_name = "statusText")] + pub fn status_text(this: &ResponseInit) -> Option; + #[wasm_bindgen(method, setter, js_name = "statusText")] + pub fn set_status_text(this: &ResponseInit, val: &str); +} +impl ResponseInit { + pub fn new() -> ResponseInit { + Self::builder().build() + } + pub fn builder() -> ResponseInitBuilder { + ResponseInitBuilder { + inner: JsCast::unchecked_into(js_sys::Object::new()), + } + } +} +impl Default for ResponseInit { + fn default() -> Self { + Self::new() + } +} +pub struct ResponseInitBuilder { + inner: ResponseInit, +} +impl ResponseInitBuilder { + pub fn status(self, val: u16) -> Self { + self.inner.set_status(val); + self + } + pub fn status_text(self, val: &str) -> Self { + self.inner.set_status_text(val); + self + } + pub fn build(self) -> ResponseInit { + self.inner + } +} +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Cache; + #[wasm_bindgen(method, catch)] + pub async fn put(this: &Cache, url: &str) -> Result<(), JsValue>; + #[wasm_bindgen(method, js_name = "put")] + pub fn put_promise(this: &Cache, url: &str) -> Promise; + #[wasm_bindgen(method, catch, js_name = "put")] + pub async fn put_with_ttl(this: &Cache, url: &str, ttl: u32) -> Result<(), JsValue>; + #[wasm_bindgen(method, js_name = "put")] + pub fn put_with_ttl_promise(this: &Cache, url: &str, ttl: u32) -> Promise; + #[wasm_bindgen(method, catch, js_name = "put")] + pub async fn put_with_request(this: &Cache, request: &Response) -> Result<(), JsValue>; + #[wasm_bindgen(method, js_name = "put")] + pub fn put_with_request_promise(this: &Cache, request: &Response) -> Promise; + #[wasm_bindgen(method, catch, js_name = "put")] + pub async fn put_with_request_and_ttl( + this: &Cache, + request: &Response, + ttl: u32, + ) -> Result<(), JsValue>; + #[wasm_bindgen(method, js_name = "put")] + pub fn put_with_request_and_ttl_promise( + this: &Cache, + request: &Response, + ttl: u32, + ) -> Promise; +} +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object , is_type_of = JsValue :: is_object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Storage; + #[wasm_bindgen(method, catch)] + pub async fn delete(this: &Storage, key: &str) -> Result; + #[wasm_bindgen(method, js_name = "delete")] + pub fn delete_promise(this: &Storage, key: &str) -> Promise; + #[wasm_bindgen(method, catch, js_name = "delete")] + pub async fn delete_with_keys(this: &Storage, keys: &[String]) -> Result; + #[wasm_bindgen(method, js_name = "delete")] + pub fn delete_with_keys_promise(this: &Storage, keys: &[String]) -> Promise; +} +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object , is_type_of = JsValue :: is_object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Console; + #[wasm_bindgen(method, variadic)] + pub fn log(this: &Console, data: &[JsValue]); +} +#[wasm_bindgen] +extern "C" { + #[allow(non_upper_case_globals)] + #[wasm_bindgen(thread_local_v2)] + pub static console: Console; +} +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = "setTimeout")] + pub fn set_timeout(callback: &Function Undefined>) -> i32; +} +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(catch, js_name = "setTimeout")] + pub fn try_set_timeout(callback: &Function Undefined>) -> Result; +} +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = "setTimeout")] + pub fn set_timeout_with_ms(callback: &Function Undefined>, ms: f64) -> i32; +} +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(catch, js_name = "setTimeout")] + pub fn try_set_timeout_with_ms( + callback: &Function Undefined>, + ms: f64, + ) -> Result; +} +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = "setInterval")] + pub fn set_interval(callback: &Function Undefined>) -> i32; +} +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = "setInterval")] + pub fn set_interval_with_ms(callback: &Function Undefined>, ms: f64) -> i32; +} +#[wasm_bindgen] +extern "C" { + pub fn size(key: &str) -> u32; +} +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(catch, js_name = "size")] + pub fn try_size(key: &str) -> Result; +} +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = "size")] + pub fn size_with_keys_and_n(keys: &[String], n: u32) -> u32; +} +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(catch, js_name = "size")] + pub fn try_size_with_keys_and_n(keys: &[String], n: u32) -> Result; +} +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object , is_type_of = JsValue :: is_object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Incoming; + #[wasm_bindgen(method, getter)] + pub fn colo(this: &Incoming) -> String; +} +impl Incoming { + #[allow(clippy::new_without_default)] + pub fn new() -> Incoming { + #[allow(unused_unsafe)] + unsafe { + JsValue::from(js_sys::Object::new()).unchecked_into() + } + } +} +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object , is_type_of = JsValue :: is_object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Outgoing; + #[wasm_bindgen(method, getter, js_name = "cacheTtl")] + pub fn cache_ttl(this: &Outgoing) -> f64; +} +impl Outgoing { + #[allow(clippy::new_without_default)] + pub fn new() -> Outgoing { + #[allow(unused_unsafe)] + unsafe { + JsValue::from(js_sys::Object::new()).unchecked_into() + } + } +} +#[wasm_bindgen(private)] +#[derive(Clone, Debug)] +pub enum CfProperties { + Incoming(Incoming), + Outgoing(Outgoing), +} +impl ::core::convert::From for CfProperties { + fn from(value: Incoming) -> Self { + Self::Incoming(value) + } +} +impl ::core::convert::From<&Incoming> for CfProperties { + fn from(value: &Incoming) -> Self { + Self::Incoming(value.clone()) + } +} +impl ::core::convert::From for CfProperties { + fn from(value: Outgoing) -> Self { + Self::Outgoing(value) + } +} +impl ::core::convert::From<&Outgoing> for CfProperties { + fn from(value: &Outgoing) -> Self { + Self::Outgoing(value.clone()) + } +} +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object , is_type_of = JsValue :: is_object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Request< + Meta: ::wasm_bindgen::JsGeneric = JsValue, + Cf: ::wasm_bindgen::JsGeneric = Incoming, + >; + #[wasm_bindgen(method, getter)] + pub fn cf( + this: &Request, + ) -> Option; +} +impl Request { + #[allow(clippy::new_without_default)] + pub fn new() -> Request { + #[allow(unused_unsafe)] + unsafe { + JsValue::from(js_sys::Object::new()).unchecked_into() + } + } +} +impl Default for Request { + fn default() -> Self { + Self::new() + } +} +#[allow(dead_code)] +pub type RequestInfo = JsValue; +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(catch)] + pub async fn fetch(input: &Request) -> Result; +} +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = "fetch")] + pub fn fetch_promise(input: &Request) -> Promise; +} +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(catch, js_name = "fetch")] + pub async fn fetch_with_str(input: &str) -> Result; +} +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = "fetch")] + pub fn fetch_with_str_promise(input: &str) -> Promise; +} diff --git a/tests/snapshots/annotate/mono.rs b/tests/snapshots/annotate/mono.rs new file mode 100644 index 00000000..887a5a17 --- /dev/null +++ b/tests/snapshots/annotate/mono.rs @@ -0,0 +1,308 @@ +// Generated by ts-gen. Do not edit. + +#[allow(unused_imports)] +use js_sys::*; +#[allow(unused_imports)] +use wasm_bindgen::prelude::*; +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + # [wasm_bindgen (extends = Object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Response; + #[wasm_bindgen(method, getter)] + pub fn status(this: &Response) -> u16; + #[wasm_bindgen(method, getter)] + pub fn ok(this: &Response) -> bool; + #[doc = " Creates a clone of the response."] + #[doc = ""] + #[doc = " ## Errors"] + #[doc = ""] + #[doc = " * `TypeError` — if the body has already been used"] + #[wasm_bindgen(method, catch)] + pub fn clone(this: &Response) -> Result; +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + # [wasm_bindgen (extends = Object , is_type_of = JsValue :: is_object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type ResponseInit; + #[wasm_bindgen(method, getter)] + pub fn status(this: &ResponseInit) -> Option; + #[wasm_bindgen(method, setter)] + pub fn set_status(this: &ResponseInit, val: u16); + #[wasm_bindgen(method, getter, js_name = "statusText")] + pub fn status_text(this: &ResponseInit) -> Option; + #[wasm_bindgen(method, getter, js_name = "statusText")] + pub fn status_text_js_string(this: &ResponseInit) -> Option; + #[wasm_bindgen(method, setter, js_name = "statusText")] + pub fn set_status_text(this: &ResponseInit, val: impl ::wasm_bindgen::JsStringLike); +} +impl ResponseInit { + pub fn new() -> ResponseInit { + Self::builder().build() + } + pub fn builder() -> ResponseInitBuilder { + ResponseInitBuilder { + inner: JsCast::unchecked_into(js_sys::Object::new()), + } + } +} +impl Default for ResponseInit { + fn default() -> Self { + Self::new() + } +} +pub struct ResponseInitBuilder { + inner: ResponseInit, +} +impl ResponseInitBuilder { + pub fn status(self, val: u16) -> Self { + self.inner.set_status(val); + self + } + pub fn status_text(self, val: impl ::wasm_bindgen::JsStringLike) -> Self { + self.inner.set_status_text(val); + self + } + pub fn build(self) -> ResponseInit { + self.inner + } +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + # [wasm_bindgen (extends = Object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Cache; + #[wasm_bindgen(method, catch)] + pub async fn put(this: &Cache, url: impl ::wasm_bindgen::JsStringLike) -> Result<(), JsValue>; + #[wasm_bindgen(method, js_name = "put")] + pub fn put_promise(this: &Cache, url: impl ::wasm_bindgen::JsStringLike) -> Promise; + #[wasm_bindgen(method, catch, js_name = "put")] + pub async fn put_with_ttl( + this: &Cache, + url: impl ::wasm_bindgen::JsStringLike, + ttl: u32, + ) -> Result<(), JsValue>; + #[wasm_bindgen(method, js_name = "put")] + pub fn put_with_ttl_promise( + this: &Cache, + url: impl ::wasm_bindgen::JsStringLike, + ttl: u32, + ) -> Promise; + #[wasm_bindgen(method, catch, js_name = "put")] + pub async fn put_with_request(this: &Cache, request: &Response) -> Result<(), JsValue>; + #[wasm_bindgen(method, js_name = "put")] + pub fn put_with_request_promise(this: &Cache, request: &Response) -> Promise; + #[wasm_bindgen(method, catch, js_name = "put")] + pub async fn put_with_request_and_ttl( + this: &Cache, + request: &Response, + ttl: u32, + ) -> Result<(), JsValue>; + #[wasm_bindgen(method, js_name = "put")] + pub fn put_with_request_and_ttl_promise( + this: &Cache, + request: &Response, + ttl: u32, + ) -> Promise; +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + # [wasm_bindgen (extends = Object , is_type_of = JsValue :: is_object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Storage; + #[wasm_bindgen(method, catch)] + pub async fn delete( + this: &Storage, + key: impl ::wasm_bindgen::JsStringLike, + ) -> Result; + #[wasm_bindgen(method, js_name = "delete")] + pub fn delete_promise( + this: &Storage, + key: impl ::wasm_bindgen::JsStringLike, + ) -> Promise; + #[wasm_bindgen(method, catch, js_name = "delete")] + pub async fn delete_with_keys( + this: &Storage, + keys: &[impl ::wasm_bindgen::JsStringLike], + ) -> Result; + #[wasm_bindgen(method, js_name = "delete")] + pub fn delete_with_keys_promise( + this: &Storage, + keys: &[impl ::wasm_bindgen::JsStringLike], + ) -> Promise; +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + # [wasm_bindgen (extends = Object , is_type_of = JsValue :: is_object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Console; + #[wasm_bindgen(method, variadic)] + pub fn log(this: &Console, data: &[JsValue]); +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + #[allow(non_upper_case_globals)] + #[wasm_bindgen(thread_local_v2)] + pub static console: Console; +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + #[wasm_bindgen(catch, js_name = "setTimeout")] + pub fn set_timeout(callback: &Function Undefined>) -> Result; +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + #[wasm_bindgen(catch, js_name = "setTimeout")] + pub fn set_timeout_with_ms( + callback: &Function Undefined>, + ms: f64, + ) -> Result; +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + #[wasm_bindgen(js_name = "setInterval")] + pub fn set_interval(callback: &Function Undefined>) -> i32; +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + #[wasm_bindgen(js_name = "setInterval")] + pub fn set_interval_with_ms(callback: &Function Undefined>, ms: f64) -> i32; +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + #[wasm_bindgen(catch)] + pub fn size(key: impl ::wasm_bindgen::JsStringLike) -> Result; +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + #[wasm_bindgen(catch, js_name = "size")] + pub fn size_with_keys_and_n( + keys: &[impl ::wasm_bindgen::JsStringLike], + n: u32, + ) -> Result; +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + # [wasm_bindgen (extends = Object , is_type_of = JsValue :: is_object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Incoming; + #[wasm_bindgen(method, getter)] + pub fn colo(this: &Incoming) -> String; + #[wasm_bindgen(method, getter, js_name = "colo")] + pub fn colo_js_string(this: &Incoming) -> JsString; +} +impl Incoming { + #[allow(clippy::new_without_default)] + pub fn new() -> Incoming { + #[allow(unused_unsafe)] + unsafe { + JsValue::from(js_sys::Object::new()).unchecked_into() + } + } +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + # [wasm_bindgen (extends = Object , is_type_of = JsValue :: is_object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Outgoing; + #[wasm_bindgen(method, getter, js_name = "cacheTtl")] + pub fn cache_ttl(this: &Outgoing) -> f64; +} +impl Outgoing { + #[allow(clippy::new_without_default)] + pub fn new() -> Outgoing { + #[allow(unused_unsafe)] + unsafe { + JsValue::from(js_sys::Object::new()).unchecked_into() + } + } +} +#[diagnostic::on_unimplemented( + message = "`{Self}` is not a `CfProperties`", + label = "expected Incoming or Outgoing" +)] +pub trait CfProperties: ::wasm_bindgen::convert::IntoWasmAbi {} +impl CfProperties for Incoming {} +impl CfProperties for &Incoming {} +impl CfProperties for Outgoing {} +impl CfProperties for &Outgoing {} +impl CfProperties for CfPropertiesKind {} +#[wasm_bindgen(private)] +#[derive(Clone, Debug)] +pub enum CfPropertiesKind { + Incoming(Incoming), + Outgoing(Outgoing), +} +impl ::core::convert::From for CfPropertiesKind { + fn from(value: Incoming) -> Self { + Self::Incoming(value) + } +} +impl ::core::convert::From<&Incoming> for CfPropertiesKind { + fn from(value: &Incoming) -> Self { + Self::Incoming(value.clone()) + } +} +impl ::core::convert::From for CfPropertiesKind { + fn from(value: Outgoing) -> Self { + Self::Outgoing(value) + } +} +impl ::core::convert::From<&Outgoing> for CfPropertiesKind { + fn from(value: &Outgoing) -> Self { + Self::Outgoing(value.clone()) + } +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + # [wasm_bindgen (extends = Object , is_type_of = JsValue :: is_object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Request; + #[wasm_bindgen(method, getter)] + pub fn cf(this: &Request) -> Option + where + Cf: ::wasm_bindgen::convert::OptionFromWasmAbi; +} +impl + Request +{ + #[allow(clippy::new_without_default)] + pub fn new() -> Request { + #[allow(unused_unsafe)] + unsafe { + JsValue::from(js_sys::Object::new()).unchecked_into() + } + } +} +impl Default + for Request +{ + fn default() -> Self { + Self::new() + } +} +#[allow(dead_code)] +pub type RequestInfo = JsValue; +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + #[wasm_bindgen(catch)] + pub async fn fetch(input: &Request) -> Result; +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + #[wasm_bindgen(js_name = "fetch")] + pub fn fetch_promise(input: &Request) -> Promise; +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + #[wasm_bindgen(catch, js_name = "fetch")] + pub async fn fetch_with_str( + input: impl ::wasm_bindgen::JsStringLike, + ) -> Result; +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + #[wasm_bindgen(js_name = "fetch")] + pub fn fetch_with_str_promise(input: impl ::wasm_bindgen::JsStringLike) -> Promise; +} diff --git a/tests/snapshots/rust-tags/default.rs b/tests/snapshots/rust-tags/default.rs new file mode 100644 index 00000000..26d7ec1f --- /dev/null +++ b/tests/snapshots/rust-tags/default.rs @@ -0,0 +1,141 @@ +// Generated by ts-gen. Do not edit. + +#[allow(unused_imports)] +use js_sys::*; +#[allow(unused_imports)] +use wasm_bindgen::prelude::*; +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object , is_type_of = JsValue :: is_object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Response; + #[wasm_bindgen(method, getter)] + pub fn status(this: &Response) -> u16; + #[wasm_bindgen(method, getter, js_name = "contentLength")] + pub fn content_length(this: &Response) -> Nullable; + #[wasm_bindgen(method, js_name = "withStatus")] + pub fn with_status(this: &Response, status: u16) -> u32; + #[wasm_bindgen(method, catch, js_name = "withStatus")] + pub fn try_with_status(this: &Response, status: u16) -> Result; + #[wasm_bindgen(method, js_name = "withStatus")] + pub fn with_status_with_reason(this: &Response, status: u16, reason: &str) -> u32; + #[wasm_bindgen(method, catch, js_name = "withStatus")] + pub fn try_with_status_with_reason( + this: &Response, + status: u16, + reason: &str, + ) -> Result; + #[wasm_bindgen(method, catch)] + pub async fn count(this: &Response) -> Result; + #[wasm_bindgen(method, js_name = "count")] + pub fn count_promise(this: &Response) -> Promise; + #[wasm_bindgen(method, catch, js_name = "maybeCount")] + pub async fn maybe_count(this: &Response) -> Result, JsValue>; + #[wasm_bindgen(method, js_name = "maybeCount")] + pub fn maybe_count_promise(this: &Response) -> Promise>; + #[wasm_bindgen(method, js_name = "uploadPart")] + pub fn upload_part(this: &Response, part_number: u16); + #[wasm_bindgen(method, catch, js_name = "uploadPart")] + pub fn try_upload_part(this: &Response, part_number: u16) -> Result<(), JsValue>; + #[wasm_bindgen(method)] + pub fn bytes(this: &Response) -> Vec; + #[wasm_bindgen(method, catch, js_name = "bytes")] + pub fn try_bytes(this: &Response) -> Result, JsValue>; + #[wasm_bindgen(method)] + pub fn listen(this: &Response, #[wasm_bindgen(slice = "array")] ports: &[u16]); + #[wasm_bindgen(method, catch, js_name = "listen")] + pub fn try_listen( + this: &Response, + #[wasm_bindgen(slice = "array")] ports: &[u16], + ) -> Result<(), JsValue>; + #[wasm_bindgen(method, getter)] + pub fn offset(this: &Response) -> u64; + #[wasm_bindgen(method, getter)] + pub fn size(this: &Response) -> f64; + #[wasm_bindgen(method, getter, js_name = "statusText")] + pub fn status_text(this: &Response) -> String; + #[wasm_bindgen(method, getter)] + pub fn closed(this: &Response) -> Promise; + #[wasm_bindgen(method, getter)] + pub fn total(this: &Response) -> Promise; + #[wasm_bindgen(method, js_name = "whenReady")] + pub fn when_ready(this: &Response, ready: &Promise); + #[wasm_bindgen(method, catch, js_name = "whenReady")] + pub fn try_when_ready(this: &Response, ready: &Promise) -> Result<(), JsValue>; + #[wasm_bindgen(method)] + pub fn matrix(this: &Response) -> Vec>; + #[wasm_bindgen(method, catch, js_name = "matrix")] + pub fn try_matrix(this: &Response) -> Result>, JsValue>; + #[wasm_bindgen(method, js_name = "setMatrix")] + pub fn set_matrix(this: &Response, rows: &[Array]); + #[wasm_bindgen(method, catch, js_name = "setMatrix")] + pub fn try_set_matrix(this: &Response, rows: &[Array]) -> Result<(), JsValue>; + #[wasm_bindgen(method)] + pub fn codes(this: &Response) -> Vec>; + #[wasm_bindgen(method, catch, js_name = "codes")] + pub fn try_codes(this: &Response) -> Result>, JsValue>; + #[wasm_bindgen(method, js_name = "setCodes")] + pub fn set_codes(this: &Response, codes: &[JsNullable]); + #[wasm_bindgen(method, catch, js_name = "setCodes")] + pub fn try_set_codes(this: &Response, codes: &[JsNullable]) -> Result<(), JsValue>; +} +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object , is_type_of = JsValue :: is_object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type ResponseInit; + #[wasm_bindgen(method, getter)] + pub fn status(this: &ResponseInit) -> Option; + #[wasm_bindgen(method, setter)] + pub fn set_status(this: &ResponseInit, val: u16); +} +impl ResponseInit { + pub fn new() -> ResponseInit { + Self::builder().build() + } + pub fn builder() -> ResponseInitBuilder { + ResponseInitBuilder { + inner: JsCast::unchecked_into(js_sys::Object::new()), + } + } +} +impl Default for ResponseInit { + fn default() -> Self { + Self::new() + } +} +pub struct ResponseInitBuilder { + inner: ResponseInit, +} +impl ResponseInitBuilder { + pub fn status(self, val: u16) -> Self { + self.inner.set_status(val); + self + } + pub fn build(self) -> ResponseInit { + self.inner + } +} +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = "setTimeout")] + pub fn set_timeout(callback: &Function Undefined>) -> i32; +} +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(catch, js_name = "setTimeout")] + pub fn try_set_timeout(callback: &Function Undefined>) -> Result; +} +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = "setTimeout")] + pub fn set_timeout_with_ms(callback: &Function Undefined>, ms: u32) -> i32; +} +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(catch, js_name = "setTimeout")] + pub fn try_set_timeout_with_ms( + callback: &Function Undefined>, + ms: u32, + ) -> Result; +} diff --git a/tests/snapshots/rust-tags/mono.rs b/tests/snapshots/rust-tags/mono.rs new file mode 100644 index 00000000..2dd68888 --- /dev/null +++ b/tests/snapshots/rust-tags/mono.rs @@ -0,0 +1,113 @@ +// Generated by ts-gen. Do not edit. + +#[allow(unused_imports)] +use js_sys::*; +#[allow(unused_imports)] +use wasm_bindgen::prelude::*; +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + # [wasm_bindgen (extends = Object , is_type_of = JsValue :: is_object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Response; + #[wasm_bindgen(method, getter)] + pub fn status(this: &Response) -> u16; + #[wasm_bindgen(method, getter, js_name = "contentLength")] + pub fn content_length(this: &Response) -> Nullable; + #[wasm_bindgen(method, catch, js_name = "withStatus")] + pub fn with_status(this: &Response, status: u16) -> Result; + #[wasm_bindgen(method, catch, js_name = "withStatus")] + pub fn with_status_with_reason( + this: &Response, + status: u16, + reason: impl ::wasm_bindgen::JsStringLike, + ) -> Result; + #[wasm_bindgen(method, catch)] + pub async fn count(this: &Response) -> Result; + #[wasm_bindgen(method, js_name = "count")] + pub fn count_promise(this: &Response) -> Promise; + #[wasm_bindgen(method, catch, js_name = "maybeCount")] + pub async fn maybe_count(this: &Response) -> Result, JsValue>; + #[wasm_bindgen(method, js_name = "maybeCount")] + pub fn maybe_count_promise(this: &Response) -> Promise>; + #[wasm_bindgen(method, catch, js_name = "uploadPart")] + pub fn upload_part(this: &Response, part_number: u16) -> Result<(), JsValue>; + #[wasm_bindgen(method, catch)] + pub fn bytes(this: &Response) -> Result, JsValue>; + #[wasm_bindgen(method, catch)] + pub fn listen( + this: &Response, + #[wasm_bindgen(slice = "array")] ports: &[u16], + ) -> Result<(), JsValue>; + #[wasm_bindgen(method, getter)] + pub fn offset(this: &Response) -> u64; + #[wasm_bindgen(method, getter)] + pub fn size(this: &Response) -> f64; + #[wasm_bindgen(method, getter, js_name = "statusText")] + pub fn status_text(this: &Response) -> String; + #[wasm_bindgen(method, getter, js_name = "statusText")] + pub fn status_text_js_string(this: &Response) -> JsString; + #[wasm_bindgen(method, getter)] + pub fn closed(this: &Response) -> Promise; + #[wasm_bindgen(method, getter)] + pub fn total(this: &Response) -> Promise; + #[wasm_bindgen(method, catch, js_name = "whenReady")] + pub fn when_ready(this: &Response, ready: &Promise) -> Result<(), JsValue>; + #[wasm_bindgen(method, catch)] + pub fn matrix(this: &Response) -> Result>, JsValue>; + #[wasm_bindgen(method, catch, js_name = "setMatrix")] + pub fn set_matrix(this: &Response, rows: &[Array]) -> Result<(), JsValue>; + #[wasm_bindgen(method, catch)] + pub fn codes(this: &Response) -> Result>, JsValue>; + #[wasm_bindgen(method, catch, js_name = "setCodes")] + pub fn set_codes(this: &Response, codes: &[JsNullable]) -> Result<(), JsValue>; +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + # [wasm_bindgen (extends = Object , is_type_of = JsValue :: is_object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type ResponseInit; + #[wasm_bindgen(method, getter)] + pub fn status(this: &ResponseInit) -> Option; + #[wasm_bindgen(method, setter)] + pub fn set_status(this: &ResponseInit, val: u16); +} +impl ResponseInit { + pub fn new() -> ResponseInit { + Self::builder().build() + } + pub fn builder() -> ResponseInitBuilder { + ResponseInitBuilder { + inner: JsCast::unchecked_into(js_sys::Object::new()), + } + } +} +impl Default for ResponseInit { + fn default() -> Self { + Self::new() + } +} +pub struct ResponseInitBuilder { + inner: ResponseInit, +} +impl ResponseInitBuilder { + pub fn status(self, val: u16) -> Self { + self.inner.set_status(val); + self + } + pub fn build(self) -> ResponseInit { + self.inner + } +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + #[wasm_bindgen(catch, js_name = "setTimeout")] + pub fn set_timeout(callback: &Function Undefined>) -> Result; +} +#[wasm_bindgen(experimental_generic_mono)] +extern "C" { + #[wasm_bindgen(catch, js_name = "setTimeout")] + pub fn set_timeout_with_ms( + callback: &Function Undefined>, + ms: u32, + ) -> Result; +}