Skip to content

Commit badf663

Browse files
committed
chore(len_without_is_empty): extract to a separate module
It didn't share any logic at all with the lints in `len_zero.rs`, whereas the helper functions were partially mixed together, so that the file was hard to navigate
1 parent d6b5613 commit badf663

File tree

4 files changed

+352
-338
lines changed

4 files changed

+352
-338
lines changed

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,8 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
245245
crate::large_stack_arrays::LARGE_STACK_ARRAYS_INFO,
246246
crate::large_stack_frames::LARGE_STACK_FRAMES_INFO,
247247
crate::legacy_numeric_constants::LEGACY_NUMERIC_CONSTANTS_INFO,
248+
crate::len_without_is_empty::LEN_WITHOUT_IS_EMPTY_INFO,
248249
crate::len_zero::COMPARISON_TO_EMPTY_INFO,
249-
crate::len_zero::LEN_WITHOUT_IS_EMPTY_INFO,
250250
crate::len_zero::LEN_ZERO_INFO,
251251
crate::let_if_seq::USELESS_LET_IF_SEQ_INFO,
252252
crate::let_underscore::LET_UNDERSCORE_FUTURE_INFO,
Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
use clippy_utils::diagnostics::{span_lint, span_lint_and_then};
2+
use clippy_utils::res::MaybeDef;
3+
use clippy_utils::{fulfill_or_allowed, get_parent_as_impl, sym};
4+
use rustc_hir::def::Res;
5+
use rustc_hir::def_id::{DefId, DefIdSet};
6+
use rustc_hir::{
7+
FnRetTy, GenericArg, GenericBound, HirId, ImplItem, ImplItemKind, ImplicitSelfKind, Item, ItemKind, Mutability,
8+
Node, OpaqueTyOrigin, PathSegment, PrimTy, QPath, TraitItemId, TyKind,
9+
};
10+
use rustc_lint::{LateContext, LateLintPass};
11+
use rustc_middle::ty::{self, FnSig, Ty};
12+
use rustc_session::declare_lint_pass;
13+
use rustc_span::symbol::kw;
14+
use rustc_span::{Ident, Span, Symbol};
15+
use rustc_trait_selection::traits::supertrait_def_ids;
16+
17+
declare_clippy_lint! {
18+
/// ### What it does
19+
/// Checks for items that implement `.len()` but not
20+
/// `.is_empty()`.
21+
///
22+
/// ### Why is this bad?
23+
/// It is good custom to have both methods, because for
24+
/// some data structures, asking about the length will be a costly operation,
25+
/// whereas `.is_empty()` can usually answer in constant time. Also it used to
26+
/// lead to false positives on the [`len_zero`](#len_zero) lint – currently that
27+
/// lint will ignore such entities.
28+
///
29+
/// ### Example
30+
/// ```ignore
31+
/// impl X {
32+
/// pub fn len(&self) -> usize {
33+
/// ..
34+
/// }
35+
/// }
36+
/// ```
37+
#[clippy::version = "pre 1.29.0"]
38+
pub LEN_WITHOUT_IS_EMPTY,
39+
style,
40+
"traits or impls with a public `len` method but no corresponding `is_empty` method"
41+
}
42+
43+
declare_lint_pass!(LenWithoutIsEmpty => [LEN_WITHOUT_IS_EMPTY]);
44+
45+
impl<'tcx> LateLintPass<'tcx> for LenWithoutIsEmpty {
46+
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) {
47+
if let ItemKind::Trait(_, _, _, ident, _, _, trait_items) = item.kind
48+
&& !item.span.from_expansion()
49+
{
50+
check_trait_items(cx, item, ident, trait_items);
51+
}
52+
}
53+
54+
fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx ImplItem<'_>) {
55+
if item.ident.name == sym::len
56+
&& let ImplItemKind::Fn(sig, _) = &item.kind
57+
&& sig.decl.implicit_self.has_implicit_self()
58+
&& sig.decl.inputs.len() == 1
59+
&& cx.effective_visibilities.is_exported(item.owner_id.def_id)
60+
&& matches!(sig.decl.output, FnRetTy::Return(_))
61+
&& let Some(imp) = get_parent_as_impl(cx.tcx, item.hir_id())
62+
&& imp.of_trait.is_none()
63+
&& let TyKind::Path(ty_path) = &imp.self_ty.kind
64+
&& let Some(ty_id) = cx.qpath_res(ty_path, imp.self_ty.hir_id).opt_def_id()
65+
&& let Some(local_id) = ty_id.as_local()
66+
&& let ty_hir_id = cx.tcx.local_def_id_to_hir_id(local_id)
67+
&& let Some(output) = LenOutput::new(cx, cx.tcx.fn_sig(item.owner_id).instantiate_identity().skip_binder())
68+
{
69+
let (name, kind) = match cx.tcx.hir_node(ty_hir_id) {
70+
Node::ForeignItem(x) => (x.ident.name, "extern type"),
71+
Node::Item(x) => match x.kind {
72+
ItemKind::Struct(ident, ..) => (ident.name, "struct"),
73+
ItemKind::Enum(ident, ..) => (ident.name, "enum"),
74+
ItemKind::Union(ident, ..) => (ident.name, "union"),
75+
_ => (x.kind.ident().unwrap().name, "type"),
76+
},
77+
_ => return,
78+
};
79+
check_for_is_empty(
80+
cx,
81+
sig.span,
82+
sig.decl.implicit_self,
83+
output,
84+
ty_id,
85+
name,
86+
kind,
87+
item.hir_id(),
88+
ty_hir_id,
89+
);
90+
}
91+
}
92+
}
93+
94+
fn check_trait_items(cx: &LateContext<'_>, visited_trait: &Item<'_>, ident: Ident, trait_items: &[TraitItemId]) {
95+
fn is_named_self(cx: &LateContext<'_>, item: TraitItemId, name: Symbol) -> bool {
96+
cx.tcx.item_name(item.owner_id) == name
97+
&& matches!(
98+
cx.tcx.fn_arg_idents(item.owner_id),
99+
[Some(Ident {
100+
name: kw::SelfLower,
101+
..
102+
})],
103+
)
104+
}
105+
106+
// fill the set with current and super traits
107+
fn fill_trait_set(traitt: DefId, set: &mut DefIdSet, cx: &LateContext<'_>) {
108+
if set.insert(traitt) {
109+
for supertrait in supertrait_def_ids(cx.tcx, traitt) {
110+
fill_trait_set(supertrait, set, cx);
111+
}
112+
}
113+
}
114+
115+
if cx.effective_visibilities.is_exported(visited_trait.owner_id.def_id)
116+
&& trait_items.iter().any(|&i| is_named_self(cx, i, sym::len))
117+
{
118+
let mut current_and_super_traits = DefIdSet::default();
119+
fill_trait_set(visited_trait.owner_id.to_def_id(), &mut current_and_super_traits, cx);
120+
let is_empty_method_found = current_and_super_traits
121+
.items()
122+
.flat_map(|&i| cx.tcx.associated_items(i).filter_by_name_unhygienic(sym::is_empty))
123+
.any(|i| i.is_method() && cx.tcx.fn_sig(i.def_id).skip_binder().inputs().skip_binder().len() == 1);
124+
125+
if !is_empty_method_found {
126+
span_lint(
127+
cx,
128+
LEN_WITHOUT_IS_EMPTY,
129+
visited_trait.span,
130+
format!(
131+
"trait `{}` has a `len` method but no (possibly inherited) `is_empty` method",
132+
ident.name
133+
),
134+
);
135+
}
136+
}
137+
}
138+
139+
fn extract_future_output<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<&'tcx PathSegment<'tcx>> {
140+
if let ty::Alias(_, alias_ty) = ty.kind()
141+
&& let Some(Node::OpaqueTy(opaque)) = cx.tcx.hir_get_if_local(alias_ty.def_id)
142+
&& let OpaqueTyOrigin::AsyncFn { .. } = opaque.origin
143+
&& let [GenericBound::Trait(trait_ref)] = &opaque.bounds
144+
&& let Some(segment) = trait_ref.trait_ref.path.segments.last()
145+
&& let Some(generic_args) = segment.args
146+
&& let [constraint] = generic_args.constraints
147+
&& let Some(ty) = constraint.ty()
148+
&& let TyKind::Path(QPath::Resolved(_, path)) = ty.kind
149+
&& let [segment] = path.segments
150+
{
151+
return Some(segment);
152+
}
153+
154+
None
155+
}
156+
157+
fn is_first_generic_integral<'tcx>(segment: &'tcx PathSegment<'tcx>) -> bool {
158+
if let Some(generic_args) = segment.args
159+
&& let [GenericArg::Type(ty), ..] = &generic_args.args
160+
&& let TyKind::Path(QPath::Resolved(_, path)) = ty.kind
161+
&& let [segment, ..] = &path.segments
162+
&& matches!(segment.res, Res::PrimTy(PrimTy::Uint(_) | PrimTy::Int(_)))
163+
{
164+
true
165+
} else {
166+
false
167+
}
168+
}
169+
170+
#[derive(Debug, Clone, Copy)]
171+
enum LenOutput {
172+
Integral,
173+
Option(DefId),
174+
Result(DefId),
175+
}
176+
177+
impl LenOutput {
178+
fn new<'tcx>(cx: &LateContext<'tcx>, sig: FnSig<'tcx>) -> Option<Self> {
179+
if let Some(segment) = extract_future_output(cx, sig.output()) {
180+
let res = segment.res;
181+
182+
if matches!(res, Res::PrimTy(PrimTy::Uint(_) | PrimTy::Int(_))) {
183+
return Some(Self::Integral);
184+
}
185+
186+
if let Res::Def(_, def_id) = res
187+
&& let Some(res) = match cx.tcx.get_diagnostic_name(def_id) {
188+
Some(sym::Option) => Some(Self::Option(def_id)),
189+
Some(sym::Result) => Some(Self::Result(def_id)),
190+
_ => None,
191+
}
192+
&& is_first_generic_integral(segment)
193+
{
194+
return Some(res);
195+
}
196+
197+
return None;
198+
}
199+
200+
match *sig.output().kind() {
201+
ty::Int(_) | ty::Uint(_) => Some(Self::Integral),
202+
ty::Adt(adt, subs) => match cx.tcx.get_diagnostic_name(adt.did()) {
203+
Some(sym::Option) => subs.type_at(0).is_integral().then(|| Self::Option(adt.did())),
204+
Some(sym::Result) => subs.type_at(0).is_integral().then(|| Self::Result(adt.did())),
205+
_ => None,
206+
},
207+
_ => None,
208+
}
209+
}
210+
211+
fn matches_is_empty_output<'tcx>(self, cx: &LateContext<'tcx>, is_empty_output: Ty<'tcx>) -> bool {
212+
if let Some(segment) = extract_future_output(cx, is_empty_output) {
213+
return match (self, segment.res) {
214+
(_, Res::PrimTy(PrimTy::Bool)) => true,
215+
(Self::Option(_), Res::Def(_, def_id)) if cx.tcx.is_diagnostic_item(sym::Option, def_id) => true,
216+
(Self::Result(_), Res::Def(_, def_id)) if cx.tcx.is_diagnostic_item(sym::Result, def_id) => true,
217+
_ => false,
218+
};
219+
}
220+
221+
match (self, is_empty_output.kind()) {
222+
(_, &ty::Bool) => true,
223+
(Self::Option(id), &ty::Adt(adt, subs)) if id == adt.did() => subs.type_at(0).is_bool(),
224+
(Self::Result(id), &ty::Adt(adt, subs)) if id == adt.did() => subs.type_at(0).is_bool(),
225+
_ => false,
226+
}
227+
}
228+
}
229+
230+
/// The expected signature of `is_empty`, based on that of `len`
231+
fn expected_is_empty_sig(len_output: LenOutput, len_self_kind: ImplicitSelfKind) -> String {
232+
let self_ref = match len_self_kind {
233+
ImplicitSelfKind::RefImm => "&",
234+
ImplicitSelfKind::RefMut => "&(mut) ",
235+
_ => "",
236+
};
237+
match len_output {
238+
LenOutput::Integral => format!("expected signature: `({self_ref}self) -> bool`"),
239+
LenOutput::Option(_) => {
240+
format!("expected signature: `({self_ref}self) -> bool` or `({self_ref}self) -> Option<bool>")
241+
},
242+
LenOutput::Result(..) => {
243+
format!("expected signature: `({self_ref}self) -> bool` or `({self_ref}self) -> Result<bool>")
244+
},
245+
}
246+
}
247+
248+
/// Checks if the given signature matches the expectations for `is_empty`
249+
fn check_is_empty_sig<'tcx>(
250+
cx: &LateContext<'tcx>,
251+
is_empty_sig: FnSig<'tcx>,
252+
len_self_kind: ImplicitSelfKind,
253+
len_output: LenOutput,
254+
) -> bool {
255+
if let [is_empty_self_arg, is_empty_output] = &**is_empty_sig.inputs_and_output
256+
&& len_output.matches_is_empty_output(cx, *is_empty_output)
257+
{
258+
match (is_empty_self_arg.kind(), len_self_kind) {
259+
// if `len` takes `&self`, `is_empty` should do so as well
260+
(ty::Ref(_, _, Mutability::Not), ImplicitSelfKind::RefImm)
261+
// if `len` takes `&mut self`, `is_empty` may take that _or_ `&self` (#16190)
262+
| (ty::Ref(_, _, Mutability::Mut | Mutability::Not), ImplicitSelfKind::RefMut) => true,
263+
// if len takes `self`, `is_empty` should do so as well
264+
// XXX: we might want to relax this to allow `&self` and `&mut self`
265+
(_, ImplicitSelfKind::Imm | ImplicitSelfKind::Mut) if !is_empty_self_arg.is_ref() => true,
266+
_ => false,
267+
}
268+
} else {
269+
false
270+
}
271+
}
272+
273+
/// Checks if the given type has an `is_empty` method with the appropriate signature.
274+
#[expect(clippy::too_many_arguments)]
275+
fn check_for_is_empty(
276+
cx: &LateContext<'_>,
277+
len_span: Span,
278+
len_self_kind: ImplicitSelfKind,
279+
len_output: LenOutput,
280+
impl_ty: DefId,
281+
item_name: Symbol,
282+
item_kind: &str,
283+
len_method_hir_id: HirId,
284+
ty_decl_hir_id: HirId,
285+
) {
286+
// Implementor may be a type alias, in which case we need to get the `DefId` of the aliased type to
287+
// find the correct inherent impls.
288+
let impl_ty = if let Some(adt) = cx.tcx.type_of(impl_ty).skip_binder().ty_adt_def() {
289+
adt.did()
290+
} else {
291+
return;
292+
};
293+
294+
let is_empty = cx
295+
.tcx
296+
.inherent_impls(impl_ty)
297+
.iter()
298+
.flat_map(|&id| cx.tcx.associated_items(id).filter_by_name_unhygienic(sym::is_empty))
299+
.find(|item| item.is_fn());
300+
301+
let (msg, is_empty_span, is_empty_expected_sig) = match is_empty {
302+
None => (
303+
format!("{item_kind} `{item_name}` has a public `len` method, but no `is_empty` method"),
304+
None,
305+
None,
306+
),
307+
Some(is_empty) if !cx.effective_visibilities.is_exported(is_empty.def_id.expect_local()) => (
308+
format!("{item_kind} `{item_name}` has a public `len` method, but a private `is_empty` method"),
309+
Some(cx.tcx.def_span(is_empty.def_id)),
310+
None,
311+
),
312+
Some(is_empty)
313+
if !(is_empty.is_method()
314+
&& check_is_empty_sig(
315+
cx,
316+
cx.tcx.fn_sig(is_empty.def_id).instantiate_identity().skip_binder(),
317+
len_self_kind,
318+
len_output,
319+
)) =>
320+
{
321+
(
322+
format!(
323+
"{item_kind} `{item_name}` has a public `len` method, but the `is_empty` method has an unexpected signature",
324+
),
325+
Some(cx.tcx.def_span(is_empty.def_id)),
326+
Some(expected_is_empty_sig(len_output, len_self_kind)),
327+
)
328+
},
329+
Some(_) => return,
330+
};
331+
332+
if !fulfill_or_allowed(cx, LEN_WITHOUT_IS_EMPTY, [len_method_hir_id, ty_decl_hir_id]) {
333+
span_lint_and_then(cx, LEN_WITHOUT_IS_EMPTY, len_span, msg, |db| {
334+
if let Some(span) = is_empty_span {
335+
db.span_note(span, "`is_empty` defined here");
336+
}
337+
if let Some(expected_sig) = is_empty_expected_sig {
338+
db.note(expected_sig);
339+
}
340+
});
341+
}
342+
}

0 commit comments

Comments
 (0)