Skip to content

Introduce #[diagnostic::on_type_error(message)]#155200

Draft
Unique-Usman wants to merge 1 commit intorust-lang:mainfrom
Unique-Usman:ua/diagnostic_on_type_error
Draft

Introduce #[diagnostic::on_type_error(message)]#155200
Unique-Usman wants to merge 1 commit intorust-lang:mainfrom
Unique-Usman:ua/diagnostic_on_type_error

Conversation

@Unique-Usman
Copy link
Copy Markdown
Contributor

@Unique-Usman Unique-Usman commented Apr 12, 2026

No description provided.

@rustbot
Copy link
Copy Markdown
Collaborator

rustbot commented Apr 12, 2026

Some changes occurred in compiler/rustc_passes/src/check_attr.rs

cc @jdonszelmann, @JonathanBrouwer

Some changes occurred in compiler/rustc_attr_parsing

cc @jdonszelmann, @JonathanBrouwer

Some changes occurred in compiler/rustc_hir/src/attrs

cc @jdonszelmann, @JonathanBrouwer

@rustbot rustbot added A-attributes Area: Attributes (`#[…]`, `#![…]`) S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Apr 12, 2026
@rustbot
Copy link
Copy Markdown
Collaborator

rustbot commented Apr 12, 2026

r? @davidtwco

rustbot has assigned @davidtwco.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: compiler
  • compiler expanded to 69 candidates
  • Random selection from 12 candidates

@Unique-Usman
Copy link
Copy Markdown
Contributor Author

r? estebank

@rustbot rustbot assigned estebank and unassigned davidtwco Apr 12, 2026
@Unique-Usman Unique-Usman marked this pull request as draft April 12, 2026 14:20
@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Apr 12, 2026
@mejrs
Copy link
Copy Markdown
Contributor

mejrs commented Apr 12, 2026

Before you sink more time and effort into this you should explain what you want on_type_error to do.

One expectation I have is that it can specialize for different types. For example if you put this attribute on struct A but the typeerror finds a B or C instead you'll want to emit different messages for them, similar to how the filtering in #[rustc_on_unimplemented] works. But implementing that properly is actually a lot of work and effort, and requires significant changes to rustc.

I'm not convinced this attribute can be implemented in a satisfying way at this time. I'm open to being convinced otherwise though, and am happy to hear your thoughts.

@estebank
Copy link
Copy Markdown
Contributor

@mejrs two things: I agree that we want filtering in the same way as rustc_on_unimplemented, and we need the same kind of filtering for diagnostic::on_unimplemented that we don't have today. It should work exactly the same way. I believe that we can get away with a similar (but less powerful) version of what the rustc_ attr provides. Minimally, for now, I think we can get away with supporting the following:

#[diagnostic::on_type_error(
    note = "text",
)]
struct S<T>(T);

with an eye for something along the lines of

#[diagnostic::on_type_error(
    on(expected="Self", found="crate::K", T = "i32", note = "a"),
    on(expected="crate::K", found="Self", note = "b"),
    note = "c",
)]
struct S<T>(T);

some time later.

I believe that having the minimal functionality at least lets crate authors include the "filtering information" in the text itself ("if this is the found type, then..." or "if type parameter T is blah, ..."), and that there are already several cases in the std of unconditional addition of notes during error reporting for given types, so I think the minimal functionality is already adding value.

For the filtering I would like to share the same parser between on_type_error and on_unimplemented, as it is effectively the same functionality.

if let ty::Adt(_, _) = parent_ty.kind() { Some(parent_ty) } else { None }
} else {
None
};
Copy link
Copy Markdown
Contributor

@estebank estebank Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View changes since the review

as discussed we shouldn't do this now, only deal with the simple case that can be gleaned from only the expected/found mismatch.

@mejrs
Copy link
Copy Markdown
Contributor

mejrs commented Apr 15, 2026

Did you two have a discussion about this beforehand? I'd like to read it.

that there are already several cases in the std of unconditional addition of notes during error reporting for given types

I presume you are talking about this?

