Skip to content
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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 199 additions & 4 deletions compiler/rustc_resolve/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -1016,6 +1016,7 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> {
parent_scope,
false,
false,
None,
) {
suggestions.extend(
ext.helper_attrs
Expand Down Expand Up @@ -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) {
Comment on lines +1119 to +1121
Copy link
Contributor Author

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 no use serde::Serialize;) is handled.

Copy link
Contributor

Choose a reason for hiding this comment

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

How is this different from lookup_import_candidates_from_module?

Only macros that would be suggested as imports (and their helper attributes) can be suggested here.
So I suggest removing this separate logic and retrieve helper attributes in the lookup_import_candidates_from_module pass if the import candidate happens to be a derive macro and helper attributes satisfy filter_fn.

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 early_lookup_typo_candidate pass.

Copy link
Contributor

Choose a reason for hiding this comment

The 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,
Expand Down Expand Up @@ -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 &macros[..] {
[] => 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);
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_resolve/src/ident.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> {
parent_scope,
true,
force,
None,
) {
Ok((Some(ext), _)) => {
if ext.helper_attrs.contains(&ident.name) {
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_resolve/src/late.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3706,7 +3706,7 @@ impl<'a: 'ast, 'b, 'ast, 'tcx> LateResolutionVisitor<'a, 'b, 'ast, 'tcx> {
let path_seg = |seg: &Segment| PathSegment::from_ident(seg.ident);
let path = Path { segments: path.iter().map(path_seg).collect(), span, tokens: None };
if let Ok((_, res)) =
self.r.resolve_macro_path(&path, None, &self.parent_scope, false, false)
self.r.resolve_macro_path(&path, None, &self.parent_scope, false, false, None)
{
return Ok(Some(PartialRes::new(res)));
}
Expand Down
4 changes: 2 additions & 2 deletions compiler/rustc_resolve/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -974,9 +974,9 @@ pub struct Resolver<'a, 'tcx> {
proc_macro_stubs: FxHashSet<LocalDefId>,
/// Traces collected during macro resolution and validated when it's complete.
single_segment_macro_resolutions:
Vec<(Ident, MacroKind, ParentScope<'a>, Option<&'a NameBinding<'a>>)>,
Vec<(Ident, MacroKind, ParentScope<'a>, Option<&'a NameBinding<'a>>, Option<Span>)>,
multi_segment_macro_resolutions:
Vec<(Vec<Segment>, Span, MacroKind, ParentScope<'a>, Option<Res>)>,
Vec<(Vec<Segment>, Span, MacroKind, ParentScope<'a>, Option<Res>, Option<Span>)>,
builtin_attrs: Vec<(Ident, ParentScope<'a>)>,
/// `derive(Copy)` marks items they are applied to so they are treated specially later.
/// Derive macros cannot modify the item themselves and have to store the markers in the global
Expand Down
Loading