cargo-cgp turns a compiler's raw diagnostics into readable, root-cause-first CGP errors, and the
string-level logic that does the turning lives in one rustc-free crate,
cargo-cgp-error-processing. This document records what
that crate holds, why it is kept apart from the driver, how the driver drives it, and how it is
tested.
The crate is driven entirely by the driver; the front-end no longer touches diagnostics. It
exists as a separate crate for one reason: it links no compiler internals, so it builds and its
tests run on any toolchain, without the driver's rustc_private linkage. The driver depends on it
and calls into it from its emitter; the crate never depends on the driver or the front-end. Keeping
the logic here is what lets a plain unit test exercise a transform over a hand-built string, with no
compiler, no cargo, and no cargo-cgp process in the loop.
The crate has six tenants, all driven by the driver's emitter. Grouping them here, apart from the driver, is what keeps them unit-testable.
- Post-processing (
postprocess) is the set of fallback text transforms the driver applies to a diagnostic's messages so raw CGP constructs do not look confusing. Each is a pure&str -> Option<String>—Somewhen it changed the text — andpostprocess_messagechains them. - The wiring rewrite (
rewrite) is the string transform that renames CGP wiring messages, over theComponentNameMapthe driver fills in from the compiler. It also rewrites theE0275wiring-overflow header into its[CGP-E010]form. It is documented where it is used, in The driver. - The diagnosis model and its wording
(
diagnosis) is the rustc-free root-cause model — theResolvedfailure the driver's typed resolver produces, in ownedStringform — and the wording that turns it into diagnostic text.plan_resolvedcoalesces causes that share one fix (coalesce_underived_fields) and composes the rewritten header, the derivehelps, and the per-causeroot cause:notes into aDiagnosisPlan, which the emitter only maps onto rustc'sDiagInner; keeping every piece rustc-free is what makes the whole diagnosis-to-text layer unit-testable without aTyCtxt. The same module holds the pure dependency-tree label constructors and the repeated-generics elision (labels.rs), so every label template the driver's tree builder uses lives here. It is documented in Typed root-cause resolution. The module also holds the duplicate-key conflict wording (wiring.rs): theWiringConflictmodel andplan_wiring_conflict, which words the[CGP-E004]–[CGP-E008]headers (one per conflict shape) the driver'sresolve::conflictclassifier feeds it (see The driver). - The dependency-tree renderer
(
tree) is theDependencyTreetype, itscargo tree-style renderer over the tinytermtreecrate, andmerge_dependency_forest— the merge that fuses several root-cause chains sharing a common ancestor into one branching tree, so a multi-cause failure shows the shared prefix once. The driver's resolver builds the per-cause chains, and the diagnosis wording merges and renders them. It is documented in Typed root-cause resolution. - The de-duplication ledger (
dedup) is theDedupLedgerthe emitter records each transformed diagnostic in, so the re-reports one lazy-wiring mistake produces at many sites are suppressed. The key scheme — the recovered cause, the rendered text, and the coded header — lives with the ledger, documented in The driver. - The text signals (
signals) are the stable rustc phrasings the emitter's candidate checks key on — the wiring-trait mention that makes a diagnostic a resolution candidate, the method-boundsE0599shape the resolver may safely run on, the method-probe advice the emitter strips, and the?-operator cascade wording — each a pure predicate, so the wording dependence on rustc's phrasing is documented and tested in one place.
A further module, code, holds the CGP-E
error-code constants the rewrite and the diagnosis wording stamp on classified main messages
(catalogued in error-code.md). This document covers the post-processing transforms
in full and points at the other tenants' own documents.
Post-processing is the driver's final diagnostic pass, and it runs on every diagnostic the driver emits, after whatever transform came before it (see The error pipeline). The driver's emitter first tries the typed root-cause resolver; if that declines, it renames the CGP wiring messages it recognizes; then, either way, the diagnostic passes through the post-processing transforms.
The pass does two jobs depending on what came before it, and both keep raw CGP spellings out of the
output. For a diagnostic the tool left un-rewritten, post-processing is the whole cleanup — it
strips the cgp:: prefixes, resugars the Symbol! and Path! spines, and rewords an unmet
HasField bound, so a diagnostic the tool does not classify still reads cleanly. For a rewritten
one, only the prefix strip and the Symbol!/Path! resugaring bite: they tidy the compiler-formatted
CGP type names a rewrite embeds — a provider like RedirectLookup<…, PathCons<Symbol<…>>> in a coded
header, folded to RedirectLookup<…, Path!(@…)>, say — while the missing-field reword finds nothing
to match, because the resolver's tree never carries the `HasField<…>` is not implemented clause
the reword keys on.
The driver applies post-processing over a DiagInner before its inner emitter renders it, so the
rendered output reflects the change. Each transform is a text function, and the driver runs
[postprocess_message] over every plain-string message and span label of the diagnostic and its
children — the same reach the compiler's renderer has, so the caret label, the notes, and the header
are all covered. Editing the structured DiagInner rather than a rendered blob means the transform
never touches the source snippets the emitter pulls from the SourceMap, so it cannot corrupt a line
of the user's own code the way a whole-text rewrite could.
One transform needs a fact about the whole diagnostic rather than one message, and the driver
computes it once up front. The missing-field reword must know whether the context implements
HasField for any field, because that decides between two wordings whose fixes differ, and the
"similar impl" landmark that tells it can sit in a different child than the clause. So the driver
scans every message and label with
context_has_hasfield_impls
first, then passes the answer into each per-message rewrite.
Post-processing is a chain of transforms applied in order, so the output of one feeds the next, and
the order matters. Module-path stripping runs first so the later transforms match the bare names
(Symbol, Chars, …) rather than their fully-qualified forms; Symbol! resugaring runs before
Path! resugaring (which reads the already-resugared Symbol!("…") segments), before list resugaring
(which reads a Field's Symbol!("…") tag when naming a struct field or enum variant), and before
the field rewrite (which matches the resugared HasField<Symbol!("…")> form). Six transforms exist
today:
-
strip_module_pathscollapses everya::b::Cidentifier run to its bare final segment, so a fully-qualifiedcontexts::app::MockAppbecomesMockApp,interfaces::types::QuantityTypeProviderComponentbecomesQuantityTypeProviderComponent, andf64: std::cmp::Eqbecomesf64: Eq— the module qualifiers are noise a CGP diagnostic reads better without. It scans by byte for the ASCII identifier run but copies every other character whole by its UTF-8 width, so multi-byte text (a rendered tree's└─) is never split; it skips string literals and leaves a turbofish, an associated-type>::Assoctail, and a lone identifier alone. It subsumesstrip_cgp_prefixes(acgp::prelude::Charsis just one such run), which still runs after it for the specific CGP re-export list. -
strip_cgp_prefixesremoves the CGP module paths rustc prints in front of CGP type names —cgp::prelude::CharsbecomesChars. The prefixes it strips are a constant list,CGP_PREFIXES(cgp::prelude::,cgp::macro_prelude::,cgp::cgp_core::,cgp::cgp_extra::); withstrip_module_pathsnow running first this is largely redundant, kept as the explicit CGP-specific fallback. -
resugar_symbolreverses aSymbol!expansion back to its surface form:Symbol<2, Chars<'x', Chars<'y', Nil>>>becomesSymbol!("xy"). It parses the spine and rewrites it only on an exact structural match — the declared length must equal the decoded string's byte length (the lengthSymbol!bakes in isstr::len(), not the character count), the spine must beChars/Nilall the way down, and eachCharshead must be a single plain character literal. ASymbol<…>that does not match exactly is left untouched, because another type could share the name; this caution is essential to every resugaring transform, not just this one. -
resugar_pathreverses aPath!expansion back to its surface form:PathCons<Symbol!("app"), PathCons<GreeterComponent, Nil>>becomes@app.GreeterComponent. Itswrapparameter chooses the shape: in a rewritten diagnostic (the tool constructed the message) it renders the bare@app.GreeterComponent, since it reads as a path the message names; in the resugaring fallback (an un-rewritten message showing a raw type in source form) it wraps it as thePath!(@app.GreeterComponent)macro form. The emitter passeswrapaccordingly — bare on a rewrite (a wiring-conflict or typed resolution), wrapped on a diagnostic it left untouched. It walks the right-nestedPathCons/Nilspine and rewrites it on a well-formed match, mirroring howPath!classifies each segment forward: aSymbol!("name")head becomes the bare segmentnameonly whennameis a lowercase, non-primitive identifierPath!would encode as aSymbol, and every other head is rendered back verbatim as its type — a capitalized component marker, a primitive, or a compound value type anopenstatement dispatches on (Vec<u8>,&Coord), which is exactly what lets anopen-dispatched redirect path read as@ItemEncoderComponent.Vec<u8>rather than a raw spine. Two shapes still decline, leaving thePathConsspine untouched: a module-qualified segment (a residual::the earlier module strip should have removed) folds to its final identifier only when that is a plain type, and a bare lowercase identifier — whichPath!would have made aSymbol— is ambiguous as a plain type. A spine that is notPathCons/Nilall the way down is also left untouched. One spine that is notNil-terminated is still rewritten: an open-ended path, whose spine ends in a generic "rest of path" parameter that rustc prints as the placeholder_(as in the conflicting-wiringE0119blocks over a duplicated@-path key). Such a tail resugars to a trailing.*wildcard segment, soPathCons<Symbol!("foo"), PathCons<Symbol!("bar"), _>>becomesPath!(@foo.bar.*). The.*is not realPath!syntax and would not parse back, but it reads far better than the raw spine; only a bare_tail triggers it, since_is never a concrete segment, and any other non-Niltail still declines. It runs afterresugar_symbol, so its symbol segments are already inSymbol!("…")form. -
resugar_listsreverses aProduct!/Sum!expansion back to its surface form:Cons<u64, Cons<String, Nil>>becomesProduct![u64, String]andEither<A, Either<B, Void>>becomesSum![A, B]. A spine whose elements are all named fieldsField<Symbol!("name"), Type>folds one step further to the record or variant it describes — a product toStruct! { name: Type, … }, a sum toEnum! { Name(Type), … }(presentation-only forms, not real CGP macros). This is the fallback counterpart of the driver's typedrender_ty, which resugars the same spines in the dependency tree anchored byDefId; this one exists to catch a raw spine in a diagnostic the resolver declined and left to rustc's own text, so a fallback message reads the same as a reshaped one. Running on plain text it cannot check the defining crate, so — the caution every resugaring transform shares — it rewrites only on an exact structural match: the spine must close on the exactNil(product) orVoid(sum) terminator, an open-ended or wrong-terminated spine is left untouched, and aCons<that is only the tail ofPathCons<is not mistaken for a cell. It runs afterresugar_symbol(so aField's tag reads asSymbol!("…")) and resugars each element recursively, so a nested list folds in turn. -
rewrite_missing_fieldsturns an unmetHasFieldbound into a field-oriented message. It matches (after the two transforms above)the trait `HasField<Symbol!("name")>` is not implemented for `Context`and distinguishes two cases whose fixes differ. When the context implementsHasFieldfor some other field, it is a single missing field (missing field `name` on `Context`); when it implementsHasFieldfor no field at all, the whole derive is missing (`#[derive(HasField)]` is required to access field `name` on `Context`). Neither rewrite carries a CGP error code: the clause it rewrites sits in a sub-message, and codes classify only a rewritten main message (see error-code.md). The tell that separates the two cases is rustc's "similar impl" landmark, which the CGP check-trait-failure catalog entry documents: its presence — either inline (but traitHasField<…>is implemented for it, one other field) or as a separate`Context` implements trait `HasField<…>`note (several other fields) — means a single missing field; its absence means the missing derive.The empty-derived-struct case is fine, not a defect. The single-vs-derive classification is exact except for one degenerate input, and that input needs no fix. A context that derives
HasFieldbut declares no fields at all gets the missing-derive message even though the derive is present — but that is correct, because#[derive(HasField)]emits one impl per field, so on a fieldless struct it emits nothing, identical to no derive at all. A fieldless derive leaves no trace in the generated program, so it is genuinely impossible to tell whether it was written; the two are the same program whereverHasFieldis concerned.
In practice the missing-field reword rarely fires today, because the typed root-cause resolver recovers most missing-field failures from the compiler and replaces them with a dependency tree before this fallback is reached. The transform remains for the diagnostics the resolver declines, where the text clause is all there is to work with.
Because each transform is a pure function over a string, it is tested without running the tool. A
test drives one transform over the case under test and asserts on the returned Option<String>;
nothing compiles, no driver runs, and the test is fast and deterministic, so a whole catalog of
cases can be exercised as ordinary library tests. This is what the rustc-free design buys.
tests/postprocess.rs drives each
transform — a stripped prefix, an exactly-matched Symbol!, a wrong length or foreign type left
alone, the single-field (inline and separate-note landmark) versus missing-derive branches, and the
postprocess_message chain end to end. The diagnosis wording is tested the same way, only over a
hand-built Resolved rather than a string:
tests/diagnosis.rs drives
plan_resolved through the missing-field, missing-derive, Deref-target, field-type-mismatch,
use-site, kept-ordinary-bound, and provider-header cases, asserting the header, helps, and notes it
returns with no compiler in the loop.
The UI snapshot suite exercises the transforms a second way, over real diagnostics:
every fixture's .cgp.stderr is what the driver rendered after applying them, so a change to a
transform that affects an emitted diagnostic shows up as a snapshot diff. The two levels guard
different seams — this crate's tests pin a transform on a curated input, while the UI suite proves
the transforms stay consistent with what the driver emits across the whole catalog.
The transforms still ahead extend the per-diagnostic cleanup, each a new function added to the
post-processing chain, each applying the same exact-match caution resugar_symbol sets the precedent
for. Decoding the remaining type-level encodings is the nearest: Symbol! and Path! are done;
Cons<A, Cons<B, Nil>> is Product![A, B], Either<…>/Void spines are Sum![…], and so on —
rewriting these back to their surface form removes the rest of the visual noise a CGP error carries.
Recognizing more error classes is the other direction, each rewriting its message the way the
missing-field transform does.
One larger transformation is deliberately out of scope for this crate: collapsing a cascade — the one deep mistake reported at every transitively dependent provider — into a single root cause. That is a cross-diagnostic transform, and it can only be decided by looking at the whole diagnostic set, which this crate's per-string transforms never see. It would have to live in the driver's emitter, which would need to buffer the compilation's diagnostics before emitting them; it does not exist yet.
Clippy offers no prior art for this crate, and the absence is itself informative. Clippy defines no
diagnostic type of its own and never rewrites a diagnostic: it reuses rustc's Diag/DiagCtxt
machinery and emits its lints through the compiler's own emitters, so its output is rustc's,
unmodified (clippy_utils/src/diagnostics.rs).
cargo-cgp does the opposite — it rewrites the diagnostics rustc already produced — which is why it
needs a body of string transforms Clippy has no equivalent of. Keeping those transforms in a
rustc-free crate is the design that makes them testable without the compiler, precisely because
Clippy's "just use the compiler's emitter" approach is not open to a tool that rewrites.
crates/cargo-cgp-error-processing/tests/postprocess.rs— drives each post-processing transform over crafted inputs: the stripped/left-alone prefix cases, the exact-match casesresugar_symbolmust skip, thePathCons→Path!resugaring (symbol, type, and primitive segments, the open_tail folded to a.*wildcard, and the non-round-trippable casesresugar_pathmust skip), the single-field (inline and separate-note landmark) versus missing-derive branches ofrewrite_missing_fields, and thepostprocess_messagechain.crates/cargo-cgp-error-processing/tests/rewrite.rs— the wiring-message rewrite over a hand-built name map (see The driver).crates/cargo-cgp-error-processing/tests/diagnosis.rs—plan_resolvedand the wording over hand-builtResolvedvalues: the missing-field, missing-derive, andDeref-target field cases, a field-type mismatch, a use-site method failure, a kept ordinary bound (header dropped, lead-less note), a provider header via the text rewrite, and the pluralized consumer header.crates/cargo-cgp-error-processing/tests/tree.rs— the dependency-tree renderer andDependencyTree::from_chain(see Typed root-cause resolution).crates/cargo-cgp-error-processing/tests/wiring.rs—plan_wiring_conflictandwiring_conflict_helpover hand-builtWiringConflictvalues: the duplicate, overlap (component and path forms), multiple-namespaces, redirect (header plus its help), and same-/different-path duplicate-redirect headers.crates/cargo-cgp-error-processing/tests/coalesce.rs—coalesce_underived_fieldsover hand-built causes: the merged group, its lead and single derive help, and the boundaries (a lone underived field, genuinely missing fields, different owners, a group beside a missing field).crates/cargo-cgp-error-processing/tests/labels.rs— the pure label constructors and their codes,elide_repeated_generics(a repeated hop elided, a changing hop kept), and the text signals.crates/cargo-cgp-error-processing/tests/dedup.rs— theDedupLedgerkey scheme: a re-reported cause suppressed, distinct causes kept, the text key, the coded-header key collapsing a declined fallback, and a kept rustc header never keying.- The UI snapshot suite exercises the transforms over every fixture's real diagnostics:
each
.cgp.stderris what the driver rendered after applying them.
crates/cargo-cgp-error-processing/src/postprocess/— the post-processing transforms:mod.rs(re-exports),chain.rs(thepostprocess_messagechain, threading thebare_pathsflag),strip_modules.rs(strip_module_paths, the UTF-8-safe module-qualifier collapse),strip_prefixes.rs(strip_cgp_prefixesand theCGP_PREFIXESconstant),resugar_symbol.rs(the exact-matchSymbol!parser),resugar_path.rs(thePathCons→@…/Path!(@…)resugarer, the form chosen by itswrapparameter), andmissing_field.rs(rewrite_missing_fields,context_has_hasfield_impls, and the single-field-vs-missing-derive classification).crates/cargo-cgp-error-processing/src/rewrite/— the wiring-message rewrite andComponentNameMapthe driver drives:mod.rs(re-exports),message.rs(rewrite_messageand the note/header forms — the code-stampingrewrite_trait_boundand theE0275rewrite_wiring_overflowwith itswiring_overflow_help),names.rs(ComponentNameMap/ComponentTraitNames),parse.rs(parse_trait_bound), andtext.rs(the segment/generics splitters). See The driver.crates/cargo-cgp-error-processing/src/diagnosis/— the rustc-free root-cause model and its wording:leaf.rs(Leaf/FieldIssue),resolved.rs(Cause/Resolved), thewording/directory —header.rs(consumer_header,field_mismatch_header),lead.rs(root_cause_leadand the leaf codes),note.rs(cause_notes, which groups causes sharing a dependency root and words each group as a singlecause_noteor a mergedroot causes:note),help.rs(derive_help_messages), andsignature.rs(cause_signature) —coalesce.rs(coalesce_underived_fields),labels.rs(the pure tree-label constructors andelide_repeated_generics),plan.rs(DiagKind,DiagnosisPlan, andplan_resolvedwith itscategorized_header), andwiring.rs(WiringConflict/WiringKey,plan_wiring_conflictfor the[CGP-E004]–[CGP-E008]duplicate-key headers, andwiring_conflict_helpfor the redirect fix). See Typed root-cause resolution and The driver.crates/cargo-cgp-error-processing/src/tree.rs— theDependencyTreetype (withfrom_chain), itscargo tree-style renderer, andmerge_dependency_forest(fusing root-cause chains that share a common ancestor into one branching tree).crates/cargo-cgp-error-processing/src/dedup.rs— theDedupLedgerand its span-independent key scheme.crates/cargo-cgp-error-processing/src/signals.rs— the rustc-phrasing predicates the emitter's candidate checks and cleanups key on.crates/cargo-cgp-error-processing/src/code.rs— theCGP-Eerror-code constants, catalogued in error-code.md.crates/cargo-cgp-driver/src/emitter/— the driver-side caller: applies the transforms over aDiagInner's messages and span labels after its own rewrite.
- The error pipeline — the surrounding stages: how rustc is configured to emit better diagnostics, how each diagnostic is transformed, and how the result is rendered.
- The driver — the emitter that drives these transforms, and the wiring rewrite in full.
- CGP error catalog — the error classes the transforms must learn to recognize, and where each class hides or surfaces its root cause.