fn suggest_unwrapping_inner_self(
&self,
err: &mut Diag<'_>,
source: SelfSource<'tcx>,
actual: Ty<'tcx>,
item_name: Ident,
) {
let tcx = self.tcx;
let SelfSource::MethodCall(expr) = source else {
return;
};
let call_expr = tcx.hir_expect_expr(tcx.parent_hir_id(expr.hir_id));
let ty::Adt(kind, args) = actual.kind() else {
return;
};
match kind.adt_kind() {
ty::AdtKind::Enum => {
let matching_variants: Vec<_> = kind
.variants()
.iter()
.flat_map(|variant| {
let [field] = &variant.fields.raw[..] else {
return None;
};
let field_ty = field.ty(tcx, args);
// Skip `_`, since that'll just lead to ambiguity.
if self.resolve_vars_if_possible(field_ty).is_ty_var() {
return None;
}
self.lookup_probe_for_diagnostic(
item_name,
field_ty,
call_expr,
ProbeScope::TraitsInScope,
None,
)
.ok()
.map(|pick| (variant, field, pick))
})
.collect();
let ret_ty_matches = |diagnostic_item| {
if let Some(ret_ty) = self
.ret_coercion
.as_ref()
.map(|c| self.resolve_vars_if_possible(c.borrow().expected_ty()))
&& let ty::Adt(kind, _) = ret_ty.kind()
&& tcx.get_diagnostic_item(diagnostic_item) == Some(kind.did())
{
true
} else {
false
}
};
match &matching_variants[..] {
[(_, field, pick)] => {
let self_ty = field.ty(tcx, args);
err.span_note(
tcx.def_span(pick.item.def_id),
format!("the method `{item_name}` exists on the type `{self_ty}`"),
);
let (article, kind, variant, question) = if tcx.is_diagnostic_item(sym::Result, kind.did())
// Do not suggest `.expect()` in const context where it's not available. rust-lang/rust#149316
&& !tcx.hir_is_inside_const_context(expr.hir_id)
{
("a", "Result", "Err", ret_ty_matches(sym::Result))
} else if tcx.is_diagnostic_item(sym::Option, kind.did()) {
("an", "Option", "None", ret_ty_matches(sym::Option))
} else {
return;
};
if question {
err.span_suggestion_verbose(
expr.span.shrink_to_hi(),
format!(
"use the `?` operator to extract the `{self_ty}` value, propagating \
{article} `{kind}::{variant}` value to the caller"
),
"?",
Applicability::MachineApplicable,
);
} else {
err.span_suggestion_verbose(
expr.span.shrink_to_hi(),
format!(
"consider using `{kind}::expect` to unwrap the `{self_ty}` value, \
panicking if the value is {article} `{kind}::{variant}`"
),
".expect(\"REASON\")",
Applicability::HasPlaceholders,
);
}
}
// FIXME(compiler-errors): Support suggestions for other matching enum variants
_ => {}
}
}
// Target wrapper types - types that wrap or pretend to wrap another type,
// perhaps this inner type is meant to be called?
ty::AdtKind::Struct | ty::AdtKind::Union => {
let [first] = ***args else {
return;
};
let ty::GenericArgKind::Type(ty) = first.kind() else {
return;
};
let Ok(pick) = self.lookup_probe_for_diagnostic(
item_name,
ty,
call_expr,
ProbeScope::TraitsInScope,
None,
) else {
return;
};
let name = self.ty_to_value_string(actual);
let inner_id = kind.did();
let mutable = if let Some(AutorefOrPtrAdjustment::Autoref { mutbl, .. }) =
pick.autoref_or_ptr_adjustment
{
Some(mutbl)
} else {
None
};
if tcx.is_diagnostic_item(sym::LocalKey, inner_id) {
err.help("use `with` or `try_with` to access thread local storage");
} else if tcx.is_lang_item(kind.did(), LangItem::MaybeUninit) {
err.help(format!(
"if this `{name}` has been initialized, \
use one of the `assume_init` methods to access the inner value"
));
} else if tcx.is_diagnostic_item(sym::RefCell, inner_id) {
let (suggestion, borrow_kind, panic_if) = match mutable {
Some(Mutability::Not) => (".borrow()", "borrow", "a mutable borrow exists"),
Some(Mutability::Mut) => {
(".borrow_mut()", "mutably borrow", "any borrows exist")
}
None => return,
};
err.span_suggestion_verbose(
expr.span.shrink_to_hi(),
format!(
"use `{suggestion}` to {borrow_kind} the `{ty}`, \
panicking if {panic_if}"
),
suggestion,
Applicability::MaybeIncorrect,
);
} else if tcx.is_diagnostic_item(sym::Mutex, inner_id) {
err.span_suggestion_verbose(
expr.span.shrink_to_hi(),
format!(
"use `.lock().unwrap()` to borrow the `{ty}`, \
blocking the current thread until it can be acquired"
),
".lock().unwrap()",
Applicability::MaybeIncorrect,
);
} else if tcx.is_diagnostic_item(sym::RwLock, inner_id) {
let (suggestion, borrow_kind) = match mutable {
Some(Mutability::Not) => (".read().unwrap()", "borrow"),
Some(Mutability::Mut) => (".write().unwrap()", "mutably borrow"),
None => return,
};
err.span_suggestion_verbose(
expr.span.shrink_to_hi(),
format!(
"use `{suggestion}` to {borrow_kind} the `{ty}`, \
blocking the current thread until it can be acquired"
),
suggestion,
Applicability::MaybeIncorrect,
);
} else {
return;
};
err.span_note(
tcx.def_span(pick.item.def_id),
format!("the method `{item_name}` exists on the type `{ty}`"),
);
}
}
}

