Skip to content

Commit 59ad765

Browse files
committed
fix(resolve): suggest splitting grouped imports with private items
Add suggestion for E0603 in grouped use statements. When a private item is inside , suggest a new import line with the correct path and remove the item from the group. Single-item groups get a full-line replacement instead.
1 parent 1a3795c commit 59ad765

8 files changed

Lines changed: 244 additions & 1 deletion

compiler/rustc_resolve/src/error_helper.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2525,6 +2525,7 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
25252525
outermost_res,
25262526
parent_scope,
25272527
single_nested,
2528+
use_stmt_span,
25282529
dedup_span,
25292530
ref source,
25302531
} = *privacy_error;
@@ -2740,6 +2741,146 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
27402741
err.subdiagnostic(sugg);
27412742
break;
27422743
}
2744+
} else if single_nested
2745+
&& let Some(stmt_span) = use_stmt_span
2746+
&& !shown_candidates
2747+
&& !outermost_res.is_some_and(|(_, outer)| outer.span != ident.span)
2748+
{
2749+
sugg_paths.sort_by_key(|(p, reexport)| (p.len(), p[0].name == sym::core, *reexport));
2750+
for (sugg, reexport) in sugg_paths {
2751+
if sugg.len() <= 1 {
2752+
continue;
2753+
}
2754+
let path = join_path_idents(sugg);
2755+
let Ok(source_text) = self.tcx.sess.source_map().span_to_snippet(stmt_span) else {
2756+
continue;
2757+
};
2758+
2759+
let lo_offset = (ident.span.lo() - stmt_span.lo()).0 as usize;
2760+
let hi_offset = (ident.span.hi() - stmt_span.lo()).0 as usize;
2761+
2762+
if lo_offset > source_text.len() || hi_offset > source_text.len() {
2763+
continue;
2764+
}
2765+
2766+
let mut start = lo_offset;
2767+
let mut end = hi_offset;
2768+
2769+
let mut found_trailing_comma = false;
2770+
let mut temp_end = end;
2771+
while temp_end < source_text.len() {
2772+
let ch = source_text[temp_end..].chars().next().unwrap();
2773+
if ch.is_whitespace() {
2774+
temp_end += ch.len_utf8();
2775+
} else if ch == ',' {
2776+
temp_end += ch.len_utf8();
2777+
found_trailing_comma = true;
2778+
break;
2779+
} else {
2780+
break;
2781+
}
2782+
}
2783+
2784+
if found_trailing_comma {
2785+
while temp_end < source_text.len() {
2786+
let ch = source_text[temp_end..].chars().next().unwrap();
2787+
if ch.is_whitespace() {
2788+
temp_end += ch.len_utf8();
2789+
} else {
2790+
break;
2791+
}
2792+
}
2793+
end = temp_end;
2794+
} else {
2795+
let mut temp_start = start;
2796+
while temp_start > 0 {
2797+
let ch = source_text[..temp_start].chars().next_back().unwrap();
2798+
if ch.is_whitespace() {
2799+
temp_start -= ch.len_utf8();
2800+
} else if ch == ',' {
2801+
temp_start -= ch.len_utf8();
2802+
break;
2803+
} else {
2804+
break;
2805+
}
2806+
}
2807+
start = temp_start;
2808+
}
2809+
2810+
let mut replacement = String::new();
2811+
replacement.push_str(&source_text[..start]);
2812+
replacement.push_str(&source_text[end..]);
2813+
2814+
// If removing the ident leaves an empty group, replace the path entirely
2815+
if replacement.contains("{}") {
2816+
let msg = if reexport {
2817+
format!("import `{ident}` through the re-export")
2818+
} else {
2819+
format!("import `{ident}` directly")
2820+
};
2821+
2822+
let line_span = self.tcx.sess.source_map().span_extend_to_line(stmt_span);
2823+
let indentation = {
2824+
if let Ok(line) = self.tcx.sess.source_map().span_to_snippet(line_span) {
2825+
let indent_len = line.chars().take_while(|c| c.is_whitespace()).count();
2826+
" ".repeat(indent_len)
2827+
} else {
2828+
let pos = self.tcx.sess.source_map().lookup_char_pos(stmt_span.lo());
2829+
" ".repeat(pos.col.0.saturating_sub(4) as usize)
2830+
}
2831+
};
2832+
2833+
let suggestion_text = format!("{}use {};", indentation, path);
2834+
err.multipart_suggestion(
2835+
msg,
2836+
vec![(line_span, suggestion_text)],
2837+
rustc_errors::Applicability::MachineApplicable,
2838+
);
2839+
break;
2840+
}
2841+
2842+
// If only one item remains in the group, remove the braces
2843+
if let Some(open) = replacement.find('{') {
2844+
if let Some(close) = replacement.rfind('}') {
2845+
let inner = replacement[open + 1..close].trim();
2846+
if !inner.contains(',') && !inner.is_empty() {
2847+
// Replace `{ident}` with just `ident`
2848+
replacement = format!("{}{}", replacement[..open].trim_end(), inner);
2849+
}
2850+
}
2851+
}
2852+
2853+
let msg = if reexport {
2854+
format!("import `{ident}` through the re-export")
2855+
} else {
2856+
format!("import `{ident}` directly")
2857+
};
2858+
2859+
// Calculate the indentation of the original `use` statement to ensure the
2860+
// suggested import aligns with the existing code.
2861+
let indentation = {
2862+
if let Ok(line) = self.tcx.sess.source_map().span_to_snippet(
2863+
self.tcx.sess.source_map().span_extend_to_line(stmt_span.shrink_to_lo()),
2864+
) {
2865+
let indent_len = line.chars().take_while(|c| c.is_whitespace()).count();
2866+
" ".repeat(indent_len)
2867+
} else {
2868+
let pos = self.tcx.sess.source_map().lookup_char_pos(stmt_span.lo());
2869+
" ".repeat(pos.col.0.saturating_sub(4) as usize)
2870+
}
2871+
};
2872+
2873+
// Insert before the path to avoid duplicating `use`; stmt_span doesn't include the keyword.
2874+
err.multipart_suggestion(
2875+
msg,
2876+
vec![
2877+
(stmt_span.shrink_to_lo(), format!("{path};\n{indentation}use ")),
2878+
(stmt_span, replacement),
2879+
],
2880+
rustc_errors::Applicability::MachineApplicable,
2881+
);
2882+
break;
2883+
}
27432884
}
27442885

