Skip to content

Commit c87fc9e

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 71e0027 commit c87fc9e

17 files changed

Lines changed: 547 additions & 13 deletions

compiler/rustc_feature/src/builtin_attrs.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1585,6 +1585,7 @@ pub fn is_stable_diagnostic_attribute(sym: Symbol, features: &Features) -> bool
15851585
match sym {
15861586
sym::on_unimplemented | sym::do_not_recommend => true,
15871587
sym::on_const => features.diagnostic_on_const(),
1588+
sym::on_unknown_item => features.diagnostic_on_unknown_item(),
15881589
_ => false,
15891590
}
15901591
}

compiler/rustc_feature/src/unstable.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,8 @@ declare_features! (
464464
(unstable, derive_from, "1.91.0", Some(144889)),
465465
/// Allows giving non-const impls custom diagnostic messages if attempted to be used as const
466466
(unstable, diagnostic_on_const, "1.93.0", Some(143874)),
467+
/// Allows giving unresolved imports a custom diagnostic message
468+
(unstable, diagnostic_on_unknown_item, "CURRENT_RUSTC_VERSION", Some(152900)),
467469
/// Allows `#[doc(cfg(...))]`.
468470
(unstable, doc_cfg, "1.21.0", Some(43781)),
469471
/// Allows `#[doc(masked)]`.

compiler/rustc_passes/src/check_attr.rs

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ use rustc_middle::{bug, span_bug};
4343
use rustc_session::config::CrateType;
4444
use rustc_session::lint;
4545
use rustc_session::lint::builtin::{
46-
CONFLICTING_REPR_HINTS, INVALID_DOC_ATTRIBUTES, MISPLACED_DIAGNOSTIC_ATTRIBUTES,
47-
UNUSED_ATTRIBUTES,
46+
CONFLICTING_REPR_HINTS, INVALID_DOC_ATTRIBUTES, MALFORMED_DIAGNOSTIC_ATTRIBUTES,
47+
MISPLACED_DIAGNOSTIC_ATTRIBUTES, UNUSED_ATTRIBUTES,
4848
};
4949
use rustc_session::parse::feature_err;
5050
use rustc_span::edition::Edition;
@@ -74,6 +74,38 @@ struct DiagnosticOnConstOnlyForNonConstTraitImpls {
7474
item_span: Span,
7575
}
7676

77+
#[derive(LintDiagnostic)]
78+
#[diag("`#[diagnostic::on_unknown_item]` can only be applied to `use` statements")]
79+
struct DiagnosticOnUnknownItemOnlyForImports;
80+
81+
#[derive(LintDiagnostic)]
82+
#[diag("malformed `#[diagnostic::on_unknown_item]` attribute")]
83+
#[help("at least one of the following options is required: `message`, `label` or `note`")]
84+
pub(crate) struct MalformedOnUnknownItemAttr {
85+
#[label("the `#[diagnostic::on_unknown_item]` attribute expects at least one option")]
86+
pub span: Span,
87+
}
88+
89+
#[derive(LintDiagnostic)]
90+
#[diag("`{$option_name}` is ignored due to previous definition of `{$option_name}`")]
91+
#[help("consider removing the second `{$option_name}` as it is ignored anyway")]
92+
pub(crate) struct IgnoredDiagnosticOption {
93+
pub option_name: &'static str,
94+
#[label("`{$option_name}` is already declared here")]
95+
pub span: Span,
96+
#[label("`{$option_name}` is first declared here")]
97+
pub prev_span: Span,
98+
}
99+
100+
#[derive(LintDiagnostic)]
101+
#[diag("unknown option `{$option_name}` for the `#[diagnostic::on_unknown_item]` attribute")]
102+
#[help("only `message`, `note` and `label` are allowed as options")]
103+
pub(crate) struct UnknownOptionForOnUnknownItemAttr {
104+
pub option_name: String,
105+
#[label("`{$option_name}` is an invalid option")]
106+
pub span: Span,
107+
}
108+
77109
fn target_from_impl_item<'tcx>(tcx: TyCtxt<'tcx>, impl_item: &hir::ImplItem<'_>) -> Target {
78110
match impl_item.kind {
79111
hir::ImplItemKind::Const(..) => Target::AssocConst,
@@ -388,6 +420,9 @@ impl<'tcx> CheckAttrVisitor<'tcx> {
388420
[sym::diagnostic, sym::on_const, ..] => {
389421
self.check_diagnostic_on_const(attr.span(), hir_id, target, item)
390422
}
423+
[sym::diagnostic, sym::on_unknown_item, ..] => {
424+
self.check_diagnostic_on_unknown_item(attr.span(), hir_id, target, attr)
425+
}
391426
[sym::autodiff_forward, ..] | [sym::autodiff_reverse, ..] => {
392427
self.check_autodiff(hir_id, attr, span, target)
393428
}
@@ -619,6 +654,78 @@ impl<'tcx> CheckAttrVisitor<'tcx> {
619654
}
620655
}
621656

657+
/// Checks if `#[diagnostic::on_unknown_item]` is applied to an import definition
658+
fn check_diagnostic_on_unknown_item(
659+
&self,
660+
attr_span: Span,
661+
hir_id: HirId,
662+
target: Target,
663+
attr: &Attribute,
664+
) {
665+
if !matches!(target, Target::Use) {
666+
self.tcx.emit_node_span_lint(
667+
MISPLACED_DIAGNOSTIC_ATTRIBUTES,
668+
hir_id,
669+
attr_span,
670+
DiagnosticOnUnknownItemOnlyForImports,
671+
);
672+
}
673+
if let Some(meta) = attr.meta_item_list() {
674+
let mut message = None;
675+
let mut label = None;
676+
for item in meta {
677+
if item.has_name(sym::message) {
678+
if let Some(message_span) = message {
679+
self.tcx.emit_node_span_lint(
680+
MALFORMED_DIAGNOSTIC_ATTRIBUTES,
681+
hir_id,
682+
item.span(),
683+
IgnoredDiagnosticOption {
684+
option_name: "message",
685+
span: item.span(),
686+
prev_span: message_span,
687+
},
688+
);
689+
}
690+
message = Some(item.span());
691+
} else if item.has_name(sym::label) {
692+
if let Some(label_span) = label {
693+
self.tcx.emit_node_span_lint(
694+
MALFORMED_DIAGNOSTIC_ATTRIBUTES,
695+
hir_id,
696+
item.span(),
697+
IgnoredDiagnosticOption {
698+
option_name: "label",
699+
span: item.span(),
700+
prev_span: label_span,
701+
},
702+
);
703+
}
704+
label = Some(item.span());
705+
} else if item.has_name(sym::note) {
706+
// accept any number of notes
707+
} else {
708+
self.tcx.emit_node_span_lint(
709+
MALFORMED_DIAGNOSTIC_ATTRIBUTES,
710+
hir_id,
711+
item.span(),
712+
UnknownOptionForOnUnknownItemAttr {
713+
option_name: item.name().map(|s| s.to_string()).unwrap_or_default(),
714+
span: item.span(),
715+
},
716+
);
717+
}
718+
}
719+
} else {
720+
self.tcx.emit_node_span_lint(
721+
MALFORMED_DIAGNOSTIC_ATTRIBUTES,
722+
hir_id,
723+
attr_span,
724+
MalformedOnUnknownItemAttr { span: attr_span },
725+
);
726+
}
727+
}
728+
622729
/// Checks if `#[diagnostic::on_const]` is applied to a trait impl
623730
fn check_diagnostic_on_const(
624731
&self,

compiler/rustc_resolve/src/build_reduced_graph.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use tracing::debug;
3232

3333
use crate::Namespace::{MacroNS, TypeNS, ValueNS};
3434
use crate::def_collector::collect_definitions;
35-
use crate::imports::{ImportData, ImportKind};
35+
use crate::imports::{ImportData, ImportKind, OnUnknownItemData};
3636
use crate::macros::{MacroRulesDecl, MacroRulesScope, MacroRulesScopeRef};
3737
use crate::ref_mut::CmCell;
3838
use crate::{
@@ -538,6 +538,7 @@ impl<'a, 'ra, 'tcx> BuildReducedGraphVisitor<'a, 'ra, 'tcx> {
538538
root_id,
539539
vis,
540540
vis_span: item.vis.span,
541+
on_unknown_item_attr: OnUnknownItemData::from_attrs(&item.attrs),
541542
});
542543

543544
self.r.indeterminate_imports.push(import);
@@ -1035,6 +1036,7 @@ impl<'a, 'ra, 'tcx> BuildReducedGraphVisitor<'a, 'ra, 'tcx> {
10351036
module_path: Vec::new(),
10361037
vis,
10371038
vis_span: item.vis.span,
1039+
on_unknown_item_attr: OnUnknownItemData::from_attrs(&item.attrs),
10381040
});
10391041
if used {
10401042
self.r.import_use_map.insert(import, Used::Other);
@@ -1167,6 +1169,7 @@ impl<'a, 'ra, 'tcx> BuildReducedGraphVisitor<'a, 'ra, 'tcx> {
11671169
module_path: Vec::new(),
11681170
vis: Visibility::Restricted(CRATE_DEF_ID),
11691171
vis_span: item.vis.span,
1172+
on_unknown_item_attr: OnUnknownItemData::from_attrs(&item.attrs),
11701173
})
11711174
};
11721175

@@ -1338,6 +1341,7 @@ impl<'a, 'ra, 'tcx> BuildReducedGraphVisitor<'a, 'ra, 'tcx> {
13381341
module_path: Vec::new(),
13391342
vis,
13401343
vis_span: item.vis.span,
1344+
on_unknown_item_attr: OnUnknownItemData::from_attrs(&item.attrs),
13411345
});
13421346
self.r.import_use_map.insert(import, Used::Other);
13431347
let import_decl = self.r.new_import_decl(decl, import);