The bit about unwrapping Option/Result is actually really impactful for learners and the way you propose the attribute it should be able to do that sort of thing (minus the inline code suggestions, obviously).

So 💯 from me.

Let's go for a minimal version for now, like estebank said:

#[diagnostic::on_type_error(
    note = "text",
)]
struct S<T>(T);

I'm a bit worried about this thing being too powerful and showing up in places where authors didn't quite expect or intend. There are after all quite a few ways and places to get type errors.

I think the following semantics would be reasonably useful but also restrictive enough to not spook me or the lang people.

  • only note is supported (not message or label)
  • no filtering (on) of any kind, that's a whole design space we should get into elsewhere
  • the annotated ADT must have exactly one generic type parameter
  • if the annotated type (here, S) is found but the type parameter type (here, T) is expected, then the note is displayed.
  • let's also do that for "S has no method named foo but T does".

Then in the future, when we evolve the attribute forwards, we can specify something like "if you dont supply any filter then this wrapper kind of thing is all it can do".

@Unique-Usman
Copy link
Copy Markdown
Contributor Author

Did you two have a discussion about this beforehand? I'd like to read it.

that there are already several cases in the std of unconditional addition of notes during error reporting for given types

There was a discussion initally, it was through video meet though, but, nothing much more than whatever is here actually.

I presume you are talking about this?

I think the following semantics would be reasonably useful but also restrictive enough to not spook me or the lang people.

  • only note is supported (not message or label)
  • no filtering (on) of any kind, that's a whole design space we should get into elsewhere
  • the annotated ADT must have exactly one generic type parameter
  • if the annotated type (here, S) is found but the type parameter type (here, T) is expected, then the note is displayed.
  • let's also do that for "S has no method named foo but T does".

Then in the future, when we evolve the attribute forwards, we can specify something like "if you dont supply any filter then this wrapper kind of thing is all it can do".

Agreed.

Suggested-by: Esteban Küber <[email protected]>
Signed-off-by: Usman Akinyemi <[email protected]>
@Unique-Usman Unique-Usman force-pushed the ua/diagnostic_on_type_error branch from c6cbaa7 to 5b090d2 Compare April 16, 2026 09:36
@rust-log-analyzer
Copy link
Copy Markdown
Collaborator

The job tidy failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
fmt: checked 6791 files
tidy check
tidy [rustdoc_json (src)]: `rustdoc-json-types` modified, checking format version
tidy: Skipping binary file check, read-only filesystem
Expected a gate test for the feature 'diagnostic_on_type_error'.
Hint: create a failing test file named 'tests/ui/feature-gates/feature-gate-diagnostic-on-type-error.rs',
      with its failures due to missing usage of `#![feature(diagnostic_on_type_error)]`.
Hint: If you already have such a test and don't want to rename it,
      you can also add a // gate-test-diagnostic_on_type_error line to the test file.
tidy [features]: Found 1 features without a gate test.
tidy [features]: FAIL
removing old virtual environment
creating virtual environment at '/checkout/obj/build/venv' using 'python3.10' and 'venv'
creating virtual environment at '/checkout/obj/build/venv' using 'python3.10' and 'virtualenv'
Requirement already satisfied: pip in ./build/venv/lib/python3.10/site-packages (26.0.1)
linting python files
---
    ╭▸ compiler/rustc_feature/src/unstable.rs:479:64
    │
479 │     /// Allows giving custom types diagnostic messages on type erros
    ╰╴                                                               ━━━━━
rerun tidy with `--extra-checks=spellcheck --bless` to fix typos
tidy [extra_checks:spellcheck]: checks with external tool 'typos' failed
tidy [extra_checks:spellcheck]: FAIL
yarn install v1.22.22
warning package.json: No license field
warning ../../package.json: License should be a valid SPDX license expression
warning No license field
[1/4] Resolving packages...
---
linting javascript files
Running eslint on rustdoc JS files
info: ES-Check: there were no ES version matching errors!  🎉
typechecking javascript files
tidy: The following checks failed: extra_checks:spellcheck, features
Bootstrap failed while executing `test src/tools/tidy tidyselftest --extra-checks=py,cpp,js,spellcheck`
Command `/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools-bin/rust-tidy --root-path=/checkout --cargo-path=/checkout/obj/build/x86_64-unknown-linux-gnu/stage0/bin/cargo --output-dir=/checkout/obj/build --concurrency=4 --npm-path=/node/bin/yarn --ci=true --extra-checks=py,cpp,js,spellcheck` failed with exit code 1
Created at: src/bootstrap/src/core/build_steps/tool.rs:1618:23
Executed at: src/bootstrap/src/core/build_steps/test.rs:1417:29

