Skip to content

Commit 130da4a

Browse files
committed
fix: [std_instead_of_core] false positive for multi-imports
1 parent a6b9266 commit 130da4a

6 files changed

Lines changed: 295 additions & 58 deletions

clippy_lints/src/std_instead_of_core.rs

Lines changed: 154 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ use clippy_config::Conf;
22
use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg};
33
use clippy_utils::is_from_proc_macro;
44
use clippy_utils::msrvs::Msrv;
5-
use rustc_errors::Applicability;
5+
use rustc_errors::{Applicability, MultiSpan};
66
use rustc_hir::def::{DefKind, Res};
77
use rustc_hir::def_id::DefId;
88
use rustc_hir::{Block, Body, HirId, Path, PathSegment, StabilityLevel, StableSince};
9-
use rustc_lint::{LateContext, LateLintPass, Lint, LintContext};
9+
use rustc_lint::{LateContext, LateLintPass, LintContext};
1010
use rustc_session::impl_lint_pass;
1111
use rustc_span::symbol::kw;
12-
use rustc_span::{Span, sym};
12+
use rustc_span::{Span, Symbol, sym};
1313

1414
declare_clippy_lint! {
1515
/// ### What it does
@@ -116,44 +116,33 @@ impl StdReexports {
116116
}
117117

118118
#[derive(Debug)]
119-
enum LintPoint {
120-
Available(Span, &'static Lint, &'static str, &'static str),
121-
Conflict,
119+
struct LintPoint {
120+
span: Span,
121+
used_from: Symbol,
122+
available_from_core: bool,
123+
available_from_alloc: bool,
122124
}
123125

124126
impl<'tcx> LateLintPass<'tcx> for StdReexports {
125127
fn check_path(&mut self, cx: &LateContext<'tcx>, path: &Path<'tcx>, _: HirId) {
126-
if let Res::Def(def_kind, def_id) = path.res
128+
if let Some(def_id) = path.res.opt_def_id()
127129
&& let Some(first_segment) = get_first_segment(path)
128-
&& is_stable(cx, def_id, self.msrv)
129130
&& !path.span.in_external_macro(cx.sess().source_map())
130131
&& !is_from_proc_macro(cx, &first_segment.ident)
131-
&& !matches!(def_kind, DefKind::Macro(_))
132132
&& let Some(last_segment) = path.segments.last()
133133
&& let Res::Def(DefKind::Mod, crate_def_id) = first_segment.res
134134
&& crate_def_id.is_crate_root()
135135
{
136-
let (lint, used_mod, replace_with) = match first_segment.ident.name {
137-
sym::std => match cx.tcx.crate_name(def_id.krate) {
138-
sym::core => (STD_INSTEAD_OF_CORE, "std", "core"),
139-
sym::alloc => (STD_INSTEAD_OF_ALLOC, "std", "alloc"),
140-
_ => {
141-
self.lint_if_finish(cx, first_segment.ident.span, LintPoint::Conflict);
142-
return;
143-
},
144-
},
145-
sym::alloc if cx.tcx.crate_name(def_id.krate) == sym::core => (ALLOC_INSTEAD_OF_CORE, "alloc", "core"),
146-
_ => {
147-
self.lint_if_finish(cx, first_segment.ident.span, LintPoint::Conflict);
148-
return;
149-
},
136+
let is_stable = is_stable(cx, def_id, self.msrv);
137+
138+
let lint_point = LintPoint {
139+
span: last_segment.ident.span,
140+
used_from: first_segment.ident.name,
141+
available_from_core: is_stable && is_available_from(cx, sym::core, path),
142+
available_from_alloc: is_stable && is_available_from(cx, sym::alloc, path),
150143
};
151144

152-
self.lint_if_finish(
153-
cx,
154-
first_segment.ident.span,
155-
LintPoint::Available(last_segment.ident.span, lint, used_mod, replace_with),
156-
);
145+
self.lint_if_finish(cx, first_segment.ident.span, lint_point);
157146
}
158147
}
159148

@@ -171,27 +160,77 @@ impl<'tcx> LateLintPass<'tcx> for StdReexports {
171160
}
172161

173162
fn emit_lints(cx: &LateContext<'_>, lint_points: Option<(Span, Vec<LintPoint>)>) {
174-
let Some((krate_span, lint_points)) = lint_points else {
163+
let Some((krate_span, mut lint_points)) = lint_points else {
175164
return;
176165
};
177166

178-
let mut lint: Option<(&'static Lint, &'static str, &'static str)> = None;
179-
let mut has_conflict = false;
180-
for lint_point in &lint_points {
181-
match lint_point {
182-
LintPoint::Available(_, l, used_mod, replace_with)
183-
if lint.is_none_or(|(prev_l, ..)| l.name == prev_l.name) =>
184-
{
185-
lint = Some((l, used_mod, replace_with));
186-
},
187-
_ => {
188-
has_conflict = true;
189-
break;
190-
},
167+
// It's possible for multiple items to come from the same path.
168+
// For example, `std::vec` refers to a macro and a module.
169+
// In these cases, it's possible for them to have different availabilities.
170+
// `std::vec` as a macro and a module are both defined in `alloc` and unavailable in `core`.
171+
// Whereas, `std::env` is not available in `alloc`, and only the macro is available in `core`.
172+
// Since we aren't checking which of the shadowed items the user needs, we take the intersection
173+
// of these availabilities to ensure we don't provide the user a false positive.
174+
lint_points.sort_by_key(|lint_point| lint_point.span);
175+
lint_points.dedup_by(|a, b| {
176+
if a.span == b.span {
177+
b.available_from_alloc &= a.available_from_alloc;
178+
b.available_from_core &= a.available_from_core;
179+
true
180+
} else {
181+
false
182+
}
183+
});
184+
185+
let mut core_span = MultiSpan::new();
186+
let mut alloc_span = MultiSpan::new();
187+
let mut all_from_std = true;
188+
let mut all_from_alloc = true;
189+
let mut all_core = true;
190+
let mut all_alloc = true;
191+
192+
for lint_point in lint_points {
193+
all_from_std &= lint_point.used_from == sym::std;
194+
all_from_alloc &= lint_point.used_from == sym::alloc;
195+
196+
if lint_point.available_from_core {
197+
core_span.push_primary_span(lint_point.span);
198+
} else {
199+
all_core = false;
200+
}
201+
202+
if !lint_point.available_from_alloc {
203+
all_alloc = false;
204+
}
205+
206+
if !lint_point.available_from_core && lint_point.available_from_alloc {
207+
alloc_span.push_primary_span(lint_point.span);
191208
}
192209
}
193210

194-
if !has_conflict && let Some((lint, used_mod, replace_with)) = lint {
211+
let mut helps = Vec::new();
212+
let mut suggestions = Vec::new();
213+
214+
if all_from_std {
215+
if all_core {
216+
suggestions.push((STD_INSTEAD_OF_CORE, &sym::std, &sym::core));
217+
helps.push((STD_INSTEAD_OF_ALLOC, &sym::std, &sym::alloc, alloc_span));
218+
} else if all_alloc {
219+
suggestions.push((STD_INSTEAD_OF_ALLOC, &sym::std, &sym::alloc));
220+
helps.push((STD_INSTEAD_OF_CORE, &sym::std, &sym::core, core_span));
221+
} else {
222+
helps.push((STD_INSTEAD_OF_CORE, &sym::std, &sym::core, core_span));
223+
helps.push((STD_INSTEAD_OF_ALLOC, &sym::std, &sym::alloc, alloc_span));
224+
}
225+
} else if all_from_alloc {
226+
if all_core {
227+
suggestions.push((ALLOC_INSTEAD_OF_CORE, &sym::alloc, &sym::core));
228+
} else {
229+
helps.push((ALLOC_INSTEAD_OF_CORE, &sym::alloc, &sym::core, core_span));
230+
}
231+
}
232+
233+
for (lint, used_mod, replace_with) in suggestions {
195234
span_lint_and_sugg(
196235
cx,
197236
lint,
@@ -201,13 +240,12 @@ fn emit_lints(cx: &LateContext<'_>, lint_points: Option<(Span, Vec<LintPoint>)>)
201240
(*replace_with).to_string(),
202241
Applicability::MachineApplicable,
203242
);
204-
return;
205243
}
206244

207-
for lint_point in lint_points {
208-
let LintPoint::Available(span, lint, used_mod, replace_with) = lint_point else {
245+
for (lint, used_mod, replace_with, span) in helps {
246+
if span.primary_spans().is_empty() {
209247
continue;
210-
};
248+
}
211249
span_lint_and_help(
212250
cx,
213251
lint,
@@ -219,6 +257,75 @@ fn emit_lints(cx: &LateContext<'_>, lint_points: Option<(Span, Vec<LintPoint>)>)
219257
}
220258
}
221259

260+
/// Checks if the provided `path` could have its crate replaced with `krate` and still be valid.
261+
fn is_available_from(cx: &LateContext<'_>, krate: Symbol, path: &Path<'_>) -> bool {
262+
// Determine DefId for krate
263+
let Some(krate) = cx
264+
.tcx
265+
.crates(())
266+
.iter()
267+
.find(|&&krate_num| cx.tcx.is_user_visible_dep(krate_num) && cx.tcx.crate_name(krate_num) == krate)
268+
.map(|&krate| {
269+
Res::Def(
270+
DefKind::Mod,
271+
DefId {
272+
index: rustc_span::def_id::CRATE_DEF_INDEX,
273+
krate,
274+
},
275+
)
276+
})
277+
else {
278+
return false;
279+
};
280+
281+
path.segments
282+
.iter()
283+
.enumerate()
284+
.skip_while(|(_, segment)| {
285+
// Skip `::crate` and `crate` segments
286+
segment.ident.name == kw::PathRoot || segment.res.opt_def_id().is_some_and(DefId::is_crate_root)
287+
})
288+
.scan(krate, |cursor, (index, segment)| {
289+
// Some, but not all, items from core and reexported by alloc.
290+
// To find these cases, we must walk the alloc and core crates along the provided path
291+
// to see if the desired item is accessible.
292+
*cursor = cx
293+
.tcx
294+
.module_children({
295+
// TyCtxt::module_children will ICE if the provided DefId isn't "module like".
296+
cursor.module_like_def_id()?
297+
})
298+
.iter()
299+
.filter(|child| {
300+
// Since this lint is only designed to work on `std`, `core`, and `alloc` from a
301+
// user's crate, only public items are of concern.
302+
child.vis.is_public()
303+
})
304+
.filter(|child| {
305+
// We search by name to allow cases like `core::str` and `std::str`.
306+
// `std::str` is actually a reexport of `alloc::str`, but `alloc::str`
307+
// reexports the contents of `core::str`, so many instances of `std::str`
308+
// can be replaced with `core::str`, even if they are distinct items.
309+
child.ident.name == segment.ident.name
310+
})
311+
.map(|child| child.res.map_id(|x| x))
312+
.find(|child| {
313+
if index + 1 == path.segments.len() {
314+
// The last segment must resolve to the same item as the original path.
315+
*child == path.res
316+
} else {
317+
// We allow intermediate segments to resolve to different items,
318+
// but we only care about module-like items.
319+
child.module_like_def_id().is_some()
320+
}
321+
})?;
322+
323+
Some(*cursor)
324+
})
325+
.last()
326+
.is_some_and(|cursor| cursor == path.res)
327+
}
328+
222329
/// Returns the first named segment of a [`Path`].
223330
///
224331
/// If this is a global path (such as `::std::fmt::Debug`), then the segment after [`kw::PathRoot`]

tests/ui/std_instead_of_core.fixed

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,24 @@ fn issue13158_msrv_1_80(_: &dyn std::error::Error) {}
116116
#[clippy::msrv = "1.81"]
117117
fn issue13158_msrv_1_81(_: &dyn core::error::Error) {}
118118
//~^ std_instead_of_core
119+
120+
#[warn(clippy::std_instead_of_alloc)]
121+
fn pr17252() {
122+
use core::result::{self, Iter, Result};
123+
//~^ std_instead_of_core
124+
125+
use alloc::str::{self as _};
126+
//~^ std_instead_of_alloc
127+
128+
use alloc::str::{self as _, FromStr as _};
129+
//~^ std_instead_of_alloc
130+
//~| std_instead_of_core
131+
132+
use alloc::fmt::{self, Write};
133+
//~^ std_instead_of_alloc
134+
//~| std_instead_of_core
135+
136+
// Ensure macros can be linted.
137+
use core::writeln;
138+
//~^ std_instead_of_core
139+
}

tests/ui/std_instead_of_core.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,24 @@ fn issue13158_msrv_1_80(_: &dyn std::error::Error) {}
116116
#[clippy::msrv = "1.81"]
117117
fn issue13158_msrv_1_81(_: &dyn std::error::Error) {}
118118
//~^ std_instead_of_core
119+
120+
#[warn(clippy::std_instead_of_alloc)]
121+
fn pr17252() {
122+
use std::result::{self, Iter, Result};
123+
//~^ std_instead_of_core
124+
125+
use std::str::{self as _};
126+
//~^ std_instead_of_alloc
127+
128+
use std::str::{self as _, FromStr as _};
129+
//~^ std_instead_of_alloc
130+
//~| std_instead_of_core
131+
132+
use std::fmt::{self, Write};
133+
//~^ std_instead_of_alloc
134+
//~| std_instead_of_core
135+
136+
// Ensure macros can be linted.
137+
use std::writeln;
138+
//~^ std_instead_of_core
139+
}

tests/ui/std_instead_of_core.stderr

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,5 +109,51 @@ error: used import from `std` instead of `core`
109109
LL | fn issue13158_msrv_1_81(_: &dyn std::error::Error) {}
110110
| ^^^ help: consider importing the item from `core`: `core`
111111

112-
error: aborting due to 17 previous errors
112+
error: used import from `std` instead of `core`
113+
--> tests/ui/std_instead_of_core.rs:122:9
114+
|
115+
LL | use std::result::{self, Iter, Result};
116+
| ^^^ help: consider importing the item from `core`: `core`
117+
118+
error: used import from `std` instead of `alloc`
119+
--> tests/ui/std_instead_of_core.rs:125:9
120+
|
121+
LL | use std::str::{self as _};
122+
| ^^^ help: consider importing the item from `alloc`: `alloc`
123+
124+
error: used import from `std` instead of `alloc`
125+
--> tests/ui/std_instead_of_core.rs:128:9
126+
|
127+
LL | use std::str::{self as _, FromStr as _};
128+
| ^^^ help: consider importing the item from `alloc`: `alloc`
129+
130+
error: used import from `std` instead of `core`
131+
--> tests/ui/std_instead_of_core.rs:128:31
132+
|
133+
LL | use std::str::{self as _, FromStr as _};
134+
| ^^^^^^^
135+
|
136+
= help: consider importing the item from `core`
137+
138+
error: used import from `std` instead of `alloc`
139+
--> tests/ui/std_instead_of_core.rs:132:9
140+
|
141+
LL | use std::fmt::{self, Write};
142+
| ^^^ help: consider importing the item from `alloc`: `alloc`
143+
144+
error: used import from `std` instead of `core`
145+
--> tests/ui/std_instead_of_core.rs:132:26
146+
|
147+
LL | use std::fmt::{self, Write};
148+
| ^^^^^
149+
|
150+
= help: consider importing the item from `core`
151+
152+
error: used import from `std` instead of `core`
153+
--> tests/ui/std_instead_of_core.rs:137:9
154+
|
155+
LL | use std::writeln;
156+
| ^^^ help: consider importing the item from `core`: `core`
157+
158+
error: aborting due to 24 previous errors
113159

0 commit comments

Comments
 (0)