Skip to content

Commit c974a5b

Browse files
committed
fix: [std_instead_of_core] false positive for multi-imports
1 parent efb5560 commit c974a5b

3 files changed

Lines changed: 193 additions & 116 deletions

File tree

clippy_lints/src/std_instead_of_core.rs

Lines changed: 166 additions & 113 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, DiagMessage};
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};
99
use rustc_lint::{LateContext, LateLintPass, Lint, LintContext};
1010
use rustc_session::impl_lint_pass;
1111
use rustc_span::symbol::kw;
12-
use rustc_span::{Span, sym};
12+
use rustc_span::{Ident, Span, Symbol, sym};
1313

1414
declare_clippy_lint! {
1515
/// ### What it does
@@ -93,129 +93,181 @@ impl_lint_pass!(StdReexports => [
9393
]);
9494

9595
pub struct StdReexports {
96-
lint_points: Option<(Span, Vec<LintPoint>)>,
96+
root: Span,
97+
lint_points: Vec<LintPoint>,
98+
has_conflicts: bool,
9799
msrv: Msrv,
98100
}
99101

100102
impl StdReexports {
101103
pub fn new(conf: &'static Conf) -> Self {
102104
Self {
103-
lint_points: Option::default(),
105+
root: Span::default(),
106+
lint_points: Vec::new(),
107+
has_conflicts: false,
104108
msrv: conf.msrv,
105109
}
106110
}
107111

108-
fn lint_if_finish(&mut self, cx: &LateContext<'_>, krate: Span, lint_point: LintPoint) {
109-
match &mut self.lint_points {
110-
Some((prev_krate, prev_lints)) if prev_krate.overlaps(krate) => {
111-
prev_lints.push(lint_point);
112-
},
113-
_ => emit_lints(cx, self.lint_points.replace((krate, vec![lint_point]))),
112+
/// If all the contained [`LintPoint`]s refer to the same lint _and_ there
113+
/// aren't any conflicts (e.g, multi-imports which aren't covered by this lint),
114+
/// combine them into a single lint suitable for a suggestion.
115+
fn combine_lints(&self) -> Option<LintPoint> {
116+
if !self.has_conflicts
117+
&& let Some(lint_point) = self
118+
.lint_points
119+
.iter()
120+
.copied()
121+
.map(Some)
122+
.reduce(|prev, next| prev?.combine(next?))
123+
.flatten()
124+
{
125+
Some(LintPoint {
126+
span: self.root,
127+
..lint_point
128+
})
129+
} else {
130+
None
114131
}
115132
}
116-
}
117133

118-
#[derive(Debug)]
119-
enum LintPoint {
120-
Available(Span, &'static Lint, &'static str, &'static str),
121-
Conflict,
134+
/// Emit any currently stored [`LintPoint`]s and reset internal storage.
135+
fn emit(&mut self, cx: &LateContext<'_>) {
136+
if let Some(lint_point) = self.combine_lints() {
137+
span_lint_and_sugg(
138+
cx,
139+
lint_point.lint,
140+
lint_point.span,
141+
lint_point.message(),
142+
lint_point.help(),
143+
lint_point.replace_with.to_string(),
144+
Applicability::MachineApplicable,
145+
);
146+
} else {
147+
for lint_point in &self.lint_points {
148+
span_lint_and_help(
149+
cx,
150+
lint_point.lint,
151+
lint_point.span,
152+
lint_point.message(),
153+
None,
154+
lint_point.help(),
155+
);
156+
}
157+
}
158+
159+
self.root = Span::default();
160+
self.lint_points.clear();
161+
self.has_conflicts = false;
162+
}
122163
}
123164

124165
impl<'tcx> LateLintPass<'tcx> for StdReexports {
125166
fn check_path(&mut self, cx: &LateContext<'tcx>, path: &Path<'tcx>, _: HirId) {
126-
if let Res::Def(def_kind, def_id) = path.res
127-
&& let Some(first_segment) = get_first_segment(path)
128-
&& is_stable(cx, def_id, self.msrv)
129-
&& !path.span.in_external_macro(cx.sess().source_map())
130-
&& !is_from_proc_macro(cx, &first_segment.ident)
131-
&& !matches!(def_kind, DefKind::Macro(_))
132-
&& let Some(last_segment) = path.segments.last()
133-
&& let Res::Def(DefKind::Mod, crate_def_id) = first_segment.res
134-
&& crate_def_id.is_crate_root()
135-
{
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-
},
150-
};
167+
// Path must resolve to a definition where the first segment is at least a module.
168+
let (
169+
Res::Def(def_kind, def_id),
170+
Some(&PathSegment {
171+
ident: ref first_segment @ Ident { name: used, .. },
172+
res: Res::Def(DefKind::Mod, crate_def_id),
173+
..
174+
}),
175+
Some(PathSegment {
176+
ident: last_segment, ..
177+
}),
178+
) = (path.res, get_first_segment(path), path.segments.last())
179+
else {
180+
return;
181+
};
151182

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-
);
183+
// Paths within or from macros do not need to be linted.
184+
if path.span.in_external_macro(cx.sess().source_map()) || is_from_proc_macro(cx, first_segment) {
185+
return;
186+
}
187+
188+
// If the first segment of the path is not a crate (std, core, alloc, etc.), then
189+
// we cannot determine if it should be replaced.
190+
if !crate_def_id.is_crate_root() {
191+
return;
192+
}
193+
194+
// Exclude paths that refer to macros.
195+
if matches!(def_kind, DefKind::Macro(_)) {
196+
return;
197+
}
198+
199+
// Determine if this path may be a candidate for linting.
200+
let potential_lint_point = LintPoint::new(last_segment.span, used, cx.tcx.crate_name(def_id.krate));
201+
202+
// Emit existing lint points if this lint refers to a new path.
203+
if !self.root.overlaps(first_segment.span) {
204+
self.emit(cx);
205+
self.root = first_segment.span;
206+
}
207+
208+
match potential_lint_point {
209+
Some(lint_point) if is_stable(cx, def_id, self.msrv) => {
210+
self.lint_points.push(lint_point);
211+
},
212+
_ => {
213+
self.has_conflicts = true;
214+
},
157215
}
158216
}
159217

160218
fn check_block_post(&mut self, cx: &LateContext<'tcx>, _: &Block<'tcx>) {
161-
emit_lints(cx, self.lint_points.take());
219+
self.emit(cx);
162220
}
163221

164222
fn check_body_post(&mut self, cx: &LateContext<'tcx>, _: &Body<'tcx>) {
165-
emit_lints(cx, self.lint_points.take());
223+
self.emit(cx);
166224
}
167225

168226
fn check_crate_post(&mut self, cx: &LateContext<'tcx>) {
169-
emit_lints(cx, self.lint_points.take());
227+
self.emit(cx);
170228
}
171229
}
172230

173-
fn emit_lints(cx: &LateContext<'_>, lint_points: Option<(Span, Vec<LintPoint>)>) {
174-
let Some((krate_span, lint_points)) = lint_points else {
175-
return;
176-
};
231+
#[derive(Debug, Clone, Copy)]
232+
struct LintPoint {
233+
span: Span,
234+
lint: &'static Lint,
235+
used: Symbol,
236+
replace_with: Symbol,
237+
}
177238

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-
},
191-
}
192-
}
239+
impl LintPoint {
240+
fn new(span: Span, used: Symbol, replace_with: Symbol) -> Option<Self> {
241+
let lint = match (used, replace_with) {
242+
(sym::std, sym::core) => Some(STD_INSTEAD_OF_CORE),
243+
(sym::std, sym::alloc) => Some(STD_INSTEAD_OF_ALLOC),
244+
(sym::alloc, sym::core) => Some(ALLOC_INSTEAD_OF_CORE),
245+
_ => None,
246+
}?;
193247

194-
if !has_conflict && let Some((lint, used_mod, replace_with)) = lint {
195-
span_lint_and_sugg(
196-
cx,
248+
Some(Self {
249+
span,
197250
lint,
198-
krate_span,
199-
format!("used import from `{used_mod}` instead of `{replace_with}`"),
200-
format!("consider importing the item from `{replace_with}`"),
201-
(*replace_with).to_string(),
202-
Applicability::MachineApplicable,
203-
);
204-
return;
251+
used,
252+
replace_with,
253+
})
205254
}
206255

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-
);
256+
/// Combine two [`LintPoint`]s if they cover the same lint.
257+
/// Does not check if they refer to a meaningfully similar span.
258+
fn combine(self, other: Self) -> Option<Self> {
259+
core::ptr::eq(self.lint, other.lint).then_some(Self {
260+
span: Span::default(),
261+
..self
262+
})
263+
}
264+
265+
fn message(&self) -> impl Into<DiagMessage> {
266+
format!("used import from `{}` instead of `{}`", self.used, self.replace_with)
267+
}
268+
269+
fn help(&self) -> impl Into<DiagMessage> {
270+
format!("consider importing the item from `{}`", self.replace_with)
219271
}
220272
}
221273