27452886
err.emit();

compiler/rustc_resolve/src/ident.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1355,6 +1355,13 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
13551355
source: None,
13561356
parent_scope: *parent_scope,
13571357
single_nested: path_span != root_span,
1358+
use_stmt_span: if path_span != root_span {
1359+
// `root_span` spans the entire `use` tree, which is needed
1360+
// to correctly generate a multipart suggestion for a grouped import.
1361+
Some(root_span)
1362+
} else {
1363+
None
1364+
},
13581365
});
13591366
} else {
13601367
return Err(ControlFlow::Break(Determined));

compiler/rustc_resolve/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,6 +1051,9 @@ struct PrivacyError<'ra> {
10511051
parent_scope: ParentScope<'ra>,
10521052
/// Is the format `use a::{b,c}`?
10531053
single_nested: bool,
1054+
/// Span of the entire `use` statement, including the `use` keyword.
1055+
/// Only set for grouped imports.
1056+
use_stmt_span: Option<Span>,
10541057
source: Option<ast::Expr>,
10551058
}
10561059

tests/ui/imports/private-import-grouped-suggestion-157453.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,22 @@ mod test_grouped {
1111
use crate::two::{One, Two}; //~ ERROR struct import `One` is private [E0603]
1212
}
1313

14+
mod test_single_item {
15+
use crate::two::{One}; //~ ERROR struct import `One` is private [E0603]
16+
}
17+
18+
mod outer {
19+
pub mod inner {
20+
pub struct MyPath;
21+
}
22+
}
23+
24+
mod reexport {
25+
use crate::outer::inner::MyPath;
26+
}
27+
28+
mod test_std_style {
29+
use crate::reexport::{MyPath}; //~ ERROR struct import `MyPath` is private [E0603]
30+
}
31+
1432
fn main() {}

tests/ui/imports/private-import-grouped-suggestion-157453.stderr

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,56 @@ note: ...and refers to the struct `One` which is defined here
1414
|
1515
LL | pub struct One();
1616
| ^^^^^^^^^^^^^^^^^ you could import this directly
17+
help: import `One` directly
18+
|
19+
LL ~ use crate::one::One;
20+
LL ~ use crate::two::Two;
21+
|
22+
23+
error[E0603]: struct import `One` is private
24+
--> $DIR/private-import-grouped-suggestion-157453.rs:15:22
25+
|
26+
LL | use crate::two::{One};
27+
| ^^^ private struct import
28+
|
29+
note: the struct import `One` is defined here...
30+
--> $DIR/private-import-grouped-suggestion-157453.rs:6:9
31+
|
32+
LL | use crate::one::One;
33+
| ^^^^^^^^^^^^^^^
34+
note: ...and refers to the struct `One` which is defined here
35+
--> $DIR/private-import-grouped-suggestion-157453.rs:2:5
36+
|
37+
LL | pub struct One();
38+
| ^^^^^^^^^^^^^^^^^ you could import this directly
39+
help: import `One` directly
40+
|
41+
LL - use crate::two::{One};
42+
LL + use crate::one::One;
43+
|
44+
45+
error[E0603]: struct import `MyPath` is private
46+
--> $DIR/private-import-grouped-suggestion-157453.rs:29:27
47+
|
48+
LL | use crate::reexport::{MyPath};
49+
| ^^^^^^ private struct import
50+
|
51+
note: the struct import `MyPath` is defined here...
52+
--> $DIR/private-import-grouped-suggestion-157453.rs:25:9
53+
|
54+
LL | use crate::outer::inner::MyPath;
55+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
56+
note: ...and refers to the struct `MyPath` which is defined here
57+
--> $DIR/private-import-grouped-suggestion-157453.rs:20:9
58+
|
59+
LL | pub struct MyPath;
60+
| ^^^^^^^^^^^^^^^^^^ you could import this directly
61+
help: import `MyPath` directly
62+
|
63+
LL - use crate::reexport::{MyPath};
64+
LL + use crate::outer::inner::MyPath;
65+
|
1766

18-
error: aborting due to 1 previous error
67+
error: aborting due to 3 previous errors
1968

2069
For more information about this error, try `rustc --explain E0603`.

tests/ui/imports/private-import-nested-suggestion-156060.stderr

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ note: ...and refers to the struct `One` which is defined here
1414
|
1515
LL | pub struct One();
1616
| ^^^^^^^^^^^^^^^^^ you could import this directly
17+
help: import `One` directly
18+
|
19+
LL ~ use crate::one::One;
20+
LL ~ use crate::two::Two;
21+
|
1722

1823
error: aborting due to 1 previous error
1924

tests/ui/imports/private-import-suggestion-path-156244.edition_2015.stderr

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ note: ...and refers to the struct `One` which is defined here
3636
|
3737
LL | pub struct One;
3838
| ^^^^^^^^^^^^^^^ you could import this directly
39+
help: import `One` directly
40+
|
41+
LL ~ use crate::a::One;
42+
LL ~ use crate::b::Two;
43+
|
3944

4045
error[E0603]: struct import `Two` is private
4146
--> $DIR/private-import-suggestion-path-156244.rs:35:25
@@ -53,6 +58,11 @@ note: ...and refers to the struct `Two` which is defined here
5358
|
5459
LL | pub struct Two;
5560
| ^^^^^^^^^^^^^^^ you could import this directly
61+
help: import `Two` directly
62+
|
63+
LL ~ use crate::a::Two;
64+
LL ~ use crate::b::One;
65+
|
5666

5767
error[E0603]: module import `inner` is private
5868
--> $DIR/private-import-suggestion-path-156244.rs:38:24

tests/ui/imports/private-import-suggestion-path-156244.edition_2018.stderr

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ note: ...and refers to the struct `One` which is defined here
3636
|
3737
LL | pub struct One;
3838
| ^^^^^^^^^^^^^^^ you could import this directly
39+
help: import `One` directly
40+
|
41+
LL ~ use crate::a::One;
42+
LL ~ use crate::b::Two;
43+
|
3944

4045
error[E0603]: struct import `Two` is private
4146
--> $DIR/private-import-suggestion-path-156244.rs:35:25
@@ -53,6 +58,11 @@ note: ...and refers to the struct `Two` which is defined here
5358
|
5459
LL | pub struct Two;
5560
| ^^^^^^^^^^^^^^^ you could import this directly
61+
help: import `Two` directly
62+
|
63+
LL ~ use crate::a::Two;
64+
LL ~ use crate::b::One;
65+
|
5666

5767
error[E0603]: module import `inner` is private
5868
--> $DIR/private-import-suggestion-path-156244.rs:38:24

0 commit comments

Comments
 (0)