Skip to content

Commit 20399ee

Browse files
committed
Introduce a #[diagnostic::on_unknown_item] attribute
This PR introduces a `#[diagnostic::on_unknown_item]` attribute that allows crate authors to customize the error messages emitted by unresolved imports. The main usecase for this is using this attribute as part of a proc macro that expects a certain external module structure to exist or certain dependencies to be there. For me personally the motivating use-case are several derives in diesel, that expect to refer to a `tabe` module. That is done either implicitly (via the name of the type with the derive) or explicitly by the user. This attribute would allow us to improve the error message in both cases: * For the implicit case we could explicity call out our assumptions (turning the name into lower case, adding an `s` in the end) + point to the explicit variant as alternative * For the explicit variant we would add additional notes to tell the user why this is happening and what they should look for to fix the problem (be more explicit about certain diesel specific assumptions of the module structure) I assume that similar use-cases exist for other proc-macros as well, therefore I decided to put in the work implementing this new attribute. I would also assume that this is likely not useful for std-lib internal usage.
1 parent a26e97b commit 20399ee

File tree

13 files changed

+419
-8
lines changed

13 files changed

+419
-8
lines changed

compiler/rustc_feature/src/builtin_attrs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1193,7 +1193,7 @@ pub static BUILTIN_ATTRIBUTE_MAP: LazyLock<FxHashMap<Symbol, &BuiltinAttribute>>
11931193

11941194
pub fn is_stable_diagnostic_attribute(sym: Symbol, _features: &Features) -> bool {
11951195
match sym {
1196-
sym::on_unimplemented | sym::do_not_recommend => true,
1196+
sym::on_unimplemented | sym::do_not_recommend | sym::on_unknown_item => true,
11971197
_ => false,
11981198
}
11991199
}

compiler/rustc_passes/messages.ftl

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ passes_deprecated_attribute =
152152
passes_diagnostic_diagnostic_on_unimplemented_only_for_traits =
153153
`#[diagnostic::on_unimplemented]` can only be applied to trait definitions
154154
155+
passes_diagnostic_diagnostic_on_unknown_item_only_for_imports =
156+
`#[diagnostic::on_unknown_item]` can only be applied to `use` statements
157+
155158
passes_diagnostic_item_first_defined =
156159
the diagnostic item is first defined here
157160
@@ -356,6 +359,11 @@ passes_ignored_derived_impls =
356359
*[other] traits {$trait_list}, but these are
357360
} intentionally ignored during dead code analysis
358361
362+
passes_ignored_diagnostic_option = `{$option_name}` is ignored due to previous definition of `{$option_name}`
363+
.other_label = `{$option_name}` is first declared here
364+
.label = `{$option_name}` is already declared here
365+
.help = consider removing the second `{$option_name}` as it is ignored anyway
366+
359367
passes_implied_feature_not_exist =
360368
feature `{$implied_by}` implying `{$feature}` does not exist
361369
@@ -474,6 +482,11 @@ passes_macro_export_on_decl_macro =
474482
passes_macro_use =
475483
`#[{$name}]` only has an effect on `extern crate` and modules
476484
485+
passes_malformed_on_unknown_item_attr =
486+
malformed `#[diagnostic::on_unknown_item]` attribute
487+
.label = the `#[diagnostic::on_unknown_item]` attribute expects at least one option
488+
.help = at least one of the following options is required: `message`, `label` or `note`
489+
477490
passes_may_dangle =
478491
`#[may_dangle]` must be applied to a lifetime or type generic parameter in `Drop` impl
479492
@@ -752,6 +765,11 @@ passes_unknown_lang_item =
752765
definition of an unknown lang item: `{$name}`
753766
.label = definition of unknown lang item `{$name}`
754767
768+
passes_unknown_option_for_on_unknown_item =
769+
unknown `{$option_name}` option for `#[diagnostic::on_unknown_item]` attribute
770+
.help = only `message`, `note` and `label` are allowed as options
771+
.label = invalid option found here
772+
755773
passes_unlabeled_cf_in_while_condition =
756774
`break` or `continue` with no label in the condition of a `while` loop
757775
.label = unlabeled `{$cf_type}` in the condition of a `while` loop

compiler/rustc_passes/src/check_attr.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,38 @@ use crate::{errors, fluent_generated as fluent};
4444
#[diag(passes_diagnostic_diagnostic_on_unimplemented_only_for_traits)]
4545
struct DiagnosticOnUnimplementedOnlyForTraits;
4646

