From 1867b66630ff2a10019217252229e7fb41178a90 Mon Sep 17 00:00:00 2001 From: Sasha Pourcelot Date: Sun, 23 Aug 2026 14:39:13 +0200 Subject: [PATCH 1/3] Add test for attribute in use tree --- tests/ui/use/attr-in-use-tree.rs | 23 +++++++++++++++++++++++ tests/ui/use/attr-in-use-tree.stderr | 8 ++++++++ 2 files changed, 31 insertions(+) create mode 100644 tests/ui/use/attr-in-use-tree.rs create mode 100644 tests/ui/use/attr-in-use-tree.stderr diff --git a/tests/ui/use/attr-in-use-tree.rs b/tests/ui/use/attr-in-use-tree.rs new file mode 100644 index 0000000000000..afe9844d69dc0 --- /dev/null +++ b/tests/ui/use/attr-in-use-tree.rs @@ -0,0 +1,23 @@ +#![allow(unused_imports)] + +use foo::{ + #[cfg(true)] + //~^ ERROR expected identifier, found `#` + bar, + #[cfg(false)] + baz, +}; + +// Make sure we handle reserved symbols (leading `::` is `sym::PathRoot`). +use ::foo::{ + #[cfg(false)] + qux, +}; + +mod foo { + pub(crate) mod bar {} + pub(crate) mod baz {} + pub(crate) mod qux {} +} + +fn main() {} diff --git a/tests/ui/use/attr-in-use-tree.stderr b/tests/ui/use/attr-in-use-tree.stderr new file mode 100644 index 0000000000000..79478f28667ac --- /dev/null +++ b/tests/ui/use/attr-in-use-tree.stderr @@ -0,0 +1,8 @@ +error: expected identifier, found `#` + --> $DIR/attr-in-use-tree.rs:4:5 + | +LL | #[cfg(true)] + | ^ expected identifier + +error: aborting due to 1 previous error + From e6e1f048cb3065309935105032f409144d9dc8ea Mon Sep 17 00:00:00 2001 From: Sasha Pourcelot Date: Fri, 10 Apr 2026 14:10:02 +0000 Subject: [PATCH 2/3] Recover on attribute in use tree --- compiler/rustc_parse/src/diagnostics.rs | 21 +++++ compiler/rustc_parse/src/parser/attr.rs | 14 +-- compiler/rustc_parse/src/parser/item.rs | 111 ++++++++++++++++++++++-- compiler/rustc_span/src/source_map.rs | 10 ++- tests/ui/use/attr-in-use-tree.fixed | 33 +++++++ tests/ui/use/attr-in-use-tree.rs | 6 +- tests/ui/use/attr-in-use-tree.stderr | 53 ++++++++++- 7 files changed, 227 insertions(+), 21 deletions(-) create mode 100644 tests/ui/use/attr-in-use-tree.fixed diff --git a/compiler/rustc_parse/src/diagnostics.rs b/compiler/rustc_parse/src/diagnostics.rs index 7897239d248ea..397baa0ae4ddb 100644 --- a/compiler/rustc_parse/src/diagnostics.rs +++ b/compiler/rustc_parse/src/diagnostics.rs @@ -1118,6 +1118,27 @@ pub(crate) struct ArrayBracketsInsteadOfBracesSugg { pub right: Span, } +#[derive(Diagnostic)] +#[diag("attributes are not allowed inside imports")] +pub(crate) struct AttrInUseTree { + #[primary_span] + pub attr_span: Span, + #[subdiagnostic] + pub sub: Option, +} + +#[derive(Subdiagnostic)] +#[multipart_suggestion("move the import to its own item", style = "verbose")] +pub(crate) struct AttrInUseTreeSugg { + #[suggestion_part(code = "{code}")] + pub use_lo: Span, + #[suggestion_part(code = "")] + pub attr_span: Span, + #[suggestion_part(code = "")] + pub tree_span: Span, + pub code: String, +} + #[derive(Diagnostic)] #[diag("`match` arm body without braces")] pub(crate) struct MatchArmBodyWithoutBraces { diff --git a/compiler/rustc_parse/src/parser/attr.rs b/compiler/rustc_parse/src/parser/attr.rs index fae58c29954d0..0f49e3c02873d 100644 --- a/compiler/rustc_parse/src/parser/attr.rs +++ b/compiler/rustc_parse/src/parser/attr.rs @@ -310,11 +310,15 @@ impl<'a> Parser<'a> { /// Parses an inner part of an attribute (the path and following tokens). /// The tokens must be either a delimited token stream, or empty token stream, /// or the "legacy" key-value form. - /// PATH `(` TOKEN_STREAM `)` - /// PATH `[` TOKEN_STREAM `]` - /// PATH `{` TOKEN_STREAM `}` - /// PATH - /// PATH `=` UNSUFFIXED_LIT + /// + /// ```text + /// PATH `(` TOKEN_STREAM `)` + /// PATH `[` TOKEN_STREAM `]` + /// PATH `{` TOKEN_STREAM `}` + /// PATH + /// PATH `=` UNSUFFIXED_LIT + /// ``` + /// /// The delimiters or `=` are still put into the resulting token stream. pub fn parse_attr_item( &mut self, diff --git a/compiler/rustc_parse/src/parser/item.rs b/compiler/rustc_parse/src/parser/item.rs index 1306f1fcfb1ce..9de1491030b39 100644 --- a/compiler/rustc_parse/src/parser/item.rs +++ b/compiler/rustc_parse/src/parser/item.rs @@ -434,7 +434,8 @@ impl<'a> Parser<'a> { } fn parse_use_item(&mut self) -> PResult<'a, ItemKind> { - let tree = self.parse_use_tree()?; + let use_token_span = self.prev_token.span; + let tree = self.parse_use_tree(use_token_span, None)?; if let Err(mut e) = self.expect_semi() { match tree.kind { UseTreeKind::Glob(_) => { @@ -1317,7 +1318,11 @@ impl<'a> Parser<'a> { /// PATH `::` `{` USE_TREE_LIST `}` | /// PATH [`as` IDENT] /// ``` - fn parse_use_tree(&mut self) -> PResult<'a, UseTree> { + fn parse_use_tree<'b>( + &mut self, + use_token_span: Span, + use_path: Option<&'b UsePathList<'b>>, + ) -> PResult<'a, UseTree> { let lo = self.token.span; let mut prefix = ast::Path { segments: ThinVec::new(), span: lo.shrink_to_lo() }; @@ -1331,13 +1336,14 @@ impl<'a> Parser<'a> { .push(PathSegment::path_root(lo.shrink_to_lo().with_ctxt(mod_sep_ctxt))); } - self.parse_use_tree_glob_or_nested()? + self.parse_use_tree_glob_or_nested(use_token_span, use_path)? } else { // `use path::*;` or `use path::{...};` or `use path;` or `use path as bar;` prefix = self.parse_path(PathStyle::Mod)?; if self.eat_path_sep() { - self.parse_use_tree_glob_or_nested()? + let use_path = UsePathList { elements: &prefix.segments, prev: use_path }; + self.parse_use_tree_glob_or_nested(use_token_span, Some(&use_path))? } else { // Recover from using a colon as path separator. while self.eat_noexpect(&token::Colon) { @@ -1358,13 +1364,17 @@ impl<'a> Parser<'a> { } /// Parses `*` or `{...}`. - fn parse_use_tree_glob_or_nested(&mut self) -> PResult<'a, UseTreeKind> { + fn parse_use_tree_glob_or_nested<'b>( + &mut self, + use_token_span: Span, + use_path: Option<&'b UsePathList<'b>>, + ) -> PResult<'a, UseTreeKind> { Ok(if self.eat(exp!(Star)) { UseTreeKind::Glob(self.prev_token.span) } else { let lo = self.token.span; UseTreeKind::Nested { - items: self.parse_use_tree_list()?, + items: self.parse_use_tree_list(use_token_span, use_path)?, span: lo.to(self.prev_token.span), } }) @@ -1375,14 +1385,93 @@ impl<'a> Parser<'a> { /// ```text /// USE_TREE_LIST = ∅ | (USE_TREE `,`)* USE_TREE [`,`] /// ``` - fn parse_use_tree_list(&mut self) -> PResult<'a, ThinVec<(UseTree, ast::NodeId)>> { + fn parse_use_tree_list<'b>( + &mut self, + use_token_span: Span, + prefix: Option<&'b UsePathList<'b>>, + ) -> PResult<'a, ThinVec<(UseTree, ast::NodeId)>> { self.parse_delim_comma_seq(exp!(OpenBrace), exp!(CloseBrace), |p| { p.recover_vcs_conflict_marker(); - Ok((p.parse_use_tree()?, DUMMY_NODE_ID)) + let mut attr_span = None; + let attrs = p.parse_outer_attributes()?; + if !attrs.is_empty() { + let raw_attrs = attrs.take_for_recovery(&p.psess); + attr_span = + Some(raw_attrs.first().unwrap().span.to(raw_attrs.last().unwrap().span)); + } + let use_tree = p.parse_use_tree(use_token_span, prefix)?; + if let Some(attr_span) = attr_span { + p.emit_error_attr_in_use_tree(use_token_span, prefix, use_tree.span(), attr_span); + } + + Ok((use_tree, DUMMY_NODE_ID)) }) .map(|(r, _)| r) } + fn emit_error_attr_in_use_tree<'b>( + &self, + use_token_span: Span, + prefix: Option<&'b UsePathList<'b>>, + use_tree_span: Span, + attr_span: Span, + ) { + { + let mut prefix = prefix; + let Ok(attr) = self.psess.source_map().span_to_snippet(attr_span) else { + return; + }; + + let prefix = { + let mut tmp = Vec::new(); + while let Some(prefix_) = prefix { + tmp.push(prefix_.elements); + prefix = prefix_.prev; + } + tmp.reverse(); + tmp.iter().flat_map(|segments| segments.iter()).collect::>() + }; + + let prefix = + prefix + .iter() + .map(|segment| { + if segment.ident.name == kw::PathRoot { "" } else { segment.ident.as_str() } + }) + .collect::>() + .join("::"); + + let mut comma_reached = false; + let Ok(tree_span) = self.psess.source_map().span_extend_while(use_tree_span, |c| { + if comma_reached { + return false; + } + comma_reached = c == ','; + c.is_whitespace() || comma_reached + }) else { + return; + }; + + let Ok(use_tree) = self.psess.source_map().span_to_snippet(use_tree_span) else { + return; + }; + + // FIXME: duplicate the attributes that are at the root of the initial use-item. + let code = format!("{attr}\nuse {prefix}::{use_tree};\n"); + + let err = crate::diagnostics::AttrInUseTree { + attr_span, + sub: Some(crate::diagnostics::AttrInUseTreeSugg { + use_lo: use_token_span.shrink_to_lo(), + attr_span, + tree_span, + code, + }), + }; + self.dcx().emit_err(err); + } + } + fn parse_rename(&mut self) -> PResult<'a, Option> { if self.eat_keyword(exp!(As)) { self.parse_ident_or_underscore().map(Some) @@ -2737,7 +2826,13 @@ impl<'a> Parser<'a> { } } } + enum IsMacroRulesItem { Yes { has_bang: bool }, No, } + +struct UsePathList<'a> { + elements: &'a [ast::PathSegment], + prev: Option<&'a Self>, +} diff --git a/compiler/rustc_span/src/source_map.rs b/compiler/rustc_span/src/source_map.rs index 80d1bae71ae89..f394765ab9f8e 100644 --- a/compiler/rustc_span/src/source_map.rs +++ b/compiler/rustc_span/src/source_map.rs @@ -596,9 +596,13 @@ impl SourceMap { /// Extracts the source surrounding the given `Span` using the `extract_source` function. The /// extract function takes three arguments: a string slice containing the source, an index in /// the slice for the beginning of the span and an index in the slice for the end of the span. - pub fn span_to_source(&self, sp: Span, extract_source: F) -> Result + pub fn span_to_source( + &self, + sp: Span, + mut extract_source: F, + ) -> Result where - F: Fn(&str, usize, usize) -> Result, + F: FnMut(&str, usize, usize) -> Result, { let local_begin = self.lookup_byte_offset(sp.lo()); let local_end = self.lookup_byte_offset(sp.hi()); @@ -753,7 +757,7 @@ impl SourceMap { pub fn span_extend_while( &self, span: Span, - f: impl Fn(char) -> bool, + mut f: impl FnMut(char) -> bool, ) -> Result { self.span_to_source(span, |s, _start, end| { let n = s[end..].char_indices().find(|&(_, c)| !f(c)).map_or(s.len() - end, |(i, _)| i); diff --git a/tests/ui/use/attr-in-use-tree.fixed b/tests/ui/use/attr-in-use-tree.fixed new file mode 100644 index 0000000000000..49d3ce7ed8cae --- /dev/null +++ b/tests/ui/use/attr-in-use-tree.fixed @@ -0,0 +1,33 @@ +//@ run-rustfix + +#![allow(unused_imports)] + +#[cfg(true)] +use foo::bar; +#[cfg(false)] +use foo::baz; +use foo::{ + + //~^ ERROR attributes are not allowed inside imports + + + //~^ ERROR attributes are not allowed inside imports + +}; + +// Make sure we handle reserved symbols (leading `::` is `sym::PathRoot`). +#[cfg(false)] +use ::foo::qux; +use ::foo::{ + + //~^ ERROR attributes are not allowed inside imports + +}; + +mod foo { + pub(crate) mod bar {} + pub(crate) mod baz {} + pub(crate) mod qux {} +} + +fn main() {} diff --git a/tests/ui/use/attr-in-use-tree.rs b/tests/ui/use/attr-in-use-tree.rs index afe9844d69dc0..20a21d89e2721 100644 --- a/tests/ui/use/attr-in-use-tree.rs +++ b/tests/ui/use/attr-in-use-tree.rs @@ -1,16 +1,20 @@ +//@ run-rustfix + #![allow(unused_imports)] use foo::{ #[cfg(true)] - //~^ ERROR expected identifier, found `#` + //~^ ERROR attributes are not allowed inside imports bar, #[cfg(false)] + //~^ ERROR attributes are not allowed inside imports baz, }; // Make sure we handle reserved symbols (leading `::` is `sym::PathRoot`). use ::foo::{ #[cfg(false)] + //~^ ERROR attributes are not allowed inside imports qux, }; diff --git a/tests/ui/use/attr-in-use-tree.stderr b/tests/ui/use/attr-in-use-tree.stderr index 79478f28667ac..e8c76a90635ac 100644 --- a/tests/ui/use/attr-in-use-tree.stderr +++ b/tests/ui/use/attr-in-use-tree.stderr @@ -1,8 +1,53 @@ -error: expected identifier, found `#` - --> $DIR/attr-in-use-tree.rs:4:5 +error: attributes are not allowed inside imports + --> $DIR/attr-in-use-tree.rs:6:5 | LL | #[cfg(true)] - | ^ expected identifier + | ^^^^^^^^^^^^ + | +help: move the import to its own item + | +LL + #[cfg(true)] +LL + use foo::bar; +LL | use foo::{ +LL ~ +LL | +LL ~ + | + +error: attributes are not allowed inside imports + --> $DIR/attr-in-use-tree.rs:9:5 + | +LL | #[cfg(false)] + | ^^^^^^^^^^^^^ + | +help: move the import to its own item + | +LL + #[cfg(false)] +LL + use foo::baz; +LL | use foo::{ +LL | #[cfg(true)] +LL | +LL | bar, +LL ~ +LL | +LL ~ + | + +error: attributes are not allowed inside imports + --> $DIR/attr-in-use-tree.rs:16:5 + | +LL | #[cfg(false)] + | ^^^^^^^^^^^^^ + | +help: move the import to its own item + | +LL + #[cfg(false)] +LL + use ::foo::qux; +LL | use ::foo::{ +LL ~ +LL | +LL ~ + | -error: aborting due to 1 previous error +error: aborting due to 3 previous errors From 1333ad790ae46e4c9893d88fd4bcd26d7712fc35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Le=C3=B3n=20Orell=20Valerian=20Liehr?= Date: Sun, 23 Aug 2026 14:38:37 +0200 Subject: [PATCH 3/3] Small tweaks to parse error recovery code --- compiler/rustc_parse/src/parser/item.rs | 90 +++++++++++-------------- 1 file changed, 41 insertions(+), 49 deletions(-) diff --git a/compiler/rustc_parse/src/parser/item.rs b/compiler/rustc_parse/src/parser/item.rs index 9de1491030b39..44ce647568d00 100644 --- a/compiler/rustc_parse/src/parser/item.rs +++ b/compiler/rustc_parse/src/parser/item.rs @@ -1392,6 +1392,7 @@ impl<'a> Parser<'a> { ) -> PResult<'a, ThinVec<(UseTree, ast::NodeId)>> { self.parse_delim_comma_seq(exp!(OpenBrace), exp!(CloseBrace), |p| { p.recover_vcs_conflict_marker(); + let mut attr_span = None; let attrs = p.parse_outer_attributes()?; if !attrs.is_empty() { @@ -1399,7 +1400,9 @@ impl<'a> Parser<'a> { attr_span = Some(raw_attrs.first().unwrap().span.to(raw_attrs.last().unwrap().span)); } + let use_tree = p.parse_use_tree(use_token_span, prefix)?; + if let Some(attr_span) = attr_span { p.emit_error_attr_in_use_tree(use_token_span, prefix, use_tree.span(), attr_span); } @@ -1409,67 +1412,56 @@ impl<'a> Parser<'a> { .map(|(r, _)| r) } - fn emit_error_attr_in_use_tree<'b>( + fn emit_error_attr_in_use_tree( &self, use_token_span: Span, - prefix: Option<&'b UsePathList<'b>>, + mut prefix: Option<&UsePathList<'_>>, use_tree_span: Span, attr_span: Span, ) { - { - let mut prefix = prefix; - let Ok(attr) = self.psess.source_map().span_to_snippet(attr_span) else { - return; - }; + let Ok(attr) = self.psess.source_map().span_to_snippet(attr_span) else { return }; - let prefix = { - let mut tmp = Vec::new(); - while let Some(prefix_) = prefix { - tmp.push(prefix_.elements); - prefix = prefix_.prev; - } - tmp.reverse(); - tmp.iter().flat_map(|segments| segments.iter()).collect::>() - }; + let prefix: Vec<_> = { + let mut tmp = Vec::new(); + while let Some(prefix_) = prefix { + tmp.push(prefix_.elements); + prefix = prefix_.prev; + } + tmp.reverse(); + tmp.into_iter().flatten().collect() + }; - let prefix = - prefix - .iter() - .map(|segment| { - if segment.ident.name == kw::PathRoot { "" } else { segment.ident.as_str() } - }) - .collect::>() - .join("::"); + let prefix: String = prefix + .iter() + .map(|seg| if seg.ident.name == kw::PathRoot { "" } else { seg.ident.as_str() }) + .intersperse("::") + .collect(); - let mut comma_reached = false; - let Ok(tree_span) = self.psess.source_map().span_extend_while(use_tree_span, |c| { - if comma_reached { - return false; - } - comma_reached = c == ','; - c.is_whitespace() || comma_reached - }) else { - return; - }; + let mut comma_reached = false; + let Ok(tree_span) = self.psess.source_map().span_extend_while(use_tree_span, |c| { + if comma_reached { + return false; + } + comma_reached = c == ','; + c.is_whitespace() || comma_reached + }) else { + return; + }; - let Ok(use_tree) = self.psess.source_map().span_to_snippet(use_tree_span) else { - return; - }; + let Ok(use_tree) = self.psess.source_map().span_to_snippet(use_tree_span) else { return }; - // FIXME: duplicate the attributes that are at the root of the initial use-item. - let code = format!("{attr}\nuse {prefix}::{use_tree};\n"); + // FIXME: duplicate the attributes that are at the root of the initial use-item. + let code = format!("{attr}\nuse {prefix}::{use_tree};\n"); - let err = crate::diagnostics::AttrInUseTree { + self.dcx().emit_err(crate::diagnostics::AttrInUseTree { + attr_span, + sub: Some(crate::diagnostics::AttrInUseTreeSugg { + use_lo: use_token_span.shrink_to_lo(), attr_span, - sub: Some(crate::diagnostics::AttrInUseTreeSugg { - use_lo: use_token_span.shrink_to_lo(), - attr_span, - tree_span, - code, - }), - }; - self.dcx().emit_err(err); - } + tree_span, + code, + }), + }); } fn parse_rename(&mut self) -> PResult<'a, Option> {