Skip to content

AI Updates #4: Fix spans for IDEs, more permissive #[uses], and improve modern idioms#252

Merged
soareschen merged 9 commits into
mainfrom
ai-updates-4
Jul 4, 2026
Merged

AI Updates #4: Fix spans for IDEs, more permissive #[uses], and improve modern idioms#252
soareschen merged 9 commits into
mainfrom
ai-updates-4

Conversation

@soareschen

@soareschen soareschen commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

AI Overview

This PR hardens the CGP procedural macros along two independent axes and fixes one code-generation bug. The macros now place the spans on their generated code so that both the compiler's error carets and rust-analyzer's go-to-definition land on the right token, where before only the compiler was considered. The #[uses(...)] attribute now accepts any trait bound a where clause allows, not just the plain Trait<Params> form. And #[cgp_impl] now honors an explicit component-name override instead of always deriving the conventional {Provider}Component name. Everything else in the diff — new tests, a restructured AGENTS.md, and documentation edits — supports or documents these three changes. No wiring semantics change; the expansions are identical apart from where their tokens are spanned.

High-level concepts

Spans now serve two tools, not one

The central change is a shift in how the macros think about spans. A CGP macro builds its output from quasi-quoted tokens, and every token it emits carries the macro's call_site span — the range of the whole invocation. That default misleads two separate tools, and this PR treats both as first-class concerns. The compiler is one: an error on a generated item's header, such as a coherence conflict (E0119) or an unsatisfied bound, underlines the entire macro block instead of the specific entry the user wrote. The IDE is the other, and it was previously ignored: rust-analyzer maps a source token to its expansion purely by source range, so dragging a generated item's tokens onto a user token's range hijacks go-to-definition on that token, and a leaked derived name makes navigation offer the defining macro alongside the item.

The old approach re-spanned every top-level token of a generated item onto the originating token, then restored the generics afterward. This satisfied the compiler but broke the IDE, because it dragged synthesized references like IsProviderFor and DelegateComponent onto the user's key token. The new approach, embodied in a new override_item_span helper, re-spans only the item's two boundary tokens — its leading keyword and its trailing brace group — and leaves everything in between untouched. The compiler derives an item's error caret from its first and last tokens joined together, so moving just those two is enough to aim the caret; and leaving the interior alone keeps every user-written token (the wired provider, the target type, a per-entry generic) and every synthesized reference at a span that never collides with a narrow user token, so navigation resolves to the one correct definition. Each boundary token is moved only if it is itself synthesized, detected by a new is_synthesized helper that compares a token's source text against the call_site text, so a hand-assembled item whose boundary the user wrote is left alone.

The same two-audience thinking drives two smaller span fixes on the component machinery. The #[cgp_component] marker struct — the {Provider}Component definition — is now spanned on the provider identifier the user wrote rather than on call_site, so go-to-definition on a delegate key lands cleanly on the component instead of also offering the cgp_component macro path. Its reference-side dual, the {Provider}Component reference that #[cgp_provider] synthesizes inside the generated IsProviderFor impl, moves the opposite way: it is now spanned on call_site rather than on the provider trait, because it is an interior token that anchors no caret and would otherwise make go-to-definition on the provider trait wrongly offer the component struct too.

#[uses(...)] accepts any trait bound

The #[uses(...)] attribute previously parsed only a simplified trait-path form, so a dependency that needed associated-type equality — for example HasErrorType<Error = String> — had to be written as a hand-rolled where clause. The parser now accepts a full syn::TypeParamBound for each entry, so an associated-type binding, a higher-ranked bound, or a lifetime bound is imported verbatim onto the generated impl's where clause. The simple Trait<Params> form remains the idiomatic one, and the documentation is careful to steer abstract-type pins toward the #[use_type] equality form instead; the relaxation exists for the general bounds that neither #[use_type] nor the old syntax could express. As a side benefit the parsing path simplified, since the attribute no longer re-parses each entry through parse_internal! to reach a bound.

#[cgp_impl] honors an explicit component override