47+
#[derive(LintDiagnostic)]
48+
#[diag(passes_diagnostic_diagnostic_on_unknown_item_only_for_imports)]
49+
struct DiagnosticOnUnknownItemOnlyForImports;
50+
51+
#[derive(LintDiagnostic)]
52+
#[diag(passes_malformed_on_unknown_item_attr)]
53+
#[help]
54+
pub(crate) struct MalformedOnUnknownItemAttr {
55+
#[label]
56+
pub span: Span,
57+
}
58+
59+
#[derive(LintDiagnostic)]
60+
#[diag(passes_unknown_option_for_on_unknown_item)]
61+
#[help]
62+
pub(crate) struct UnknownOptionForOnUnknownItemAttr {
63+
pub option_name: String,
64+
#[label]
65+
pub span: Span,
66+
}
67+
68+
#[derive(LintDiagnostic)]
69+
#[diag(passes_ignored_diagnostic_option)]
70+
#[help]
71+
pub(crate) struct IgnoredDiagnositcOption {
72+
pub option_name: &'static str,
73+
#[label]
74+
pub span: Span,
75+
#[label(passes_other_label)]
76+
pub prev_span: Span,
77+
}
78+
4779
fn target_from_impl_item<'tcx>(tcx: TyCtxt<'tcx>, impl_item: &hir::ImplItem<'_>) -> Target {
4880
match impl_item.kind {
4981
hir::ImplItemKind::Const(..) => Target::AssocConst,
@@ -120,6 +152,9 @@ impl<'tcx> CheckAttrVisitor<'tcx> {
120152
[sym::diagnostic, sym::on_unimplemented, ..] => {
121153
self.check_diagnostic_on_unimplemented(attr.span, hir_id, target)
122154
}
155+
[sym::diagnostic, sym::on_unknown_item, ..] => {
156+
self.check_diagnostic_on_unknown_item(attr.span, hir_id, target, attr)
157+
}
123158
[sym::inline, ..] => self.check_inline(hir_id, attr, span, target),
124159
[sym::coverage, ..] => self.check_coverage(attr, span, target),
125160
[sym::optimize, ..] => self.check_optimize(hir_id, attr, span, target),
@@ -393,6 +428,78 @@ impl<'tcx> CheckAttrVisitor<'tcx> {
393428
}
394429
}
395430

431+
/// Checks if `#[diagnostic::on_unknown_item]` is applied to an import definition
432+
fn check_diagnostic_on_unknown_item(
433+
&self,
434+
attr_span: Span,
435+
hir_id: HirId,
436+
target: Target,
437+
attr: &Attribute,
438+
) {
439+
if !matches!(target, Target::Use) {
440+
self.tcx.emit_node_span_lint(
441+
UNKNOWN_OR_MALFORMED_DIAGNOSTIC_ATTRIBUTES,
442+
hir_id,
443+
attr_span,
444+
DiagnosticOnUnknownItemOnlyForImports,
445+
);
446+
}
447+
if let Some(meta) = attr.meta_item_list() {
448+
let mut message = None;
449+
let mut label = None;
450+
for item in meta {
451+
if item.has_name(sym::message) {
452+
if let Some(message_span) = message {
453+
self.tcx.emit_node_span_lint(
454+
UNKNOWN_OR_MALFORMED_DIAGNOSTIC_ATTRIBUTES,
455+
hir_id,
456+
item.span(),
457+
IgnoredDiagnositcOption {
458+
option_name: "message",
459+
span: item.span(),
460+
prev_span: message_span,
461+
},
462+
);
463+
}
464+
message = Some(item.span());
465+
} else if item.has_name(sym::label) {
466+
if let Some(label_span) = label {
467+
self.tcx.emit_node_span_lint(
468+
UNKNOWN_OR_MALFORMED_DIAGNOSTIC_ATTRIBUTES,
469+
hir_id,
470+
item.span(),
471+
IgnoredDiagnositcOption {
472+
option_name: "label",
473+
span: item.span(),
474+
prev_span: label_span,
475+
},
476+
);
477+
}
478+
label = Some(item.span());
479+
} else if item.has_name(sym::note) {
480+
// accept any number of notes
481+
} else {
482+
self.tcx.emit_node_span_lint(
483+
UNKNOWN_OR_MALFORMED_DIAGNOSTIC_ATTRIBUTES,
484+
hir_id,
485+
item.span(),
486+
UnknownOptionForOnUnknownItemAttr {
487+
option_name: item.name_or_empty().to_string(),
488+
span: item.span(),
489+
},
490+
);
491+
}
492+
}
493+
} else {
494+
self.tcx.emit_node_span_lint(
495+
UNKNOWN_OR_MALFORMED_DIAGNOSTIC_ATTRIBUTES,
496+
hir_id,
497+
attr_span,
498+
MalformedOnUnknownItemAttr { span: attr_span },
499+
);
500+
}
501+
}
502+
396503
/// Checks if an `#[inline]` is applied to a function or a closure.
397504
fn check_inline(&self, hir_id: HirId, attr: &Attribute, span: Span, target: Target) {
398505
match target {

compiler/rustc_resolve/src/build_reduced_graph.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use tracing::debug;
2828

2929
use crate::Namespace::{MacroNS, TypeNS, ValueNS};
3030
use crate::def_collector::collect_definitions;
31-
use crate::imports::{ImportData, ImportKind};
31+
use crate::imports::{ImportData, ImportKind, OnUnknownItemData};
3232
use crate::macros::{MacroRulesBinding, MacroRulesScope, MacroRulesScopeRef};
3333
use crate::{
3434
BindingKey, Determinacy, ExternPreludeEntry, Finalize, MacroData, Module, ModuleKind,
@@ -446,6 +446,7 @@ impl<'a, 'ra, 'tcx> BuildReducedGraphVisitor<'a, 'ra, 'tcx> {
446446
root_span,
447447
root_id,
448448
vis,
449+
on_unknown_item_attr: OnUnknownItemData::from_attrs(&item.attrs),
449450
});
450451

451452
self.r.indeterminate_imports.push(import);
@@ -939,6 +940,7 @@ impl<'a, 'ra, 'tcx> BuildReducedGraphVisitor<'a, 'ra, 'tcx> {
939940
span: item.span,
940941
module_path: Vec::new(),
941942
vis,
943+
on_unknown_item_attr: OnUnknownItemData::from_attrs(&item.attrs),
942944
});
943945
if used {
944946
self.r.import_use_map.insert(import, Used::Other);
@@ -1071,7 +1073,7 @@ impl<'a, 'ra, 'tcx> BuildReducedGraphVisitor<'a, 'ra, 'tcx> {
10711073
}
10721074
}
10731075

1074-
let macro_use_import = |this: &Self, span, warn_private| {
1076+
let macro_use_import = |this: &mut Self, span, warn_private| {
10751077
this.r.arenas.alloc_import(ImportData {
10761078
kind: ImportKind::MacroUse { warn_private },
10771079
root_id: item.id,
@@ -1084,6 +1086,7 @@ impl<'a, 'ra, 'tcx> BuildReducedGraphVisitor<'a, 'ra, 'tcx> {
10841086
span,
10851087
module_path: Vec::new(),
10861088
vis: ty::Visibility::Restricted(CRATE_DEF_ID),
1089+
on_unknown_item_attr: OnUnknownItemData::from_attrs(&item.attrs),
10871090
})
10881091
};
10891092

@@ -1252,6 +1255,7 @@ impl<'a, 'ra, 'tcx> BuildReducedGraphVisitor<'a, 'ra, 'tcx> {
12521255
span,
12531256
module_path: Vec::new(),
12541257
vis,
1258+
on_unknown_item_attr: OnUnknownItemData::from_attrs(&item.attrs),
12551259
});
12561260
self.r.import_use_map.insert(import, Used::Other);
12571261
let import_binding = self.r.import(binding, import);

compiler/rustc_resolve/src/imports.rs

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use std::cell::Cell;
44
use std::mem;
55

6+
use rustc_ast as ast;
67
use rustc_ast::NodeId;
78
use rustc_data_structures::fx::FxHashSet;
89
use rustc_data_structures::intern::Interned;
@@ -140,6 +141,57 @@ impl<'ra> std::fmt::Debug for ImportKind<'ra> {
140141
}
141142
}
142143

144+
#[derive(Debug, Clone, Default)]
145+
pub(crate) struct OnUnknownItemData {
146+
pub(crate) message: Option<String>,
147+
pub(crate) label: Option<String>,
148+
pub(crate) notes: Option<Vec<String>>,
149+
}
150+
151+
impl OnUnknownItemData {
152+
pub(crate) fn from_attrs(attrs: &[ast::Attribute]) -> Option<OnUnknownItemData> {
153+
// the attribute syntax is checked in the check_attr
154+
// ast pass, so we just consume any valid
155+
// options here and ignore everything else
156+
let mut out = OnUnknownItemData::default();
157+
for attr in
158+
attrs.iter().filter(|a| a.path_matches(&[sym::diagnostic, sym::on_unknown_item]))
159+
{
160+
if let Some(meta) = attr.meta_item_list() {
161+
for item in meta {
162+
if item.has_name(sym::message) {
163+
if out.message.is_none()
164+
&& let Some(message) = item.value_str()
165+
{
166+
out.message = Some(message.as_str().to_owned());
167+
}
168+
} else if item.has_name(sym::label) {
169+
if out.label.is_none()
170+
&& let Some(label) = item.value_str()
171+
{
172+
out.label = Some(label.as_str().to_owned());
173+
}
174+
} else if item.has_name(sym::note) {
175+
if let Some(note) = item.value_str() {
176+
out.notes = Some(out.notes.unwrap_or_default());
177+
out.notes
178+
.as_mut()
179+
.expect("We initialized it above")
180+
.push(note.as_str().to_owned());
181+
}
182+
}
183+
}
184+
}
185+
}
186+
187+
if out.message.is_none() && out.label.is_none() && out.notes.is_none() {
188+
None
189+
} else {
190+
Some(out)
191+
}
192+
}
193+
}
194+
143195
/// One import.
144196
#[derive(Debug, Clone)]
145197
pub(crate) struct ImportData<'ra> {
@@ -176,6 +228,8 @@ pub(crate) struct ImportData<'ra> {
176228
/// The resolution of `module_path`.
177229
pub imported_module: Cell<Option<ModuleOrUniformRoot<'ra>>>,
178230
pub vis: ty::Visibility,
231+
232+
pub on_unknown_item_attr: Option<OnUnknownItemData>,
179233
}
180234

181235
/// All imports are unique and allocated on a same arena,
@@ -251,6 +305,7 @@ struct UnresolvedImportError {
251305
segment: Option<Symbol>,
252306
/// comes from `PathRes::Failed { module }`
253307
module: Option<DefId>,
308+
on_unknown_item_attr: Option<OnUnknownItemData>,
254309
}
255310

256311
// Reexports of the form `pub use foo as bar;` where `foo` is `extern crate foo;`
@@ -589,6 +644,7 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
589644
candidates: None,
590645
segment: None,
591646
module: None,
647+
on_unknown_item_attr: import.on_unknown_item_attr.clone(),
592648
};
593649
errors.push((*import, err))
594650
}
@@ -693,16 +749,34 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
693749
format!("`{path}`")
694750
})
695751
.collect::<Vec<_>>();
696-
let msg = format!("unresolved import{} {}", pluralize!(paths.len()), paths.join(", "),);
752+
let msg = if errors.len() == 1
753+
&& let Some(message) =
754+
errors[0].1.on_unknown_item_attr.as_mut().and_then(|a| a.message.take())
755+
{
756+
message
757+
} else {
758+
format!("unresolved import{} {}", pluralize!(paths.len()), paths.join(", "),)
759+
};
697760

