Skip to content

v0.67.3

Choose a tag to compare

@Goldziher Goldziher released this 24 Aug 07:45
· 713 commits to main since this release
v0.67.3
93bb512

Fixed

  • e2e/swift: a getter's bridged shape is now read from the binding backend instead of
    re-derived. build_swift_first_class_map tracked Vec<Vec<_>>/Map<_> plus two hand-enumerated
    Option<Vec<Named(..)>> cases, so every other optional Vec was called countable —
    Option<Vec<String>> among them, which really emits fn og_locale_alternates(&self) -> String,
    making the generator emit ?.count against a RustString. It now calls
    field_needs_json_bridge, the same predicate wrappers::getters::emit_getters uses to pick a
    getter's return type, so the two generators can no longer disagree about one field.

  • e2e/swift: two assertion bugs with one cause — the renderer was asked to describe a leaf it
    does not model. The JSON-bridge guard was keyed on the trailing accessor's spelling (a
    length/count/size suffix), so it refused a count on a bridged leaf while emitting an
    indexed accessor against that same leaf — the generator wrote the correct "JSON-bridges it to
    RustString" skip and a broken assertion on adjacent lines. Keying on whether the path steps past
    a bridged leaf at all collapses the suffix, index and wildcard cases into one. Separately,
    field_expr.contains("?.") proves an ANCESTOR was optional and never the leaf, yet took
    precedence over the leaf's own optionality, emitting article()?.publishedTime().toString()
    where publishedTime() returns Optional<RustString>. The leaf's optionality now comes from the
    type cursor.

  • e2e: namespace_stripped_path no longer drops a real struct segment the result_fields
    config omits. Any leading segment absent from that hand-maintained list was treated as a virtual
    namespace prefix and removed, so a consumer who listed a nested leaf without also listing its
    parent had the parent silently stripped and the accessor built on the wrong receiver
    (result.favicons() against a result type with no such field). The IR is now asked instead: the
    enum and collection maps already anchor the call's declared result type, so a first segment that
    type declares as a struct field is a real nested step whatever the config omits. Absent IR still
    answers false, leaving the config-only behaviour intact.

  • e2e/zig: JSON-mode assertions no longer navigate a virtual namespace prefix as a real JSON
    key. A fixture field like batch.completed_count emitted
    result.object.get("batch").?.object.get("completed_count").?, force-unwrapping a key absent
    from every real payload and aborting the generated zig test. The conditional namespace
    stripping — previously duplicated in the brew and C e2e generators — is now
    FieldResolver::result_relative_path, shared by all three. A genuinely nested path
    (metrics.total_lines) still keeps its full chain.

  • docs: rustdoc fence attributes are no longer copied verbatim into generated markdown. A doc
    comment fence of ```rust,no_run produced a page whose fence language was the literal
    rust,no_run — a markdown info string's language is its first whitespace-delimited token —
    which alef snippets audit --docs correctly rejected as an unknown fence language. Recognised
    rustdoc attributes (no_run, ignore, should_panic, compile_fail, test_harness,
    standalone_crate, edition####, ignore-<target>, E####) are dropped; unrecognised comma
    tokens move into the fence's meta slot so the language token stays intact. Consumers could not
    fix this at the source: dropping no_run makes the doctest actually execute.

  • cli: alef snippets audit now names its coverage when no --docs root is given. A
    snippets-only invocation printed a bare Audit clean: no issues found. while the
    documentation-page checks (fence languages, include targets) never ran, so a CI job that
    omitted --docs read green for a check class it had skipped.

  • Wire src/codegen/config_gen/tests/generators.rs into the module tree
    (src/codegen/config_gen/tests.rs was missing mod generators;), so its 18 config-generator
    unit tests actually compile and run. Fixed 14 stale FieldDef/TypeDef struct literals
    predating the version and has_private_fields IR fields, and one test function missing its
    own #[test] attribute -- all silently dead until now (#211).

  • Fix a stale assertion in the Rustler kwargs-constructor test, which asserted the pre-fix
    buggy output (unwrap_or_default(), silently producing "" for a String field with a real
    default) rather than the already-correct unwrap_or("default".to_string()). The generator was
    right; only the expectation was wrong.

  • Remove three dead, never-compiled test files under src/codegen/generators/trait_bridge/tests/
    (spec.rs, type_formatting.rs, helpers.rs). All 41 of their #[test] bodies are byte-identical
    to ones in the wired spec_and_formatting.rs; helpers.rs carried no tests at all.

  • alef generate/alef build now fail loudly, before invoking flutter_rust_bridge_codegen,
    when the flutter_rust_bridge_codegen binary on PATH reports a version that disagrees with
    the project's declared [crates.dart] frb_version pin. Previously the locally installed
    codegen binary's version was baked into generated Dart/Rust bridge output with no check at
    all, so two developers (or a developer and CI) with different flutter_rust_bridge_codegen
    installs produced different committed bytes from identical input (#204).

  • snippets: a snippet that does not compile is no longer reported as unavailable. Every
    is_dependency_error implementation that could not distinguish "the binding package was never
    built" from "the generated code is wrong" now accepts only diagnostics that can mean nothing
    else: Rust E0432/E0433/E0463/E0583 (no longer E0425, E0308, E0599, E0609,
    E0061, or the could not compile summary rustc prints on every failed build), Java package ... does not exist (no longer bare cannot find symbol), C# CS0246/CS0234 (no longer
    CS0103/CS5001), Go cannot find package/no required module (no longer bare undefined:),
    Swift no such module (no longer cannot find ... in scope). Rust, Java and C# additionally
    require every diagnostic in the output to be a dependency diagnostic, matching the TypeScript
    validator. Reclassification took real failures out of the failure tally entirely — 283 Rust and
    51 Java snippets in two consumer repos were counted unavailable, so nothing went red.

  • e2e: a docs snippet no longer emits an accessor for an assertion field that is not a member
    of the call's result. The operations derived from a fixture's own assertions (added in 0.66.x)
    are now filtered through the oracles the assertion renderers already consult, so an error-path
    fixture, a result_is_simple/result_is_bytes call, a streaming pseudo-field
    (stream.has_page_event), an assertion grouping prefix (rate_limit.) and a field the
    availability oracle rejects all fall back to showing the whole result instead of emitting
    result.error(), result.Audio, result.CostTracked or result.stream.hasPageEvent.

  • e2e/rust: a snippet presenting derived fields now binds the result it references and
    unwraps a Result-returning call first. Fixture::has_docs_presentation — the one predicate
    the call emitter consults — could not see assertion-derived operations, so the emitter wrote
    let _ = convert(...) while the snippet printed result.content (E0425).

  • e2e/csharp: indexing an optional collection now emits the same null-forgiving operator as
    reading its .Count, so a single snippet no longer contains both
    result.Metadata.Headings!.Count and result.Metadata.Headings[0].Level (CS8602).

  • e2e/brew: the generated run_tests.sh harness reported PASS when any assertion but the
    last one failed. run_test invoked each test function as the condition of an if, which
    disables errexit for the entire call, so a failing assertion's return 1 no longer aborted
    the function and the function's exit status was just its last command's. Assertion helpers now
    record failures in a per-test counter that run_test consults alongside the exit status, and
    the harness core is emitted from a Minijinja template. Treat every historical brew pass as
    unverified. (#227)

  • e2e/brew: namespace-prefixed fixture fields produced jq paths that never matched the CLI
    payload. Brew built its path from FieldResolver::resolve, which only applies aliases, so a
    field like batch.completed_count — where batch is a virtual grouping label rather than a
    JSON object — became .batch.completed_count, null against every real payload. Brew now
    applies the same namespace stripping the C backend uses; genuinely nested paths whose first
    segment is a declared result field are unchanged. (#228)

  • Vendoring no longer strips a crate's inherited lint configuration. alef publish prepare
    (both VendorMode::CoreOnly and VendorMode::Full, the latter being R/CRAN's default) copied
    the core crate out of its workspace and deleted its [lints]\nworkspace = true without
    inlining anything, so the vendored crate compiled under a different lint configuration than
    the sources it was copied from. The [workspace.lints.rust] unexpected_cfgs check-cfg
    allowlist went with it, which is what declares the crate's own #[cfg(...)] gates as expected
    cfg names — every gate in the vendored copy then became an unexpected_cfgs diagnostic. That
    is silent in a default build and a hard error under the RUSTFLAGS="-D warnings" CI sets, so
    the breakage was invisible to every local run and only ever surfaced in CI. Vendoring now
    materializes the whole [workspace.lints] sub-tree into the vendored manifest verbatim; a
    crate that spells out its own [lints] instead of inheriting is left untouched, and a
    workspace that declares no lints still just has the inheritance marker removed.

  • e2e/zig: equals string assertions no longer wrap std.mem.trim around the actual value while emitting the fixture's expected literal verbatim. Any expectation ending in a newline was unsatisfiable by construction. Both sides are now compared exactly, matching every other e2e backend. Applies to the JSON-struct assertion template and to the metadata.format discriminated-union path, which carried its own copy of the trim.

  • e2e/kotlin: a fully-qualified [e2e.call.overrides.<lang>] class is no longer double-qualified. The binding-class import split the name into an import path while the trait-bridge import prefixed the binding package onto it unconditionally, so generated test files carried both the correct import and an unresolvable <pkg>.<pkg>.<Class>, failing the Kotlin compile with Unresolved reference. Package qualification is now centralized in naming::qualified_type_path, and the import block is collected in one de-duplicating ImportBlock rendered from a template.

  • alef verify no longer reports create-once scaffold seeds stale forever. The stamping pass
    decided which files to re-stamp from the in-memory GeneratedFile, while alef verify decides
    from the marker on disk. A seed an earlier alef wrote with a header (packages/go/go.mod,
    packages/zig/build.zig.zon, the Swift RustBridge placeholder) failed the in-memory predicate
    and was never re-stamped, so the first input change after that pinned it stale permanently:
    regenerating wrote nothing, alef adopt refused it as already alef-owned, and the file was
    content-correct throughout. The stamp scope is now computed by stampable_output_paths, which
    asks the same question verify asks — does the file on disk carry an alef marker — so the two
    sides can no longer disagree. Only the hash line is rewritten; the seed's body is untouched.

  • PHP e2e fixtures no longer drop a deliberate empty string on a String/Option<String> config
    field. The handle-arg and options_via = "json" call sites ran fixture input through a
    type-blind filter that removed every "", making PHP the only one of 21 backends that never
    forwarded the value the fixture was testing (a fixture writing bm25_query = "" exercised the
    default-config path instead). Both sites now use the type-aware filter, which drops "" only on
    an enum-typed field where it names no variant.

  • Swift documentation snippets cast an optional expression to Any before printing it.
    print/debugPrint take Any, and Swift raises expression implicitly coerced from 'T?' to 'Any' — an error under the -warnings-as-errors the snippet validator compiles with. Every
    prefix of the shown path is consulted, so an optional link (result.markdown()?.content()) is
    covered alongside an optional leaf (result.finalUrl()); total expressions stay uncast.

  • Swift bindings now emit and parse serde's adjacent wire form for enums declared
    #[serde(tag = "...", content = "...")]. The trait-bridge result encoder hardcoded serde's
    external default, sending the bare string "Variant" where Rust expected
    {"tag":"variant","content":payload} and rejecting every callback with
    invalid type: string "...", expected adjacently tagged enum ...; the generated Codable
    conformance had the same gap in the decoding direction, and trait-bridge result enums got no
    custom Codable at all.

  • e2e/snippets: docs-snippet field facts are now resolved against the call's own declared
    result type instead of by bare field name across the whole crate IR. presentation::resolve
    and apply_derived_shows take the functions registry, resolve the call's return type via
    resolve_declared_result_type, and anchor a new IrResultFieldMap at it — the same shape
    IrEnumMap/IrCollectionMap already use. A call whose return type does not resolve keeps
    every previous answer unchanged.

  • e2e/node: a snippet reaching through a field the Node binding declares optional now emits
    ?.. Optionality was decided by a unanimity vote over every declaration of the name in the
    crate, and it never saw the NAPI backend's own widening — a type implementing Default has
    every field emitted as Option<T>, so metadata: PageMetadata reaches TypeScript as
    readonly metadata?: PageMetadata. Generated snippets emitted result.metadata.title and
    failed tsc with TS18048. The e2e resolver now asks the binding backend's own
    napi_field_is_optional predicate, so the two cannot drift.

  • e2e/snippets: an inferred accessor is no longer derived for a field the call's result type
    does not declare. FieldResolver::result_field_oracle_knows accepted any name declared on any
    IR type, so a non-error fixture asserting on a field that exists on an unrelated struct emitted
    a non-compiling member access. is_valid_for_result deliberately still default-allows an
    unrecognised name — a hand-authored assertion knows the type the oracle may not — and both
    directions of that asymmetry are covered by tests.

  • java: An adjacently tagged enum (#[serde(tag, content)]) now puts its payload under serde's
    content key. The Jackson serializer flattened the payload beside the tag — serde's internal
    shape, which Rust rejects — and only did so when the payload happened to be a JSON object, so a
    newtype variant's scalar payload was dropped outright and crossed the FFI boundary as
    {"tag":"variant"} with the data gone. Both codecs now classify through
    codegen::serde_enum_repr, and an adjacently tagged enum always uses the hand-written codecs
    because @JsonTypeInfo can only express the internal shape. The deserializer moves from raw
    push_str to sealed_union_deserializer.jinja.

  • csharp: Same fix for the sealed-union JsonConverter, which was parameterised by the tag
    alone and gated its payload write on ValueKind == Object, dropping a scalar payload. It now
    takes a SerdeEnumRepr and serialises the payload whole under the content key, reading it back
    from there.

  • kotlin / kotlin-android: Same fix for the shared Jackson codecs. The serializer additionally
    cast the payload tree to ObjectNode, so a newtype variant with a String payload threw
    ClassCastException at runtime rather than reaching the wire. Emission moves from raw push_str
    to tagged_serializer.jinja / tagged_deserializer.jinja; internally tagged output is
    byte-identical.

  • pyo3: The generated per-variant getter returned the whole serialized document, which is the
    tag envelope rather than the payload under adjacent tagging, and the .pyi TypedDicts declared
    the payload's fields flat beside the tag. Both now read serde's content key; an adjacent struct
    variant's payload gets its own TypedDict.

Note: src/codegen/serde_enum_wire_cross_backend_tests.rs records all five hand-writing backends
as AdjacentSupport::Correct; only Swift and Go were correct before.

Changed

  • e2e: render_snippet_body_with_functions is now implemented by the r, zig, node,
    kotlin, php, ruby, elixir, python and rust e2e generators, which previously had no
    access to the free-function registry when rendering a docs snippet.

Added

  • Golden vectors pinning the alef:hash: recipe (compute_inputs_hash / compute_file_hash) to
    CODEGEN_FORMAT_VERSION, the recorded revision of that recipe. Changing the framing now fails a
    test instead of silently invalidating every stamp in every consumer repo.

  • codegen::serde_enum_repr — the single classifier for serde's four enum representations
    (external, internal, adjacent, untagged), derived from serde_tag / serde_content /
    serde_untagged in the IR. Backends must classify through it instead of re-deriving the wire
    form.

  • A cross-backend guard (codegen::serde_enum_wire_cross_backend_tests) that measures each
    backend's generated JSON for an adjacently tagged fixture enum against what serde_json
    actually writes for an equivalent Rust enum, and records which backends hand-write that JSON so
    a new one cannot drift in unexamined.

  • tests/test_src_module_reachability_gate.rs: fails if any .rs file under src/ containing a
    #[test] function is unreachable from a crate root. It re-derives the real module graph from
    src/lib.rs/src/main.rs, following mod name;, inline mod name { .. } bodies, #[path]
    redirects and include! splicing the way rustc resolves them -- the durable guard against a
    test file silently never compiling (#211).

Removed

  • template_versions::cargo::FLUTTER_RUST_BRIDGE_CODEGEN: the constant carried a renovate marker
    but was read nowhere in the tree. The flutter_rust_bridge_codegen version gate ships through
    [crates.dart] frb_version (resolved by backends::dart::naming::dart_frb_version, defaulting
    to the sibling FLUTTER_RUST_BRIDGE), so the second constant was a renovate-bumpable duplicate
    of the same version with nothing keeping the two in sync (#218).

Notes

  • The Swift generator now fails at generation time, rather than emitting JSON Rust cannot accept,
    for two shapes it does not support: a newtype variant of an internally tagged enum (which serde
    itself cannot serialize) and a multi-field tuple variant of an adjacently tagged enum (whose
    content serde writes as a JSON array).