Skip to main content

rustc_expand/mbe/
macro_check.rs

1//! Checks that meta-variables in macro definition are correctly declared and used.
2//!
3//! # What is checked
4//!
5//! ## Meta-variables must not be bound twice
6//!
7//! ```compile_fail
8//! macro_rules! foo { ($x:tt $x:tt) => { $x }; }
9//! ```
10//!
11//! This check is sound (no false-negative) and complete (no false-positive).
12//!
13//! ## Meta-variables must not be free
14//!
15//! ```
16//! macro_rules! foo { () => { $x }; }
17//! ```
18//!
19//! This check is also done at macro instantiation but only if the branch is taken.
20//!
21//! ## Meta-variables must repeat at least as many times as their binder
22//!
23//! ```
24//! macro_rules! foo { ($($x:tt)*) => { $x }; }
25//! ```
26//!
27//! This check is also done at macro instantiation but only if the branch is taken.
28//!
29//! ## Meta-variables must repeat with the same Kleene operators as their binder
30//!
31//! ```
32//! macro_rules! foo { ($($x:tt)+) => { $($x)* }; }
33//! ```
34//!
35//! This check is not done at macro instantiation.
36//!
37//! # Disclaimer
38//!
39//! In the presence of nested macros (a macro defined in a macro), those checks may have false
40//! positives and false negatives. We try to detect those cases by recognizing potential macro
41//! definitions in RHSes, but nested macros may be hidden through the use of particular values of
42//! meta-variables.
43//!
44//! ## Examples of false positive
45//!
46//! False positives can come from cases where we don't recognize a nested macro, because it depends
47//! on particular values of meta-variables. In the following example, we think both instances of
48//! `$x` are free, which is a correct statement if `$name` is anything but `macro_rules`. But when
49//! `$name` is `macro_rules`, like in the instantiation below, then `$x:tt` is actually a binder of
50//! the nested macro and `$x` is bound to it.
51//!
52//! ```
53//! macro_rules! foo { ($name:ident) => { $name! bar { ($x:tt) => { $x }; } }; }
54//! foo!(macro_rules);
55//! ```
56//!
57//! False positives can also come from cases where we think there is a nested macro while there
58//! isn't. In the following example, we think `$x` is free, which is incorrect because `bar` is not
59//! a nested macro since it is not evaluated as code by `stringify!`.
60//!
61//! ```
62//! macro_rules! foo { () => { stringify!(macro_rules! bar { () => { $x }; }) }; }
63//! ```
64//!
65//! ## Examples of false negative
66//!
67//! False negatives can come from cases where we don't recognize a meta-variable, because it depends
68//! on particular values of meta-variables. In the following examples, we don't see that if `$d` is
69//! instantiated with `$` then `$d z` becomes `$z` in the nested macro definition and is thus a free
70//! meta-variable. Note however, that if `foo` is instantiated, then we would check the definition
71//! of `bar` and would see the issue.
72//!
73//! ```
74//! macro_rules! foo { ($d:tt) => { macro_rules! bar { ($y:tt) => { $d z }; } }; }
75//! ```
76//!
77//! # How it is checked
78//!
79//! There are 3 main functions: `check_binders`, `check_occurrences`, and `check_nested_macro`. They
80//! all need some kind of environment.
81//!
82//! ## Environments
83//!
84//! Environments are used to pass information.
85//!
86//! ### From LHS to RHS
87//!
88//! When checking a LHS with `check_binders`, we produce (and use) an environment for binders,
89//! namely `Binders`. This is a mapping from binder name to information about that binder: the span
90//! of the binder for error messages and the stack of Kleene operators under which it was bound in
91//! the LHS.
92//!
93//! This environment is used by both the LHS and RHS. The LHS uses it to detect duplicate binders.
94//! The RHS uses it to detect the other errors.
95//!
96//! ### From outer macro to inner macro
97//!
98//! When checking the RHS of an outer macro and we detect a nested macro definition, we push the
99//! current state, namely `MacroState`, to an environment of nested macro definitions. Each state
100//! stores the LHS binders when entering the macro definition as well as the stack of Kleene
101//! operators under which the inner macro is defined in the RHS.
102//!
103//! This environment is a stack representing the nesting of macro definitions. As such, the stack of
104//! Kleene operators under which a meta-variable is repeating is the concatenation of the stacks
105//! stored when entering a macro definition starting from the state in which the meta-variable is
106//! bound.
107
108use rustc_ast::token::{Delimiter, IdentKind, Token, TokenKind};
109use rustc_ast::{DUMMY_NODE_ID, NodeId};
110use rustc_data_structures::fx::FxHashMap;
111use rustc_errors::DecorateDiagCompat;
112use rustc_lint_defs::builtin::META_VARIABLE_MISUSE;
113use rustc_session::parse::ParseSess;
114use rustc_span::{ErrorGuaranteed, MacroRulesNormalizedIdent, Span, kw};
115use smallvec::SmallVec;
116
117use crate::diagnostics;
118use crate::mbe::{KleeneToken, TokenTree};
119
120/// Stack represented as linked list.
121///
122/// Those are used for environments because they grow incrementally and are not mutable.
123enum Stack<'a, T> {
124    /// Empty stack.
125    Empty,
126    /// A non-empty stack.
127    Push {
128        /// The top element.
129        top: T,
130        /// The previous elements.
131        prev: &'a Stack<'a, T>,
132    },
133}
134
135impl<'a, T> Stack<'a, T> {
136    /// Returns whether a stack is empty.
137    fn is_empty(&self) -> bool {
138        #[allow(non_exhaustive_omitted_patterns)] match *self {
    Stack::Empty => true,
    _ => false,
}matches!(*self, Stack::Empty)
139    }
140
141    /// Returns a new stack with an element of top.
142    fn push(&'a self, top: T) -> Stack<'a, T> {
143        Stack::Push { top, prev: self }
144    }
145}
146
147impl<'a, T> Iterator for &'a Stack<'a, T> {
148    type Item = &'a T;
149
150    // Iterates from top to bottom of the stack.
151    fn next(&mut self) -> Option<&'a T> {
152        match self {
153            Stack::Empty => None,
154            Stack::Push { top, prev } => {
155                *self = prev;
156                Some(top)
157            }
158        }
159    }
160}
161
162impl From<&Stack<'_, KleeneToken>> for SmallVec<[KleeneToken; 1]> {
163    fn from(ops: &Stack<'_, KleeneToken>) -> SmallVec<[KleeneToken; 1]> {
164        let mut ops: SmallVec<[KleeneToken; 1]> = ops.cloned().collect();
165        // The stack is innermost on top. We want outermost first.
166        ops.reverse();
167        ops
168    }
169}
170
171/// Information attached to a meta-variable binder in LHS.
172struct BinderInfo {
173    /// The span of the meta-variable in LHS.
174    span: Span,
175    /// The stack of Kleene operators (outermost first).
176    ops: SmallVec<[KleeneToken; 1]>,
177}
178
179/// An environment of meta-variables to their binder information.
180type Binders = FxHashMap<MacroRulesNormalizedIdent, BinderInfo>;
181
182/// The state at which we entered a macro definition in the RHS of another macro definition.
183struct MacroState<'a> {
184    /// The binders of the branch where we entered the macro definition.
185    binders: &'a Binders,
186    /// The stack of Kleene operators (outermost first) where we entered the macro definition.
187    ops: SmallVec<[KleeneToken; 1]>,
188}
189
190/// Checks that meta-variables are used correctly in one rule of a macro definition.
191///
192/// Arguments:
193/// - `psess` is used to emit diagnostics and lints
194/// - `node_id` is used to emit lints
195/// - `args`, `lhs`, and `rhs` represent the rule
196pub(super) fn check_meta_variables(
197    psess: &ParseSess,
198    node_id: NodeId,
199    args: Option<&TokenTree>,
200    lhs: &TokenTree,
201    rhs: &TokenTree,
202) -> Result<(), ErrorGuaranteed> {
203    let mut guar = None;
204    let mut binders = Binders::default();
205    if let Some(args) = args {
206        check_binders(psess, node_id, args, &Stack::Empty, &mut binders, &Stack::Empty, &mut guar);
207    }
208    check_binders(psess, node_id, lhs, &Stack::Empty, &mut binders, &Stack::Empty, &mut guar);
209    check_occurrences(psess, node_id, rhs, &Stack::Empty, &binders, &Stack::Empty, &mut guar);
210    guar.map_or(Ok(()), Err)
211}
212
213/// Checks `lhs` as part of the LHS of a macro definition.
214///
215/// Arguments:
216/// - `psess` is used to emit diagnostics and lints
217/// - `node_id` is used to emit lints
218/// - `lhs` is checked as part of a LHS
219/// - `macros` is the stack of possible outer macros
220/// - `binders` contains the binders of the LHS
221/// - `ops` is the stack of Kleene operators from the LHS
222/// - `guar` is set in case of errors
223fn check_binders(
224    psess: &ParseSess,
225    node_id: NodeId,
226    lhs: &TokenTree,
227    macros: &Stack<'_, MacroState<'_>>,
228    binders: &mut Binders,
229    ops: &Stack<'_, KleeneToken>,
230    guar: &mut Option<ErrorGuaranteed>,
231) {
232    match *lhs {
233        TokenTree::Token(..) => {}
234        // This can only happen when checking a nested macro because this LHS is then in the RHS of
235        // the outer macro. See ui/macros/macro-of-higher-order.rs where $y:$fragment in the
236        // LHS of the nested macro (and RHS of the outer macro) is parsed as MetaVar(y) Colon
237        // MetaVar(fragment) and not as MetaVarDecl(y, fragment).
238        TokenTree::MetaVar(span, name) => {
239            if macros.is_empty() {
240                psess.dcx().span_bug(span, "unexpected MetaVar in lhs");
241            }
242            let name = MacroRulesNormalizedIdent::new(name);
243            // There are 3 possibilities:
244            if let Some(prev_info) = binders.get(&name) {
245                // 1. The meta-variable is already bound in the current LHS: This is an error.
246                buffer_lint(
247                    psess,
248                    span,
249                    node_id,
250                    diagnostics::DuplicateMatcherBindingLint { span, prev: prev_info.span },
251                );
252            } else if get_binder_info(macros, binders, name).is_none() {
253                // 2. The meta-variable is free: This is a binder.
254                binders.insert(name, BinderInfo { span, ops: ops.into() });
255            } else {
256                // 3. The meta-variable is bound: This is an occurrence.
257                check_occurrences(psess, node_id, lhs, macros, binders, ops, guar);
258            }
259        }
260        // Similarly, this can only happen when checking a toplevel macro.
261        TokenTree::MetaVarDecl { span, name, .. } => {
262            if !macros.is_empty() {
263                psess.dcx().span_bug(span, "unexpected MetaVarDecl in nested lhs");
264            }
265            let name = MacroRulesNormalizedIdent::new(name);
266            if let Some(prev_info) = get_binder_info(macros, binders, name) {
267                // Duplicate binders at the top-level macro definition are errors. The lint is only
268                // for nested macro definitions.
269                *guar =
270                    Some(psess.dcx().emit_err(diagnostics::DuplicateMatcherBinding {
271                        span,
272                        prev: prev_info.span,
273                    }));
274            } else {
275                binders.insert(name, BinderInfo { span, ops: ops.into() });
276            }
277        }
278        // `MetaVarExpr` can not appear in the LHS of a macro arm
279        TokenTree::MetaVarExpr(..) => {}
280        TokenTree::Delimited(.., ref del) => {
281            for tt in &del.tts {
282                check_binders(psess, node_id, tt, macros, binders, ops, guar);
283            }
284        }
285        TokenTree::Sequence(_, ref seq) => {
286            let ops = ops.push(seq.kleene);
287            for tt in &seq.tts {
288                check_binders(psess, node_id, tt, macros, binders, &ops, guar);
289            }
290        }
291    }
292}
293
294/// Returns the binder information of a meta-variable.
295///
296/// Arguments:
297/// - `macros` is the stack of possible outer macros
298/// - `binders` contains the current binders
299/// - `name` is the name of the meta-variable we are looking for
300fn get_binder_info<'a>(
301    mut macros: &'a Stack<'a, MacroState<'a>>,
302    binders: &'a Binders,
303    name: MacroRulesNormalizedIdent,
304) -> Option<&'a BinderInfo> {
305    binders.get(&name).or_else(|| macros.find_map(|state| state.binders.get(&name)))
306}
307
308/// Checks `rhs` as part of the RHS of a macro definition.
309///
310/// Arguments:
311/// - `psess` is used to emit diagnostics and lints
312/// - `node_id` is used to emit lints
313/// - `rhs` is checked as part of a RHS
314/// - `macros` is the stack of possible outer macros
315/// - `binders` contains the binders of the associated LHS
316/// - `ops` is the stack of Kleene operators from the RHS
317/// - `guar` is set in case of errors
318fn check_occurrences(
319    psess: &ParseSess,
320    node_id: NodeId,
321    rhs: &TokenTree,
322    macros: &Stack<'_, MacroState<'_>>,
323    binders: &Binders,
324    ops: &Stack<'_, KleeneToken>,
325    guar: &mut Option<ErrorGuaranteed>,
326) {
327    match *rhs {
328        TokenTree::Token(..) => {}
329        TokenTree::MetaVarDecl { span, .. } => {
330            psess.dcx().span_bug(span, "unexpected MetaVarDecl in rhs")
331        }
332        TokenTree::MetaVar(span, name) => {
333            let name = MacroRulesNormalizedIdent::new(name);
334            check_ops_is_prefix(psess, node_id, macros, binders, ops, span, name);
335        }
336        TokenTree::MetaVarExpr(dl, ref mve) => {
337            mve.for_each_metavar((), |_, ident| {
338                let name = MacroRulesNormalizedIdent::new(*ident);
339                check_ops_is_prefix(psess, node_id, macros, binders, ops, dl.entire(), name);
340            });
341        }
342        TokenTree::Delimited(.., ref del) => {
343            check_nested_occurrences(psess, node_id, &del.tts, macros, binders, ops, guar);
344        }
345        TokenTree::Sequence(_, ref seq) => {
346            let ops = ops.push(seq.kleene);
347            check_nested_occurrences(psess, node_id, &seq.tts, macros, binders, &ops, guar);
348        }
349    }
350}
351
352/// Represents the processed prefix of a nested macro.
353#[derive(#[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for NestedMacroState { }
#[automatically_derived]
impl ::core::clone::Clone for NestedMacroState {
    #[inline]
    fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for NestedMacroState { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for NestedMacroState { }
#[automatically_derived]
impl ::core::cmp::PartialEq for NestedMacroState {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        ::core::intrinsics::discriminant_value(self) ==
            ::core::intrinsics::discriminant_value(other)
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for NestedMacroState { }Eq)]
354enum NestedMacroState {
355    /// Nothing that matches a nested macro definition was processed yet.
356    Empty,
357    /// The token `macro_rules` was processed.
358    MacroRules,
359    /// The tokens `macro_rules!` were processed.
360    MacroRulesBang,
361    /// The tokens `macro_rules!` followed by a name were processed. The name may be either directly
362    /// an identifier or a meta-variable (that hopefully would be instantiated by an identifier).
363    MacroRulesBangName,
364    /// The keyword `macro` was processed.
365    Macro,
366    /// The keyword `macro` followed by a name was processed.
367    MacroName,
368    /// The keyword `macro` followed by a name and a token delimited by parentheses was processed.
369    MacroNameParen,
370}
371
372/// Checks `tts` as part of the RHS of a macro definition, tries to recognize nested macro
373/// definitions.
374///
375/// Arguments:
376/// - `psess` is used to emit diagnostics and lints
377/// - `node_id` is used to emit lints
378/// - `tts` is checked as part of a RHS and may contain macro definitions
379/// - `macros` is the stack of possible outer macros
380/// - `binders` contains the binders of the associated LHS
381/// - `ops` is the stack of Kleene operators from the RHS
382/// - `guar` is set in case of errors
383fn check_nested_occurrences(
384    psess: &ParseSess,
385    node_id: NodeId,
386    tts: &[TokenTree],
387    macros: &Stack<'_, MacroState<'_>>,
388    binders: &Binders,
389    ops: &Stack<'_, KleeneToken>,
390    guar: &mut Option<ErrorGuaranteed>,
391) {
392    let mut state = NestedMacroState::Empty;
393    let nested_macros = macros.push(MacroState { binders, ops: ops.into() });
394    let mut nested_binders = Binders::default();
395    for tt in tts {
396        match (state, tt) {
397            (
398                NestedMacroState::Empty,
399                &TokenTree::Token(Token {
400                    kind: TokenKind::Ident(name, IdentKind::Normal), ..
401                }),
402            ) => {
403                if name == kw::MacroRules {
404                    state = NestedMacroState::MacroRules;
405                } else if name == kw::Macro {
406                    state = NestedMacroState::Macro;
407                }
408            }
409            (
410                NestedMacroState::MacroRules,
411                &TokenTree::Token(Token { kind: TokenKind::Bang, .. }),
412            ) => {
413                state = NestedMacroState::MacroRulesBang;
414            }
415            (
416                NestedMacroState::MacroRulesBang,
417                &TokenTree::Token(Token { kind: TokenKind::Ident(..), .. }),
418            ) => {
419                state = NestedMacroState::MacroRulesBangName;
420            }
421            (NestedMacroState::MacroRulesBang, &TokenTree::MetaVar(..)) => {
422                state = NestedMacroState::MacroRulesBangName;
423                // We check that the meta-variable is correctly used.
424                check_occurrences(psess, node_id, tt, macros, binders, ops, guar);
425            }
426            (NestedMacroState::MacroRulesBangName, TokenTree::Delimited(.., del))
427            | (NestedMacroState::MacroName, TokenTree::Delimited(.., del))
428                if del.delim == Delimiter::Brace =>
429            {
430                let macro_rules = state == NestedMacroState::MacroRulesBangName;
431                state = NestedMacroState::Empty;
432                let rest =
433                    check_nested_macro(psess, node_id, macro_rules, &del.tts, &nested_macros, guar);
434                // If we did not check the whole macro definition, then check the rest as if outside
435                // the macro definition.
436                check_nested_occurrences(
437                    psess,
438                    node_id,
439                    &del.tts[rest..],
440                    macros,
441                    binders,
442                    ops,
443                    guar,
444                );
445            }
446            (
447                NestedMacroState::Macro,
448                &TokenTree::Token(Token { kind: TokenKind::Ident(..), .. }),
449            ) => {
450                state = NestedMacroState::MacroName;
451            }
452            (NestedMacroState::Macro, &TokenTree::MetaVar(..)) => {
453                state = NestedMacroState::MacroName;
454                // We check that the meta-variable is correctly used.
455                check_occurrences(psess, node_id, tt, macros, binders, ops, guar);
456            }
457            (NestedMacroState::MacroName, TokenTree::Delimited(.., del))
458                if del.delim == Delimiter::Parenthesis =>
459            {
460                state = NestedMacroState::MacroNameParen;
461                nested_binders = Binders::default();
462                check_binders(
463                    psess,
464                    node_id,
465                    tt,
466                    &nested_macros,
467                    &mut nested_binders,
468                    &Stack::Empty,
469                    guar,
470                );
471            }
472            (NestedMacroState::MacroNameParen, TokenTree::Delimited(.., del))
473                if del.delim == Delimiter::Brace =>
474            {
475                state = NestedMacroState::Empty;
476                check_occurrences(
477                    psess,
478                    node_id,
479                    tt,
480                    &nested_macros,
481                    &nested_binders,
482                    &Stack::Empty,
483                    guar,
484                );
485            }
486            (_, tt) => {
487                state = NestedMacroState::Empty;
488                check_occurrences(psess, node_id, tt, macros, binders, ops, guar);
489            }
490        }
491    }
492}
493
494/// Checks the body of nested macro, returns where the check stopped.
495///
496/// The token trees are checked as long as they look like a list of (LHS) => {RHS} token trees. This
497/// check is a best-effort to detect a macro definition. It returns the position in `tts` where we
498/// stopped checking because we detected we were not in a macro definition anymore.
499///
500/// Arguments:
501/// - `psess` is used to emit diagnostics and lints
502/// - `node_id` is used to emit lints
503/// - `macro_rules` specifies whether the macro is `macro_rules`
504/// - `tts` is checked as a list of (LHS) => {RHS}
505/// - `macros` is the stack of outer macros
506/// - `guar` is set in case of errors
507fn check_nested_macro(
508    psess: &ParseSess,
509    node_id: NodeId,
510    macro_rules: bool,
511    tts: &[TokenTree],
512    macros: &Stack<'_, MacroState<'_>>,
513    guar: &mut Option<ErrorGuaranteed>,
514) -> usize {
515    let n = tts.len();
516    let mut i = 0;
517    let separator = if macro_rules { TokenKind::Semi } else { TokenKind::Comma };
518    loop {
519        // We expect 3 token trees: `(LHS) => {RHS}`. The separator is checked after.
520        if i + 2 >= n
521            || !tts[i].is_delimited()
522            || !tts[i + 1].is_token(&TokenKind::FatArrow)
523            || !tts[i + 2].is_delimited()
524        {
525            break;
526        }
527        let lhs = &tts[i];
528        let rhs = &tts[i + 2];
529        let mut binders = Binders::default();
530        check_binders(psess, node_id, lhs, macros, &mut binders, &Stack::Empty, guar);
531        check_occurrences(psess, node_id, rhs, macros, &binders, &Stack::Empty, guar);
532        // Since the last semicolon is optional for `macro_rules` macros and decl_macro are not terminated,
533        // we increment our checked position by how many token trees we already checked (the 3
534        // above) before checking for the separator.
535        i += 3;
536        if i == n || !tts[i].is_token(&separator) {
537            break;
538        }
539        // We increment our checked position for the semicolon.
540        i += 1;
541    }
542    i
543}
544
545/// Checks that a meta-variable occurrence is valid.
546///
547/// Arguments:
548/// - `psess` is used to emit diagnostics and lints
549/// - `node_id` is used to emit lints
550/// - `macros` is the stack of possible outer macros
551/// - `binders` contains the binders of the associated LHS
552/// - `ops` is the stack of Kleene operators from the RHS
553/// - `span` is the span of the meta-variable to check
554/// - `name` is the name of the meta-variable to check
555fn check_ops_is_prefix(
556    psess: &ParseSess,
557    node_id: NodeId,
558    macros: &Stack<'_, MacroState<'_>>,
559    binders: &Binders,
560    ops: &Stack<'_, KleeneToken>,
561    span: Span,
562    name: MacroRulesNormalizedIdent,
563) {
564    let macros = macros.push(MacroState { binders, ops: ops.into() });
565    // Accumulates the stacks the operators of each state until (and including when) the
566    // meta-variable is found. The innermost stack is first.
567    let mut acc: SmallVec<[&SmallVec<[KleeneToken; 1]>; 1]> = SmallVec::new();
568    for state in &macros {
569        acc.push(&state.ops);
570        if let Some(binder) = state.binders.get(&name) {
571            // This variable concatenates the stack of operators from the RHS of the LHS where the
572            // meta-variable was defined to where it is used (in possibly nested macros). The
573            // outermost operator is first.
574            let mut occurrence_ops: SmallVec<[KleeneToken; 2]> = SmallVec::new();
575            // We need to iterate from the end to start with outermost stack.
576            for ops in acc.iter().rev() {
577                occurrence_ops.extend_from_slice(ops);
578            }
579            ops_is_prefix(psess, node_id, span, name, &binder.ops, &occurrence_ops);
580            return;
581        }
582    }
583    buffer_lint(psess, span, node_id, diagnostics::UnknownMacroVariable { name });
584}
585
586/// Returns whether `binder_ops` is a prefix of `occurrence_ops`.
587///
588/// The stack of Kleene operators of a meta-variable occurrence just needs to have the stack of
589/// Kleene operators of its binder as a prefix.
590///
591/// Consider $i in the following example:
592/// ```ignore (illustrative)
593/// ( $( $i:ident = $($j:ident),+ );* ) => { $($( $i += $j; )+)* }
594/// ```
595/// It occurs under the Kleene stack ["*", "+"] and is bound under ["*"] only.
596///
597/// Arguments:
598/// - `psess` is used to emit diagnostics and lints
599/// - `node_id` is used to emit lints
600/// - `span` is the span of the meta-variable being check
601/// - `name` is the name of the meta-variable being check
602/// - `binder_ops` is the stack of Kleene operators for the binder
603/// - `occurrence_ops` is the stack of Kleene operators for the occurrence
604fn ops_is_prefix(
605    psess: &ParseSess,
606    node_id: NodeId,
607    span: Span,
608    ident: MacroRulesNormalizedIdent,
609    binder_ops: &[KleeneToken],
610    occurrence_ops: &[KleeneToken],
611) {
612    for (i, binder) in binder_ops.iter().enumerate() {
613        if i >= occurrence_ops.len() {
614            buffer_lint(
615                psess,
616                span,
617                node_id,
618                diagnostics::MetaVarStillRepeatingLint { label: binder.span, ident },
619            );
620            return;
621        }
622        let occurrence = &occurrence_ops[i];
623        if occurrence.op != binder.op {
624            buffer_lint(
625                psess,
626                span,
627                node_id,
628                diagnostics::MetaVariableWrongOperator {
629                    binder: binder.span,
630                    occurrence: occurrence.span,
631                },
632            );
633            return;
634        }
635    }
636}
637
638fn buffer_lint(
639    psess: &ParseSess,
640    span: Span,
641    node_id: NodeId,
642    diag: impl Into<DecorateDiagCompat>,
643) {
644    // Macros loaded from other crates have dummy node ids.
645    if node_id != DUMMY_NODE_ID {
646        psess.buffer_lint(META_VARIABLE_MISUSE, span, node_id, diag);
647    }
648}