A provider written for a component whose marker name does not follow the {Provider}Component convention was mis-compiled. #[cgp_impl]'s component_type() always derived the conventional name and ignored an explicit #[cgp_impl(new Provider: Component)] override, so the generated IsProviderFor impl referenced a component that did not exist and failed to resolve. The fix returns the user-supplied component type directly when an override is present, falling back to the derived name only when none is given. Because the override is a user-written type, it also keeps its own span for free.

Structural changes

The span work reshapes override_span.rs. The file previously held one function; it now holds three. override_span survives but is narrowed to a single documented purpose — check_components! re-spanning the one shared context token onto each checked component in turn, which is the one place clobbering a user token's span is the actual intent. The new override_item_span is the boundary-only re-spanner that delegate_components! (eval.rs) and the default_impl attribute now call in place of the old whole-item re-span, both of which dropped their clone-and-restore-generics dance in the process. The new private is_synthesized helper backs the boundary check.

The #[uses] relaxation changes a field type in three parser structs. UsesAttributes, FunctionAttributes, and CgpImplAttributes each changed their uses/imports field from Vec<PathWithTypeArgs> to Vec<TypeParamBound>, dropped the now-unneeded PathWithTypeArgs imports, and replaced the per-entry parse_internal! re-parsing in uses.rs and cgp_fn/preprocessed.rs with a direct clone into the bound list. The #[cgp_impl] override fix is localized to cgp_provider/item.rs, which grows an early return in component_type().

The remaining changes are tests and prose. Three new tests land: a snapshot-plus-behavior test that a non-conventionally-named component works through the : Component override, and two tests — one on #[cgp_fn], one on #[cgp_impl] — that #[uses] now carries an associated-type-equality bound, each registered in its module tree. AGENTS.md restructures the "scrutinize the macro codegen" review checklist from a flat bullet list into titled subsections, promoting spans to a two-part concern (compiler carets and IDE go-to-definition) and cross-linking the implementation notes. The documentation set is updated in step: uses.md, modern-idioms.md, the implicit/use_type references, the CGP skill files, and the money-transfer example, which is rewritten to prefer an #[implicit] argument over a #[cgp_auto_getter] trait for reading a context field.

Impacts

The impacts fall into distinct buckets, listed here because they touch separable parts of the developer experience.

  • Compiler diagnostics are preserved, not regressed. Coherence conflicts and unsatisfied bounds arising from delegate_components! and default-impl wiring still point at the offending entry rather than the whole macro block; the boundary-only re-span keeps the caret placement that the trybuild .stderr fixtures pin.
  • IDE navigation is fixed for the first time. Go-to-definition on a wired provider, a target type, or a per-entry generic now resolves to its real definition instead of being hijacked by a synthesized reference; go-to-definition on a delegate key or a provider trait no longer offers spurious extra targets (the defining macro, or the component struct). This holds cross-crate, which is where the derived-name leak was actually visible.
  • #[uses] gains expressive power. Providers can now import bounds such as HasErrorType<Error = String>, higher-ranked bounds, and lifetime bounds directly, removing a class of fall-back-to-where workarounds. The guidance still prefers #[use_type]'s equality form for pinning an abstract type, so the new generality is a capability, not a new default.
  • A latent miscompilation is fixed. Providers targeting a component whose marker name is not {Provider}Component now compile and wire correctly through the : Component override, where before the generated IsProviderFor impl silently referenced a non-existent component.
  • No runtime or wiring-semantics change. The generated impls are byte-for-byte identical apart from token spans and the override fix, so existing wiring, checking, and dispatch behave exactly as before; the snapshot tests confirm the expansions.
  • Reviewer and author guidance is broadened. AGENTS.md now instructs reviewers to audit spans for both the compiler and the IDE on every item-emitting macro, flags the IDE half as untested and manual, and the documentation steers new code toward implicit arguments, combined attributes, and the #[use_type] equality form.

@soareschen soareschen added the AI label Jul 4, 2026
@soareschen soareschen changed the title AI Updates #4: Fix spans for IDEs and improve modern idioms AI Updates #4: Fix spans for IDEs, more permissive #[uses], and improve modern idioms Jul 4, 2026
@soareschen soareschen merged commit 1b2a9f9 into main Jul 4, 2026
3 checks passed
@soareschen soareschen deleted the ai-updates-4 branch July 4, 2026 22:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant