diff --git a/library/core/src/ops/control_flow.rs b/library/core/src/ops/control_flow.rs index b760a7c4e21eb..3cc184f0ab75c 100644 --- a/library/core/src/ops/control_flow.rs +++ b/library/core/src/ops/control_flow.rs @@ -1,3 +1,4 @@ +use crate::marker::Destruct; use crate::{convert, ops}; /// Used to tell an operation whether it should exit early or go on as usual. @@ -150,7 +151,8 @@ impl ControlFlow { /// ``` #[inline] #[stable(feature = "control_flow_enum_is", since = "1.59.0")] - pub fn is_break(&self) -> bool { + #[rustc_const_unstable(feature = "min_const_control_flow", issue = "148738")] + pub const fn is_break(&self) -> bool { matches!(*self, ControlFlow::Break(_)) } @@ -166,7 +168,8 @@ impl ControlFlow { /// ``` #[inline] #[stable(feature = "control_flow_enum_is", since = "1.59.0")] - pub fn is_continue(&self) -> bool { + #[rustc_const_unstable(feature = "min_const_control_flow", issue = "148738")] + pub const fn is_continue(&self) -> bool { matches!(*self, ControlFlow::Continue(_)) } @@ -183,7 +186,11 @@ impl ControlFlow { /// ``` #[inline] #[stable(feature = "control_flow_enum", since = "1.83.0")] - pub fn break_value(self) -> Option { + #[rustc_const_unstable(feature = "const_control_flow", issue = "148739")] + pub const fn break_value(self) -> Option + where + Self: [const] Destruct, + { match self { ControlFlow::Continue(..) => None, ControlFlow::Break(x) => Some(x), @@ -257,7 +264,8 @@ impl ControlFlow { /// ``` #[inline] #[unstable(feature = "control_flow_ok", issue = "140266")] - pub fn break_ok(self) -> Result { + #[rustc_const_unstable(feature = "min_const_control_flow", issue = "148738")] + pub const fn break_ok(self) -> Result { match self { ControlFlow::Continue(c) => Err(c), ControlFlow::Break(b) => Ok(b), @@ -268,7 +276,11 @@ impl ControlFlow { /// to the break value in case it exists. #[inline] #[stable(feature = "control_flow_enum", since = "1.83.0")] - pub fn map_break(self, f: impl FnOnce(B) -> T) -> ControlFlow { + #[rustc_const_unstable(feature = "const_control_flow", issue = "148739")] + pub const fn map_break(self, f: F) -> ControlFlow + where + F: [const] FnOnce(B) -> T + [const] Destruct, + { match self { ControlFlow::Continue(x) => ControlFlow::Continue(x), ControlFlow::Break(x) => ControlFlow::Break(f(x)), @@ -288,7 +300,11 @@ impl ControlFlow { /// ``` #[inline] #[stable(feature = "control_flow_enum", since = "1.83.0")] - pub fn continue_value(self) -> Option { + #[rustc_const_unstable(feature = "const_control_flow", issue = "148739")] + pub const fn continue_value(self) -> Option + where + Self: [const] Destruct, + { match self { ControlFlow::Continue(x) => Some(x), ControlFlow::Break(..) => None, @@ -361,7 +377,8 @@ impl ControlFlow { /// ``` #[inline] #[unstable(feature = "control_flow_ok", issue = "140266")] - pub fn continue_ok(self) -> Result { + #[rustc_const_unstable(feature = "min_const_control_flow", issue = "148738")] + pub const fn continue_ok(self) -> Result { match self { ControlFlow::Continue(c) => Ok(c), ControlFlow::Break(b) => Err(b), @@ -372,7 +389,11 @@ impl ControlFlow { /// to the continue value in case it exists. #[inline] #[stable(feature = "control_flow_enum", since = "1.83.0")] - pub fn map_continue(self, f: impl FnOnce(C) -> T) -> ControlFlow { + #[rustc_const_unstable(feature = "const_control_flow", issue = "148739")] + pub const fn map_continue(self, f: F) -> ControlFlow + where + F: [const] FnOnce(C) -> T + [const] Destruct, + { match self { ControlFlow::Continue(x) => ControlFlow::Continue(f(x)), ControlFlow::Break(x) => ControlFlow::Break(x), diff --git a/src/librustdoc/html/highlight.rs b/src/librustdoc/html/highlight.rs index dbff5c510af29..798fbd284ca87 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 NON_MACRO_KEYWORDS: &[&str] = &["if", "while", "match", "break", "return", "impl"]; + /// 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,46 @@ 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() => - { - 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 }); + TokenKind::RawIdent if let Some((TokenKind::Bang, _)) = self.peek_non_trivia() => { + self.new_macro_span(text, sink, before, file_span); 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 !NON_MACRO_KEYWORDS.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 +1292,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 diff --git a/src/tools/compiletest/src/directives.rs b/src/tools/compiletest/src/directives.rs index f25294d8d198d..7c1bbe940321f 100644 --- a/src/tools/compiletest/src/directives.rs +++ b/src/tools/compiletest/src/directives.rs @@ -11,7 +11,7 @@ use crate::debuggers::{extract_cdb_version, extract_gdb_version}; pub(crate) use crate::directives::auxiliary::AuxProps; use crate::directives::auxiliary::parse_and_update_aux; use crate::directives::directive_names::{ - KNOWN_DIRECTIVE_NAMES, KNOWN_HTMLDOCCK_DIRECTIVE_NAMES, KNOWN_JSONDOCCK_DIRECTIVE_NAMES, + KNOWN_DIRECTIVE_NAMES_SET, KNOWN_HTMLDOCCK_DIRECTIVE_NAMES, KNOWN_JSONDOCCK_DIRECTIVE_NAMES, }; pub(crate) use crate::directives::file::FileDirectives; use crate::directives::line::{DirectiveLine, line_directive}; @@ -53,13 +53,10 @@ impl EarlyProps { config: &Config, file_directives: &FileDirectives<'_>, ) -> Self { - let testfile = file_directives.path; let mut props = EarlyProps::default(); - let mut poisoned = false; iter_directives( config.mode, - &mut poisoned, file_directives, // (dummy comment to force args into vertical layout) &mut |ln: &DirectiveLine<'_>| { @@ -67,11 +64,6 @@ impl EarlyProps { }, ); - if poisoned { - eprintln!("errors encountered during EarlyProps parsing: {}", testfile); - panic!("errors encountered during EarlyProps parsing"); - } - props } } @@ -358,12 +350,10 @@ impl TestProps { let file_contents = fs::read_to_string(testfile).unwrap(); let file_directives = FileDirectives::from_file_contents(testfile, &file_contents); - let mut poisoned = false; - iter_directives( config.mode, - &mut poisoned, &file_directives, + // (dummy comment to force args into vertical layout) &mut |ln: &DirectiveLine<'_>| { if !ln.applies_to_test_revision(test_revision) { return; @@ -634,11 +624,6 @@ impl TestProps { ); }, ); - - if poisoned { - eprintln!("errors encountered during TestProps parsing: {}", testfile); - panic!("errors encountered during TestProps parsing"); - } } if self.should_ice { @@ -775,6 +760,34 @@ impl TestProps { } } +pub(crate) fn do_early_directives_check( + mode: TestMode, + file_directives: &FileDirectives<'_>, +) -> Result<(), String> { + let testfile = file_directives.path; + + for directive_line @ DirectiveLine { line_number, .. } in &file_directives.lines { + let CheckDirectiveResult { is_known_directive, trailing_directive } = + check_directive(directive_line, mode); + + if !is_known_directive { + return Err(format!( + "ERROR: unknown compiletest directive `{directive}` at {testfile}:{line_number}", + directive = directive_line.display(), + )); + } + + if let Some(trailing_directive) = &trailing_directive { + return Err(format!( + "ERROR: detected trailing compiletest directive `{trailing_directive}` at {testfile}:{line_number}\n\ + HELP: put the directive on its own line: `//@ {trailing_directive}`" + )); + } + } + + Ok(()) +} + pub(crate) struct CheckDirectiveResult<'ln> { is_known_directive: bool, trailing_directive: Option<&'ln str>, @@ -786,7 +799,7 @@ fn check_directive<'a>( ) -> CheckDirectiveResult<'a> { let &DirectiveLine { name: directive_name, .. } = directive_ln; - let is_known_directive = KNOWN_DIRECTIVE_NAMES.contains(&directive_name) + let is_known_directive = KNOWN_DIRECTIVE_NAMES_SET.contains(&directive_name) || match mode { TestMode::Rustdoc => KNOWN_HTMLDOCCK_DIRECTIVE_NAMES.contains(&directive_name), TestMode::RustdocJson => KNOWN_JSONDOCCK_DIRECTIVE_NAMES.contains(&directive_name), @@ -799,7 +812,7 @@ fn check_directive<'a>( let trailing_directive = directive_ln .remark_after_space() .map(|remark| remark.trim_start().split(' ').next().unwrap()) - .filter(|token| KNOWN_DIRECTIVE_NAMES.contains(token)); + .filter(|token| KNOWN_DIRECTIVE_NAMES_SET.contains(token)); // FIXME(Zalathar): Consider emitting specialized error/help messages for // bogus directive names that are similar to real ones, e.g.: @@ -811,7 +824,6 @@ fn check_directive<'a>( fn iter_directives( mode: TestMode, - poisoned: &mut bool, file_directives: &FileDirectives<'_>, it: &mut dyn FnMut(&DirectiveLine<'_>), ) { @@ -837,36 +849,7 @@ fn iter_directives( } } - for directive_line @ &DirectiveLine { line_number, .. } in &file_directives.lines { - // Perform unknown directive check on Rust files. - if testfile.extension() == Some("rs") { - let CheckDirectiveResult { is_known_directive, trailing_directive } = - check_directive(directive_line, mode); - - if !is_known_directive { - *poisoned = true; - - error!( - "{testfile}:{line_number}: detected unknown compiletest test directive `{}`", - directive_line.display(), - ); - - return; - } - - if let Some(trailing_directive) = &trailing_directive { - *poisoned = true; - - error!( - "{testfile}:{line_number}: detected trailing compiletest test directive `{}`", - trailing_directive, - ); - help!("put the trailing directive in its own line: `//@ {}`", trailing_directive); - - return; - } - } - + for directive_line in &file_directives.lines { it(directive_line); } } @@ -1304,12 +1287,9 @@ pub(crate) fn make_test_description( let mut ignore_message = None; let mut should_fail = false; - let mut local_poisoned = false; - // Scan through the test file to handle `ignore-*`, `only-*`, and `needs-*` directives. iter_directives( config.mode, - &mut local_poisoned, file_directives, &mut |ln @ &DirectiveLine { line_number, .. }| { if !ln.applies_to_test_revision(test_revision) { @@ -1358,11 +1338,6 @@ pub(crate) fn make_test_description( }, ); - if local_poisoned { - eprintln!("errors encountered when trying to make test description: {}", path); - panic!("errors encountered when trying to make test description"); - } - // The `should-fail` annotation doesn't apply to pretty tests, // since we run the pretty printer across all tests by default. // If desired, we could add a `should-fail-pretty` annotation. diff --git a/src/tools/compiletest/src/directives/directive_names.rs b/src/tools/compiletest/src/directives/directive_names.rs index d60819653f6c4..d2f5b3dba3dd7 100644 --- a/src/tools/compiletest/src/directives/directive_names.rs +++ b/src/tools/compiletest/src/directives/directive_names.rs @@ -1,3 +1,9 @@ +use std::collections::HashSet; +use std::sync::LazyLock; + +pub(crate) static KNOWN_DIRECTIVE_NAMES_SET: LazyLock> = + LazyLock::new(|| KNOWN_DIRECTIVE_NAMES.iter().copied().collect()); + /// This was originally generated by collecting directives from ui tests and then extracting their /// directive names. This is **not** an exhaustive list of all possible directives. Instead, this is /// a best-effort approximation for diagnostics. Add new directives to this list when needed. diff --git a/src/tools/compiletest/src/directives/tests.rs b/src/tools/compiletest/src/directives/tests.rs index 0bf2bcd3af434..e221c3a2daf2e 100644 --- a/src/tools/compiletest/src/directives/tests.rs +++ b/src/tools/compiletest/src/directives/tests.rs @@ -3,8 +3,8 @@ use semver::Version; use crate::common::{Config, Debugger, TestMode}; use crate::directives::{ - AuxProps, DirectivesCache, EarlyProps, Edition, EditionRange, FileDirectives, - extract_llvm_version, extract_version_range, iter_directives, line_directive, parse_edition, + self, AuxProps, DirectivesCache, EarlyProps, Edition, EditionRange, FileDirectives, + extract_llvm_version, extract_version_range, line_directive, parse_edition, parse_normalize_rule, }; use crate::executor::{CollectedTestDesc, ShouldFail}; @@ -767,7 +767,10 @@ fn threads_support() { fn run_path(poisoned: &mut bool, path: &Utf8Path, file_contents: &str) { let file_directives = FileDirectives::from_file_contents(path, file_contents); - iter_directives(TestMode::Ui, poisoned, &file_directives, &mut |_| {}); + let result = directives::do_early_directives_check(TestMode::Ui, &file_directives); + if result.is_err() { + *poisoned = true; + } } #[test] diff --git a/src/tools/compiletest/src/lib.rs b/src/tools/compiletest/src/lib.rs index 89f5623c1fc99..a3a40a1a7ee87 100644 --- a/src/tools/compiletest/src/lib.rs +++ b/src/tools/compiletest/src/lib.rs @@ -869,6 +869,12 @@ fn make_test(cx: &TestCollectorCx, collector: &mut TestCollector, testpaths: &Te let file_contents = fs::read_to_string(&test_path).expect("reading test file for directives should succeed"); let file_directives = FileDirectives::from_file_contents(&test_path, &file_contents); + + if let Err(message) = directives::do_early_directives_check(cx.config.mode, &file_directives) { + // FIXME(Zalathar): Overhaul compiletest error handling so that we + // don't have to resort to ad-hoc panics everywhere. + panic!("directives check failed:\n{message}"); + } let early_props = EarlyProps::from_file_directives(&cx.config, &file_directives); // Normally we create one structure per revision, with two exceptions: 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..e5f1a7a3ea0e5 --- /dev/null +++ b/tests/rustdoc/source-code-pages/keyword-macros.rs @@ -0,0 +1,30 @@ +// 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"] +#![feature(negative_impls)] + +//@ 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]; + +trait X {} + +//@ has - '//*[@class="rust"]//*[@class="kw"]' 'impl' +impl !X for i32 {} + +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; + }; +}