compiler/rustc_resolve/src/imports.rs

Lines changed: 95 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use std::mem;
44

5-
use rustc_ast::NodeId;
5+
use rustc_ast::{NodeId, ast};
66
use rustc_data_structures::fx::{FxHashSet, FxIndexSet};
77
use rustc_data_structures::intern::Interned;
88
use rustc_errors::codes::*;
@@ -140,6 +140,57 @@ impl<'ra> std::fmt::Debug for ImportKind<'ra> {
140140
}
141141
}
142142

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

187238
/// Span of the visibility.
188239
pub vis_span: Span,
240+
241+
/// A `#[diagnostic::on_unknown_item]` attribute applied
242+
/// to the given import. This allows crates to specify
243+
/// custom error messages for a specific import
244+
pub on_unknown_item_attr: Option<OnUnknownItemData>,
189245
}
190246

191247
/// All imports are unique and allocated on a same arena,
@@ -284,6 +340,7 @@ struct UnresolvedImportError {
284340
segment: Option<Symbol>,
285341
/// comes from `PathRes::Failed { module }`
286342
module: Option<DefId>,
343+
on_unknown_item_attr: Option<OnUnknownItemData>,
287344
}
288345

289346
// Reexports of the form `pub use foo as bar;` where `foo` is `extern crate foo;`
@@ -693,6 +750,7 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
693750
candidates: None,
694751
segment: None,
695752
module: None,
753+
on_unknown_item_attr: import.on_unknown_item_attr.clone(),
696754
};
697755
errors.push((*import, err))
698756
}
@@ -815,19 +873,45 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
815873
format!("`{path}`")
816874
})
817875
.collect::<Vec<_>>();
818-
let msg = format!("unresolved import{} {}", pluralize!(paths.len()), paths.join(", "),);
819-
820-
let mut diag = struct_span_code_err!(self.dcx(), span, E0432, "{msg}");
876+
let default_message =
877+
format!("unresolved import{} {}", pluralize!(paths.len()), paths.join(", "),);
878+
let mut diag = if self.tcx.features().diagnostic_on_unknown_item()
879+
&& let Some(message) =
880+
errors[0].1.on_unknown_item_attr.as_mut().and_then(|a| a.message.take())
881+
{
882+
let mut diag = struct_span_code_err!(self.dcx(), span, E0432, "{message}");
883+
diag.note(default_message);
884+
diag
885+
} else {
886+
struct_span_code_err!(self.dcx(), span, E0432, "{default_message}")
887+
};
821888

