diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/finalize_build_impl.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/finalize_build_impl.rs index 8a7ac82f..da20bb30 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/finalize_build_impl.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/finalize_build_impl.rs @@ -4,6 +4,7 @@ use syn::token::Comma; use syn::{FieldValue, Ident, ItemImpl, ItemStruct, Type}; use crate::exports::{FinalizeBuild, IsPresent}; +use crate::functions::override_item_span; use crate::parse_internal; use crate::types::cgp_data::{field_to_member, field_value_expr, to_generic_args}; @@ -49,5 +50,7 @@ pub fn derive_finalize_build_impl( } }; - Ok(item_impl) + // Key the error span on the struct name the user wrote, not the whole + // derive. See docs/implementation/README.md#spans. + override_item_span(context_ident.span(), &item_impl) } diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/has_builder_impl.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/has_builder_impl.rs index b9d75d50..57c60b7c 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/has_builder_impl.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/has_builder_impl.rs @@ -4,6 +4,7 @@ use syn::token::Comma; use syn::{FieldValue, Ident, ItemImpl, ItemStruct}; use crate::exports::{HasBuilder, IsNothing}; +use crate::functions::override_item_span; use crate::parse_internal; use crate::types::cgp_data::{field_to_member, field_value_expr, to_generic_args}; @@ -44,5 +45,7 @@ pub fn derive_has_builder_impl( } }; - Ok(item_impl) + // Key the error span on the struct name the user wrote, not the whole + // derive. See docs/implementation/README.md#spans. + override_item_span(context_ident.span(), &item_impl) } diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/has_field_impls.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/has_field_impls.rs index 1e8097f2..2abda01c 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/has_field_impls.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/has_field_impls.rs @@ -1,6 +1,8 @@ +use syn::spanned::Spanned; use syn::{Ident, ItemImpl, ItemStruct}; use crate::exports::{HasField, IsPresent, MapType}; +use crate::functions::override_item_span; use crate::parse_internal; use crate::types::cgp_data::{ field_to_member, field_to_tag, index_to_generic_ident, to_generic_args, @@ -55,7 +57,13 @@ pub fn derive_has_field_impls( } }; - item_impls.push(item_impl); + // Point an error on this per-field impl at the field the user wrote + // rather than at the whole derive. See docs/implementation/README.md#spans. + let field_span = current_field + .ident + .as_ref() + .map_or_else(|| current_field.span(), |ident| ident.span()); + item_impls.push(override_item_span(field_span, &item_impl)?); } Ok(item_impls) diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/into_builder_impl.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/into_builder_impl.rs index 2a8524e0..08a3064b 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/into_builder_impl.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/into_builder_impl.rs @@ -4,6 +4,7 @@ use syn::token::Comma; use syn::{FieldValue, Ident, ItemImpl, ItemStruct}; use crate::exports::{IntoBuilder, IsPresent}; +use crate::functions::override_item_span; use crate::parse_internal; use crate::types::cgp_data::{field_to_member, field_value_expr, to_generic_args}; @@ -47,5 +48,7 @@ pub fn derive_into_builder_impl( } }; - Ok(item_impl) + // Key the error span on the struct name the user wrote, not the whole + // derive. See docs/implementation/README.md#spans. + override_item_span(context_ident.span(), &item_impl) } diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/partial_data.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/partial_data.rs index f157d765..253c4a44 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/partial_data.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/partial_data.rs @@ -1,6 +1,7 @@ use syn::{Ident, ItemImpl, ItemStruct}; use crate::exports::{MapType, PartialData}; +use crate::functions::override_item_span; use crate::parse_internal; use crate::types::cgp_data::index_to_generic_ident; @@ -32,5 +33,7 @@ pub fn derive_partial_data_impl_from_struct( } }; - Ok(item_impl) + // Key the error span on the struct name the user wrote, not the whole + // derive. See docs/implementation/README.md#spans. + override_item_span(context_ident.span(), &item_impl) } diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/update_field_impls.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/update_field_impls.rs index 29ae1e6b..d785e06b 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/update_field_impls.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_builder/update_field_impls.rs @@ -1,9 +1,11 @@ use quote::quote; use syn::punctuated::Punctuated; +use syn::spanned::Spanned; use syn::token::Comma; use syn::{FieldValue, GenericArgument, Ident, ItemImpl, ItemStruct, Type}; use crate::exports::{MapType, UpdateField}; +use crate::functions::override_item_span; use crate::parse_internal; use crate::types::cgp_data::{ field_to_member, field_to_tag, field_value_expr, index_to_generic_ident, to_generic_args, @@ -99,7 +101,13 @@ pub fn derive_update_field_impls( } }; - item_impls.push(item_impl); + // Point an error on this per-field impl at the field the user wrote + // rather than at the whole derive. See docs/implementation/README.md#spans. + let field_span = current_field + .ident + .as_ref() + .map_or_else(|| current_field.span(), |ident| ident.span()); + item_impls.push(override_item_span(field_span, &item_impl)?); } Ok(item_impls) diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/extract_field_impls.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/extract_field_impls.rs index beb9d1fb..9f7a549e 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/extract_field_impls.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/extract_field_impls.rs @@ -1,6 +1,7 @@ use syn::{Arm, GenericArgument, Ident, ItemEnum, ItemImpl, Type}; use crate::exports::{ExtractField, IsPresent, IsVoid, MapType, MapTypeRef}; +use crate::functions::override_item_span; use crate::parse_internal; use crate::types::cgp_data::{get_variant_type, index_to_generic_ident, to_generic_args}; use crate::types::field::Symbol; @@ -116,7 +117,12 @@ pub fn derive_extract_field_impls( } }; - item_impls.push(item_impl); + // Point an error on this per-variant impl at the variant the user wrote + // rather than at the whole derive. See docs/implementation/README.md#spans. + item_impls.push(override_item_span( + current_variant.ident.span(), + &item_impl, + )?); } Ok(item_impls) diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/finalize_extract_impl.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/finalize_extract_impl.rs index b725a015..0847b028 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/finalize_extract_impl.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/finalize_extract_impl.rs @@ -1,6 +1,7 @@ use syn::{Ident, ItemEnum, ItemImpl, Type}; use crate::exports::{FinalizeExtract, IsVoid, MapTypeRef}; +use crate::functions::override_item_span; use crate::parse_internal; use crate::types::cgp_data::to_generic_args; @@ -61,5 +62,7 @@ pub fn derive_finalize_extract_impl( } }; - Ok(item_impl) + // Key the error span on the enum name the user wrote, not the whole derive. + // See docs/implementation/README.md#spans. + override_item_span(context_enum.ident.span(), &item_impl) } diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/has_extractor_impl.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/has_extractor_impl.rs index 71d6bde3..f5be8111 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/has_extractor_impl.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/has_extractor_impl.rs @@ -2,6 +2,7 @@ use quote::quote; use syn::{Arm, Ident, ItemEnum, ItemImpl}; use crate::exports::{HasExtractor, HasExtractorMut, HasExtractorRef, IsMut, IsPresent, IsRef}; +use crate::functions::override_item_span; use crate::parse_internal; use crate::types::cgp_data::to_generic_args; @@ -62,7 +63,9 @@ pub fn derive_has_extractor_impl( } }; - Ok(item_impl) + // Key the error span on the enum name the user wrote, not the whole derive. + // See docs/implementation/README.md#spans. + override_item_span(context_ident.span(), &item_impl) } /// Emit the `HasExtractorRef` impl over the borrowed partial enum (`IsRef`). Its @@ -137,7 +140,9 @@ pub fn derive_has_extractor_ref_impl( } }; - Ok(item_impl) + // Key the error span on the enum name the user wrote, not the whole derive. + // See docs/implementation/README.md#spans. + override_item_span(context_ident.span(), &item_impl) } /// Emit the `HasExtractorMut` impl: the `IsMut` mirror of @@ -209,5 +214,7 @@ pub fn derive_has_extractor_mut_impl( } }; - Ok(item_impl) + // Key the error span on the enum name the user wrote, not the whole derive. + // See docs/implementation/README.md#spans. + override_item_span(context_ident.span(), &item_impl) } diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/partial_data.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/partial_data.rs index 01b359ad..7cc33d9b 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/partial_data.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_extractor/partial_data.rs @@ -1,6 +1,7 @@ use syn::{Ident, ItemEnum, ItemImpl}; use crate::exports::{MapType, MapTypeRef, PartialData}; +use crate::functions::override_item_span; use crate::parse_internal; use crate::types::cgp_data::index_to_generic_ident; @@ -56,5 +57,7 @@ pub fn derive_partial_data_impl_from_enum( } }; - Ok(item_impl) + // Key the error span on the enum name the user wrote, not the whole derive. + // See docs/implementation/README.md#spans. + override_item_span(context_ident.span(), &item_impl) } diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_from_variant.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_from_variant.rs index 3605ef1d..751e8654 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_from_variant.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_from_variant.rs @@ -1,6 +1,7 @@ use syn::{ItemEnum, ItemImpl}; use crate::exports::FromVariant; +use crate::functions::override_item_span; use crate::parse_internal; use crate::types::cgp_data::get_variant_type; use crate::types::field::Symbol; @@ -32,7 +33,11 @@ pub fn derive_from_variant_from_enum(item_enum: &ItemEnum) -> syn::Result syn::Resu for field in fields.named.iter() { let field_ident = field.ident.as_ref().unwrap(); + // Aim a compiler error on either generated impl — a coherence + // conflict (`E0119`) with a hand-written `HasField` impl for the + // same tag, say — at the field the user wrote rather than at the + // whole `#[derive(HasField)]`, which is where the impl's + // `call_site`-spanned `impl`/`{ … }` boundary would otherwise put + // the caret. See docs/implementation/README.md#spans. + let field_span = field_ident.span(); + let field_symbol = Symbol::from_ident(field_ident.clone()); let field_type = &field.ty; @@ -53,18 +62,23 @@ pub fn derive_has_field_impls_from_struct(item_struct: &ItemStruct) -> syn::Resu } }; - item_impls.push(has_field_impl); - item_impls.push(has_field_mut_impl); + item_impls.push(override_item_span(field_span, &has_field_impl)?); + item_impls.push(override_item_span(field_span, &has_field_mut_impl)?); } } Fields::Unnamed(fields) => { for (i, field) in fields.unnamed.iter().enumerate() { + // A tuple field has no identifier, so its whole `syn::Field` span + // is the narrowest token the user wrote; re-span each generated + // impl onto it for the same reason as the named case above. + let field_span = field.span(); + let field_tag = Index { index: i, - span: field.span(), + span: field_span, }; - let field_ident = LitInt::new(&format!("{i}"), field.span()); + let field_ident = LitInt::new(&format!("{i}"), field_span); let field_type = &field.ty; @@ -100,8 +114,8 @@ pub fn derive_has_field_impls_from_struct(item_struct: &ItemStruct) -> syn::Resu } }; - item_impls.push(has_field_impl); - item_impls.push(has_field_mut_impl); + item_impls.push(override_item_span(field_span, &has_field_impl)?); + item_impls.push(override_item_span(field_span, &has_field_mut_impl)?); } } _ => {} diff --git a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_has_fields/derive_enum.rs b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_has_fields/derive_enum.rs index ad8de5b8..43fd86ab 100644 --- a/crates/macros/cgp-macro-core/src/types/cgp_data/derive_has_fields/derive_enum.rs +++ b/crates/macros/cgp-macro-core/src/types/cgp_data/derive_has_fields/derive_enum.rs @@ -3,6 +3,7 @@ use quote::quote; use syn::{ItemEnum, ItemImpl}; use crate::exports::{HasFields, HasFieldsRef}; +use crate::functions::override_item_span; use crate::parse_internal; use crate::types::cgp_data::{ derive_from_fields_for_enum, derive_to_fields_for_enum, derive_to_fields_ref_for_enum, @@ -45,11 +46,17 @@ pub fn derive_has_fields_impls_from_enum(item_enum: &ItemEnum) -> syn::Result syn::Res let to_fields_ref_impl = derive_to_fields_ref_for_struct(item_struct)?; - Ok(vec![ + // These impls are all keyed on the whole struct, so aim an error on any of + // their headers at the struct name the user wrote rather than at the whole + // `#[derive(...)]`. See docs/implementation/README.md#spans. + [ has_fields_impl, has_fields_ref_impl, from_fields_impl, to_fields_impl, to_fields_ref_impl, - ]) + ] + .into_iter() + .map(|item_impl| override_item_span(struct_name.span(), &item_impl)) + .collect() } diff --git a/docs/errors/checks/check-trait-failure.md b/docs/errors/checks/check-trait-failure.md index 1faa234e..9137636c 100644 --- a/docs/errors/checks/check-trait-failure.md +++ b/docs/errors/checks/check-trait-failure.md @@ -66,10 +66,18 @@ A distinct sub-case of this class is worth recognizing because its fix differs a Read the absence as its own signal: a checked context that implements `HasField` for nothing behind a `#[derive(HasField)]`-shaped requirement has most likely forgotten the derive, and the fix is to add `#[derive(HasField)]` to the struct, not to add fields one at a time. A tool handling this class should special-case it — zero `HasField` impls plus an unmet `HasField` leaf means the derive, and the headline should say so rather than sending the user after a single field. +## When the dependency is an equality-pinned abstract type + +A second sub-case is worth recognizing because its diagnostic is an `E0271` rather than an `E0277`, and because the wiring it points at is a *type* choice rather than a field or a provider. An impl-side dependency can pin an [abstract type](../../concepts/abstract-types.md) to a concrete one — `#[use_type(HasErrorType.{Error = AppError})]` on a provider, which emits `Self: HasErrorType` — while the context binds that same abstract type elsewhere, by wiring its component to `UseType` or by implementing the trait directly. When the two disagree, the *trait* half of the bound still holds (the context genuinely implements `HasErrorType`) and only the associated-type projection fails, so the compiler reports a type mismatch rather than an unimplemented trait. + +The tell is the headline shape: `` type mismatch resolving `::Error == AppError` ``, followed by an `expected this to be …` note whose caret lands on the `#[cgp_type]` attribute that *defined* the abstract type — nowhere near either side of the disagreement. Two things make this harder to read than the field cases above. The type the context actually supplies is nowhere in the message: rustc names only what was required, so a reader must find the wiring themselves to learn what it was required *against*. And because one abstract type is shared by everything in the context that touches it, a single wrong binding surfaces at every consumer that raises through it, multiplying into a [verbose cascade](verbose-cascade.md). The fix is to reconcile the two: bind the component to the type the provider requires, or relax the provider to accept the one the context supplies. + ## How cargo-cgp presents it `cargo-cgp` recognizes this class and leads with the cause. It keeps the `E0277` code but rewrites the headline to `[CGP-E001] the consumer trait \`CanGreet\` is not implemented for context \`Person\``, and replaces the `required for …` scaffolding with a single `root cause:` note over a compact dependency tree — `[CGP-E101]` consumer-trait-impl hop → `[CGP-E102]` provider-trait-impl hop → the leaf. The leaf is `[CGP-E106] missing field \`name\` on \`Person\`` for a genuinely absent field, or `[CGP-E108]` for the [derive-missing variant](#when-the-derive-is-missing-entirely), which coalesces every underived field on the struct into one "add `#[derive(HasField)]`" cause rather than listing them singly. The `#[use_type]` foreign-import form bottoms out on `[CGP-E107]` (the context supplies no wiring for the capability). The `Symbol!("name")` spine is resugared and the `CanUseComponent`/`IsProviderFor`/`__Check…` frames are dropped, so what remains is the field to fix and the wiring entry it hangs from. The codes are defined in the [cargo-cgp error-code catalog](https://github.com/contextgeneric/cargo-cgp/blob/main/docs/error-code.md). +The [equality-pinned abstract type](#when-the-dependency-is-an-equality-pinned-abstract-type) is reshaped the same way into its own class, `[CGP-E017]`: `` expected the abstract type `Error` of `HasErrorType` on `MockApp` to be `AppError`, but found `String` ``, over a `[CGP-E112]` leaf. Both halves of the disagreement are named — the required type read off the failing projection, the supplied one by normalizing it, so a `UseType` wiring and a direct impl are read alike — and a `help` names the wiring entry to change (`` wire `ErrorTypeProviderComponent` to `UseType` ``), recovered from the trait rather than guessed. Because one abstract type is shared, the cascade this class produces is also coalesced: every consumer that fails through it is listed in one block over a single root-cause tree. + ## Resolving it The fix is what the diagnostic already points to: satisfy the named leaf bound. Add the `name` field to `Person`, or wire the getter component to the existing field, so `Person` implements `HasName` and `GreetHello` becomes a valid provider for the component. Because the check surfaced both the concrete bound *and* the wiring entry, no further tracing is usually needed — which is exactly why the standard remedy for a [hidden](../hidden/unsatisfied-dependency.md) failure is to add a check and read this class instead. @@ -83,6 +91,7 @@ This class is the *target* `cargo-cgp` normalizes toward, and it already handles - [`acceptable/fields/missing_dependency.rs`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/acceptable/fields/missing_dependency.rs) — the surfaced case for `GreetHello`'s unmet `Self: HasName`; its `.rust.stderr` pins the raw `help:` note naming `HasField` and the caret on `GreeterComponent`, and its `.cgp.stderr` the `[CGP-E001]`/`[CGP-E106]` reshaping. Its use-site counterpart, [`acceptable/use-site/missing_dependency.rs`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/acceptable/use-site/missing_dependency.rs), reaches the same mistake by a method call — the [hidden](../hidden/unsatisfied-dependency.md) `E0599` in the raw output, which `cargo-cgp`'s next-gen solver recovers to the same headline. - [`acceptable/use-type/use_type_foreign_unsatisfied.rs`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/acceptable/use-type/use_type_foreign_unsatisfied.rs) — the capability bound reached through a `#[use_type]` foreign import instead of a check: naming a component for a `Types` that does not implement the imported `HasScalarType` surfaces `E0277`, pinning that the foreign bound is *enforced on the generated trait* rather than silently dropped. - [`acceptable/use-type/use_type_nested_unsatisfied.rs`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/acceptable/use-type/use_type_nested_unsatisfied.rs) — the same through a *nested* two-hop import, so the grounded bound `::Types: HasScalarType` is the one enforced, confirming the transitively-grounded foreign bound is checked at depth. +- [`acceptable/types/abstract_type_mismatch.rs`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/acceptable/types/abstract_type_mismatch.rs) — the [equality-pinned abstract type](#when-the-dependency-is-an-equality-pinned-abstract-type): a context wiring `HasScalarType` to `UseType` under a provider that pins it to `f64`. Its `.rust.stderr` pins the raw `E0271` with its caret on the `#[cgp_type]` attribute and no mention of `u32`, and its `.cgp.stderr` the `[CGP-E017]`/`[CGP-E112]` reshaping with the `UseType` help. - [`acceptable/fields/missing_has_field_derive.rs`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/acceptable/fields/missing_has_field_derive.rs) — the [derive-missing variant](#when-the-derive-is-missing-entirely): a `Person` with the `name` field but no `#[derive(HasField)]`, so the raw output drops the "but trait `HasField<…>` is implemented" landmark (the signal the whole derive is missing), and `cargo-cgp` coalesces it to one `[CGP-E108]` cause. Related field-derive fixtures — [`base_area_2`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/acceptable/fields/base_area_2.rs), [`empty_field_struct`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/acceptable/fields/empty_field_struct.rs), [`underived_and_missing_field`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/acceptable/fields/underived_and_missing_field.rs) — pin the coalescing and the mixed missing-plus-underived case. ## Related diff --git a/docs/errors/checks/verbose-cascade.md b/docs/errors/checks/verbose-cascade.md index c8304f2b..8fe64434 100644 --- a/docs/errors/checks/verbose-cascade.md +++ b/docs/errors/checks/verbose-cascade.md @@ -44,9 +44,9 @@ The root cause is **present and repeated** — the same `HasField` failure as that entry's primary). Within each surviving block the recovery is complete — the tree threads the full `[CGP-E101]`→`[CGP-E102]` chain down to the field — so a reader no longer has to apply the concrete-item test to sort signal from noise. +`cargo-cgp` collapses the whole cascade to a single block. First it **drops every intermediate provider-trait block** as derived noise and recovers each *checked* component's failure to a `root cause:` tree that descends all the way to the leaf. Then it **coalesces** the checked components that share that leaf: rather than one block per component, it emits one `[CGP-E001]` headline naming every affected consumer trait — `` the consumer traits `CanBaz`, `CanBar`, and `CanFoo` are not implemented for context `App` `` — with a caret at each check entry and a single `root cause: [CGP-E106] missing field \`name\`` tree beneath. So a six-block raw cascade over three checked components becomes one block, and cargo's re-count reports one error. A component rustc happened to surface provider-side (a lone `[CGP-E002]` block) is worded uniformly as a consumer in the merged headline, since a `check_components!` entry failing *is* the consumer trait failing. -What `cargo-cgp` does *not* do is coalesce those identical per-component root causes into a single headline: three checked components that all bottom out on the one missing `name` field still print three trees naming it. That last deduplication — recognizing that several checked components share one leaf and reporting "`App` is missing field `name`, needed by `ProvideFoo` (via `Bar`, `Baz`)" once — is the remaining usability gap this fixture pins. The codes are defined in the [cargo-cgp error-code catalog](https://github.com/contextgeneric/cargo-cgp/blob/main/docs/error-code.md). +Listing every affected consumer in one headline is possible because the tool holds the compilation's diagnostics until they have all arrived (there is no end-of-compilation hook), then groups them by a consumer-independent cause signature. The codes are defined in the [cargo-cgp error-code catalog](https://github.com/contextgeneric/cargo-cgp/blob/main/docs/error-code.md). ## Resolving it @@ -54,12 +54,12 @@ Fix the single root cause and the entire cascade collapses at once — supply th ## Notes for tooling -`cargo-cgp` already performs the first half of the deduplication this class calls for — it drops the intermediate provider-trait blocks and leads each surviving block with the innermost concrete cause. The **cross-component coalescing** is what remains: collapsing several checked components that share one leaf into a single reported cause, so a cascade whose three trees all name the missing `name` field prints one headline — "`App` is missing field `name`, needed by `ProvideFoo` (via `Bar`, `Baz`)" — instead of three. Closing that gap is what would move this class's fixture from `usability/` to `acceptable/`. +`cargo-cgp` reshapes this class in full, as [How cargo-cgp presents it](#how-cargo-cgp-presents-it) describes: it drops the intermediate provider-trait blocks, recovers each checked component's innermost concrete cause, and coalesces the components that share one leaf into a single headline that lists every affected consumer over one root-cause tree. The coalescing keys on a consumer-independent cause signature, so it generalizes to any set of checked components bottoming out on the same root cause, not just a field. ## Backing fixtures -- [`usability/duplication/dependency_cascade.rs`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/usability/duplication/dependency_cascade.rs) — a three-deep provider chain over one missing `name` field, checked across all three components; its `.rust.stderr` pins the six-block cascade, the concrete `HasField` in each `CanUseComponent` block's `help:` note, and the intermediate `ProvideFoo: Foo` / `ProvideBar: Bar` noise blocks, while its `.cgp.stderr` shows the three surviving per-component blocks, each with an identical `root cause: [CGP-E106] missing field \`name\`` tree (the first headed `[CGP-E002]`, the rest `[CGP-E001]`) — the `usability/` placement recording that the identical causes are not yet coalesced. -- [`usability/duplication/density_3.rs`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/usability/duplication/density_3.rs) — a denser two-component check over one missing `height` field where the second component (`CanCalculateDensity`) depends transitively on the first (`CanCalculateArea`); its `.cgp.stderr` pins the same per-component recovery, the deeper block's tree threading `[CGP-E101]`/`[CGP-E102]` hops through the intermediate consumer before reaching the shared `[CGP-E106]` leaf. +- [`acceptable/duplication/dependency_cascade.rs`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/acceptable/duplication/dependency_cascade.rs) — a three-deep provider chain over one missing `name` field, checked across all three components; its `.rust.stderr` pins the six-block raw cascade (the concrete `HasField` in each `CanUseComponent` block's `help:` note, and the intermediate `ProvideFoo: Foo` / `ProvideBar: Bar` noise blocks), while its `.cgp.stderr` shows the single coalesced block — `` the consumer traits `CanBaz`, `CanBar`, and `CanFoo` `` over one `[CGP-E106] missing field \`name\`` tree, with a caret at each entry. +- [`acceptable/duplication/density_3.rs`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/acceptable/duplication/density_3.rs) — a denser two-component check over one missing `height` field where the second component (`CanCalculateDensity`) depends transitively on the first (`CanCalculateArea`); its `.cgp.stderr` pins the two components coalesced into one `[CGP-E001]` headline over the shared `[CGP-E106]` leaf. ## Related diff --git a/docs/errors/wiring/orphan-rule.md b/docs/errors/wiring/orphan-rule.md index 7fdacc5d..c4764a87 100644 --- a/docs/errors/wiring/orphan-rule.md +++ b/docs/errors/wiring/orphan-rule.md @@ -28,7 +28,7 @@ Every form reduces to the same fact: a foreign namespace trait implemented for a ## The raw diagnostic -This section describes what plain `cargo check` prints — and, for this class, what `cargo-cgp` shows too, since the tool passes the error through unchanged (see [How cargo-cgp presents it](#how-cargo-cgp-presents-it)). The compiler reports **`E0210`** — "type parameter `…` must be used as the type parameter for some local type" — naming the impl's uncovered table parameter, with two explanatory notes: "implementing a foreign trait is only possible if at least one of the types for which it is implemented is local," and "only traits defined in the current crate can be implemented for a type parameter." Which parameter is named, and where the caret lands, follows the construct that generated the impl: a `#[default_impl]` names **`__Components__`**, lands the caret on the `#[cgp_impl]` attribute, and attributes the error to the `cgp_impl` macro; a `cgp_namespace!` re-open names **`__Table__`** (the namespace table parameter), lands the caret on the whole `cgp_namespace!` block, and attributes the error to the `cgp_namespace` macro. The parameter differs only because the two constructs name their table parameter differently; the violation is identical. Depending on the exact shape of the generated impl, the sibling orphan error **`E0117`** ("only traits defined in the current crate can be implemented for arbitrary types") can appear instead; both are the orphan rule rejecting the same foreign-trait-for-foreign-type impl. +This section describes what plain `cargo check` prints; `cargo-cgp` now reshapes this class into a CGP-framed message (see [How cargo-cgp presents it](#how-cargo-cgp-presents-it)), so what follows is the raw baseline the tool starts from. The compiler reports **`E0210`** — "type parameter `…` must be used as the type parameter for some local type" — naming the impl's uncovered table parameter, with two explanatory notes: "implementing a foreign trait is only possible if at least one of the types for which it is implemented is local," and "only traits defined in the current crate can be implemented for a type parameter." Which parameter is named, and where the caret lands, follows the construct that generated the impl: a `#[default_impl]` names **`__Components__`**, lands the caret on the `#[cgp_impl]` attribute, and attributes the error to the `cgp_impl` macro; a `cgp_namespace!` re-open names **`__Table__`** (the namespace table parameter), lands the caret on the whole `cgp_namespace!` block, and attributes the error to the `cgp_namespace` macro. The parameter differs only because the two constructs name their table parameter differently; the violation is identical. Depending on the exact shape of the generated impl, the sibling orphan error **`E0117`** ("only traits defined in the current crate can be implemented for arbitrary types") can appear instead; both are the orphan rule rejecting the same foreign-trait-for-foreign-type impl. The rule the compiler is enforcing here is coherence, and understanding it explains why the error frames the fix as a matter of *ownership*. Coherence requires that for any trait and type there is at most one impl, and the orphan rule preserves that across crates by forbidding an impl of a foreign trait unless a local type is *covered* by it — appears before any uncovered type parameter. Were an orphan impl allowed, two unrelated crates could each implement the foreign trait for the foreign type in incompatible ways, and adding a dependency could silently break a build; the orphan rule rejects the impl up front precisely so that can never happen. `E0210` is the specific form of this rule for an impl whose only "type" in the covering position is a bare type parameter (`__Components__`), which no local type covers. The current rule and its wording come from [RFC 2451 (re-rebalancing coherence)](https://rust-lang.github.io/rfcs/2451-re-rebalancing-coherence.html); the [`E0210`](../error_codes/e0210.md) reference summarizes it, alongside its sibling [`E0117`](../error_codes/e0117.md). @@ -38,7 +38,9 @@ The mechanical cause is **present** — the error names the foreign trait and th ## How cargo-cgp presents it -`cargo-cgp` does **not** rewrite this class today: it passes `rustc`'s `E0210` (or `E0117`) through unchanged, uncoded, so the reader sees exactly the raw diagnostic above. This is why the three fixtures sit under `usability/wiring/orphan/` rather than `acceptable/` — the cause is *present* in the output (the error names the foreign trait and the uncovered parameter), but the *actionable* remedy is CGP-specific and the tool does not yet supply it, so each fixture's `.cgp.stderr` matches its `.rust.stderr` byte for byte. The gap is a recognized one, not an oversight: the shape is distinctive enough to key on — an `E0210`/`E0117` whose trait is a namespace trait and whose `Self` is a foreign component marker or a `PathCons>` spine — so it is a candidate for a future rewrite into the ownership-framed remedy the [Resolving it](#resolving-it) section states. Until then, no `[CGP-Exxx]` code is stamped; the [cargo-cgp error-code catalog](https://github.com/contextgeneric/cargo-cgp/blob/main/docs/error-code.md) covers the classes it does rewrite. +`cargo-cgp` reshapes this class into a `[CGP-E011]` message that names the foreign namespace and key rather than the machinery parameter. The header reads `` cannot register the foreign into the foreign namespace `` `` — where `` is `` component `GreeterComponent` `` for a bare marker or `` path `@app.AnnouncerComponent` `` for a prefix path (resugared from its `PathCons>` spine) — and the raw coherence notes are replaced by one `help` carrying the ownership-based fix from [Resolving it](#resolving-it) below. The caret still points at the offending `#[cgp_impl]` attribute or `cgp_namespace!` block, and the Rust code stays `E0210`/`E0117`, kept alongside the `[CGP-E011]` tag. + +The tool recovers what it names from the compiler, not the error text: it finds the offending impl at the caret, confirms it is a foreign namespace trait — recognized by the single-`Delegate` fingerprint every `cgp_namespace!` trait and the built-in `DefaultNamespace`/`DefaultImpls…` share — implemented for a foreign key, and reads the trigger off the impl's own parameter (`__Table__` for a `cgp_namespace!` re-open, `__Components__` for a `#[default_impl]`/`#[prefix]` registration) so the `help` names the fix that fits: keying the registration on a local component (or registering it from the namespace's crate) for a registration, and inheriting the namespace into a new local one for a re-open. The three triggers are pinned by the fixtures under [`acceptable/wiring/orphan/`](https://github.com/contextgeneric/cargo-cgp/tree/main/tests/ui/acceptable/wiring/orphan); `[CGP-E011]` is catalogued in the [cargo-cgp error-code catalog](https://github.com/contextgeneric/cargo-cgp/blob/main/docs/error-code.md). ## Resolving it @@ -46,15 +48,15 @@ Own one end of the impl. Register the default from the crate that owns the names ## Notes for tooling -This is the remaining gap: `cargo-cgp` passes the orphan `E0210`/`E0117` through untranslated (above), so a post-processor that recognizes the shape — a namespace trait (a `#[cgp_namespace]` trait, `DefaultNamespace`, or a `DefaultImpls…`) implemented for a foreign component marker or a `PathCons>` spine — could translate the generic orphan message into the CGP remedy: "registering into a foreign namespace needs the crate to own the namespace or a local key; register from the namespace's own crate, key on a local component, use a namespace body entry, or inherit the namespace into a new local one." The raw diagnostic is accurate but frames a CGP wiring decision as a bare coherence rule, so the value the rewrite would add is the translation, not the extraction — which is why the fixtures live in `usability/` until it lands. +`cargo-cgp` reshapes this class, as [How cargo-cgp presents it](#how-cargo-cgp-presents-it) describes: it recognizes the shape — a namespace trait (a `#[cgp_namespace]` trait, `DefaultNamespace`, or a `DefaultImpls…`, told apart by their single-`Delegate` fingerprint) implemented for a foreign component marker or a `PathCons>` spine — and translates the generic orphan message into the CGP remedy: own one end, by keying the registration on a local component, registering it from the namespace's own crate, or (for a `cgp_namespace!` re-open) inheriting the namespace into a new local one. The raw diagnostic was accurate but framed a CGP wiring decision as a bare coherence rule, so the value the rewrite adds is the translation, not the extraction. ## Backing fixtures -Each fixture sits under `usability/wiring/orphan/` because `cargo-cgp` does not yet rewrite the class, so its `.cgp.stderr` and `.rust.stderr` are identical — both pin the raw `E0210`. +Each fixture sits under `acceptable/wiring/orphan/`, where its `.cgp.stderr` pins the reshaped `[CGP-E011]` output and its `.rust.stderr` the raw `E0210` baseline. -- [`usability/wiring/orphan/default_impl_foreign_prefix_path.rs`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/usability/wiring/orphan/default_impl_foreign_prefix_path.rs) — a downstream crate registering a `#[default_impl]` for an upstream *prefixed* component into the upstream namespace, so the key is a foreign `PathCons>` path; the snapshots pin the `E0210` on `__Components__`, the caret on the `#[cgp_impl]` attribute, and the "implementing a foreign trait" notes. -- [`usability/wiring/orphan/default_impl_foreign_component.rs`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/usability/wiring/orphan/default_impl_foreign_component.rs) — the same `#[default_impl]` orphan keyed on a foreign *unprefixed* component marker rather than a path, showing the restriction is not specific to prefixes; the snapshots pin the `E0210` on `__Components__`. -- [`usability/wiring/orphan/reopen_foreign_namespace.rs`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/usability/wiring/orphan/reopen_foreign_namespace.rs) — a `cgp_namespace!` block without `new` re-opening a foreign namespace; the snapshots pin the `E0210` on `__Table__` with the caret on the whole `cgp_namespace!` block, attributed to the `cgp_namespace` macro. +- [`acceptable/wiring/orphan/default_impl_foreign_prefix_path.rs`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/acceptable/wiring/orphan/default_impl_foreign_prefix_path.rs) — a downstream crate registering a `#[default_impl]` for an upstream *prefixed* component into the upstream namespace, so the key is a foreign `PathCons>` path; the reshaped snapshot names the `` path `@app.AnnouncerComponent` `` and the foreign namespace. +- [`acceptable/wiring/orphan/default_impl_foreign_component.rs`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/acceptable/wiring/orphan/default_impl_foreign_component.rs) — the same `#[default_impl]` orphan keyed on a foreign *unprefixed* component marker rather than a path, showing the restriction is not specific to prefixes; the snapshot names the `` component `GreeterComponent` `` instead. +- [`acceptable/wiring/orphan/reopen_foreign_namespace.rs`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/acceptable/wiring/orphan/reopen_foreign_namespace.rs) — a `cgp_namespace!` block without `new` re-opening a foreign namespace; its `__Table__` trigger selects the inherit-a-new-namespace `help`, with the caret on the whole `cgp_namespace!` block. The orphan-*safe* counterpart is the positive fixture [`ok/cross_crate_wiring.rs`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/ok/cross_crate_wiring.rs), which compiles clean: its auxiliary crates (`cgp-test-crate-a` upstream, `cgp-test-crate-b` downstream) wire a foreign component to a foreign provider, join an upstream namespace, and register a *local* component into the upstream namespace with `#[default_impl]` — every cross-crate impl coherent because one end is always local. diff --git a/docs/implementation/entrypoints/derive_build_field.md b/docs/implementation/entrypoints/derive_build_field.md index 8305b908..019f87eb 100644 --- a/docs/implementation/entrypoints/derive_build_field.md +++ b/docs/implementation/entrypoints/derive_build_field.md @@ -45,6 +45,10 @@ A named field is keyed by [`Symbol!`](../../reference/macros/symbol.md) and a tu An **empty struct** produces a `__Partial{Name}` with no field markers, a trivial `HasBuilder`/`FinalizeBuild`, and no `UpdateField`/`HasField` impls. This derive emits no `HasField`/`HasFieldMut` getters on the original struct and no `HasFields` representation impls — those come from [`#[derive(HasField)]`](derive_has_field.md) and [`#[derive(HasFields)]`](derive_has_fields.md); `BuildField` is purely the construction slice, included wholesale by [`#[derive(CgpRecord)]`](derive_cgp_record.md) and [`#[derive(CgpData)]`](derive_cgp_data.md). +## Error spans + +Each generated impl is re-spanned onto the token it derives from, so a compiler error points at that token rather than at the whole `#[derive(BuildField)]`. The per-field `UpdateField` and `HasField`-on-partial impls are aimed at the field they read (the field identifier, or the whole `syn::Field` for a tuple field), and the whole-struct `HasBuilder`/`IntoBuilder`/`PartialData`/`FinalizeBuild` impls at the struct name. Each goes through [`override_item_span`](../README.md#spans-aim-generated-items-at-the-token-the-user-wrote), moving only the `impl`/`{ … }` boundary — the mechanism the [`#[derive(HasField)]`](derive_has_field.md#error-spans) doc explains in full. The `__Partial{Name}` companion struct is cloned from the user's own struct, so its tokens already carry meaningful spans and need no re-spanning. + ## Tests `#[derive(BuildField)]` has no snapshot macro of its own; the builder items it emits are part of the record expansion pinned by the `snapshot_derive_cgp_data!` snapshots indexed in [derive_cgp_data.md's Snapshots section](derive_cgp_data.md#snapshots). The behavioral builder tests in [crates/tests/cgp-tests/tests/extensible_records/](../../../crates/tests/cgp-tests/tests/extensible_records/) exercise the machinery: diff --git a/docs/implementation/entrypoints/derive_cgp_data.md b/docs/implementation/entrypoints/derive_cgp_data.md index a40449a0..d88ae149 100644 --- a/docs/implementation/entrypoints/derive_cgp_data.md +++ b/docs/implementation/entrypoints/derive_cgp_data.md @@ -34,6 +34,8 @@ Field tagging follows the same rule as the whole family: a named struct field or The shape-specific corner cases are inherited from the building blocks rather than introduced here: a single-field tuple struct is special-cased in the `HasFields` product (see [`derive_has_fields`](derive_has_fields.md)), and an enum whose variants are not each single-unnamed-field tuple variants fails in the extractor and `FromVariant` codegen (see [`derive_extract_field`](derive_extract_field.md) and [`derive_from_variant`](derive_from_variant.md)). `CgpData` on such an enum therefore fails the same way, because it runs the same helpers. +Error spans are inherited the same way. Because `CgpData` runs exactly the slice helpers, each generated impl is already re-spanned onto the token it derives from — a per-field impl onto its field, a per-variant impl onto its variant, and a whole-type impl onto the struct or enum name — so a coherence conflict points at that token rather than at the whole `#[derive(CgpData)]`. The mechanism is documented under [`#[derive(HasField)]`](derive_has_field.md#error-spans) and repeated in each slice's Error spans section. + ## Snapshots Every `snapshot_derive_cgp_data!` invocation across the suite is indexed here, since these snapshots all belong to this entrypoint. The record expansion is owned by the `extensible_records` target and the variant expansion by `extensible_variants`: diff --git a/docs/implementation/entrypoints/derive_cgp_record.md b/docs/implementation/entrypoints/derive_cgp_record.md index f45f8a20..68d6799e 100644 --- a/docs/implementation/entrypoints/derive_cgp_record.md +++ b/docs/implementation/entrypoints/derive_cgp_record.md @@ -17,7 +17,7 @@ let items = record.to_items()?; There is no multi-stage transform. `ItemCgpRecord::to_items` concatenates three slices in a fixed order — the per-field `HasField`/`HasFieldMut` getters, the five representation impls (`HasFields`, `HasFieldsRef`, `FromFields`, `ToFields`, `ToFieldsRef`), and the incremental builder items (`__Partial{Name}`, `HasBuilder`, `IntoBuilder`, `PartialData`, `FinalizeBuild`, then per-field `UpdateField` and `HasField`). These are exactly the outputs of [`#[derive(HasField)]`](derive_has_field.md), [`#[derive(HasFields)]`](derive_has_fields.md), and [`#[derive(BuildField)]`](derive_build_field.md) respectively; this document does not repeat their item shapes. -The record's corner cases — `Symbol!` versus `Index` tagging, the newtype `HasFields` special case, and generic threading — are the same as those building blocks describe, because `CgpRecord` runs the same helpers. The [`cgp_data` AST stack](../asts/cgp_data.md) documents `ItemCgpRecord` and its methods, and [`derive_cgp_data`](derive_cgp_data.md) covers the umbrella derive this is a restriction of. +The record's corner cases — `Symbol!` versus `Index` tagging, the newtype `HasFields` special case, and generic threading — are the same as those building blocks describe, because `CgpRecord` runs the same helpers. Error spans are inherited the same way: each generated impl is already re-spanned onto the token it derives from (a per-field impl onto its field, a whole-struct impl onto the struct name), so a coherence conflict points there rather than at the whole `#[derive(CgpRecord)]` — see [`#[derive(HasField)]`](derive_has_field.md#error-spans). The [`cgp_data` AST stack](../asts/cgp_data.md) documents `ItemCgpRecord` and its methods, and [`derive_cgp_data`](derive_cgp_data.md) covers the umbrella derive this is a restriction of. ## Tests diff --git a/docs/implementation/entrypoints/derive_cgp_variant.md b/docs/implementation/entrypoints/derive_cgp_variant.md index 1a307769..7ce170ca 100644 --- a/docs/implementation/entrypoints/derive_cgp_variant.md +++ b/docs/implementation/entrypoints/derive_cgp_variant.md @@ -17,7 +17,7 @@ let items = variant.to_items()?; There is no multi-stage transform. `ItemCgpVariant::to_items` concatenates three slices in a fixed order — the five representation impls over a *sum* (`HasFields`, `HasFieldsRef`, `FromFields`, `ToFields`, `ToFieldsRef`), the per-variant `FromVariant` constructors, and the incremental extractor items (the `__Partial{Name}` and `__PartialRef{Name}` enums, `PartialData` for each, `HasExtractor`/`HasExtractorRef`/`HasExtractorMut`, `FinalizeExtract` for each, then the per-variant `ExtractField` impls). These are exactly the outputs of the enum path of [`#[derive(HasFields)]`](derive_has_fields.md), [`#[derive(FromVariant)]`](derive_from_variant.md), and [`#[derive(ExtractField)]`](derive_extract_field.md) respectively; this document does not repeat their item shapes. -The variant corner cases are inherited from those building blocks: variant names are keyed by [`Symbol!`](../../reference/macros/symbol.md), generics — including a lifetime parameter named `'a` — are threaded onto every impl and onto the partial enums, and every variant must be a single-unnamed-field tuple variant or the extractor and `FromVariant` codegen fail (see [`derive_extract_field`](derive_extract_field.md)). The borrowed extractor introduces its own lifetime under the reserved name `'__a__` precisely so it never collides with the enum's own `'a`. An enum with *no* variants is special-cased so its degenerate expansion still compiles — the borrowed partial enum becomes a bare empty enum and the borrowed accessors match the dereferenced place; see [`derive_extract_field`'s Behavior and corner cases](derive_extract_field.md#behavior-and-corner-cases). The [`cgp_data` AST stack](../asts/cgp_data.md) documents `ItemCgpVariant` and its methods, and [`derive_cgp_data`](derive_cgp_data.md) covers the umbrella derive this is a restriction of. +The variant corner cases are inherited from those building blocks: variant names are keyed by [`Symbol!`](../../reference/macros/symbol.md), generics — including a lifetime parameter named `'a` — are threaded onto every impl and onto the partial enums, and every variant must be a single-unnamed-field tuple variant or the extractor and `FromVariant` codegen fail (see [`derive_extract_field`](derive_extract_field.md)). The borrowed extractor introduces its own lifetime under the reserved name `'__a__` precisely so it never collides with the enum's own `'a`. An enum with *no* variants is special-cased so its degenerate expansion still compiles — the borrowed partial enum becomes a bare empty enum and the borrowed accessors match the dereferenced place; see [`derive_extract_field`'s Behavior and corner cases](derive_extract_field.md#behavior-and-corner-cases). Error spans are inherited the same way: each generated impl is already re-spanned onto the token it derives from (a per-variant impl onto its variant, a whole-enum impl onto the enum name), so a coherence conflict points there rather than at the whole `#[derive(CgpVariant)]` — see [`#[derive(HasField)]`](derive_has_field.md#error-spans). The [`cgp_data` AST stack](../asts/cgp_data.md) documents `ItemCgpVariant` and its methods, and [`derive_cgp_data`](derive_cgp_data.md) covers the umbrella derive this is a restriction of. ## Tests diff --git a/docs/implementation/entrypoints/derive_extract_field.md b/docs/implementation/entrypoints/derive_extract_field.md index 4d2f83eb..a9d17c66 100644 --- a/docs/implementation/entrypoints/derive_extract_field.md +++ b/docs/implementation/entrypoints/derive_extract_field.md @@ -48,6 +48,10 @@ A variantless enum is special-cased so its degenerate expansion still compiles. This derive emits no `HasFields` representation impls and no `FromVariant` constructors — those come from [`#[derive(HasFields)]`](derive_has_fields.md) and [`#[derive(FromVariant)]`](derive_from_variant.md); `ExtractField` is purely the deconstruction slice, included wholesale by [`#[derive(CgpVariant)]`](derive_cgp_variant.md) and [`#[derive(CgpData)]`](derive_cgp_data.md). +## Error spans + +Each generated impl is re-spanned onto the token it derives from, so a compiler error points at that token rather than at the whole `#[derive(ExtractField)]`. The per-variant `ExtractField` impls are aimed at the variant they match, and the whole-enum `HasExtractor`/`HasExtractorRef`/`HasExtractorMut`/`FinalizeExtract`/`PartialData` impls at the enum name. Each goes through [`override_item_span`](../README.md#spans-aim-generated-items-at-the-token-the-user-wrote), moving only the `impl`/`{ … }` boundary — the mechanism the [`#[derive(HasField)]`](derive_has_field.md#error-spans) doc explains in full. The `__Partial{Name}`/`__PartialRef{Name}` companion enums are cloned from the user's own enum, so their tokens already carry meaningful spans and need no re-spanning. + ## Known issues The extractor codegen requires every variant to be a single-unnamed-field tuple variant (enforced by `get_variant_type` in the `derive_extractor/utils.rs` helper). A fieldless variant like `Empty`, a multi-field variant like `Pair(A, B)`, or a struct-style variant like `Named { x: A }` makes the macro fail with "Expected variant to contain exactly one unnamed field." There is no per-variant opt-out, so an enum mixing variant shapes cannot derive the extractor at all; the same requirement applies to [`#[derive(FromVariant)]`](derive_from_variant.md) and therefore to `#[derive(CgpVariant)]`/`#[derive(CgpData)]` on such an enum. The reference document records the user-visible form of this limitation in its own Known issues. diff --git a/docs/implementation/entrypoints/derive_from_variant.md b/docs/implementation/entrypoints/derive_from_variant.md index 792eb872..379311e0 100644 --- a/docs/implementation/entrypoints/derive_from_variant.md +++ b/docs/implementation/entrypoints/derive_from_variant.md @@ -34,6 +34,10 @@ The `PhantomData` argument exists only to let a caller select which variant The enum's generic parameters are threaded onto every impl. This derive emits no `HasFields` representation impls and no extractor — those come from [`#[derive(HasFields)]`](derive_has_fields.md) and [`#[derive(ExtractField)]`](derive_extract_field.md); `FromVariant` is purely the construction slice, included wholesale by [`#[derive(CgpVariant)]`](derive_cgp_variant.md) and [`#[derive(CgpData)]`](derive_cgp_data.md). +## Error spans + +Each per-variant impl is re-spanned onto the variant it constructs, so a coherence conflict (`E0119`) with a hand-written `FromVariant` impl lands its caret on the variant the user wrote rather than on the whole `#[derive(FromVariant)]`. `derive_from_variant_from_enum` passes each finished impl through [`override_item_span`](../README.md#spans-aim-generated-items-at-the-token-the-user-wrote), moving only the `impl`/`{ … }` boundary — the mechanism the [`#[derive(HasField)]`](derive_has_field.md#error-spans) doc explains in full. + ## Known issues Like the extractor derive, `#[derive(FromVariant)]` requires every variant to be a single-unnamed-field tuple variant. A fieldless, multi-field, or struct-style variant makes the macro fail with "Expected variant to contain exactly one unnamed field," with no per-variant opt-out. The requirement is described alongside the extractor's in [`derive_extract_field`](derive_extract_field.md#known-issues), and the reference document records its user-visible form. diff --git a/docs/implementation/entrypoints/derive_has_field.md b/docs/implementation/entrypoints/derive_has_field.md index da3c3b9e..db947db2 100644 --- a/docs/implementation/entrypoints/derive_has_field.md +++ b/docs/implementation/entrypoints/derive_has_field.md @@ -46,6 +46,14 @@ A **raw-identifier field** such as `r#type` is tagged by its logical name: `Symb Field access through **smart pointers** is not the derive's doing: `HasField`/`HasFieldMut` have blanket impls over `Deref`/`DerefMut` targets defined in the field crate, so a `Box` resolves through to the inner struct without the derive generating anything for the pointer type. +## Error spans + +Each generated impl is re-spanned onto the field it derives from, so a compiler error about one field's `HasField`/`HasFieldMut` impl points at that field rather than at the whole `#[derive(HasField)]`. The impls are built with `parse_internal!`, whose tokens all carry the macro's `call_site` span — the entire derived struct — so without a re-span every such error would underline the derive and say nothing about which field is involved. `derive_has_field_impls_from_struct` therefore passes each finished impl through [`override_item_span`](../README.md#spans-aim-generated-items-at-the-token-the-user-wrote), moving only its boundary tokens (the `impl` keyword and the `{ … }` body) onto the field's own span — the field identifier for a named field, the whole `syn::Field` for a tuple field, which has no identifier. This is the same technique the [`delegate_components!`](delegate_components.md#error-spans) impls use for their per-entry keys. + +Two diagnostics show the difference. A **coherence conflict** (`E0119`) — a hand-written `HasField` impl clashing with the one the derive emits — now lands its "conflicting implementation" caret on the `name` field instead of on the derive. More common in practice is the **near-impl hint** inside a missing-field check error: when a provider needs a field the struct lacks, `rustc` reports the unmet `HasField` bound and adds "but trait `HasField<…>` is implemented for it," pointing at the *nearest existing* field impl. That caret now lands on the field whose impl is cited, so a struct that derives `HasField` for several fields shows each near-miss on its own field rather than collapsing them all onto the derive attribute — where the encoded `Symbol` tags were the reader's only way to tell them apart. See the [check-trait-failure](../../errors/checks/check-trait-failure.md) class for the full diagnostic. + +Only the boundary tokens move; every interior token — the `HasField` reference, the `Symbol!`/`Index` tag, the `Value` type — keeps its own span. That is what keeps the field navigable in an editor: rust-analyzer maps a source token to its expansion by source range, so re-spanning a resolvable reference onto the field would hijack go-to-definition on the field, whereas a keyword and a delimiter cannot. The caret half is pinned by the raw `.rust.stderr` baselines of the `cargo-cgp` UI fixtures that exercise a derived context — [`fields/base_area_1`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/acceptable/fields/base_area_1.rs) and the multi-field [`duplication/density_3`](https://github.com/contextgeneric/cargo-cgp/blob/main/tests/ui/acceptable/duplication/density_3.rs) among them — so a regression that drops the re-span changes those snapshots. + ## Snapshots Every `snapshot_derive_has_field!` invocation across the suite is indexed here, since these snapshots all belong to this entrypoint. They live in the `field_access` target, which owns the `#[derive(HasField)]` expansion: diff --git a/docs/implementation/entrypoints/derive_has_fields.md b/docs/implementation/entrypoints/derive_has_fields.md index 6ae73bb8..9f50752d 100644 --- a/docs/implementation/entrypoints/derive_has_fields.md +++ b/docs/implementation/entrypoints/derive_has_fields.md @@ -47,6 +47,10 @@ The type's **generic parameters and `where` clause** are threaded onto all five An **enum** always maps each variant's payload into a `Field` entry regardless of the payload's own shape; the `HasFields` enum path does not impose the single-unnamed-field requirement that the extractor and `FromVariant` derives do, because it only names the payload type rather than deconstructing it. +## Error spans + +All five impls are keyed on the whole type, so each is re-spanned onto the struct or enum name the user wrote rather than left at the derive's `call_site` span. A coherence conflict (`E0119`) — a hand-written `HasFields` impl clashing with the derived one — therefore lands its caret on the type name instead of on the whole `#[derive(HasFields)]`. `derive_has_fields_impls_from_struct` and `derive_has_fields_impls_from_enum` pass each finished impl through [`override_item_span`](../README.md#spans-aim-generated-items-at-the-token-the-user-wrote), which moves only the `impl`/`{ … }` boundary — the mechanism the [`#[derive(HasField)]`](derive_has_field.md#error-spans) doc explains in full. + ## Snapshots Every `snapshot_derive_has_fields!` invocation across the suite is indexed here, since these snapshots all belong to this entrypoint. The struct expansion is owned by the `extensible_records` target and the enum expansion by `extensible_variants`: