Repository navigation
@rust JSDoc tags and --annotate side files - #66
guybedford wants to merge 9 commits into
Conversation
@rust JSDoc tags and --annotate side files
9530900 to
f2b4a00
Compare
f2b4a00 to
a29db92
Compare
a29db92 to
e4ec68d
Compare
e4ec68d to
d8bed42
Compare
There was a problem hiding this comment.
This PR adds @rust <scalar> JSDoc tags, which narrow a number/bigint leaf to a Rust integer or float type, and a --annotate flag that layers @rust/@throws tags (and type-parameter defaults) from a side .d.ts onto the input by name. It looks aimed at better @cloudflare/workers-types bindings: u16 statuses, an incoming cf. It chooses a tag over a branded type u16 = number alias and a side file over editing the vendored input, and the error reporting for unmatched names is thorough. The main problem: a tagged leaf inside a generic handle (Promise<number> outside an async return, number[][], (number | null)[]) lowers to a bare u32 inside Promise<…>/Array<…>/JsNullable<…>, which needs a JsGeneric type, so the generated code won't compile. There are also two smaller gaps: an alias in the side file whose name the input lacks is skipped silently, and the snapshot doesn't show the documented effect of a side default on a bare Request.
Not merge-ready:
@ruston a leaf inside a generic handle (Promise<number>getter or argument,number[][],(number | null)[]) emits a non-JsGenericu32, so the output doesn't compile.- An alias in the side file whose name the input lacks is silently skipped instead of failing the run.
- The documented side-default effect on a bare
Requestisn't reflected in the snapshot output.
TS `number` lowers to `f64`, but most numbers a platform API carries are integers with a known width: `Response.status` is a `u16`, a socket port a `u16`, a list limit a `u32`. Bindings consumers then cast at every call site, and the f64 signature is a lossy contract. `@rust <type>` on a property or callable forces the member's own type (property getter, setter and builder method, or a callable's return); `@rust <param>: <type>` forces one named parameter. The scalar replaces the leaf through `?`/`| null`, `T[]` and `Promise<...>`, and wasm-bindgen converts the JS number at the boundary. wasm-bindgen crosses u64/i64 as BigInt, so those apply to a `bigint` leaf only and every other scalar to a `number` leaf; anything else, an unknown scalar or an unknown parameter name is a diagnostic and the tag is ignored. A JSDoc tag is chosen over a branded `type u16 = number` alias because upstream declarations never use such an alias, so it would need layering anyway, and an alias suggests a TS-level distinction that does not exist.
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. A side file re-declares only the members it tags; its tags are applied to the input's declarations by declaration and member name and the side file itself is not emitted. @rust applies as if written on the input member (parameters by name), @throws replaces the member's failure mode, a side member applies to every overload of the input method or function, and its rendered doc (the `## Errors` section) is appended once. A declaration, member or parameter named in the side file that the input lacks fails the run, as does an invalid @rust tag or one the input's type cannot carry, so a mistake in the side file cannot silently miss a binding.
…input's Upstream's Request<CfHostMetadata, Cf = CfProperties<CfHostMetadata>> defaults Cf to the union of the incoming and outgoing cf shapes, which erases to JsValue, so every handler had to cast req.cf() to IncomingRequestCfProperties. The default is a TypeScript choice the side file can now override by parameter name, so the bare Request reads cf() as the incoming properties; a default for a parameter the input lacks fails the run.
9c29257 to
e9b07ab
Compare
There was a problem hiding this comment.
Since my last review the PR was rebased onto #63, and new commits fix all three earlier concerns; their threads are already resolved. A tagged leaf inside a generic handle now stays Number/BigInt, and integration tests cover it. A side alias whose name is missing or ambiguous now fails the run. Only the ## Errors section of a side doc is carried over. One problem is new: since the rebase, the snapshot gives the emitted Request Rust defaults (Cf = Incoming) and fetch takes a bare &Request, but CONVENTIONS and the fixture comment still say the opposite.
Not merge-ready:
- CONVENTIONS.md (the type-parameter defaults paragraph) and the
annotate.d.tsfixture comment say a bareRequestis unchanged andfetchtakes&Request<JsValue, Incoming>. Since the rebase, the snapshot showsRequest<…, Cf = Incoming>with Rust defaults andfetch(input: &Request).
Since type declarations carry their defaults, the side file's `interface Request<Meta = unknown, Cf = Incoming>` makes the emitted `Request<Meta = JsValue, Cf = Incoming>`, and the `RequestInfo` expansion collapses to the bare `&Request`. The type-parameter defaults paragraph and the `annotate` fixture comment now say so, matching the snapshot.
TS
numberlowers tof64, but most numbers a platform API carries are integers with a known width (Response.statusis au16, a list limit au32), and upstream declaration files cannot carry annotations.@rust <type>on a property or callable forces the member's own type;@rust <param>: <type>one named parameter. The scalar replaces the leaf through?/| null,T[]andPromise<…>, wasm-bindgen converting at the boundary; a scalar slice parameter (ports: &[u16]) crosses as a JS array (slice = "array"), like thenumber[]it types.u64/i64apply to abigintleaf (lifted fornumberin Inline wrappers:@rust u64/i64on a number, literal-keyed overloads;@throws {never}on constructors #68). A tag is chosen over a brandedtype u16 = numberalias since upstream never uses one and an alias suggests a TS-level distinction that does not exist.--annotate <side.d.ts>layers@rustand@throwstags onto the input by declaration and member name; the side file is not emitted; a side name the input lacks, an invalid tag, or a tag the type cannot carry fails the run.RequestInfo→&Request<JsValue, Incoming>in the fixture); on an interface or class they take effect once the type declaration carries its defaults (Type-level generics usable by their bare names #63), so the bareRequestcan readcf()asIncomingRequestCfPropertiesinstead of upstream's erasedCfProperties<…>union.Unit tests cover every
--annotateerror path. workers-types snapshot unchanged (annotations are supplied by the consumer).