From d08c828fb5cd286e8f5c41afae1a70ade2aa4f12 Mon Sep 17 00:00:00 2001 From: Sasha Pourcelot Date: Fri, 10 Apr 2026 14:10:02 +0000 Subject: [PATCH 1/2] 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 0e539066d6e2653b8411937c75d992eda777787b Mon Sep 17 00:00:00 2001 From: Sasha Pourcelot Date: Fri, 10 Apr 2026 14:10:02 +0000 Subject: [PATCH 2/2] Recover on attribute in use tree --- compiler/rustc_parse/src/errors.rs | 21 ++++ compiler/rustc_parse/src/parser/attr.rs | 4 + compiler/rustc_parse/src/parser/item.rs | 122 ++++++++++++++++++++++-- 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, 233 insertions(+), 16 deletions(-) create mode 100644 tests/ui/use/attr-in-use-tree.fixed diff --git a/compiler/rustc_parse/src/errors.rs b/compiler/rustc_parse/src/errors.rs index cc1e0ff85dae4..f5836e1088752 100644 --- a/compiler/rustc_parse/src/errors.rs +++ b/compiler/rustc_parse/src/errors.rs @@ -1102,6 +1102,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 78b42ee11e2dc..1a595810b0e8f 100644 --- a/compiler/rustc_parse/src/parser/attr.rs +++ b/compiler/rustc_parse/src/parser/attr.rs @@ -302,11 +302,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. + /// + /// ```none /// 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, force_collect: ForceCollect) -> PResult<'a, ast::AttrItem> { if let Some(item) = self.eat_metavar_seq_with_matcher( diff --git a/compiler/rustc_parse/src/parser/item.rs b/compiler/rustc_parse/src/parser/item.rs index ab3683f598208..be2ab40c6abd2 100644 --- a/compiler/rustc_parse/src/parser/item.rs +++ b/compiler/rustc_parse/src/parser/item.rs @@ -430,7 +430,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(_) => { @@ -1291,7 +1292,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 = @@ -1306,13 +1311,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 { element: &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) { @@ -1332,13 +1338,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), } }) @@ -1349,14 +1359,105 @@ 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; + if p.check_noexpect(&TokenKind::Pound) + && p.look_ahead(1, |tok| matches!(tok.kind, TokenKind::OpenBracket)) + { + let attr_wrapper = p.parse_outer_attributes()?; + let raw_attrs = attr_wrapper.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 attr = self + .psess + .source_map() + .span_to_source(attr_span, |s, start, end| Ok(s[start..end].to_string())) + .unwrap(); + + let prefix = { + let mut tmp = Vec::new(); + while let Some(prefix_) = prefix { + tmp.push(prefix_.element); + prefix = prefix_.prev; + } + tmp.reverse(); + tmp.iter().flat_map(|segments| segments.iter()).collect::>() + }; + + let global_path = prefix + .first() + .map(|first_segment| first_segment.ident.name == kw::PathRoot) + .unwrap_or_default(); + let global_path = if global_path { "::" } else { "" }; + + let prefix = prefix + .iter() + .filter_map(|segment| { + if !segment.ident.is_special() { Some(segment.ident.as_str()) } else { None } + }) + .collect::>() + .join("::"); + + let mut comma_reached = false; + let 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 + }) + .unwrap(); + + let use_tree = self + .psess + .source_map() + .span_to_source(use_tree_span, |s, start, end| Ok(s[start..end].to_string())) + .unwrap(); + + let code = format!("{attr}\nuse {global_path}{prefix}::{use_tree};\n"); + + let err = crate::errors::AttrInUseTree { + attr_span, + sub: Some(crate::errors::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) @@ -3657,3 +3758,8 @@ pub(super) enum FrontMatterParsingMode { /// For function pointer types, the `const` and `async` keywords are not permitted. FunctionPtrType, } + +struct UsePathList<'a> { + element: &'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 47c933e245d49..fd175663dd583 100644 --- a/compiler/rustc_span/src/source_map.rs +++ b/compiler/rustc_span/src/source_map.rs @@ -562,9 +562,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()); @@ -719,7 +723,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