v0.67.3
Fixed
-
e2e/swift: a getter's bridged shape is now read from the binding backend instead of
re-derived.build_swift_first_class_maptrackedVec<Vec<_>>/Map<_>plus two hand-enumerated
Option<Vec<Named(..)>>cases, so every other optionalVecwas called countable —
Option<Vec<String>>among them, which really emitsfn og_locale_alternates(&self) -> String,
making the generator emit?.countagainst aRustString. It now calls
field_needs_json_bridge, the same predicatewrappers::getters::emit_gettersuses 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/sizesuffix), 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, emittingarticle()?.publishedTime().toString()
wherepublishedTime()returnsOptional<RustString>. The leaf's optionality now comes from the
type cursor. -
e2e:
namespace_stripped_pathno longer drops a real struct segment theresult_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
answersfalse, 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 likebatch.completed_countemitted
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_runproduced a page whose fence language was the literal
rust,no_run— a markdown info string's language is its first whitespace-delimited token —
whichalef snippets audit --docscorrectly 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: droppingno_runmakes the doctest actually execute. -
cli:
alef snippets auditnow names its coverage when no--docsroot is given. A
snippets-only invocation printed a bareAudit clean: no issues found.while the
documentation-page checks (fence languages, include targets) never ran, so a CI job that
omitted--docsread green for a check class it had skipped. -
Wire
src/codegen/config_gen/tests/generators.rsinto the module tree
(src/codegen/config_gen/tests.rswas missingmod generators;), so its 18 config-generator
unit tests actually compile and run. Fixed 14 staleFieldDef/TypeDefstruct literals
predating theversionandhas_private_fieldsIR 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 aStringfield with a real
default) rather than the already-correctunwrap_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 wiredspec_and_formatting.rs;helpers.rscarried no tests at all. -
alef generate/alef buildnow fail loudly, before invokingflutter_rust_bridge_codegen,
when theflutter_rust_bridge_codegenbinary onPATHreports a version that disagrees with
the project's declared[crates.dart] frb_versionpin. 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 differentflutter_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_errorimplementation 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: RustE0432/E0433/E0463/E0583(no longerE0425,E0308,E0599,E0609,
E0061, or thecould not compilesummary rustc prints on every failed build), Javapackage ... does not exist(no longer barecannot find symbol), C#CS0246/CS0234(no longer
CS0103/CS5001), Gocannot find package/no required module(no longer bareundefined:),
Swiftno such module(no longercannot 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 countedunavailable, 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, aresult_is_simple/result_is_bytescall, 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.CostTrackedorresult.stream.hasPageEvent. -
e2e/rust: a snippet presenting derived fields now binds the result it references and
unwraps aResult-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 printedresult.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!.Countandresult.Metadata.Headings[0].Level(CS8602). -
e2e/brew: the generated
run_tests.shharness reportedPASSwhen any assertion but the
last one failed.run_testinvoked each test function as the condition of anif, which
disableserrexitfor the entire call, so a failing assertion'sreturn 1no 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 thatrun_testconsults 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 fromFieldResolver::resolve, which only applies aliases, so a
field likebatch.completed_count— wherebatchis a virtual grouping label rather than a
JSON object — became.batch.completed_count,nullagainst 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
(bothVendorMode::CoreOnlyandVendorMode::Full, the latter being R/CRAN's default) copied
the core crate out of its workspace and deleted its[lints]\nworkspace = truewithout
inlining anything, so the vendored crate compiled under a different lint configuration than
the sources it was copied from. The[workspace.lints.rust]unexpected_cfgscheck-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 anunexpected_cfgsdiagnostic. That
is silent in a default build and a hard error under theRUSTFLAGS="-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:
equalsstring assertions no longer wrapstd.mem.trimaround 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 themetadata.formatdiscriminated-union path, which carried its own copy of the trim. -
e2e/kotlin: a fully-qualified
[e2e.call.overrides.<lang>] classis 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 withUnresolved reference. Package qualification is now centralized innaming::qualified_type_path, and the import block is collected in one de-duplicatingImportBlockrendered from a template. -
alef verifyno longer reports create-once scaffold seeds stale forever. The stamping pass
decided which files to re-stamp from the in-memoryGeneratedFile, whilealef verifydecides
from the marker on disk. A seed an earlier alef wrote with a header (packages/go/go.mod,
packages/zig/build.zig.zon, the SwiftRustBridgeplaceholder) 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 adoptrefused it as already alef-owned, and the file was
content-correct throughout. The stamp scope is now computed bystampable_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 andoptions_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 writingbm25_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
Anybefore printing it.
print/debugPrinttakeAny, and Swift raisesexpression implicitly coerced from 'T?' to 'Any'— an error under the-warnings-as-errorsthe 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 generatedCodable
conformance had the same gap in the decoding direction, and trait-bridge result enums got no
customCodableat 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
andapply_derived_showstake thefunctionsregistry, resolve the call's return type via
resolve_declared_result_type, and anchor a newIrResultFieldMapat it — the same shape
IrEnumMap/IrCollectionMapalready 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 implementingDefaulthas
every field emitted asOption<T>, sometadata: PageMetadatareaches TypeScript as
readonly metadata?: PageMetadata. Generated snippets emittedresult.metadata.titleand
failedtscwithTS18048. The e2e resolver now asks the binding backend's own
napi_field_is_optionalpredicate, 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_knowsaccepted 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_resultdeliberately 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@JsonTypeInfocan only express the internal shape. The deserializer moves from raw
push_strtosealed_union_deserializer.jinja. -
csharp: Same fix for the sealed-union
JsonConverter, which was parameterised by the tag
alone and gated its payload write onValueKind == Object, dropping a scalar payload. It now
takes aSerdeEnumReprand 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 toObjectNode, so a newtype variant with aStringpayload threw
ClassCastExceptionat runtime rather than reaching the wire. Emission moves from rawpush_str
totagged_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.pyiTypedDicts 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_functionsis now implemented by ther,zig,node,
kotlin,php,ruby,elixir,pythonandruste2e 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 fromserde_tag/serde_content/
serde_untaggedin 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 whatserde_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.rsfile undersrc/containing a
#[test]function is unreachable from a crate root. It re-derives the real module graph from
src/lib.rs/src/main.rs, followingmod name;, inlinemod name { .. }bodies,#[path]
redirects andinclude!splicing the wayrustcresolves 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. Theflutter_rust_bridge_codegenversion gate ships through
[crates.dart] frb_version(resolved bybackends::dart::naming::dart_frb_version, defaulting
to the siblingFLUTTER_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).