mirror of
https://github.com/rust-lang/rust.git
synced 2025-06-05 03:38:29 +00:00
Implement checks for meta-variables in macros
This commit is contained in:
parent
82abc0db81
commit
6ec4584d84
@ -362,6 +362,12 @@ pub mod parser {
|
||||
Warn,
|
||||
"ill-formed attribute inputs that were previously accepted and used in practice"
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
pub META_VARIABLE_MISUSE,
|
||||
Allow,
|
||||
"possible meta-variable misuse at macro definition"
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
@ -448,6 +454,7 @@ declare_lint_pass! {
|
||||
MACRO_USE_EXTERN_CRATE,
|
||||
MACRO_EXPANDED_MACRO_EXPORTS_ACCESSED_BY_ABSOLUTE_PATHS,
|
||||
parser::ILL_FORMED_ATTRIBUTE_INPUT,
|
||||
parser::META_VARIABLE_MISUSE,
|
||||
DEPRECATED_IN_FUTURE,
|
||||
AMBIGUOUS_ASSOCIATED_ITEMS,
|
||||
NESTED_IMPL_TRAIT,
|
||||
|
@ -27,7 +27,7 @@ use crate::hir::def_id::{CrateNum, LOCAL_CRATE};
|
||||
use crate::hir::intravisit;
|
||||
use crate::hir;
|
||||
use crate::lint::builtin::BuiltinLintDiagnostics;
|
||||
use crate::lint::builtin::parser::ILL_FORMED_ATTRIBUTE_INPUT;
|
||||
use crate::lint::builtin::parser::{ILL_FORMED_ATTRIBUTE_INPUT, META_VARIABLE_MISUSE};
|
||||
use crate::session::{Session, DiagnosticMessageId};
|
||||
use crate::ty::TyCtxt;
|
||||
use crate::ty::query::Providers;
|
||||
@ -82,6 +82,7 @@ impl Lint {
|
||||
pub fn from_parser_lint_id(lint_id: BufferedEarlyLintId) -> &'static Self {
|
||||
match lint_id {
|
||||
BufferedEarlyLintId::IllFormedAttributeInput => ILL_FORMED_ATTRIBUTE_INPUT,
|
||||
BufferedEarlyLintId::MetaVariableMisuse => META_VARIABLE_MISUSE,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ use syntax_pos::MultiSpan;
|
||||
/// passed to `rustc::lint::Lint::from_parser_lint_id` to get a `rustc::lint::Lint`.
|
||||
pub enum BufferedEarlyLintId {
|
||||
IllFormedAttributeInput,
|
||||
MetaVariableMisuse,
|
||||
}
|
||||
|
||||
/// Stores buffered lint info which can later be passed to `librustc`.
|
||||
|
626
src/libsyntax/ext/tt/macro_check.rs
Normal file
626
src/libsyntax/ext/tt/macro_check.rs
Normal file
@ -0,0 +1,626 @@
|
||||
//! Checks that meta-variables in macro definition are correctly declared and used.
|
||||
//!
|
||||
//! # What is checked
|
||||
//!
|
||||
//! ## Meta-variables must not be bound twice
|
||||
//!
|
||||
//! ```
|
||||
//! macro_rules! foo { ($x:tt $x:tt) => { $x }; }
|
||||
//! ```
|
||||
//!
|
||||
//! This check is sound (no false-negative) and complete (no false-positive).
|
||||
//!
|
||||
//! ## Meta-variables must not be free
|
||||
//!
|
||||
//! ```
|
||||
//! macro_rules! foo { () => { $x }; }
|
||||
//! ```
|
||||
//!
|
||||
//! This check is also done at macro instantiation but only if the branch is taken.
|
||||
//!
|
||||
//! ## Meta-variables must repeat at least as many times as their binder
|
||||
//!
|
||||
//! ```
|
||||
//! macro_rules! foo { ($($x:tt)*) => { $x }; }
|
||||
//! ```
|
||||
//!
|
||||
//! This check is also done at macro instantiation but only if the branch is taken.
|
||||
//!
|
||||
//! ## Meta-variables must repeat with the same Kleene operators as their binder
|
||||
//!
|
||||
//! ```
|
||||
//! macro_rules! foo { ($($x:tt)+) => { $($x)* }; }
|
||||
//! ```
|
||||
//!
|
||||
//! This check is not done at macro instantiation.
|
||||
//!
|
||||
//! # Disclaimer
|
||||
//!
|
||||
//! In the presence of nested macros (a macro defined in a macro), those checks may have false
|
||||
//! positives and false negatives. We try to detect those cases by recognizing potential macro
|
||||
//! definitions in RHSes, but nested macros may be hidden through the use of particular values of
|
||||
//! meta-variables.
|
||||
//!
|
||||
//! ## Examples of false positive
|
||||
//!
|
||||
//! False positives can come from cases where we don't recognize a nested macro, because it depends
|
||||
//! on particular values of meta-variables. In the following example, we think both instances of
|
||||
//! `$x` are free, which is a correct statement if `$name` is anything but `macro_rules`. But when
|
||||
//! `$name` is `macro_rules`, like in the instantiation below, then `$x:tt` is actually a binder of
|
||||
//! the nested macro and `$x` is bound to it.
|
||||
//!
|
||||
//! ```
|
||||
//! macro_rules! foo { ($name:ident) => { $name! bar { ($x:tt) => { $x }; } }; }
|
||||
//! foo!(macro_rules);
|
||||
//! ```
|
||||
//!
|
||||
//! False positives can also come from cases where we think there is a nested macro while there
|
||||
//! isn't. In the following example, we think `$x` is free, which is incorrect because `bar` is not
|
||||
//! a nested macro since it is not evaluated as code by `stringify!`.
|
||||
//!
|
||||
//! ```
|
||||
//! macro_rules! foo { () => { stringify!(macro_rules! bar { () => { $x }; }) }; }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Examples of false negative
|
||||
//!
|
||||
//! False negatives can come from cases where we don't recognize a meta-variable, because it depends
|
||||
//! on particular values of meta-variables. In the following examples, we don't see that if `$d` is
|
||||
//! instantiated with `$` then `$d z` becomes `$z` in the nested macro definition and is thus a free
|
||||
//! meta-variable. Note however, that if `foo` is instantiated, then we would check the definition
|
||||
//! of `bar` and would see the issue.
|
||||
//!
|
||||
//! ```
|
||||
//! macro_rules! foo { ($d:tt) => { macro_rules! bar { ($y:tt) => { $d z }; } }; }
|
||||
//! ```
|
||||
//!
|
||||
//! # How it is checked
|
||||
//!
|
||||
//! There are 3 main functions: `check_binders`, `check_occurrences`, and `check_nested_macro`. They
|
||||
//! all need some kind of environment.
|
||||
//!
|
||||
//! ## Environments
|
||||
//!
|
||||
//! Environments are used to pass information.
|
||||
//!
|
||||
//! ### From LHS to RHS
|
||||
//!
|
||||
//! When checking a LHS with `check_binders`, we produce (and use) an environment for binders,
|
||||
//! namely `Binders`. This is a mapping from binder name to information about that binder: the span
|
||||
//! of the binder for error messages and the stack of Kleene operators under which it was bound in
|
||||
//! the LHS.
|
||||
//!
|
||||
//! This environment is used by both the LHS and RHS. The LHS uses it to detect duplicate binders.
|
||||
//! The RHS uses it to detect the other errors.
|
||||
//!
|
||||
//! ### From outer macro to inner macro
|
||||
//!
|
||||
//! When checking the RHS of an outer macro and we detect a nested macro definition, we push the
|
||||
//! current state, namely `MacroState`, to an environment of nested macro definitions. Each state
|
||||
//! stores the LHS binders when entering the macro definition as well as the stack of Kleene
|
||||
//! operators under which the inner macro is defined in the RHS.
|
||||
//!
|
||||
//! This environment is a stack representing the nesting of macro definitions. As such, the stack of
|
||||
//! Kleene operators under which a meta-variable is repeating is the concatenation of the stacks
|
||||
//! stored when entering a macro definition starting from the state in which the meta-variable is
|
||||
//! bound.
|
||||
use crate::ast::NodeId;
|
||||
use crate::early_buffered_lints::BufferedEarlyLintId;
|
||||
use crate::ext::tt::quoted::{KleeneToken, TokenTree};
|
||||
use crate::parse::token::TokenKind;
|
||||
use crate::parse::token::{DelimToken, Token};
|
||||
use crate::parse::ParseSess;
|
||||
use crate::symbol::{kw, sym};
|
||||
|
||||
use rustc_data_structures::fx::FxHashMap;
|
||||
use smallvec::SmallVec;
|
||||
use syntax_pos::{symbol::Ident, MultiSpan, Span};
|
||||
|
||||
/// Stack represented as linked list.
|
||||
///
|
||||
/// Those are used for environments because they grow incrementally and are not mutable.
|
||||
enum Stack<'a, T> {
|
||||
/// Empty stack.
|
||||
Empty,
|
||||
/// A non-empty stack.
|
||||
Push {
|
||||
/// The top element.
|
||||
top: T,
|
||||
/// The previous elements.
|
||||
prev: &'a Stack<'a, T>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a, T> Stack<'a, T> {
|
||||
/// Returns whether a stack is empty.
|
||||
fn is_empty(&self) -> bool {
|
||||
match *self {
|
||||
Stack::Empty => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new stack with an element of top.
|
||||
fn push(&'a self, top: T) -> Stack<'a, T> {
|
||||
Stack::Push { top, prev: self }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for &'a Stack<'a, T> {
|
||||
type Item = &'a T;
|
||||
|
||||
// Iterates from top to bottom of the stack.
|
||||
fn next(&mut self) -> Option<&'a T> {
|
||||
match *self {
|
||||
Stack::Empty => None,
|
||||
Stack::Push { ref top, ref prev } => {
|
||||
*self = prev;
|
||||
Some(top)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Stack<'_, KleeneToken>> for SmallVec<[KleeneToken; 1]> {
|
||||
fn from(ops: &Stack<'_, KleeneToken>) -> SmallVec<[KleeneToken; 1]> {
|
||||
let mut ops: SmallVec<[KleeneToken; 1]> = ops.cloned().collect();
|
||||
// The stack is innermost on top. We want outermost first.
|
||||
ops.reverse();
|
||||
ops
|
||||
}
|
||||
}
|
||||
|
||||
/// Information attached to a meta-variable binder in LHS.
|
||||
struct BinderInfo {
|
||||
/// The span of the meta-variable in LHS.
|
||||
span: Span,
|
||||
/// The stack of Kleene operators (outermost first).
|
||||
ops: SmallVec<[KleeneToken; 1]>,
|
||||
}
|
||||
|
||||
/// An environment of meta-variables to their binder information.
|
||||
type Binders = FxHashMap<Ident, BinderInfo>;
|
||||
|
||||
/// The state at which we entered a macro definition in the RHS of another macro definition.
|
||||
struct MacroState<'a> {
|
||||
/// The binders of the branch where we entered the macro definition.
|
||||
binders: &'a Binders,
|
||||
/// The stack of Kleene operators (outermost first) where we entered the macro definition.
|
||||
ops: SmallVec<[KleeneToken; 1]>,
|
||||
}
|
||||
|
||||
/// Checks that meta-variables are used correctly in a macro definition.
|
||||
///
|
||||
/// Arguments:
|
||||
/// - `sess` is used to emit diagnostics and lints
|
||||
/// - `node_id` is used to emit lints
|
||||
/// - `span` is used when no spans are available
|
||||
/// - `lhses` and `rhses` should have the same length and represent the macro definition
|
||||
pub fn check_meta_variables(
|
||||
sess: &ParseSess,
|
||||
node_id: NodeId,
|
||||
span: Span,
|
||||
lhses: &[TokenTree],
|
||||
rhses: &[TokenTree],
|
||||
) -> bool {
|
||||
if lhses.len() != rhses.len() {
|
||||
sess.span_diagnostic.span_bug(span, "length mismatch between LHSes and RHSes")
|
||||
}
|
||||
let mut valid = true;
|
||||
for (lhs, rhs) in lhses.iter().zip(rhses.iter()) {
|
||||
let mut binders = Binders::default();
|
||||
check_binders(sess, node_id, lhs, &Stack::Empty, &mut binders, &Stack::Empty, &mut valid);
|
||||
check_occurrences(sess, node_id, rhs, &Stack::Empty, &binders, &Stack::Empty, &mut valid);
|
||||
}
|
||||
valid
|
||||
}
|
||||
|
||||
/// Checks `lhs` as part of the LHS of a macro definition, extends `binders` with new binders, and
|
||||
/// sets `valid` to false in case of errors.
|
||||
///
|
||||
/// Arguments:
|
||||
/// - `sess` is used to emit diagnostics and lints
|
||||
/// - `node_id` is used to emit lints
|
||||
/// - `lhs` is checked as part of a LHS
|
||||
/// - `macros` is the stack of possible outer macros
|
||||
/// - `binders` contains the binders of the LHS
|
||||
/// - `ops` is the stack of Kleene operators from the LHS
|
||||
/// - `valid` is set in case of errors
|
||||
fn check_binders(
|
||||
sess: &ParseSess,
|
||||
node_id: NodeId,
|
||||
lhs: &TokenTree,
|
||||
macros: &Stack<'_, MacroState<'_>>,
|
||||
binders: &mut Binders,
|
||||
ops: &Stack<'_, KleeneToken>,
|
||||
valid: &mut bool,
|
||||
) {
|
||||
match *lhs {
|
||||
TokenTree::Token(..) => {}
|
||||
// This can only happen when checking a nested macro because this LHS is then in the RHS of
|
||||
// the outer macro. See run-pass/macros/macro-of-higher-order.rs where $y:$fragment in the
|
||||
// LHS of the nested macro (and RHS of the outer macro) is parsed as MetaVar(y) Colon
|
||||
// MetaVar(fragment) and not as MetaVarDecl(y, fragment).
|
||||
TokenTree::MetaVar(span, name) => {
|
||||
if macros.is_empty() {
|
||||
sess.span_diagnostic.span_bug(span, "unexpected MetaVar in lhs");
|
||||
}
|
||||
// There are 3 possibilities:
|
||||
if let Some(prev_info) = binders.get(&name) {
|
||||
// 1. The meta-variable is already bound in the current LHS: This is an error.
|
||||
let mut span = MultiSpan::from_span(span);
|
||||
span.push_span_label(prev_info.span, "previous declaration".into());
|
||||
buffer_lint(sess, span, node_id, "duplicate matcher binding");
|
||||
} else if get_binder_info(macros, binders, name).is_none() {
|
||||
// 2. The meta-variable is free: This is a binder.
|
||||
binders.insert(name, BinderInfo { span, ops: ops.into() });
|
||||
} else {
|
||||
// 3. The meta-variable is bound: This is an occurrence.
|
||||
check_occurrences(sess, node_id, lhs, macros, binders, ops, valid);
|
||||
}
|
||||
}
|
||||
// Similarly, this can only happen when checking a toplevel macro.
|
||||
TokenTree::MetaVarDecl(span, name, _kind) => {
|
||||
if !macros.is_empty() {
|
||||
sess.span_diagnostic.span_bug(span, "unexpected MetaVarDecl in nested lhs");
|
||||
}
|
||||
if let Some(prev_info) = get_binder_info(macros, binders, name) {
|
||||
// Duplicate binders at the top-level macro definition are errors. The lint is only
|
||||
// for nested macro definitions.
|
||||
sess.span_diagnostic
|
||||
.struct_span_err(span, "duplicate matcher binding")
|
||||
.span_note(prev_info.span, "previous declaration was here")
|
||||
.emit();
|
||||
*valid = false;
|
||||
} else {
|
||||
binders.insert(name, BinderInfo { span, ops: ops.into() });
|
||||
}
|
||||
}
|
||||
TokenTree::Delimited(_, ref del) => {
|
||||
for tt in &del.tts {
|
||||
check_binders(sess, node_id, tt, macros, binders, ops, valid);
|
||||
}
|
||||
}
|
||||
TokenTree::Sequence(_, ref seq) => {
|
||||
let ops = ops.push(seq.kleene);
|
||||
for tt in &seq.tts {
|
||||
check_binders(sess, node_id, tt, macros, binders, &ops, valid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the binder information of a meta-variable.
|
||||
///
|
||||
/// Arguments:
|
||||
/// - `macros` is the stack of possible outer macros
|
||||
/// - `binders` contains the current binders
|
||||
/// - `name` is the name of the meta-variable we are looking for
|
||||
fn get_binder_info<'a>(
|
||||
mut macros: &'a Stack<'a, MacroState<'a>>,
|
||||
binders: &'a Binders,
|
||||
name: Ident,
|
||||
) -> Option<&'a BinderInfo> {
|
||||
binders.get(&name).or_else(|| macros.find_map(|state| state.binders.get(&name)))
|
||||
}
|
||||
|
||||
/// Checks `rhs` as part of the RHS of a macro definition and sets `valid` to false in case of
|
||||
/// errors.
|
||||
///
|
||||
/// Arguments:
|
||||
/// - `sess` is used to emit diagnostics and lints
|
||||
/// - `node_id` is used to emit lints
|
||||
/// - `rhs` is checked as part of a RHS
|
||||
/// - `macros` is the stack of possible outer macros
|
||||
/// - `binders` contains the binders of the associated LHS
|
||||
/// - `ops` is the stack of Kleene operators from the RHS
|
||||
/// - `valid` is set in case of errors
|
||||
fn check_occurrences(
|
||||
sess: &ParseSess,
|
||||
node_id: NodeId,
|
||||
rhs: &TokenTree,
|
||||
macros: &Stack<'_, MacroState<'_>>,
|
||||
binders: &Binders,
|
||||
ops: &Stack<'_, KleeneToken>,
|
||||
valid: &mut bool,
|
||||
) {
|
||||
match *rhs {
|
||||
TokenTree::Token(..) => {}
|
||||
TokenTree::MetaVarDecl(span, _name, _kind) => {
|
||||
sess.span_diagnostic.span_bug(span, "unexpected MetaVarDecl in rhs")
|
||||
}
|
||||
TokenTree::MetaVar(span, name) => {
|
||||
check_ops_is_prefix(sess, node_id, macros, binders, ops, span, name);
|
||||
}
|
||||
TokenTree::Delimited(_, ref del) => {
|
||||
check_nested_occurrences(sess, node_id, &del.tts, macros, binders, ops, valid);
|
||||
}
|
||||
TokenTree::Sequence(_, ref seq) => {
|
||||
let ops = ops.push(seq.kleene);
|
||||
check_nested_occurrences(sess, node_id, &seq.tts, macros, binders, &ops, valid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the processed prefix of a nested macro.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum NestedMacroState {
|
||||
/// Nothing that matches a nested macro definition was processed yet.
|
||||
Empty,
|
||||
/// The token `macro_rules` was processed.
|
||||
MacroRules,
|
||||
/// The tokens `macro_rules!` were processed.
|
||||
MacroRulesNot,
|
||||
/// The tokens `macro_rules!` followed by a name were processed. The name may be either directly
|
||||
/// an identifier or a meta-variable (that hopefully would be instantiated by an identifier).
|
||||
MacroRulesNotName,
|
||||
/// The keyword `macro` was processed.
|
||||
Macro,
|
||||
/// The keyword `macro` followed by a name was processed.
|
||||
MacroName,
|
||||
/// The keyword `macro` followed by a name and a token delimited by parentheses was processed.
|
||||
MacroNameParen,
|
||||
}
|
||||
|
||||
/// Checks `tts` as part of the RHS of a macro definition, tries to recognize nested macro
|
||||
/// definitions, and sets `valid` to false in case of errors.
|
||||
///
|
||||
/// Arguments:
|
||||
/// - `sess` is used to emit diagnostics and lints
|
||||
/// - `node_id` is used to emit lints
|
||||
/// - `tts` is checked as part of a RHS and may contain macro definitions
|
||||
/// - `macros` is the stack of possible outer macros
|
||||
/// - `binders` contains the binders of the associated LHS
|
||||
/// - `ops` is the stack of Kleene operators from the RHS
|
||||
/// - `valid` is set in case of errors
|
||||
fn check_nested_occurrences(
|
||||
sess: &ParseSess,
|
||||
node_id: NodeId,
|
||||
tts: &[TokenTree],
|
||||
macros: &Stack<'_, MacroState<'_>>,
|
||||
binders: &Binders,
|
||||
ops: &Stack<'_, KleeneToken>,
|
||||
valid: &mut bool,
|
||||
) {
|
||||
let mut state = NestedMacroState::Empty;
|
||||
let nested_macros = macros.push(MacroState { binders, ops: ops.into() });
|
||||
let mut nested_binders = Binders::default();
|
||||
for tt in tts {
|
||||
match (state, tt) {
|
||||
(
|
||||
NestedMacroState::Empty,
|
||||
&TokenTree::Token(Token { kind: TokenKind::Ident(name, false), .. }),
|
||||
) => {
|
||||
if name == sym::macro_rules {
|
||||
state = NestedMacroState::MacroRules;
|
||||
} else if name == kw::Macro {
|
||||
state = NestedMacroState::Macro;
|
||||
}
|
||||
}
|
||||
(
|
||||
NestedMacroState::MacroRules,
|
||||
&TokenTree::Token(Token { kind: TokenKind::Not, .. }),
|
||||
) => {
|
||||
state = NestedMacroState::MacroRulesNot;
|
||||
}
|
||||
(
|
||||
NestedMacroState::MacroRulesNot,
|
||||
&TokenTree::Token(Token { kind: TokenKind::Ident(..), .. }),
|
||||
) => {
|
||||
state = NestedMacroState::MacroRulesNotName;
|
||||
}
|
||||
(NestedMacroState::MacroRulesNot, &TokenTree::MetaVar(..)) => {
|
||||
state = NestedMacroState::MacroRulesNotName;
|
||||
// We check that the meta-variable is correctly used.
|
||||
check_occurrences(sess, node_id, tt, macros, binders, ops, valid);
|
||||
}
|
||||
(NestedMacroState::MacroRulesNotName, &TokenTree::Delimited(_, ref del))
|
||||
| (NestedMacroState::MacroName, &TokenTree::Delimited(_, ref del))
|
||||
if del.delim == DelimToken::Brace =>
|
||||
{
|
||||
let legacy = state == NestedMacroState::MacroRulesNotName;
|
||||
state = NestedMacroState::Empty;
|
||||
let rest =
|
||||
check_nested_macro(sess, node_id, legacy, &del.tts, &nested_macros, valid);
|
||||
// If we did not check the whole macro definition, then check the rest as if outside
|
||||
// the macro definition.
|
||||
check_nested_occurrences(
|
||||
sess,
|
||||
node_id,
|
||||
&del.tts[rest..],
|
||||
macros,
|
||||
binders,
|
||||
ops,
|
||||
valid,
|
||||
);
|
||||
}
|
||||
(
|
||||
NestedMacroState::Macro,
|
||||
&TokenTree::Token(Token { kind: TokenKind::Ident(..), .. }),
|
||||
) => {
|
||||
state = NestedMacroState::MacroName;
|
||||
}
|
||||
(NestedMacroState::Macro, &TokenTree::MetaVar(..)) => {
|
||||
state = NestedMacroState::MacroName;
|
||||
// We check that the meta-variable is correctly used.
|
||||
check_occurrences(sess, node_id, tt, macros, binders, ops, valid);
|
||||
}
|
||||
(NestedMacroState::MacroName, &TokenTree::Delimited(_, ref del))
|
||||
if del.delim == DelimToken::Paren =>
|
||||
{
|
||||
state = NestedMacroState::MacroNameParen;
|
||||
nested_binders = Binders::default();
|
||||
check_binders(
|
||||
sess,
|
||||
node_id,
|
||||
tt,
|
||||
&nested_macros,
|
||||
&mut nested_binders,
|
||||
&Stack::Empty,
|
||||
valid,
|
||||
);
|
||||
}
|
||||
(NestedMacroState::MacroNameParen, &TokenTree::Delimited(_, ref del))
|
||||
if del.delim == DelimToken::Brace =>
|
||||
{
|
||||
state = NestedMacroState::Empty;
|
||||
check_occurrences(
|
||||
sess,
|
||||
node_id,
|
||||
tt,
|
||||
&nested_macros,
|
||||
&nested_binders,
|
||||
&Stack::Empty,
|
||||
valid,
|
||||
);
|
||||
}
|
||||
(_, ref tt) => {
|
||||
state = NestedMacroState::Empty;
|
||||
check_occurrences(sess, node_id, tt, macros, binders, ops, valid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks the body of nested macro, returns where the check stopped, and sets `valid` to false in
|
||||
/// case of errors.
|
||||
///
|
||||
/// The token trees are checked as long as they look like a list of (LHS) => {RHS} token trees. This
|
||||
/// check is a best-effort to detect a macro definition. It returns the position in `tts` where we
|
||||
/// stopped checking because we detected we were not in a macro definition anymore.
|
||||
///
|
||||
/// Arguments:
|
||||
/// - `sess` is used to emit diagnostics and lints
|
||||
/// - `node_id` is used to emit lints
|
||||
/// - `legacy` specifies whether the macro is legacy
|
||||
/// - `tts` is checked as a list of (LHS) => {RHS}
|
||||
/// - `macros` is the stack of outer macros
|
||||
/// - `valid` is set in case of errors
|
||||
fn check_nested_macro(
|
||||
sess: &ParseSess,
|
||||
node_id: NodeId,
|
||||
legacy: bool,
|
||||
tts: &[TokenTree],
|
||||
macros: &Stack<'_, MacroState<'_>>,
|
||||
valid: &mut bool,
|
||||
) -> usize {
|
||||
let n = tts.len();
|
||||
let mut i = 0;
|
||||
let separator = if legacy { TokenKind::Semi } else { TokenKind::Comma };
|
||||
loop {
|
||||
// We expect 3 token trees: `(LHS) => {RHS}`. The separator is checked after.
|
||||
if i + 2 >= n
|
||||
|| !tts[i].is_delimited()
|
||||
|| !tts[i + 1].is_token(&TokenKind::FatArrow)
|
||||
|| !tts[i + 2].is_delimited()
|
||||
{
|
||||
break;
|
||||
}
|
||||
let lhs = &tts[i];
|
||||
let rhs = &tts[i + 2];
|
||||
let mut binders = Binders::default();
|
||||
check_binders(sess, node_id, lhs, macros, &mut binders, &Stack::Empty, valid);
|
||||
check_occurrences(sess, node_id, rhs, macros, &binders, &Stack::Empty, valid);
|
||||
// Since the last semicolon is optional for legacy macros and decl_macro are not terminated,
|
||||
// we increment our checked position by how many token trees we already checked (the 3
|
||||
// above) before checking for the separator.
|
||||
i += 3;
|
||||
if i == n || !tts[i].is_token(&separator) {
|
||||
break;
|
||||
}
|
||||
// We increment our checked position for the semicolon.
|
||||
i += 1;
|
||||
}
|
||||
i
|
||||
}
|
||||
|
||||
/// Checks that a meta-variable occurrence is valid.
|
||||
///
|
||||
/// Arguments:
|
||||
/// - `sess` is used to emit diagnostics and lints
|
||||
/// - `node_id` is used to emit lints
|
||||
/// - `macros` is the stack of possible outer macros
|
||||
/// - `binders` contains the binders of the associated LHS
|
||||
/// - `ops` is the stack of Kleene operators from the RHS
|
||||
/// - `span` is the span of the meta-variable to check
|
||||
/// - `name` is the name of the meta-variable to check
|
||||
fn check_ops_is_prefix(
|
||||
sess: &ParseSess,
|
||||
node_id: NodeId,
|
||||
macros: &Stack<'_, MacroState<'_>>,
|
||||
binders: &Binders,
|
||||
ops: &Stack<'_, KleeneToken>,
|
||||
span: Span,
|
||||
name: Ident,
|
||||
) {
|
||||
let macros = macros.push(MacroState { binders, ops: ops.into() });
|
||||
// Accumulates the stacks the operators of each state until (and including when) the
|
||||
// meta-variable is found. The innermost stack is first.
|
||||
let mut acc: SmallVec<[&SmallVec<[KleeneToken; 1]>; 1]> = SmallVec::new();
|
||||
for state in ¯os {
|
||||
acc.push(&state.ops);
|
||||
if let Some(binder) = state.binders.get(&name) {
|
||||
// This variable concatenates the stack of operators from the RHS of the LHS where the
|
||||
// meta-variable was defined to where it is used (in possibly nested macros). The
|
||||
// outermost operator is first.
|
||||
let mut occurrence_ops: SmallVec<[KleeneToken; 2]> = SmallVec::new();
|
||||
// We need to iterate from the end to start with outermost stack.
|
||||
for ops in acc.iter().rev() {
|
||||
occurrence_ops.extend_from_slice(ops);
|
||||
}
|
||||
ops_is_prefix(sess, node_id, span, name, &binder.ops, &occurrence_ops);
|
||||
return;
|
||||
}
|
||||
}
|
||||
buffer_lint(sess, span.into(), node_id, &format!("unknown macro variable `{}`", name));
|
||||
}
|
||||
|
||||
/// Returns whether `binder_ops` is a prefix of `occurrence_ops`.
|
||||
///
|
||||
/// The stack of Kleene operators of a meta-variable occurrence just needs to have the stack of
|
||||
/// Kleene operators of its binder as a prefix.
|
||||
///
|
||||
/// Consider $i in the following example:
|
||||
///
|
||||
/// ( $( $i:ident = $($j:ident),+ );* ) => { $($( $i += $j; )+)* }
|
||||
///
|
||||
/// It occurs under the Kleene stack ["*", "+"] and is bound under ["*"] only.
|
||||
///
|
||||
/// Arguments:
|
||||
/// - `sess` is used to emit diagnostics and lints
|
||||
/// - `node_id` is used to emit lints
|
||||
/// - `span` is the span of the meta-variable being check
|
||||
/// - `name` is the name of the meta-variable being check
|
||||
/// - `binder_ops` is the stack of Kleene operators for the binder
|
||||
/// - `occurrence_ops` is the stack of Kleene operators for the occurrence
|
||||
fn ops_is_prefix(
|
||||
sess: &ParseSess,
|
||||
node_id: NodeId,
|
||||
span: Span,
|
||||
name: Ident,
|
||||
binder_ops: &[KleeneToken],
|
||||
occurrence_ops: &[KleeneToken],
|
||||
) {
|
||||
for (i, binder) in binder_ops.iter().enumerate() {
|
||||
if i >= occurrence_ops.len() {
|
||||
let mut span = MultiSpan::from_span(span);
|
||||
span.push_span_label(binder.span, "expected repetition".into());
|
||||
let message = &format!("variable '{}' is still repeating at this depth", name);
|
||||
buffer_lint(sess, span, node_id, message);
|
||||
return;
|
||||
}
|
||||
let occurrence = &occurrence_ops[i];
|
||||
if occurrence.op != binder.op {
|
||||
let mut span = MultiSpan::from_span(span);
|
||||
span.push_span_label(binder.span, "expected repetition".into());
|
||||
span.push_span_label(occurrence.span, "conflicting repetition".into());
|
||||
let message = "meta-variable repeats with different Kleene operator";
|
||||
buffer_lint(sess, span, node_id, message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn buffer_lint(sess: &ParseSess, span: MultiSpan, node_id: NodeId, message: &str) {
|
||||
sess.buffer_lint(BufferedEarlyLintId::MetaVariableMisuse, span, node_id, message);
|
||||
}
|
@ -2,6 +2,7 @@ use crate::edition::Edition;
|
||||
use crate::ext::base::{DummyResult, ExtCtxt, MacResult, TTMacroExpander};
|
||||
use crate::ext::base::{SyntaxExtension, SyntaxExtensionKind};
|
||||
use crate::ext::expand::{AstFragment, AstFragmentKind};
|
||||
use crate::ext::tt::macro_check;
|
||||
use crate::ext::tt::macro_parser::{parse, parse_failure_msg};
|
||||
use crate::ext::tt::macro_parser::{Error, Failure, Success};
|
||||
use crate::ext::tt::macro_parser::{MatchedNonterminal, MatchedSeq};
|
||||
@ -18,7 +19,7 @@ use crate::{ast, attr, attr::TransparencyError};
|
||||
|
||||
use errors::FatalError;
|
||||
use log::debug;
|
||||
use syntax_pos::{symbol::Ident, Span};
|
||||
use syntax_pos::Span;
|
||||
|
||||
use rustc_data_structures::fx::FxHashMap;
|
||||
use std::borrow::Cow;
|
||||
@ -366,14 +367,12 @@ pub fn compile(
|
||||
// don't abort iteration early, so that errors for multiple lhses can be reported
|
||||
for lhs in &lhses {
|
||||
valid &= check_lhs_no_empty_seq(sess, slice::from_ref(lhs));
|
||||
valid &= check_lhs_duplicate_matcher_bindings(
|
||||
sess,
|
||||
slice::from_ref(lhs),
|
||||
&mut FxHashMap::default(),
|
||||
def.id,
|
||||
);
|
||||
}
|
||||
|
||||
// We use CRATE_NODE_ID instead of `def.id` otherwise we may emit buffered lints for a node id
|
||||
// that is not lint-checked and trigger the "failed to process buffered lint here" bug.
|
||||
valid &= macro_check::check_meta_variables(sess, ast::CRATE_NODE_ID, def.span, &lhses, &rhses);
|
||||
|
||||
let expander: Box<_> =
|
||||
Box::new(MacroRulesMacroExpander { name: def.ident, span: def.span, lhses, rhses, valid });
|
||||
|
||||
@ -497,45 +496,6 @@ fn check_lhs_no_empty_seq(sess: &ParseSess, tts: &[quoted::TokenTree]) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Check that the LHS contains no duplicate matcher bindings. e.g. `$a:expr, $a:expr` would be
|
||||
/// illegal, since it would be ambiguous which `$a` to use if we ever needed to.
|
||||
fn check_lhs_duplicate_matcher_bindings(
|
||||
sess: &ParseSess,
|
||||
tts: &[quoted::TokenTree],
|
||||
metavar_names: &mut FxHashMap<Ident, Span>,
|
||||
node_id: ast::NodeId,
|
||||
) -> bool {
|
||||
use self::quoted::TokenTree;
|
||||
for tt in tts {
|
||||
match *tt {
|
||||
TokenTree::MetaVarDecl(span, name, _kind) => {
|
||||
if let Some(&prev_span) = metavar_names.get(&name) {
|
||||
sess.span_diagnostic
|
||||
.struct_span_err(span, "duplicate matcher binding")
|
||||
.span_note(prev_span, "previous declaration was here")
|
||||
.emit();
|
||||
return false;
|
||||
} else {
|
||||
metavar_names.insert(name, span);
|
||||
}
|
||||
}
|
||||
TokenTree::Delimited(_, ref del) => {
|
||||
if !check_lhs_duplicate_matcher_bindings(sess, &del.tts, metavar_names, node_id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
TokenTree::Sequence(_, ref seq) => {
|
||||
if !check_lhs_duplicate_matcher_bindings(sess, &seq.tts, metavar_names, node_id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn check_rhs(sess: &ParseSess, rhs: "ed::TokenTree) -> bool {
|
||||
match *rhs {
|
||||
quoted::TokenTree::Delimited(..) => return true,
|
||||
|
@ -123,6 +123,22 @@ impl TokenTree {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the given token tree is delimited.
|
||||
pub fn is_delimited(&self) -> bool {
|
||||
match *self {
|
||||
TokenTree::Delimited(..) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the given token tree is a token of the given kind.
|
||||
pub fn is_token(&self, expected_kind: &TokenKind) -> bool {
|
||||
match self {
|
||||
TokenTree::Token(Token { kind: actual_kind, .. }) => actual_kind == expected_kind,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the `index`-th sub-token-tree. This only makes sense for delimited trees and sequences.
|
||||
pub fn get_tt(&self, index: usize) -> TokenTree {
|
||||
match (self, index) {
|
||||
|
@ -174,6 +174,7 @@ pub mod ext {
|
||||
|
||||
pub mod tt {
|
||||
pub mod transcribe;
|
||||
pub mod macro_check;
|
||||
pub mod macro_parser;
|
||||
pub mod macro_rules;
|
||||
pub mod quoted;
|
||||
|
34
src/test/run-pass/macros/meta-variable-misuse.rs
Normal file
34
src/test/run-pass/macros/meta-variable-misuse.rs
Normal file
@ -0,0 +1,34 @@
|
||||
// run-pass
|
||||
#![deny(meta_variable_misuse)]
|
||||
|
||||
macro_rules! foo {
|
||||
($($m:ident $($f:ident $v:tt)+),*) => {
|
||||
$($(macro_rules! $f { () => { $v } })+)*
|
||||
$(macro_rules! $m { () => { $(fn $f() -> i32 { $v })+ } })*
|
||||
}
|
||||
}
|
||||
|
||||
foo!(m a 1 b 2, n c 3);
|
||||
m!();
|
||||
n!();
|
||||
|
||||
macro_rules! no_shadow {
|
||||
($x:tt) => { macro_rules! bar { ($x:tt) => { 42 }; } };
|
||||
}
|
||||
no_shadow!(z);
|
||||
|
||||
macro_rules! make_plus {
|
||||
($n: ident $x:expr) => { macro_rules! $n { ($y:expr) => { $x + $y }; } };
|
||||
}
|
||||
make_plus!(add3 3);
|
||||
|
||||
fn main() {
|
||||
assert_eq!(a!(), 1);
|
||||
assert_eq!(b!(), 2);
|
||||
assert_eq!(c!(), 3);
|
||||
assert_eq!(a(), 1);
|
||||
assert_eq!(b(), 2);
|
||||
assert_eq!(c(), 3);
|
||||
assert_eq!(bar!(z:tt), 42);
|
||||
assert_eq!(add3!(9), 12);
|
||||
}
|
30
src/test/ui/macros/issue-61053-different-kleene.rs
Normal file
30
src/test/ui/macros/issue-61053-different-kleene.rs
Normal file
@ -0,0 +1,30 @@
|
||||
#![deny(meta_variable_misuse)]
|
||||
|
||||
macro_rules! foo {
|
||||
() => {};
|
||||
( $( $i:ident = $($j:ident),+ );* ) => { $( $( $i = $j; )* )* };
|
||||
//~^ ERROR meta-variable repeats with
|
||||
( $( $($j:ident),+ );* ) => { $( $( $j; )+ )+ }; //~ERROR meta-variable repeats with
|
||||
}
|
||||
|
||||
macro_rules! bar {
|
||||
() => {};
|
||||
(test) => {
|
||||
macro_rules! nested {
|
||||
() => {};
|
||||
( $( $i:ident = $($j:ident),+ );* ) => { $( $( $i = $j; )* )* };
|
||||
//~^ ERROR meta-variable repeats with
|
||||
( $( $($j:ident),+ );* ) => { $( $( $j; )+ )+ }; //~ERROR meta-variable repeats with
|
||||
}
|
||||
};
|
||||
( $( $i:ident = $($j:ident),+ );* ) => {
|
||||
$(macro_rules! $i {
|
||||
() => { 0 $( + $j )* }; //~ ERROR meta-variable repeats with
|
||||
})*
|
||||
};
|
||||
}
|
||||
|
||||
fn main() {
|
||||
foo!();
|
||||
bar!();
|
||||
}
|
45
src/test/ui/macros/issue-61053-different-kleene.stderr
Normal file
45
src/test/ui/macros/issue-61053-different-kleene.stderr
Normal file
@ -0,0 +1,45 @@
|
||||
error: meta-variable repeats with different Kleene operator
|
||||
--> $DIR/issue-61053-different-kleene.rs:5:57
|
||||
|
|
||||
LL | ( $( $i:ident = $($j:ident),+ );* ) => { $( $( $i = $j; )* )* };
|
||||
| - expected repetition ^^ - conflicting repetition
|
||||
|
|
||||
note: lint level defined here
|
||||
--> $DIR/issue-61053-different-kleene.rs:1:9
|
||||
|
|
||||
LL | #![deny(meta_variable_misuse)]
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: meta-variable repeats with different Kleene operator
|
||||
--> $DIR/issue-61053-different-kleene.rs:7:41
|
||||
|
|
||||
LL | ( $( $($j:ident),+ );* ) => { $( $( $j; )+ )+ };
|
||||
| - ^^ - conflicting repetition
|
||||
| |
|
||||
| expected repetition
|
||||
|
||||
error: meta-variable repeats with different Kleene operator
|
||||
--> $DIR/issue-61053-different-kleene.rs:15:65
|
||||
|
|
||||
LL | ( $( $i:ident = $($j:ident),+ );* ) => { $( $( $i = $j; )* )* };
|
||||
| - expected repetition ^^ - conflicting repetition
|
||||
|
||||
error: meta-variable repeats with different Kleene operator
|
||||
--> $DIR/issue-61053-different-kleene.rs:17:49
|
||||
|
|
||||
LL | ( $( $($j:ident),+ );* ) => { $( $( $j; )+ )+ };
|
||||
| - ^^ - conflicting repetition
|
||||
| |
|
||||
| expected repetition
|
||||
|
||||
error: meta-variable repeats with different Kleene operator
|
||||
--> $DIR/issue-61053-different-kleene.rs:22:28
|
||||
|
|
||||
LL | ( $( $i:ident = $($j:ident),+ );* ) => {
|
||||
| - expected repetition
|
||||
LL | $(macro_rules! $i {
|
||||
LL | () => { 0 $( + $j )* };
|
||||
| ^^ - conflicting repetition
|
||||
|
||||
error: aborting due to 5 previous errors
|
||||
|
14
src/test/ui/macros/issue-61053-duplicate-binder.rs
Normal file
14
src/test/ui/macros/issue-61053-duplicate-binder.rs
Normal file
@ -0,0 +1,14 @@
|
||||
#![deny(meta_variable_misuse)]
|
||||
|
||||
macro_rules! foo {
|
||||
() => {};
|
||||
(error) => {
|
||||
macro_rules! bar {
|
||||
($x:tt $x:tt) => { $x }; //~ ERROR duplicate matcher binding
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn main() {
|
||||
foo!();
|
||||
}
|
16
src/test/ui/macros/issue-61053-duplicate-binder.stderr
Normal file
16
src/test/ui/macros/issue-61053-duplicate-binder.stderr
Normal file
@ -0,0 +1,16 @@
|
||||
error: duplicate matcher binding
|
||||
--> $DIR/issue-61053-duplicate-binder.rs:7:20
|
||||
|
|
||||
LL | ($x:tt $x:tt) => { $x };
|
||||
| -- ^^
|
||||
| |
|
||||
| previous declaration
|
||||
|
|
||||
note: lint level defined here
|
||||
--> $DIR/issue-61053-duplicate-binder.rs:1:9
|
||||
|
|
||||
LL | #![deny(meta_variable_misuse)]
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: aborting due to previous error
|
||||
|
28
src/test/ui/macros/issue-61053-missing-repetition.rs
Normal file
28
src/test/ui/macros/issue-61053-missing-repetition.rs
Normal file
@ -0,0 +1,28 @@
|
||||
#![deny(meta_variable_misuse)]
|
||||
|
||||
macro_rules! foo {
|
||||
() => {};
|
||||
($( $i:ident = $($j:ident),+ );*) => { $( $i = $j; )* };
|
||||
//~^ ERROR variable 'j' is still repeating
|
||||
}
|
||||
|
||||
macro_rules! bar {
|
||||
() => {};
|
||||
(test) => {
|
||||
macro_rules! nested {
|
||||
() => {};
|
||||
($( $i:ident = $($j:ident),+ );*) => { $( $i = $j; )* };
|
||||
//~^ ERROR variable 'j' is still repeating
|
||||
}
|
||||
};
|
||||
( $( $i:ident = $($j:ident),+ );* ) => {
|
||||
$(macro_rules! $i {
|
||||
() => { $j }; //~ ERROR variable 'j' is still repeating
|
||||
})*
|
||||
};
|
||||
}
|
||||
|
||||
fn main() {
|
||||
foo!();
|
||||
bar!();
|
||||
}
|
33
src/test/ui/macros/issue-61053-missing-repetition.stderr
Normal file
33
src/test/ui/macros/issue-61053-missing-repetition.stderr
Normal file
@ -0,0 +1,33 @@
|
||||
error: variable 'j' is still repeating at this depth
|
||||
--> $DIR/issue-61053-missing-repetition.rs:5:52
|
||||
|
|
||||
LL | ($( $i:ident = $($j:ident),+ );*) => { $( $i = $j; )* };
|
||||
| - ^^
|
||||
| |
|
||||
| expected repetition
|
||||
|
|
||||
note: lint level defined here
|
||||
--> $DIR/issue-61053-missing-repetition.rs:1:9
|
||||
|
|
||||
LL | #![deny(meta_variable_misuse)]
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: variable 'j' is still repeating at this depth
|
||||
--> $DIR/issue-61053-missing-repetition.rs:14:60
|
||||
|
|
||||
LL | ($( $i:ident = $($j:ident),+ );*) => { $( $i = $j; )* };
|
||||
| - ^^
|
||||
| |
|
||||
| expected repetition
|
||||
|
||||
error: variable 'j' is still repeating at this depth
|
||||
--> $DIR/issue-61053-missing-repetition.rs:20:21
|
||||
|
|
||||
LL | ( $( $i:ident = $($j:ident),+ );* ) => {
|
||||
| - expected repetition
|
||||
LL | $(macro_rules! $i {
|
||||
LL | () => { $j };
|
||||
| ^^
|
||||
|
||||
error: aborting due to 3 previous errors
|
||||
|
28
src/test/ui/macros/issue-61053-unbound.rs
Normal file
28
src/test/ui/macros/issue-61053-unbound.rs
Normal file
@ -0,0 +1,28 @@
|
||||
#![deny(meta_variable_misuse)]
|
||||
|
||||
macro_rules! foo {
|
||||
() => {};
|
||||
($( $i:ident = $($j:ident),+ );*) => { $( $( $i = $k; )+ )* };
|
||||
//~^ ERROR unknown macro variable
|
||||
}
|
||||
|
||||
macro_rules! bar {
|
||||
() => {};
|
||||
(test) => {
|
||||
macro_rules! nested {
|
||||
() => {};
|
||||
($( $i:ident = $($j:ident),+ );*) => { $( $( $i = $k; )+ )* };
|
||||
//~^ ERROR unknown macro variable
|
||||
}
|
||||
};
|
||||
( $( $i:ident = $($j:ident),+ );* ) => {
|
||||
$(macro_rules! $i {
|
||||
() => { $( $i = $k)+ }; //~ ERROR unknown macro variable
|
||||
})*
|
||||
};
|
||||
}
|
||||
|
||||
fn main() {
|
||||
foo!();
|
||||
bar!();
|
||||
}
|
26
src/test/ui/macros/issue-61053-unbound.stderr
Normal file
26
src/test/ui/macros/issue-61053-unbound.stderr
Normal file
@ -0,0 +1,26 @@
|
||||
error: unknown macro variable `k`
|
||||
--> $DIR/issue-61053-unbound.rs:5:55
|
||||
|
|
||||
LL | ($( $i:ident = $($j:ident),+ );*) => { $( $( $i = $k; )+ )* };
|
||||
| ^^
|
||||
|
|
||||
note: lint level defined here
|
||||
--> $DIR/issue-61053-unbound.rs:1:9
|
||||
|
|
||||
LL | #![deny(meta_variable_misuse)]
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: unknown macro variable `k`
|
||||
--> $DIR/issue-61053-unbound.rs:14:63
|
||||
|
|
||||
LL | ($( $i:ident = $($j:ident),+ );*) => { $( $( $i = $k; )+ )* };
|
||||
| ^^
|
||||
|
||||
error: unknown macro variable `k`
|
||||
--> $DIR/issue-61053-unbound.rs:20:29
|
||||
|
|
||||
LL | () => { $( $i = $k)+ };
|
||||
| ^^
|
||||
|
||||
error: aborting due to 3 previous errors
|
||||
|
Loading…
Reference in New Issue
Block a user