Repository navigation
Add missing_fused_iterator pedantic lint - #17690
saberoueslati wants to merge 5 commits into
Conversation
|
Thanks for the pull request. A reviewer will take a look after it receives 2 community reviews. In the meantime, we would highly appreciate if you could try to review any of PRs waiting on community reviews. |
|
Lintcheck changes for 2ff179f
This comment will be updated if you push new changes |
| if !matches!( | ||
| item.kind, | ||
| ItemKind::Struct(..) | ItemKind::Enum(..) | ItemKind::Union(..) | ||
| ) || !span_is_local(item.span) | ||
| || !cx.effective_visibilities.is_reachable(item.owner_id.def_id) | ||
| || is_lint_allowed(cx, MISSING_FUSED_ITERATOR, item.hir_id()) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| let Some(iterator_trait) = cx.tcx.lang_items().iterator_trait() else { | ||
| return; | ||
| }; | ||
| let Some(fused_iterator_trait) = cx.tcx.lang_items().fused_iterator_trait() else { | ||
| return; | ||
| }; | ||
| if !self.msrv.is_stable(cx, fused_iterator_trait) { | ||
| return; | ||
| } | ||
|
|
||
| let ty = cx.tcx.type_of(item.owner_id).instantiate_identity().skip_norm_wip(); | ||
| if !cx | ||
| .tcx | ||
| .non_blanket_impls_for_ty(iterator_trait, ty) | ||
| .any(|impl_id| cx.tcx.impl_polarity(impl_id) == ty::ImplPolarity::Positive) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Any implementation, including a conditional or negative one, indicates that the | ||
| // `FusedIterator` status of this nominal type has been considered explicitly. | ||
| if cx | ||
| .tcx | ||
| .non_blanket_impls_for_ty(fused_iterator_trait, ty) | ||
| .next() | ||
| .is_some() | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| span_lint_and_help( | ||
| cx, | ||
| MISSING_FUSED_ITERATOR, | ||
| item.span, | ||
| "this publicly reachable type implements `Iterator` but not `FusedIterator`", | ||
| None, | ||
| "if this iterator remains exhausted after returning `None`, consider implementing `FusedIterator`", | ||
| ); |
There was a problem hiding this comment.
I think the "usual" way to do this is by an long if-chain instead of returning, or?
| if !matches!( | |
| item.kind, | |
| ItemKind::Struct(..) | ItemKind::Enum(..) | ItemKind::Union(..) | |
| ) || !span_is_local(item.span) | |
| || !cx.effective_visibilities.is_reachable(item.owner_id.def_id) | |
| || is_lint_allowed(cx, MISSING_FUSED_ITERATOR, item.hir_id()) | |
| { | |
| return; | |
| } | |
| let Some(iterator_trait) = cx.tcx.lang_items().iterator_trait() else { | |
| return; | |
| }; | |
| let Some(fused_iterator_trait) = cx.tcx.lang_items().fused_iterator_trait() else { | |
| return; | |
| }; | |
| if !self.msrv.is_stable(cx, fused_iterator_trait) { | |
| return; | |
| } | |
| let ty = cx.tcx.type_of(item.owner_id).instantiate_identity().skip_norm_wip(); | |
| if !cx | |
| .tcx | |
| .non_blanket_impls_for_ty(iterator_trait, ty) | |
| .any(|impl_id| cx.tcx.impl_polarity(impl_id) == ty::ImplPolarity::Positive) | |
| { | |
| return; | |
| } | |
| // Any implementation, including a conditional or negative one, indicates that the | |
| // `FusedIterator` status of this nominal type has been considered explicitly. | |
| if cx | |
| .tcx | |
| .non_blanket_impls_for_ty(fused_iterator_trait, ty) | |
| .next() | |
| .is_some() | |
| { | |
| return; | |
| } | |
| span_lint_and_help( | |
| cx, | |
| MISSING_FUSED_ITERATOR, | |
| item.span, | |
| "this publicly reachable type implements `Iterator` but not `FusedIterator`", | |
| None, | |
| "if this iterator remains exhausted after returning `None`, consider implementing `FusedIterator`", | |
| ); | |
| let kind_can_impl_iter = matches!( | |
| item.kind, | |
| ItemKind::Struct(..) | ItemKind::Enum(..) | ItemKind::Union(..) | |
| ); | |
| if kind_can_impl_iter | |
| && span_is_local(item.span) | |
| && cx.effective_visibilities.is_reachable(item.owner_id.def_id) | |
| && !is_lint_allowed(cx, MISSING_FUSED_ITERATOR, item.hir_id()) | |
| && let Some(iterator_trait) = cx.tcx.lang_items().iterator_trait() | |
| && let Some(fused_iterator_trait) = cx.tcx.lang_items().fused_iterator_trait() | |
| && self.msrv.is_stable(cx, fused_iterator_trait) | |
| && let ty = cx.tcx.type_of(item.owner_id).instantiate_identity().skip_norm_wip() | |
| && cx | |
| .tcx | |
| .non_blanket_impls_for_ty(iterator_trait, ty) | |
| .any(|impl_id| cx.tcx.impl_polarity(impl_id) == ty::ImplPolarity::Positive) | |
| // Any implementation, including a conditional or negative one, indicates that the | |
| // `FusedIterator` status of this nominal type has been considered explicitly. | |
| && cx | |
| .tcx | |
| .non_blanket_impls_for_ty(fused_iterator_trait, ty) | |
| .next() | |
| .is_none() { | |
| span_lint_and_help( | |
| cx, | |
| MISSING_FUSED_ITERATOR, | |
| item.span, | |
| "this publicly reachable type implements `Iterator` but not `FusedIterator`", | |
| None, | |
| "if this iterator remains exhausted after returning `None`, consider implementing `FusedIterator`", | |
| ); | |
| } |
There was a problem hiding this comment.
Done, collapsed the early returns into a single if chain.
| MISSING_FUSED_ITERATOR, | ||
| item.span, | ||
| "this publicly reachable type implements `Iterator` but not `FusedIterator`", | ||
| None, |
There was a problem hiding this comment.
could we suggest the span of the impl Iterator here? I think it would be fairly handy if for example the users do "cursed things" such as putting the iterator impls into different files or such.
There was a problem hiding this comment.
The help now points to the local impl Iterator. If no usable local impl span is available, it falls back to an unspanned help message.
| /// this lint, even if its generic bounds do not match every `Iterator` implementation. | ||
| #[clippy::version = "1.100.0"] | ||
| pub MISSING_FUSED_ITERATOR, | ||
| pedantic, |
There was a problem hiding this comment.
should this be restriction, perf or pedantic?
Given that this does restrict the syntax in order to improve perf for users who want, I feel like pedantic might not be the right choice, or? 🤔
There was a problem hiding this comment.
I'd keep it pedantic, like iter_without_into_iter and missing_fields_in_debug. restriction doesn't fit: the lint doesn't discourage a language feature, it asks types to advertise a semantic guarantee. perf is the closer call, but Clippy can't prove the guarantee holds, and slapping FusedIterator on a resumable iterator would be wrong. Taking that false positive knowingly is what pedantic is for.
| item.span, | ||
| "this publicly reachable type implements `Iterator` but not `FusedIterator`", | ||
| None, | ||
| "if this iterator remains exhausted after returning `None`, consider implementing `FusedIterator`", |
There was a problem hiding this comment.
Should add suggestions for both
impl !FusedIterator for IteratableThing {};impl FusedIterator for IteratableThing {};
negative polarity might not be something that the wider userbase thinks about when reading this lint..
There was a problem hiding this comment.
impl !FusedIterator for T {} is still unstable and produces E0658 on stable ([rust#68318](rust-lang/rust#68318)), so I don’t think the diagnostic should recommend code most users cannot compile. I kept the positive FusedIterator guidance and added #[allow(clippy::missing_fused_iterator)] for intentionally unfused iterators. The docs mention a negative impl as a nightly-only alternative.
| //@no-rustfix | ||
| //@aux-build:proc_macros.rs | ||
|
|
||
| #![allow(dead_code)] |
There was a problem hiding this comment.
| #![allow(dead_code)] | |
| #![expect(dead_code)] |
There was a problem hiding this comment.
nit: there are a lot of impls. To keep this more "tidy", could you use one module per testcase to wrap this up nicely so that it is clearer what impl belongs to what check and such?
There was a problem hiding this comment.
Done, grouped the test cases into modules so it's obvious which impls belong to which type.
| error: this publicly reachable type implements `Iterator` but not `FusedIterator` | ||
| --> tests/ui/missing_fused_iterator.rs:24:5 | ||
| | | ||
| LL | / pub enum PublicEnum { | ||
| LL | | | ||
| LL | | Empty, | ||
| LL | | } | ||
| | |_____^ | ||
| | | ||
| help: if this iterator remains exhausted after returning `None`, consider implementing `FusedIterator` | ||
| --> tests/ui/missing_fused_iterator.rs:29:5 | ||
| | | ||
| LL | impl Iterator for PublicEnum { | ||
| | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| = help: otherwise, add `#[allow(clippy::missing_fused_iterator)]` with a comment saying why this iterator is not fused |
There was a problem hiding this comment.
I still find this diagnostic quite confusing.
I would very much prefer we skip nting where the type is defined, since I think this should be fairly obvious to the user.
error: publicly reachable type with `Iterator` implementation does not implement `FusedIterator`
--> tests/ui/missing_fused_iterator.rs:24:5
|
LL | impl Iterator for PublicEnum {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `FusedIterator` implementation
|
note: public iterators that return `None` permanently after exhaustion should implement `FusedIterator` to enable compiler optimizations
help: implement `FusedIterator` for `PublicEnum`
|
LL + impl std::iter::FusedIterator for PublicEnum {}
|
help: if this iterator is intentionally not fused, suppress the lint
|
LL + #[expect(clippy::missing_fused_iterator, reasoning = <under which circumstance the iterator can restart>)]
LL impl Iterator for PublicEnum {
There was a problem hiding this comment.
The diagnostic now points at the impl Iterator header without mentioning the type definition. It suggests a FusedIterator implementation, preserving the generics, where clause, and cfg/cfg_attr attributes.
It also suggests #[expect(clippy::missing_fused_iterator, reason = "...")], falling back to #[allow] for MSRVs below 1.81. The attribute’s key is reason.
I made the note specific to Iterator::fuse becoming a no-op through library specialization. Since the lint now fires per implementation, #[allow]/#[expect] belong on the Iterator impl rather than the type.
| impl_span, | ||
| "publicly reachable type with `Iterator` implementation does not implement `FusedIterator`", | ||
| |diag| { | ||
| diag.span_label(impl_span, "missing `FusedIterator` implementation"); |
There was a problem hiding this comment.
🤔 Still a bit akward.
How about this?
Maybe even removing the label entirely, not sure.
| diag.span_label(impl_span, "missing `FusedIterator` implementation"); | |
| diag.span_label(impl_span, "for this `Iterator`"); |
|
☔ The latest upstream changes (possibly #13598) made this pull request unmergeable. Please resolve the merge conflicts. |
Adds
clippy::missing_fused_iterator, an MSRV-awarepedanticlint for publicly reachable structs, enums, and unions that have a positiveIteratorimpl but noFusedIteratorimpl.Fixes #17608
The lint uses
is_reachablerather than the more commonis_exported, so it also covers types leaked through public signatures. AnyFusedIteratorimpl suppresses it, including a conditional or negative one. I deliberately left two cases out: impls on a reference to the type, and types reachable only through RPIT.I filed it as
pedanticbecause Clippy cannot prove an iterator stays exhausted. If your iterator resumes on purpose, allow the lint and say so in the docs..stderrfile)cargo testpasses locallycargo dev update_lintscargo dev fmtchangelog: new lint: [
missing_fused_iterator]Used an LLM to help me understand the issue and as an extra reviewer for my work.