Skip to content

Commit 28d43c3

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

6 files changed

Lines changed: 308 additions & 56 deletions

clippy_lints/src/std_instead_of_core.rs

Lines changed: 161 additions & 54 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,34 @@ 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) {
126128
if let Res::Def(def_kind, def_id) = path.res
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)
131132
&& !matches!(def_kind, DefKind::Macro(_))
132133
&& let Some(last_segment) = path.segments.last()
133134
&& let Res::Def(DefKind::Mod, crate_def_id) = first_segment.res
134135
&& crate_def_id.is_crate_root()
135136
{
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-
},
137+
let is_stable = is_stable(cx, def_id, self.msrv);
138+
139+
let lint_point = LintPoint {
140+
span: last_segment.ident.span,
141+
used_from: first_segment.ident.name,
142+
available_from_core: is_stable && is_available_from(cx, sym::core, path),
143+
available_from_alloc: is_stable && is_available_from(cx, sym::alloc, path),
150144
};
151145

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-
);
146+
self.lint_if_finish(cx, first_segment.ident.span, lint_point);
157147
}
158148
}
159149

@@ -171,27 +161,77 @@ impl<'tcx> LateLintPass<'tcx> for StdReexports {
171161
}
172162

173163
fn emit_lints(cx: &LateContext<'_>, lint_points: Option<(Span, Vec<LintPoint>)>) {
174-
let Some((krate_span, lint_points)) = lint_points else {
164+
let Some((krate_span, mut lint_points)) = lint_points else {
175165
return;
176166
};
177167

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

194-
if !has_conflict && let Some((lint, used_mod, replace_with)) = lint {
212+
let mut helps = Vec::new();
213+
let mut suggestions = Vec::new();
214+
215+
if all_from_std {
216+
if all_core {
217+
suggestions.push((STD_INSTEAD_OF_CORE, &sym::std, &sym::core));
218+
helps.push((STD_INSTEAD_OF_ALLOC, &sym::std, &sym::alloc, alloc_span));
219+
} else if all_alloc {
220+
suggestions.push((STD_INSTEAD_OF_ALLOC, &sym::std, &sym::alloc));
221+
helps.push((STD_INSTEAD_OF_CORE, &sym::std, &sym::core, core_span));
222+
} else {
223+
helps.push((STD_INSTEAD_OF_CORE, &sym::std, &sym::core, core_span));
224+
helps.push((STD_INSTEAD_OF_ALLOC, &sym::std, &sym::alloc, alloc_span));
225+
}
226+
} else if all_from_alloc {
227+
if all_core {
228+
suggestions.push((ALLOC_INSTEAD_OF_CORE, &sym::alloc, &sym::core));
229+
} else {
230+
helps.push((ALLOC_INSTEAD_OF_CORE, &sym::alloc, &sym::core, core_span));
231+
}
232+
}
233+
234+
for (lint, used_mod, replace_with) in suggestions {
195235
span_lint_and_sugg(
196236
cx,
197237
lint,
@@ -201,24 +241,91 @@ fn emit_lints(cx: &LateContext<'_>, lint_points: Option<(Span, Vec<LintPoint>)>)
201241
(*replace_with).to_string(),
202242
Applicability::MachineApplicable,
203243
);
204-
return;
205244
}
206245

207-
for lint_point in lint_points {
208-
let LintPoint::Available(span, lint, used_mod, replace_with) = lint_point else {
209-
continue;
210-
};
211-
span_lint_and_help(
212-
cx,
213-
lint,
214-
span,
215-
format!("used import from `{used_mod}` instead of `{replace_with}`"),
216-
None,
217-
format!("consider importing the item from `{replace_with}`"),
218-
);
246+
for (lint, used_mod, replace_with, span) in helps {
247+
for &span in span.primary_spans() {
248+
span_lint_and_help(
249+
cx,
250+
lint,
251+
span,
252+
format!("used import from `{used_mod}` instead of `{replace_with}`"),
253+
None,
254+
format!("consider importing the item from `{replace_with}`"),
255+
);
256+
}
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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,23 @@ 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+
// Macros aren't currently linted.
137+
use std::writeln;
138+
}

tests/ui/std_instead_of_core.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,23 @@ 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+
// Macros aren't currently linted.
137+
use std::writeln;
138+
}

tests/ui/std_instead_of_core.stderr

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,5 +109,45 @@ 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: aborting due to 23 previous errors
113153

tests/ui/std_instead_of_core_unfixable.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,26 @@ fn pr16964() {
2525
ffi::OsString,
2626
};
2727
}
28+
29+
#[warn(clippy::alloc_instead_of_core)]
30+
#[rustfmt::skip]
31+
fn pr17252() {
32+
extern crate alloc;
33+
34+
use alloc::str::{self as _, FromStr as _};
35+
//~^ alloc_instead_of_core
36+
37+
use std::sync::{
38+
Arc,
39+
Mutex,
40+
Weak,
41+
atomic::{
42+
AtomicPtr,
43+
Ordering,
44+
},
45+
};
46+
//~^^^^^^^^ std_instead_of_alloc
47+
//~^^^^^^^ std_instead_of_alloc
48+
//~^^^^^^ std_instead_of_core
49+
//~^^^^^^ std_instead_of_core
50+
}

0 commit comments

Comments
 (0)