--- BACKTRACE vvv
   0: <bootstrap::utils::exec::DeferredCommand>::finish_process
             at /checkout/src/bootstrap/src/utils/exec.rs:939:17
   1: <bootstrap::utils::exec::DeferredCommand>::wait_for_output::<&bootstrap::utils::exec::ExecutionContext>
             at /checkout/src/bootstrap/src/utils/exec.rs:831:21
   2: <bootstrap::utils::exec::ExecutionContext>::run
             at /checkout/src/bootstrap/src/utils/exec.rs:741:45
   3: <bootstrap::utils::exec::BootstrapCommand>::run::<&bootstrap::core::builder::Builder>
             at /checkout/src/bootstrap/src/utils/exec.rs:339:27
   4: <bootstrap::core::build_steps::test::Tidy as bootstrap::core::builder::Step>::run
             at /checkout/src/bootstrap/src/core/build_steps/test.rs:1417:29
   5: <bootstrap::core::builder::Builder>::ensure::<bootstrap::core::build_steps::test::Tidy>
             at /checkout/src/bootstrap/src/core/builder/mod.rs:1579:36
   6: <bootstrap::core::build_steps::test::Tidy as bootstrap::core::builder::Step>::make_run
             at /checkout/src/bootstrap/src/core/build_steps/test.rs:1339:21
   7: <bootstrap::core::builder::StepDescription>::maybe_run
             at /checkout/src/bootstrap/src/core/builder/mod.rs:476:13
   8: bootstrap::core::builder::cli_paths::match_paths_to_steps_and_run
             at /checkout/src/bootstrap/src/core/builder/cli_paths.rs:232:18
   9: <bootstrap::core::builder::Builder>::run_step_descriptions
             at /checkout/src/bootstrap/src/core/builder/mod.rs:1122:9
  10: <bootstrap::core::builder::Builder>::execute_cli
             at /checkout/src/bootstrap/src/core/builder/mod.rs:1101:14
  11: <bootstrap::Build>::build
             at /checkout/src/bootstrap/src/lib.rs:799:25
  12: bootstrap::main
             at /checkout/src/bootstrap/src/bin/main.rs:130:11
  13: <fn() as core::ops::function::FnOnce<()>>::call_once
             at /rustc/ad726b5063362ec9897ef3d67452fc5606ee70fa/library/core/src/ops/function.rs:250:5
  14: std::sys::backtrace::__rust_begin_short_backtrace::<fn(), ()>
             at /rustc/ad726b5063362ec9897ef3d67452fc5606ee70fa/library/std/src/sys/backtrace.rs:166:18
  15: std::rt::lang_start::<()>::{closure#0}
             at /rustc/ad726b5063362ec9897ef3d67452fc5606ee70fa/library/std/src/rt.rs:206:18
  16: <&dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync as core::ops::function::FnOnce<()>>::call_once
             at /rustc/ad726b5063362ec9897ef3d67452fc5606ee70fa/library/core/src/ops/function.rs:287:21
  17: std::panicking::catch_unwind::do_call::<&dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync, i32>
             at /rustc/ad726b5063362ec9897ef3d67452fc5606ee70fa/library/std/src/panicking.rs:581:40
  18: std::panicking::catch_unwind::<i32, &dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync>
             at /rustc/ad726b5063362ec9897ef3d67452fc5606ee70fa/library/std/src/panicking.rs:544:19
  19: std::panic::catch_unwind::<&dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync, i32>
             at /rustc/ad726b5063362ec9897ef3d67452fc5606ee70fa/library/std/src/panic.rs:359:14
  20: std::rt::lang_start_internal::{closure#0}
             at /rustc/ad726b5063362ec9897ef3d67452fc5606ee70fa/library/std/src/rt.rs:175:24
  21: std::panicking::catch_unwind::do_call::<std::rt::lang_start_internal::{closure#0}, isize>
             at /rustc/ad726b5063362ec9897ef3d67452fc5606ee70fa/library/std/src/panicking.rs:581:40
---
  28: __libc_start_main
  29: _start


Command has failed. Rerun with -v to see more details.
Build completed unsuccessfully in 0:02:26
  local time: Thu Apr 16 09:42:55 UTC 2026
  network time: Thu, 16 Apr 2026 09:42:55 GMT
##[error]Process completed with exit code 1.
##[group]Run echo "disk usage:"

@rust-bors
Copy link
Copy Markdown
Contributor

rust-bors bot commented Apr 16, 2026

☔ The latest upstream changes (presumably #155380) made this pull request unmergeable. Please resolve the merge conflicts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-attributes Area: Attributes (`#[…]`, `#![…]`) S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants