Skip to content

refactor: harden distributed_macros expansion and diagnostics#82

Merged
patrickleet merged 1 commit into
mainfrom
hardening/macro-crate-hardening
Jun 11, 2026
Merged

refactor: harden distributed_macros expansion and diagnostics#82
patrickleet merged 1 commit into
mainfrom
hardening/macro-crate-hardening

Conversation

@patrickleet

Copy link
Copy Markdown
Collaborator

Summary

Hardens the distributed_macros proc-macro crate to make expansion testable and misuse produce pointed, spanned diagnostics. Brings the four macros in lib.rs (sourced, aggregate!, digest, enqueue) up to the pattern already used by snapshot.rs / read_model.rs.

Generated output is unchanged. The refactor only changes how the code is structured and how errors are produced — not what the macros emit. This is verified by the full main-crate test suite (250+ tests, including tests/sourced*, tests/enqueue, tests/todos, tests/upcasting, tests/snapshots) all passing unmodified.

Changes

  1. Testable shape — each macro now has an expand_* -> syn::Result<TokenStream2> core, with a thin #[proc_macro*] entry point that calls it and converts errors via .unwrap_or_else(|e| e.to_compile_error()). This mechanical change enables unit testing of expansion.

  2. Explicit unknown-key diagnosticsparse_digest_args, parse_sourced_args, parse_event_args, and parse_enqueue_args previously left unknown keyword args silently unconsumed (surfacing later as a bare "unexpected token"). They now reject unknown keys with a spanned syn::Error naming the bad key and listing the valid ones, e.g.:
    unsupported key versoinin #[digest(...)]; expectedwhenorversion``
    Also added up-front checks for duplicate #[event] names, `#[event]` methods missing a `self` receiver, and a clear message for bare `#[sourced()]` with no entity field.

  3. #[enqueue] renamed-entity fix — the replay guard was hardcoded to self.entity.is_replaying(). A renamed entity field produced a confusing "no field entity" error pointing at the user's method. #[enqueue] now accepts entity = field (defaulting to entity), matching how the other macros configure the entity field.

  4. trybuild compile-fail suite — new distributed_macros/tests/ with a trybuild dev-dependency and five fixtures covering: unsupported #[event] signature, duplicate event names, unknown attribute keys, #[sourced] missing the entity field, and the renamed-entity #[enqueue] footgun. Plus unit tests for the new expand_*/parse_* functions.

  5. ReplayError decision — kept as String. Replay errors are flattened from heterogeneous sources (per-event decode() errors, user method errors of arbitrary E, unknown-event messages) via e.to_string(). A typed error would have to be generic over each method's error type or erase them anyway, so String is the smaller, honest representation. Rationale documented inline at both impl Aggregate blocks.

Note on dev-dependency cycle

trybuild fixtures need the distributed runtime crate to type-check generated distributed::... code, so distributed_macros now dev-depends on distributed (which depends on distributed_macros). Cargo permits dev-dependency cycles; this does not affect the normal dependency graph.

Testing

  • cargo fmt --all — clean
  • cargo clippy -p distributed_macros — clean
  • cargo test -p distributed_macros — 46 unit tests + 1 trybuild harness (5 fixtures) pass. .stderr snapshots generated with TRYBUILD=overwrite and reviewed (all pointed/spanned).
  • cargo build (default features) — clean
  • cargo test (default features) — entire workspace passes, including all macro-using integration tests with no changes to their expected behavior, confirming generated output is unchanged.

🤖 Generated with Claude Code

Restructure the four proc-macros in distributed_macros/src/lib.rs
(sourced, aggregate!, digest, enqueue) to the testable
`expand_* -> syn::Result<TokenStream2>` shape already used by
snapshot.rs and read_model.rs. The thin `#[proc_macro*]` entry points
now convert errors via `.unwrap_or_else(|e| e.to_compile_error())`.
Generated output is byte-identical — only the structure and error
plumbing changed (verified: all 250+ macro-using tests in the main
crate still pass).

Diagnostics: unknown keyword args in parse_digest_args,
parse_sourced_args, parse_event_args, and parse_enqueue_args now
produce pointed, spanned syn::Error messages that name the bad key and
list the valid ones, instead of being silently left unconsumed and
surfacing as a bare "unexpected token". Also added up-front checks for
duplicate #[event] names and #[event] methods missing a self receiver,
and a clearer "missing entity field" message for bare #[sourced()].

enqueue fix: #[enqueue] now accepts `entity = field` so a renamed
entity field produces a correct `is_replaying()` guard instead of a
confusing "no field `entity`" error pointing at the user's method.

ReplayError: kept as `String`. Replay errors are flattened from
heterogeneous sources (per-event decode errors, user method errors of
arbitrary E, unknown-event messages) via `e.to_string()`; a typed
error would have to be generic over each method's error type or erase
them anyway. Rationale documented inline.

Tests: added unit tests for the new expand_*/parse_* functions and a
trybuild compile-fail suite (tests/compile_fail/*.rs + harness)
covering an unsupported #[event] signature, duplicate event names,
unknown attribute keys, #[sourced] missing the entity field, and the
renamed-entity #[enqueue] footgun.

Implements [[tasks/macro-crate-hardening]]
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@patrickleet, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 33 minutes and 33 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: abec62a5-f5c8-47f5-aca0-116c12f917ec

📥 Commits

Reviewing files that changed from the base of the PR and between 55c2674 and 1862fed.

📒 Files selected for processing (13)
  • distributed_macros/Cargo.toml
  • distributed_macros/src/lib.rs
  • distributed_macros/tests/compile_fail.rs
  • distributed_macros/tests/compile_fail/duplicate_event_names.rs
  • distributed_macros/tests/compile_fail/duplicate_event_names.stderr
  • distributed_macros/tests/compile_fail/enqueue_renamed_entity.rs
  • distributed_macros/tests/compile_fail/enqueue_renamed_entity.stderr
  • distributed_macros/tests/compile_fail/event_bad_signature.rs
  • distributed_macros/tests/compile_fail/event_bad_signature.stderr
  • distributed_macros/tests/compile_fail/sourced_missing_entity.rs
  • distributed_macros/tests/compile_fail/sourced_missing_entity.stderr
  • distributed_macros/tests/compile_fail/unknown_digest_key.rs
  • distributed_macros/tests/compile_fail/unknown_digest_key.stderr
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hardening/macro-crate-hardening

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@patrickleet patrickleet merged commit b6cf365 into main Jun 11, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant