Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions compiler/rustc_resolve/src/diagnostics/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2523,13 +2523,76 @@ 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,
decl,
outermost_res,
parent_scope,
single_nested,
use_stmt_span,
dedup_span,
ref source,
} = *privacy_error;
Expand Down Expand Up @@ -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();
Expand Down
8 changes: 8 additions & 0 deletions compiler/rustc_resolve/src/ident.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Comment on lines +1367 to +1374

@fee1-dead fee1-dead Jul 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the significance of using an Option here?

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a doc comment explaining that the span is only needed (and set) when single_nested is true, so Option avoids carrying meaningless data for non grouped imports.

});
} else {
return Err(ControlFlow::Break(Determined));
Expand Down
3 changes: 3 additions & 0 deletions compiler/rustc_resolve/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Span>,
source: Option<ast::Expr>,
}

Expand Down
32 changes: 32 additions & 0 deletions tests/ui/imports/private-import-grouped-suggestion-157453.rs
Original file line number Diff line number Diff line change
@@ -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() {}
69 changes: 69 additions & 0 deletions tests/ui/imports/private-import-grouped-suggestion-157453.stderr
Original file line number Diff line number Diff line change
@@ -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`.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading