From 47075aec14ba8035a2fdd2a278cdbdd5f5626433 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 7 Nov 2025 16:25:40 +0100 Subject: [PATCH 1/2] Fix invalid macro tag generation for keywords which can be followed by values --- src/librustdoc/html/highlight.rs | 78 ++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/src/librustdoc/html/highlight.rs b/src/librustdoc/html/highlight.rs index dbff5c510af29..53a2be65e6907 100644 --- a/src/librustdoc/html/highlight.rs +++ b/src/librustdoc/html/highlight.rs @@ -789,6 +789,9 @@ impl<'a> Iterator for TokenIter<'a> { } } +/// Used to know if a keyword followed by a `!` should never be treated as a macro. +const KEYWORDS_FOLLOWABLE_BY_VALUE: &[&str] = &["if", "while", "match", "break", "return"]; + /// This iterator comes from the same idea than "Peekable" except that it allows to "peek" more than /// just the next item by using `peek_next`. The `peek` method always returns the next item after /// the current one whereas `peek_next` will return the next item after the last one peeked. @@ -1010,6 +1013,19 @@ impl<'src> Classifier<'src> { } } + fn new_macro_span( + &mut self, + text: &'src str, + sink: &mut dyn FnMut(Span, Highlight<'src>), + before: u32, + file_span: Span, + ) { + self.in_macro = true; + let span = new_span(before, text, file_span); + sink(DUMMY_SP, Highlight::EnterSpan { class: Class::Macro(span) }); + sink(span, Highlight::Token { text, class: None }); + } + /// Single step of highlighting. This will classify `token`, but maybe also a couple of /// following ones as well. /// @@ -1216,16 +1232,49 @@ impl<'src> Classifier<'src> { LiteralKind::Float { .. } | LiteralKind::Int { .. } => Class::Number, }, TokenKind::GuardedStrPrefix => return no_highlight(sink), - TokenKind::Ident | TokenKind::RawIdent - if let Some((TokenKind::Bang, _)) = self.peek_non_trivia() => - { + TokenKind::RawIdent if let Some((TokenKind::Bang, _)) = self.peek_non_trivia() => { self.in_macro = true; let span = new_span(before, text, file_span); sink(DUMMY_SP, Highlight::EnterSpan { class: Class::Macro(span) }); sink(span, Highlight::Token { text, class: None }); return; } - TokenKind::Ident => self.classify_ident(before, text), + // Macro non-terminals (meta vars) take precedence. + TokenKind::Ident if self.in_macro_nonterminal => { + self.in_macro_nonterminal = false; + Class::MacroNonTerminal + } + TokenKind::Ident => { + let file_span = self.file_span; + let span = || new_span(before, text, file_span); + + match text { + "ref" | "mut" => Class::RefKeyWord, + "false" | "true" => Class::Bool, + "self" | "Self" => Class::Self_(span()), + "Option" | "Result" => Class::PreludeTy(span()), + "Some" | "None" | "Ok" | "Err" => Class::PreludeVal(span()), + _ if self.is_weak_keyword(text) || is_keyword(Symbol::intern(text)) => { + // So if it's not a keyword which can be followed by a value (like `if` or + // `return`) and the next non-whitespace token is a `!`, then we consider + // it's a macro. + if !KEYWORDS_FOLLOWABLE_BY_VALUE.contains(&text) + && matches!(self.peek_non_trivia(), Some((TokenKind::Bang, _))) + { + self.new_macro_span(text, sink, before, file_span); + return; + } + Class::KeyWord + } + // If it's not a keyword and the next non whitespace token is a `!`, then + // we consider it's a macro. + _ if matches!(self.peek_non_trivia(), Some((TokenKind::Bang, _))) => { + self.new_macro_span(text, sink, before, file_span); + return; + } + _ => Class::Ident(span()), + } + } TokenKind::RawIdent | TokenKind::UnknownPrefix | TokenKind::InvalidIdent => { Class::Ident(new_span(before, text, file_span)) } @@ -1246,27 +1295,6 @@ impl<'src> Classifier<'src> { } } - fn classify_ident(&mut self, before: u32, text: &'src str) -> Class { - // Macro non-terminals (meta vars) take precedence. - if self.in_macro_nonterminal { - self.in_macro_nonterminal = false; - return Class::MacroNonTerminal; - } - - let file_span = self.file_span; - let span = || new_span(before, text, file_span); - - match text { - "ref" | "mut" => Class::RefKeyWord, - "false" | "true" => Class::Bool, - "self" | "Self" => Class::Self_(span()), - "Option" | "Result" => Class::PreludeTy(span()), - "Some" | "None" | "Ok" | "Err" => Class::PreludeVal(span()), - _ if self.is_weak_keyword(text) || is_keyword(Symbol::intern(text)) => Class::KeyWord, - _ => Class::Ident(span()), - } - } - fn is_weak_keyword(&mut self, text: &str) -> bool { // NOTE: `yeet` (`do yeet $expr`), `catch` (`do catch $block`), `default` (specialization), // `contract_{ensures,requires}`, `builtin` (builtin_syntax) & `reuse` (fn_delegation) are From 3293c83bd4f0d5e68bff5c3e10d6ecbe950193e4 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 7 Nov 2025 16:41:36 +0100 Subject: [PATCH 2/2] Add regression tests for keywords wrongly considered as macros --- .../failing-expansion-on-wrong-macro.rs | 13 ++++++++++ .../source-code-pages/keyword-macros.rs | 24 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 tests/rustdoc/source-code-pages/failing-expansion-on-wrong-macro.rs create mode 100644 tests/rustdoc/source-code-pages/keyword-macros.rs diff --git a/tests/rustdoc/source-code-pages/failing-expansion-on-wrong-macro.rs b/tests/rustdoc/source-code-pages/failing-expansion-on-wrong-macro.rs new file mode 100644 index 0000000000000..017d0be065606 --- /dev/null +++ b/tests/rustdoc/source-code-pages/failing-expansion-on-wrong-macro.rs @@ -0,0 +1,13 @@ +// This code crashed because a `if` followed by a `!` was considered a macro, +// creating an invalid class stack. +// Regression test for . + +//@ compile-flags: -Zunstable-options --generate-macro-expansion + +enum Enum { + Variant, +} + +pub fn repro() { + if !matches!(Enum::Variant, Enum::Variant) {} +} diff --git a/tests/rustdoc/source-code-pages/keyword-macros.rs b/tests/rustdoc/source-code-pages/keyword-macros.rs new file mode 100644 index 0000000000000..5637efa470fab --- /dev/null +++ b/tests/rustdoc/source-code-pages/keyword-macros.rs @@ -0,0 +1,24 @@ +// This test ensures that keywords which can be followed by values (and therefore `!`) +// are not considered as macros. +// This is a regression test for . + +#![crate_name = "foo"] + +//@ has 'src/foo/keyword-macros.rs.html' + +//@ has - '//*[@class="rust"]//*[@class="number"]' '2' +//@ has - '//*[@class="rust"]//*[@class="number"]' '0' +//@ has - '//*[@class="rust"]//*[@class="number"]' '1' +const ARR: [u8; 2] = [!0,! 1]; + +fn a() { + //@ has - '//*[@class="rust"]//*[@class="kw"]' 'if' + if! true{} + //@ has - '//*[@class="rust"]//*[@class="kw"]' 'match' + match !true { _ => {} } + //@ has - '//*[@class="rust"]//*[@class="kw"]' 'while' + let _ = while !true { + //@ has - '//*[@class="rust"]//*[@class="kw"]' 'break' + break !true; + }; +}