@@ -236,29 +288,30 @@ fn get_first_segment<'tcx>(path: &Path<'tcx>) -> Option<&'tcx PathSegment<'tcx>>
236288
/// or now stable moves that were once unstable.
237289
///
238290
/// Does not catch individually moved items
239-
fn is_stable(cx: &LateContext<'_>, mut def_id: DefId, msrv: Msrv) -> bool {
240-
loop {
241-
if let Some(stability) = cx.tcx.lookup_stability(def_id) {
242-
match stability.level {
243-
// Workaround for items from `core::intrinsics` with a stable export in a different module.
244-
// Not that we ignore the `since` field as we are already accessing the item in question.
245-
StabilityLevel::Stable {
246-
allowed_through_unstable_modules: Some(_),
247-
..
248-
} => return true,
249-
StabilityLevel::Stable { since, .. } => match since {
250-
StableSince::Version(v) if !msrv.meets(cx, v) => return false,
251-
StableSince::Current if msrv.current(cx).is_none() => return false,
252-
StableSince::Err(_) => return false,
253-
StableSince::Version(_) | StableSince::Current => {},
254-
},
255-
StabilityLevel::Unstable { .. } => return false,
256-
}
257-
}
291+
fn is_stable(cx: &LateContext<'_>, def_id: DefId, msrv: Msrv) -> bool {
292+
iter_parents(cx, def_id)
293+
.find_map(|def_id| match cx.tcx.lookup_stability(def_id)?.level {
294+
StabilityLevel::Unstable { .. } => Some(false),
295+
StabilityLevel::Stable {
296+
since,
297+
allowed_through_unstable_modules,
298+
} => {
299+
let stable = match since {
300+
StableSince::Version(v) => msrv.meets(cx, v),
301+
StableSince::Current => msrv.current(cx).is_none(),
302+
StableSince::Err(_) => false,
303+
};
258304

259-
match cx.tcx.opt_parent(def_id) {
260-
Some(parent) => def_id = parent,
261-
None => return true,
262-
}
263-
}
305+
(!stable || allowed_through_unstable_modules.is_some()).then_some(stable)
306+
},
307+
})
308+
.unwrap_or(true)
309+
}
310+
311+
/// Iterate (inclusively) over all ancestors of the provided [`DefId`].
312+
fn iter_parents(cx: &LateContext<'_>, mut def_id: DefId) -> impl Iterator<Item = DefId> {
313+
core::iter::once(def_id).chain(core::iter::from_fn(move || {
314+
def_id = cx.tcx.opt_parent(def_id)?;
315+
Some(def_id)
316+
}))
264317
}

tests/ui/std_instead_of_core_unfixable.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ fn issue15143() {
1313
use std::{error::Error, vec::Vec, fs::File};
1414
//~^ std_instead_of_core
1515
//~| std_instead_of_alloc
16+
17+
use std::sync::{
18+
Arc,
19+
//~^ std_instead_of_alloc
20+
Mutex,
21+
Weak,
22+
//~^ std_instead_of_alloc
23+
};
1624
}
1725

1826
#[rustfmt::skip]

tests/ui/std_instead_of_core_unfixable.stderr

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,36 @@ LL | use std::{error::Error, vec::Vec, fs::File};
2727
= help: to override `-D warnings` add `#[allow(clippy::std_instead_of_alloc)]`
2828

2929
error: used import from `std` instead of `alloc`
30-
--> tests/ui/std_instead_of_core_unfixable.rs:21:17
30+
--> tests/ui/std_instead_of_core_unfixable.rs:18:9
31+
|
32+
LL | Arc,
33+
| ^^^
34+
|
35+
= help: consider importing the item from `alloc`
36+
37+
error: used import from `std` instead of `alloc`
38+
--> tests/ui/std_instead_of_core_unfixable.rs:21:9
39+
|
40+
LL | Weak,
41+
| ^^^^
42+
|
43+
= help: consider importing the item from `alloc`
44+
45+
error: used import from `std` instead of `alloc`
46+
--> tests/ui/std_instead_of_core_unfixable.rs:29:17
3147
|
3248
LL | borrow::Cow,
3349
| ^^^
3450
|
3551
= help: consider importing the item from `alloc`
3652

3753
error: used import from `std` instead of `alloc`
38-
--> tests/ui/std_instead_of_core_unfixable.rs:23:22
54+
--> tests/ui/std_instead_of_core_unfixable.rs:31:22
3955
|
4056
LL | collections::BTreeSet,
4157
| ^^^^^^^^
4258
|
4359
= help: consider importing the item from `alloc`
4460

45-
error: aborting due to 5 previous errors
61+
error: aborting due to 7 previous errors
4662

0 commit comments

Comments
 (0)