Skip to content

Commit

Permalink
resolve: Implement a lint for out-of-scope use of macro_rules
Browse files Browse the repository at this point in the history
  • Loading branch information
petrochenkov committed Jun 24, 2024
1 parent 0195758 commit c4c7859
Show file tree
Hide file tree
Showing 12 changed files with 273 additions and 92 deletions.
3 changes: 3 additions & 0 deletions compiler/rustc_lint/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,9 @@ lint_opaque_hidden_inferred_bound_sugg = add this bound
lint_or_patterns_back_compat = the meaning of the `pat` fragment specifier is changing in Rust 2021, which may affect this macro
.suggestion = use pat_param to preserve semantics
lint_out_of_scope_macro_calls = cannot find macro `{$path}` in this scope
.help = import `macro_rules` with `use` to make it callable above its definition
lint_overflowing_bin_hex = literal out of range for `{$ty}`
.negative_note = the literal `{$lit}` (decimal `{$dec}`) does not fit into the type `{$ty}`
.negative_becomes_note = and the value `-{$lit}` will become `{$actually}{$ty}`
Expand Down
3 changes: 3 additions & 0 deletions compiler/rustc_lint/src/context/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,5 +434,8 @@ pub(super) fn decorate_lint(sess: &Session, diagnostic: BuiltinLintDiag, diag: &
lints::InnerAttributeUnstable::CustomInnerAttribute
}
.decorate_lint(diag),
BuiltinLintDiag::OutOfScopeMacroCalls { path } => {
lints::OutOfScopeMacroCalls { path }.decorate_lint(diag)
}
}
}
7 changes: 7 additions & 0 deletions compiler/rustc_lint/src/lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2911,3 +2911,10 @@ pub struct UnsafeAttrOutsideUnsafeSuggestion {
#[suggestion_part(code = ")")]
pub right: Span,
}

#[derive(LintDiagnostic)]
#[diag(lint_out_of_scope_macro_calls)]
#[help]
pub struct OutOfScopeMacroCalls {
pub path: String,
}
39 changes: 39 additions & 0 deletions compiler/rustc_lint_defs/src/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4945,3 +4945,42 @@ declare_lint! {
reference: "issue #123757 <https://github.com/rust-lang/rust/issues/123757>",
};
}

declare_lint! {
/// The `out_of_scope_macro_calls` lint detects `macro_rules` called when they are not in scope,
/// above their definition, which may happen in key-value attributes.
///
/// ### Example
///
/// ```rust
/// #![doc = in_root!()]
///
/// macro_rules! in_root { () => { "" } }
///
/// fn main() {}
/// ```
///
/// {{produces}}
///
/// ### Explanation
///
/// The scope in which a `macro_rules` item is visible starts at that item and continues
/// below it. This is more similar to `let` than to other items, which are in scope both above
/// and below their definition.
/// Due to a bug `macro_rules` were accidentally in scope inside some key-value attributes
/// above their definition. The lint catches such cases.
/// To address the issue turn the `macro_rules` into a regularly scoped item by importing it
/// with `use`.
///
/// This is a [future-incompatible] lint to transition this to a
/// hard error in the future.
///
/// [future-incompatible]: ../index.md#future-incompatible-lints
pub OUT_OF_SCOPE_MACRO_CALLS,
Warn,
"detects out of scope calls to `macro_rules` in key-value attributes",
@future_incompatible = FutureIncompatibleInfo {
reason: FutureIncompatibilityReason::FutureReleaseErrorDontReportInDeps,
reference: "issue #124535 <https://github.com/rust-lang/rust/issues/124535>",
};
}
3 changes: 3 additions & 0 deletions compiler/rustc_lint_defs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,9 @@ pub enum BuiltinLintDiag {
InnerAttributeUnstable {
is_macro: bool,
},
OutOfScopeMacroCalls {
path: String,
},
}