698761
let mut diag = struct_span_code_err!(self.dcx(), span, E0432, "{msg}");
699762

700-
if let Some((_, UnresolvedImportError { note: Some(note), .. })) = errors.iter().last() {
763+
if errors.len() == 1
764+
&& let Some(notes) =
765+
errors[0].1.on_unknown_item_attr.as_mut().and_then(|a| a.notes.take())
766+
{
767+
for note in notes {
768+
diag.note(note);
769+
}
770+
} else if let Some((_, UnresolvedImportError { note: Some(note), .. })) =
771+
errors.iter().last()
772+
{
701773
diag.note(note.clone());
702774
}
703775

704-
for (import, err) in errors.into_iter().take(MAX_LABEL_COUNT) {
705-
if let Some(label) = err.label {
776+
for (import, mut err) in errors.into_iter().take(MAX_LABEL_COUNT) {
777+
if let Some(label) =
778+
err.on_unknown_item_attr.as_mut().and_then(|a| a.label.take()).or(err.label)
779+
{
706780
diag.span_label(err.span, label);
707781
}
708782

@@ -956,6 +1030,7 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
9561030
candidates: None,
9571031
segment: Some(segment_name),
9581032
module,
1033+
on_unknown_item_attr: import.on_unknown_item_attr.clone(),
9591034
},
9601035
None => UnresolvedImportError {
9611036
span,
@@ -965,6 +1040,7 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
9651040
candidates: None,
9661041
segment: Some(segment_name),
9671042
module,
1043+
on_unknown_item_attr: import.on_unknown_item_attr.clone(),
9681044
},
9691045
};
9701046
return Some(err);
@@ -1014,6 +1090,7 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
10141090
candidates: None,
10151091
segment: None,
10161092
module: None,
1093+
on_unknown_item_attr: import.on_unknown_item_attr.clone(),
10171094
});
10181095
}
10191096
if !is_prelude
@@ -1229,6 +1306,7 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
12291306
}
12301307
}),
12311308
segment: Some(ident.name),
1309+
on_unknown_item_attr: import.on_unknown_item_attr.clone(),
12321310
})
12331311
} else {
12341312
// `resolve_ident_in_module` reported a privacy error.

0 commit comments

Comments
 (0)