-
Notifications
You must be signed in to change notification settings - Fork 12.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Look at proc-macro attributes when encountering unknown attribute #109278
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,7 @@ use rustc_ast::ptr::P; | |
use rustc_ast::visit::{self, Visitor}; | ||
use rustc_ast::{self as ast, Crate, ItemKind, ModKind, NodeId, Path, CRATE_NODE_ID}; | ||
use rustc_ast_pretty::pprust; | ||
use rustc_data_structures::fx::FxHashSet; | ||
use rustc_data_structures::fx::{FxHashMap, FxHashSet}; | ||
use rustc_errors::{ | ||
pluralize, Applicability, Diagnostic, DiagnosticBuilder, ErrorGuaranteed, MultiSpan, | ||
}; | ||
|
@@ -1016,6 +1016,7 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> { | |
parent_scope, | ||
false, | ||
false, | ||
None, | ||
) { | ||
suggestions.extend( | ||
ext.helper_attrs | ||
|
@@ -1115,6 +1116,83 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> { | |
} | ||
} | ||
|
||
/// Visit every module reachable from `start_module` and bring any proc and derive macro | ||
/// encountered into `self.macro_map` to be used for suggestions. | ||
fn lookup_macros_from_module(&mut self) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How is this different from Only macros that would be suggested as imports (and their helper attributes) can be suggested here. Same for typo suggestions, in addition for checking any visited item for being typo-suggestable we also need to check its helper attributes for the same thing, as a part of the regular There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest landing the core change first - extending the set of regular import and typo suggestions with helper attributes, and leave all the beautifications for later. |
||
let parent_scope = ParentScope::module(self.graph_root, self); | ||
let mut seen_modules = FxHashSet::default(); | ||
let mut worklist = vec![(self.graph_root, ThinVec::<ast::PathSegment>::new(), true)]; | ||
let mut worklist_via_import = vec![]; | ||
|
||
while let Some((in_module, path_segments, accessible)) = match worklist.pop() { | ||
None => worklist_via_import.pop(), | ||
Some(x) => Some(x), | ||
} { | ||
let in_module_is_extern = !in_module.opt_def_id().map_or(false, |id| id.is_local()); | ||
// We have to visit module children in deterministic order to avoid | ||
// instabilities in reported imports (#43552). | ||
in_module.for_each_child(self, |this, ident, _ns, name_binding| { | ||
if let Res::Def(DefKind::Macro(MacroKind::Derive | MacroKind::Attr), def_id) = | ||
name_binding.res() | ||
{ | ||
// By doing this all *imported* macros get added to the `macro_map` even if they | ||
// are *unused*, which makes the later suggestions find them and work. | ||
let _ = this.get_macro_by_def_id(def_id); | ||
} | ||
// avoid non-importable candidates | ||
if !name_binding.is_importable() { | ||
return; | ||
} | ||
|
||
let child_accessible = | ||
accessible && this.is_accessible_from(name_binding.vis, parent_scope.module); | ||
|
||
// do not venture inside inaccessible items of other crates | ||
if in_module_is_extern && !child_accessible { | ||
return; | ||
} | ||
|
||
let via_import = name_binding.is_import() && !name_binding.is_extern_crate(); | ||
|
||
// There is an assumption elsewhere that paths of variants are in the enum's | ||
// declaration and not imported. With this assumption, the variant component is | ||
// chopped and the rest of the path is assumed to be the enum's own path. For | ||
// errors where a variant is used as the type instead of the enum, this causes | ||
// funny looking invalid suggestions, i.e `foo` instead of `foo::MyEnum`. | ||
if via_import && name_binding.is_possibly_imported_variant() { | ||
return; | ||
} | ||
|
||
// #90113: Do not count an inaccessible reexported item as a candidate. | ||
if let NameBindingKind::Import { binding, .. } = name_binding.kind { | ||
if this.is_accessible_from(binding.vis, parent_scope.module) | ||
&& !this.is_accessible_from(name_binding.vis, parent_scope.module) | ||
{ | ||
return; | ||
} | ||
} | ||
|
||
// collect submodules to explore | ||
if let Some(module) = name_binding.module() { | ||
// form the path | ||
let mut path_segments = path_segments.clone(); | ||
path_segments.push(ast::PathSegment::from_ident(ident)); | ||
|
||
let is_extern_crate_that_also_appears_in_prelude = | ||
name_binding.is_extern_crate(); | ||
|
||
if !is_extern_crate_that_also_appears_in_prelude { | ||
// add the module to the lookup | ||
if seen_modules.insert(module.def_id()) { | ||
if via_import { &mut worklist_via_import } else { &mut worklist } | ||
.push((module, path_segments, child_accessible)); | ||
} | ||
} | ||
} | ||
}) | ||
} | ||
} | ||
|
||
fn lookup_import_candidates_from_module<FilterFn>( | ||
&mut self, | ||
lookup_ident: Ident, | ||
|
@@ -1331,15 +1409,126 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> { | |
macro_kind: MacroKind, | ||
parent_scope: &ParentScope<'a>, | ||
ident: Ident, | ||
sugg_span: Option<Span>, | ||
) { | ||
// Bring imported but unused `derive` macros into `macro_map` so we ensure they can be used | ||
// for suggestions. | ||
self.lookup_macros_from_module(); | ||
self.visit_scopes( | ||
ScopeSet::Macro(MacroKind::Derive), | ||
&parent_scope, | ||
ident.span.ctxt(), | ||
|this, scope, _use_prelude, _ctxt| { | ||
let Scope::Module(m, _) = scope else { return None; }; | ||
for (_, resolution) in this.resolutions(m).borrow().iter() { | ||
let Some(binding) = resolution.borrow().binding else { continue; }; | ||
let Res::Def( | ||
DefKind::Macro(MacroKind::Derive | MacroKind::Attr), | ||
def_id, | ||
) = binding.res() else { continue; }; | ||
// By doing this all *imported* macros get added to the `macro_map` even if they | ||
// are *unused*, which makes the later suggestions find them and work. | ||
let _ = this.get_macro_by_def_id(def_id); | ||
} | ||
None::<()> | ||
}, | ||
); | ||
|
||
let is_expected = &|res: Res| res.macro_kind() == Some(macro_kind); | ||
let suggestion = self.early_lookup_typo_candidate( | ||
ScopeSet::Macro(macro_kind), | ||
parent_scope, | ||
ident, | ||
is_expected, | ||
); | ||
self.add_typo_suggestion(err, suggestion, ident.span); | ||
if !self.add_typo_suggestion(err, suggestion, ident.span) { | ||
// FIXME: this only works if the crate that owns the macro that has the helper_attr | ||
// has already been imported. | ||
let mut derives = vec![]; | ||
let mut all_attrs: FxHashMap<Symbol, Vec<_>> = FxHashMap::default(); | ||
for (def_id, data) in &self.macro_map { | ||
for helper_attr in &data.ext.helper_attrs { | ||
let item_name = self.tcx.item_name(*def_id); | ||
all_attrs.entry(*helper_attr).or_default().push(item_name); | ||
if helper_attr == &ident.name { | ||
derives.push(item_name); | ||
} | ||
} | ||
} | ||
let kind = MacroKind::Derive.descr(); | ||
if !derives.is_empty() { | ||
derives.sort(); | ||
derives.dedup(); | ||
let msg = match &derives[..] { | ||
[derive] => format!(" `{derive}`"), | ||
[start @ .., last] => format!( | ||
"s {} and `{last}`", | ||
start.iter().map(|d| format!("`{d}`")).collect::<Vec<_>>().join(", ") | ||
), | ||
[] => unreachable!("we checked for this to be non-empty 10 lines above!?"), | ||
}; | ||
let msg = format!( | ||
"`{}` is an attribute that can be used by the {kind}{msg}, you might be missing a \ | ||
`derive` attribute", | ||
ident.name, | ||
); | ||
let sugg_span = | ||
if let ModuleKind::Def(DefKind::Enum, id, _) = parent_scope.module.kind { | ||
let span = self.def_span(id); | ||
if span.from_expansion() { | ||
None | ||
} else { | ||
// For enum variants sugg_span is empty but we can get the enum's Span. | ||
Some(span.shrink_to_lo()) | ||
} | ||
} else { | ||
// For items this `Span` will be populated, everything else it'll be None. | ||
sugg_span | ||
}; | ||
match sugg_span { | ||
Some(span) => { | ||
err.span_suggestion_verbose( | ||
span, | ||
&msg, | ||
format!( | ||
"#[derive({})]\n", | ||
derives | ||
.iter() | ||
.map(|d| d.to_string()) | ||
.collect::<Vec<String>>() | ||
.join(", ") | ||
), | ||
Applicability::MaybeIncorrect, | ||
); | ||
} | ||
None => { | ||
err.note(&msg); | ||
} | ||
} | ||
} else { | ||
let all_attr_names: Vec<Symbol> = all_attrs.keys().cloned().collect(); | ||
if let Some(best_match) = find_best_match_for_name(&all_attr_names, ident.name, None) | ||
&& let Some(macros) = all_attrs.get(&best_match) | ||
&& !macros.is_empty() | ||
{ | ||
let msg = match ¯os[..] { | ||
[] => unreachable!("we checked above in the if-let"), | ||
[name] => format!(" `{name}` accepts"), | ||
[start @ .., end] => format!( | ||
"s {} and `{end}` accept", | ||
start.iter().map(|m| format!("`{m}`")).collect::<Vec<_>>().join(", "), | ||
), | ||
}; | ||
let msg = format!("the {kind}{msg} the similarly named `{best_match}` attribute"); | ||
err.span_suggestion_verbose( | ||
ident.span, | ||
&msg, | ||
best_match, | ||
Applicability::MaybeIncorrect, | ||
); | ||
} | ||
} | ||
} | ||
|
||
let import_suggestions = | ||
self.lookup_import_candidates(ident, Namespace::MacroNS, parent_scope, is_expected); | ||
|
@@ -1364,14 +1553,20 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> { | |
err.help("have you added the `#[macro_use]` on the module/import?"); | ||
return; | ||
} | ||
|
||
if ident.name == kw::Default | ||
&& let ModuleKind::Def(DefKind::Enum, def_id, _) = parent_scope.module.kind | ||
{ | ||
let span = self.def_span(def_id); | ||
let source_map = self.tcx.sess.source_map(); | ||
let head_span = source_map.guess_head_span(span); | ||
if let Ok(head) = source_map.span_to_snippet(head_span) { | ||
err.span_suggestion(head_span, "consider adding a derive", format!("#[derive(Default)]\n{head}"), Applicability::MaybeIncorrect); | ||
if let Ok(_) = source_map.span_to_snippet(head_span) { | ||
err.span_suggestion( | ||
head_span.shrink_to_lo(), | ||
"consider adding a derive", | ||
format!("#[derive(Default)]\n"), | ||
Applicability::MaybeIncorrect, | ||
); | ||
} else { | ||
err.span_help( | ||
head_span, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is so that the case in
tests/ui/macros/missing-derive-4.rs
(use serde;
, but nouse serde::Serialize;
) is handled.