822-
if let Some((_, UnresolvedImportError { note: Some(note), .. })) = errors.iter().last() {
889+
if self.tcx.features().diagnostic_on_unknown_item()
890+
&& let Some(notes) =
891+
errors[0].1.on_unknown_item_attr.as_mut().and_then(|a| a.notes.take())
892+
{
893+
for note in notes {
894+
diag.note(note);
895+
}
896+
} else if let Some((_, UnresolvedImportError { note: Some(note), .. })) =
897+
errors.iter().last()
898+
{
823899
diag.note(note.clone());
824900
}
825901

826902
/// Upper limit on the number of `span_label` messages.
827903
const MAX_LABEL_COUNT: usize = 10;
828904

829-
for (import, err) in errors.into_iter().take(MAX_LABEL_COUNT) {
830-
if let Some(label) = err.label {
905+
for (import, mut err) in errors.into_iter().take(MAX_LABEL_COUNT) {
906+
if self.tcx.features().diagnostic_on_unknown_item()
907+
&& let Some(label) = err
908+
.on_unknown_item_attr
909+
.as_mut()
910+
.and_then(|a| a.label.take())
911+
.or(err.label.clone())
912+
{
913+
diag.span_label(err.span, label);
914+
} else if let Some(label) = err.label {
831915
diag.span_label(err.span, label);
832916
}
833917

@@ -1088,6 +1172,7 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
10881172
candidates: None,
10891173
segment: Some(segment_name),
10901174
module,
1175+
on_unknown_item_attr: import.on_unknown_item_attr.clone(),
10911176
},
10921177
None => UnresolvedImportError {
10931178
span,
@@ -1097,6 +1182,7 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
10971182
candidates: None,
10981183
segment: Some(segment_name),
10991184
module,
1185+
on_unknown_item_attr: import.on_unknown_item_attr.clone(),
11001186
},
11011187
};
11021188
return Some(err);
@@ -1139,6 +1225,7 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
11391225
candidates: None,
11401226
segment: None,
11411227
module: None,
1228+
on_unknown_item_attr: None,
11421229
});
11431230
}
11441231
if let Some(max_vis) = max_vis.get()
@@ -1339,7 +1426,6 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
13391426

13401427
let parent_suggestion =
13411428
self.lookup_import_candidates(ident, TypeNS, &import.parent_scope, |_| true);
1342-
13431429
Some(UnresolvedImportError {
13441430
span: import.span,
13451431
label: Some(label),
@@ -1358,6 +1444,7 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
13581444
}
13591445
}),
13601446
segment: Some(ident.name),
1447+
on_unknown_item_attr: import.on_unknown_item_attr.clone(),
13611448
})
13621449
} else {
13631450
// `resolve_ident_in_module` reported a privacy error.

0 commit comments

Comments
 (0)