diff --git a/compiler/rustc_resolve/src/diagnostics/impls.rs b/compiler/rustc_resolve/src/diagnostics/impls.rs index b56e1263c17c8..5feaaa3beefe9 100644 --- a/compiler/rustc_resolve/src/diagnostics/impls.rs +++ b/compiler/rustc_resolve/src/diagnostics/impls.rs @@ -2523,6 +2523,68 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> { *path = Path { span: path.span, segments: new_segments }; } + fn expand_span_to_surrounding_comma( + source_text: &str, + ident_start: usize, + ident_end: usize, + ) -> (usize, usize) { + let mut start = ident_start; + let end = ident_end; + + // Find the index in `source_text` just after the identifier. + let after_ident = &source_text[end..]; + let mut chars = after_ident.char_indices(); + + // Skip whitespace and look for a trailing comma. + let mut trailing_comma_end = None; + while let Some((off, ch)) = chars.next() { + if ch.is_whitespace() { + continue; + } else if ch == ',' { + // Include the comma, then skip any following whitespace. + let mut pos = end + off + ch.len_utf8(); + while let Some((off2, ch2)) = chars.next() { + if ch2.is_whitespace() { + pos = end + off2 + ch2.len_utf8(); + } else { + break; + } + } + trailing_comma_end = Some(pos); + break; + } else { + break; + } + } + + if let Some(new_end) = trailing_comma_end { + return (start, new_end); + } + + // No trailing comma. Search backwards for a leading comma. + let before_ident = &source_text[..start]; + let mut rev_chars = before_ident.char_indices().rev(); + while let Some((off, ch)) = rev_chars.next() { + if ch.is_whitespace() { + continue; + } else if ch == ',' { + start = off; // cut from the comma onward, so the comma is removed + break; + } else { + break; + } + } + + (start, end) + } + + fn get_indentation(sm: &SourceMap, span: Span) -> String { + let line_span = sm.span_extend_to_line(span.shrink_to_lo()); + sm.span_to_snippet(line_span) + .map(|line| line.chars().take_while(|c| c.is_whitespace()).collect()) + .unwrap_or_default() + } + fn report_privacy_error(&mut self, privacy_error: &PrivacyError<'ra>) { let PrivacyError { ident, @@ -2530,6 +2592,7 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> { outermost_res, parent_scope, single_nested, + use_stmt_span, dedup_span, ref source, } = *privacy_error; @@ -2808,6 +2871,86 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> { err.subdiagnostic(sugg); break; } + } else if single_nested + && let Some(stmt_span) = use_stmt_span + && !shown_candidates + && !outermost_res.is_some_and(|(_, outer)| outer.span != ident.span) + { + sugg_paths.sort_by_key(|(p, reexport)| (p.len(), p[0].name == sym::core, *reexport)); + for (sugg, reexport) in sugg_paths { + if sugg.len() <= 1 { + continue; + } + let path = join_path_idents(sugg); + let Ok(source_text) = self.tcx.sess.source_map().span_to_snippet(stmt_span) else { + continue; + }; + + let lo_offset = (ident.span.lo() - stmt_span.lo()).0 as usize; + let hi_offset = (ident.span.hi() - stmt_span.lo()).0 as usize; + + if lo_offset > source_text.len() || hi_offset > source_text.len() { + continue; + } + + let (start, end) = + Self::expand_span_to_surrounding_comma(&source_text, lo_offset, hi_offset); + + let mut replacement = String::new(); + replacement.push_str(&source_text[..start]); + replacement.push_str(&source_text[end..]); + + // If removing the ident leaves an empty group, replace the path entirely + if replacement.contains("{}") { + let msg = if reexport { + format!("import `{ident}` through the re-export") + } else { + format!("import `{ident}` directly") + }; + + let line_span = self.tcx.sess.source_map().span_extend_to_line(stmt_span); + let indentation = Self::get_indentation(self.tcx.sess.source_map(), stmt_span); + let suggestion_text = format!("{indentation}use {path};"); + err.multipart_suggestion( + msg, + vec![(line_span, suggestion_text)], + Applicability::MachineApplicable, + ); + break; + } + + // If only one item remains in the group, remove the braces + if let Some(open) = replacement.find('{') { + if let Some(close) = replacement.rfind('}') { + let inner = replacement[open + 1..close].trim(); + if !inner.contains(',') && !inner.is_empty() { + // Replace `{ident}` with just `ident` + replacement = format!("{}{}", replacement[..open].trim_end(), inner); + } + } + } + + let msg = if reexport { + format!("import `{ident}` through the re-export") + } else { + format!("import `{ident}` directly") + }; + + // Calculate the indentation of the original `use` statement to ensure the + // suggested import aligns with the existing code. + let indentation = Self::get_indentation(self.tcx.sess.source_map(), stmt_span); + + // Insert before the path to avoid duplicating `use`; stmt_span doesn't include the keyword. + err.multipart_suggestion( + msg, + vec![ + (stmt_span.shrink_to_lo(), format!("{path};\n{indentation}use ")), + (stmt_span, replacement), + ], + Applicability::MachineApplicable, + ); + break; + } } err.emit(); diff --git a/compiler/rustc_resolve/src/ident.rs b/compiler/rustc_resolve/src/ident.rs index 12dd05bfe6e60..fb1543fbbebc1 100644 --- a/compiler/rustc_resolve/src/ident.rs +++ b/compiler/rustc_resolve/src/ident.rs @@ -1364,6 +1364,14 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> { source: None, parent_scope: *parent_scope, single_nested: path_span != root_span, + use_stmt_span: if path_span != root_span { + // `root_span` spans the entire `use` tree, which is needed + // to correctly generate a multipart suggestion for a grouped import. + // Only set for grouped imports; non-nested cases don't need the span. + Some(root_span) + } else { + None + }, }); } else { return Err(ControlFlow::Break(Determined)); diff --git a/compiler/rustc_resolve/src/lib.rs b/compiler/rustc_resolve/src/lib.rs index 13576c24e6c08..d271e5dd93ca4 100644 --- a/compiler/rustc_resolve/src/lib.rs +++ b/compiler/rustc_resolve/src/lib.rs @@ -1020,6 +1020,9 @@ struct PrivacyError<'ra> { parent_scope: ParentScope<'ra>, /// Is the format `use a::{b,c}`? single_nested: bool, + /// Span of the entire `use` statement, including the `use` keyword. + /// Set only when `single_nested` is true. + use_stmt_span: Option, source: Option, } diff --git a/tests/ui/imports/private-import-grouped-suggestion-157453.rs b/tests/ui/imports/private-import-grouped-suggestion-157453.rs new file mode 100644 index 0000000000000..24a1f1b9aca28 --- /dev/null +++ b/tests/ui/imports/private-import-grouped-suggestion-157453.rs @@ -0,0 +1,32 @@ +mod one { + pub struct One(); +} + +mod two { + use crate::one::One; + pub struct Two(); +} + +mod test_grouped { + use crate::two::{One, Two}; //~ ERROR struct import `One` is private [E0603] +} + +mod test_single_item { + use crate::two::{One}; //~ ERROR struct import `One` is private [E0603] +} + +mod outer { + pub mod inner { + pub struct MyPath; + } +} + +mod reexport { + use crate::outer::inner::MyPath; +} + +mod test_std_style { + use crate::reexport::{MyPath}; //~ ERROR struct import `MyPath` is private [E0603] +} + +fn main() {} diff --git a/tests/ui/imports/private-import-grouped-suggestion-157453.stderr b/tests/ui/imports/private-import-grouped-suggestion-157453.stderr new file mode 100644 index 0000000000000..47ff51e4246cd --- /dev/null +++ b/tests/ui/imports/private-import-grouped-suggestion-157453.stderr @@ -0,0 +1,69 @@ +error[E0603]: struct import `One` is private + --> $DIR/private-import-grouped-suggestion-157453.rs:11:22 + | +LL | use crate::two::{One, Two}; + | ^^^ private struct import + | +note: the struct import `One` is defined here... + --> $DIR/private-import-grouped-suggestion-157453.rs:6:9 + | +LL | use crate::one::One; + | ^^^^^^^^^^^^^^^ +note: ...and refers to the struct `One` which is defined here + --> $DIR/private-import-grouped-suggestion-157453.rs:2:5 + | +LL | pub struct One(); + | ^^^^^^^^^^^^^^^^^ you could import this directly +help: import `One` directly + | +LL ~ use crate::one::One; +LL ~ use crate::two::Two; + | + +error[E0603]: struct import `One` is private + --> $DIR/private-import-grouped-suggestion-157453.rs:15:22 + | +LL | use crate::two::{One}; + | ^^^ private struct import + | +note: the struct import `One` is defined here... + --> $DIR/private-import-grouped-suggestion-157453.rs:6:9 + | +LL | use crate::one::One; + | ^^^^^^^^^^^^^^^ +note: ...and refers to the struct `One` which is defined here + --> $DIR/private-import-grouped-suggestion-157453.rs:2:5 + | +LL | pub struct One(); + | ^^^^^^^^^^^^^^^^^ you could import this directly +help: import `One` directly + | +LL - use crate::two::{One}; +LL + use crate::one::One; + | + +error[E0603]: struct import `MyPath` is private + --> $DIR/private-import-grouped-suggestion-157453.rs:29:27 + | +LL | use crate::reexport::{MyPath}; + | ^^^^^^ private struct import + | +note: the struct import `MyPath` is defined here... + --> $DIR/private-import-grouped-suggestion-157453.rs:25:9 + | +LL | use crate::outer::inner::MyPath; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +note: ...and refers to the struct `MyPath` which is defined here + --> $DIR/private-import-grouped-suggestion-157453.rs:20:9 + | +LL | pub struct MyPath; + | ^^^^^^^^^^^^^^^^^^ you could import this directly +help: import `MyPath` directly + | +LL - use crate::reexport::{MyPath}; +LL + use crate::outer::inner::MyPath; + | + +error: aborting due to 3 previous errors + +For more information about this error, try `rustc --explain E0603`. diff --git a/tests/ui/imports/private-import-nested-suggestion-156060.stderr b/tests/ui/imports/private-import-nested-suggestion-156060.stderr index 09d5391bd58df..372fb4781c128 100644 --- a/tests/ui/imports/private-import-nested-suggestion-156060.stderr +++ b/tests/ui/imports/private-import-nested-suggestion-156060.stderr @@ -14,6 +14,11 @@ note: ...and refers to the struct `One` which is defined here | LL | pub struct One(); | ^^^^^^^^^^^^^^^^^ you could import this directly +help: import `One` directly + | +LL ~ use crate::one::One; +LL ~ use crate::two::Two; + | error: aborting due to 1 previous error diff --git a/tests/ui/imports/private-import-suggestion-path-156244.edition_2015.stderr b/tests/ui/imports/private-import-suggestion-path-156244.edition_2015.stderr index 2a8795b6ab93c..14a196f0ee13b 100644 --- a/tests/ui/imports/private-import-suggestion-path-156244.edition_2015.stderr +++ b/tests/ui/imports/private-import-suggestion-path-156244.edition_2015.stderr @@ -36,6 +36,11 @@ note: ...and refers to the struct `One` which is defined here | LL | pub struct One; | ^^^^^^^^^^^^^^^ you could import this directly +help: import `One` directly + | +LL ~ use crate::a::One; +LL ~ use crate::b::Two; + | error[E0603]: struct import `Two` is private --> $DIR/private-import-suggestion-path-156244.rs:35:25 @@ -53,6 +58,11 @@ note: ...and refers to the struct `Two` which is defined here | LL | pub struct Two; | ^^^^^^^^^^^^^^^ you could import this directly +help: import `Two` directly + | +LL ~ use crate::a::Two; +LL ~ use crate::b::One; + | error[E0603]: module import `inner` is private --> $DIR/private-import-suggestion-path-156244.rs:38:24 diff --git a/tests/ui/imports/private-import-suggestion-path-156244.edition_2018.stderr b/tests/ui/imports/private-import-suggestion-path-156244.edition_2018.stderr index 9f112fe4e7551..988d3374ea78c 100644 --- a/tests/ui/imports/private-import-suggestion-path-156244.edition_2018.stderr +++ b/tests/ui/imports/private-import-suggestion-path-156244.edition_2018.stderr @@ -36,6 +36,11 @@ note: ...and refers to the struct `One` which is defined here | LL | pub struct One; | ^^^^^^^^^^^^^^^ you could import this directly +help: import `One` directly + | +LL ~ use crate::a::One; +LL ~ use crate::b::Two; + | error[E0603]: struct import `Two` is private --> $DIR/private-import-suggestion-path-156244.rs:35:25 @@ -53,6 +58,11 @@ note: ...and refers to the struct `Two` which is defined here | LL | pub struct Two; | ^^^^^^^^^^^^^^^ you could import this directly +help: import `Two` directly + | +LL ~ use crate::a::Two; +LL ~ use crate::b::One; + | error[E0603]: module import `inner` is private --> $DIR/private-import-suggestion-path-156244.rs:38:24