/// Lints that are buffered up early on in the `Session` before the
Expand Down
19 changes: 16 additions & 3 deletions compiler/rustc_resolve/src/build_reduced_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{Determinacy, ExternPreludeEntry, Finalize, Module, ModuleKind, Modul
use crate::{NameBinding, NameBindingKind, ParentScope, PathResult, ResolutionError};
use crate::{Resolver, ResolverArenas, Segment, ToNameBinding, Used, VisResolutionError};

use rustc_ast::visit::{self, AssocCtxt, Visitor};
use rustc_ast::visit::{self, AssocCtxt, Visitor, WalkItemKind};
use rustc_ast::{self as ast, AssocItem, AssocItemKind, MetaItemKind, StmtKind};
use rustc_ast::{Block, ForeignItem, ForeignItemKind, Impl, Item, ItemKind, NodeId};
use rustc_attr as attr;
Expand Down Expand Up @@ -1313,7 +1313,17 @@ impl<'a, 'b, 'tcx> Visitor<'b> for BuildReducedGraphVisitor<'a, 'b, 'tcx> {
_ => {
let orig_macro_rules_scope = self.parent_scope.macro_rules;
self.build_reduced_graph_for_item(item);
visit::walk_item(self, item);
match item.kind {
ItemKind::Mod(..) => {
// Visit attributes after items for backward compatibility.
// This way they can use `macro_rules` defined later.
self.visit_vis(&item.vis);
self.visit_ident(item.ident);
item.kind.walk(item, AssocCtxt::Trait, self);
visit::walk_list!(self, visit_attribute, &item.attrs);
}
_ => visit::walk_item(self, item),
}
match item.kind {
ItemKind::Mod(..) if self.contains_macro_use(&item.attrs) => {
self.parent_scope.macro_rules
Expand Down Expand Up @@ -1514,7 +1524,10 @@ impl<'a, 'b, 'tcx> Visitor<'b> for BuildReducedGraphVisitor<'a, 'b, 'tcx> {
if krate.is_placeholder {
self.visit_invoc_in_module(krate.id);
} else {
visit::walk_crate(self, krate);
// Visit attributes after items for backward compatibility.
// This way they can use `macro_rules` defined later.
visit::walk_list!(self, visit_item, &krate.items);
visit::walk_list!(self, visit_attribute, &krate.attrs);
self.contains_macro_use(&krate.attrs);
}
}
Expand Down
80 changes: 71 additions & 9 deletions compiler/rustc_resolve/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::errors::CannotDetermineMacroResolution;
use crate::errors::{self, AddAsNonDerive, CannotFindIdentInThisScope};
use crate::errors::{MacroExpectedFound, RemoveSurroundingDerive};
use crate::Namespace::*;
use crate::{BindingKey, BuiltinMacroState, Determinacy, MacroData, Used};
use crate::{BindingKey, BuiltinMacroState, Determinacy, MacroData, NameBindingKind, Used};
use crate::{DeriveData, Finalize, ParentScope, ResolutionError, Resolver, ScopeSet};
use crate::{ModuleKind, ModuleOrUniformRoot, NameBinding, PathResult, Segment, ToNameBinding};
use rustc_ast::expand::StrippedCfgItem;
Expand All @@ -18,15 +18,18 @@ use rustc_errors::{Applicability, StashKey};
use rustc_expand::base::{Annotatable, DeriveResolution, Indeterminate, ResolverExpand};
use rustc_expand::base::{SyntaxExtension, SyntaxExtensionKind};
use rustc_expand::compile_declarative_macro;
use rustc_expand::expand::{AstFragment, Invocation, InvocationKind, SupportsMacroExpansion};
use rustc_expand::expand::{
AstFragment, AstFragmentKind, Invocation, InvocationKind, SupportsMacroExpansion,
};
use rustc_hir::def::{self, DefKind, Namespace, NonMacroAttrKind};
use rustc_hir::def_id::{CrateNum, DefId, LocalDefId};
use rustc_middle::middle::stability;
use rustc_middle::ty::RegisteredTools;
use rustc_middle::ty::{TyCtxt, Visibility};
use rustc_session::lint::builtin::UNKNOWN_OR_MALFORMED_DIAGNOSTIC_ATTRIBUTES;
use rustc_session::lint::builtin::{LEGACY_DERIVE_HELPERS, SOFT_UNSTABLE};
use rustc_session::lint::builtin::{UNUSED_MACROS, UNUSED_MACRO_RULES};
use rustc_session::lint::builtin::{
LEGACY_DERIVE_HELPERS, OUT_OF_SCOPE_MACRO_CALLS, SOFT_UNSTABLE,
UNKNOWN_OR_MALFORMED_DIAGNOSTIC_ATTRIBUTES, UNUSED_MACROS, UNUSED_MACRO_RULES,
};
use rustc_session::lint::BuiltinLintDiag;
use rustc_session::parse::feature_err;
use rustc_span::edit_distance::edit_distance;
Expand Down Expand Up @@ -288,6 +291,16 @@ impl<'a, 'tcx> ResolverExpand for Resolver<'a, 'tcx> {
let parent_scope = &ParentScope { derives, ..parent_scope };
let supports_macro_expansion = invoc.fragment_kind.supports_macro_expansion();
let node_id = invoc.expansion_data.lint_node_id;
// This is a heuristic, but it's good enough for the lint.
let looks_like_invoc_in_mod_inert_attr = self
.invocation_parents
.get(&invoc_id)
.or_else(|| self.invocation_parents.get(&eager_expansion_root))
.map(|&(mod_def_id, _)| mod_def_id)
.filter(|&mod_def_id| {
invoc.fragment_kind == AstFragmentKind::Expr
&& self.tcx.def_kind(mod_def_id) == DefKind::Mod
});
let (ext, res) = self.smart_resolve_macro_path(
path,
kind,
Expand All @@ -298,6 +311,7 @@ impl<'a, 'tcx> ResolverExpand for Resolver<'a, 'tcx> {
force,
soft_custom_inner_attributes_gate(path, invoc),
deleg_impl,
looks_like_invoc_in_mod_inert_attr,
)?;

let span = invoc.span();
Expand Down Expand Up @@ -520,6 +534,7 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> {
force: bool,
soft_custom_inner_attributes_gate: bool,
deleg_impl: Option<LocalDefId>,
invoc_in_mod_inert_attr: Option<LocalDefId>,
) -> Result<(Lrc<SyntaxExtension>, Res), Indeterminate> {
let (ext, res) = match self.resolve_macro_or_delegation_path(
path,
Expand All @@ -528,6 +543,7 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> {
true,
force,
deleg_impl,
invoc_in_mod_inert_attr.map(|def_id| (def_id, node_id)),
) {
Ok((Some(ext), res)) => (ext, res),
Ok((None, res)) => (self.dummy_ext(kind), res),
Expand Down Expand Up @@ -682,20 +698,21 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> {
trace: bool,
force: bool,
) -> Result<(Option<Lrc<SyntaxExtension>>, Res), Determinacy> {
self.resolve_macro_or_delegation_path(path, kind, parent_scope, trace, force, None)
self.resolve_macro_or_delegation_path(path, kind, parent_scope, trace, force, None, None)
}

fn resolve_macro_or_delegation_path(
&mut self,
path: &ast::Path,
ast_path: &ast::Path,
kind: Option<MacroKind>,
parent_scope: &ParentScope<'a>,
trace: bool,
force: bool,
deleg_impl: Option<LocalDefId>,
invoc_in_mod_inert_attr: Option<(LocalDefId, NodeId)>,
) -> Result<(Option<Lrc<SyntaxExtension>>, Res), Determinacy> {
let path_span = path.span;
let mut path = Segment::from_path(path);
let path_span = ast_path.span;
let mut path = Segment::from_path(ast_path);

// Possibly apply the macro helper hack
if deleg_impl.is_none()
Expand Down Expand Up @@ -761,6 +778,12 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> {

let res = binding.map(|binding| binding.res());
self.prohibit_imported_non_macro_attrs(binding.ok(), res.ok(), path_span);
self.report_out_of_scope_macro_calls(
ast_path,
parent_scope,
invoc_in_mod_inert_attr,
binding.ok(),
);
res
};

Expand Down Expand Up @@ -1013,6 +1036,45 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> {
}
}

fn report_out_of_scope_macro_calls(
&mut self,
path: &ast::Path,
parent_scope: &ParentScope<'a>,
invoc_in_mod_inert_attr: Option<(LocalDefId, NodeId)>,
binding: Option<NameBinding<'a>>,
) {
if let Some((mod_def_id, node_id)) = invoc_in_mod_inert_attr
&& let Some(binding) = binding
// This is a `macro_rules` itself, not some import.
&& let NameBindingKind::Res(res) = binding.kind
&& let Res::Def(DefKind::Macro(MacroKind::Bang), def_id) = res
// And the `macro_rules` is defined inside the attribute's module,
// so it cannot be in scope unless imported.
&& self.tcx.is_descendant_of(def_id, mod_def_id.to_def_id())
{
// Try to resolve our ident ignoring `macro_rules` scopes.
// If such resolution is successful and gives the same result
// (e.g. if the macro is re-imported), then silence the lint.
let no_macro_rules = self.arenas.alloc_macro_rules_scope(MacroRulesScope::Empty);
let fallback_binding = self.early_resolve_ident_in_lexical_scope(
path.segments[0].ident,
ScopeSet::Macro(MacroKind::Bang),
&ParentScope { macro_rules: no_macro_rules, ..*parent_scope },
None,
false,
None,
);
if fallback_binding.ok().and_then(|b| b.res().opt_def_id()) != Some(def_id) {
self.tcx.sess.psess.buffer_lint(
OUT_OF_SCOPE_MACRO_CALLS,
path.span,
node_id,
BuiltinLintDiag::OutOfScopeMacroCalls { path: pprust::path_to_string(path) },
);
}
}
}

pub(crate) fn check_reserved_macro_name(&mut self, ident: Ident, res: Res) {
// Reserve some names that are not quite covered by the general check
// performed on `Resolver::builtin_attrs`.
Expand Down
18 changes: 18 additions & 0 deletions tests/ui/attributes/key-value-expansion-scope-pass.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Imports suppress the `out_of_scope_macro_calls` lint.

//@ check-pass
//@ edition:2018

#![doc = in_root!()]

macro_rules! in_root { () => { "" } }
use in_root;

mod macros_stay {
#![doc = in_mod!()]

macro_rules! in_mod { () => { "" } }
use in_mod;
}

fn main() {}
19 changes: 14 additions & 5 deletions tests/ui/attributes/key-value-expansion-scope.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#![doc = in_root!()] //~ ERROR cannot find macro `in_root` in this scope
#![doc = in_root!()] //~ WARN cannot find macro `in_root` in this scope
//~| WARN this was previously accepted by the compiler
#![doc = in_mod!()] //~ ERROR cannot find macro `in_mod` in this scope
#![doc = in_mod_escape!()] //~ ERROR cannot find macro `in_mod_escape` in this scope
#![doc = in_mod_escape!()] //~ WARN cannot find macro `in_mod_escape` in this scope
//~| WARN this was previously accepted by the compiler
#![doc = in_block!()] //~ ERROR cannot find macro `in_block` in this scope

#[doc = in_root!()] //~ ERROR cannot find macro `in_root` in this scope
Expand All @@ -16,8 +18,11 @@ fn before() {

macro_rules! in_root { () => { "" } }

#[doc = in_mod!()] //~ WARN cannot find macro `in_mod` in this scope
//~| WARN this was previously accepted by the compiler
mod macros_stay {
#![doc = in_mod!()] //~ ERROR cannot find macro `in_mod` in this scope
#![doc = in_mod!()] //~ WARN cannot find macro `in_mod` in this scope
//~| WARN this was previously accepted by the compiler

macro_rules! in_mod { () => { "" } }

Expand All @@ -28,8 +33,11 @@ mod macros_stay {
}

#[macro_use]
#[doc = in_mod_escape!()] //~ WARN cannot find macro `in_mod_escape` in this scope
//~| WARN this was previously accepted by the compiler
mod macros_escape {
#![doc = in_mod_escape!()] //~ ERROR cannot find macro `in_mod_escape` in this scope
#![doc = in_mod_escape!()] //~ WARN cannot find macro `in_mod_escape` in this scope
//~| WARN this was previously accepted by the compiler

macro_rules! in_mod_escape { () => { "" } }

Expand All @@ -39,8 +47,9 @@ mod macros_escape {
}
}

#[doc = in_block!()] //~ ERROR cannot find macro `in_block` in this scope
fn block() {
#![doc = in_block!()] //~ ERROR cannot find macro `in_block` in this scope
#![doc = in_block!()] //~ ERROR cannot find macro `in_block` in this scope

macro_rules! in_block { () => { "" } }

Expand Down
Loading

0 comments on commit c4c7859

Please sign in to comment.