Skip to content

Add option_zip_none lint - #17465

Open
Amit5601 wants to merge 1 commit into
rust-lang:masterfrom
Amit5601:fix/issue-17462-option-zip-none
Open

Add option_zip_none lint#17465
Amit5601 wants to merge 1 commit into
rust-lang:masterfrom
Amit5601:fix/issue-17462-option-zip-none

Conversation

@Amit5601

@Amit5601 Amit5601 commented Jul 27, 2026

Copy link
Copy Markdown

View all comments

fixes #17462
Adds a new suspicious late lint option_zip_none that checks for calls of the form Option::zip(None).

As noted in the issue, this always yields None and is almost certainly a logic bug where the user intended to either just write None or use .map(|x| (x, None)).

  • Followed lint naming conventions
  • Added passing UI tests (including committed .stderr file)
  • cargo test passes locally
  • Executed cargo dev update_lints
  • Added lint documentation
  • Run cargo dev fmt

changelog: [option_zip_none]: Add new lint to check for Option::zip(None)

@rustbot rustbot added the S-waiting-on-community-reviews Status: This is awaiting for positive reviews from the community before a maintainer is assigned. label Jul 27, 2026
@rustbot

rustbot commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the pull request, and welcome!

You should hear from one of our reviewers after this PR gets at least 2 reviews from the community.

Please see the contribution instructions for more information. Namely, in order to ensure the minimum review times lag, PR authors and assigned reviewers should ensure that the review label (S-waiting-on-review and S-waiting-on-author) stays updated, invoking these commands when appropriate:

  • @rustbot author: the review is finished, PR author should check the comments and take action accordingly
  • @rustbot review: the author is ready for a review, this PR will be queued again in the reviewer's queue

@rustbot rustbot added needs-fcp PRs that add, remove, or rename lints and need an FCP S-waiting-on-review Status: Awaiting review from the assignee but also interested parties labels Jul 27, 2026
@Amit5601
Amit5601 force-pushed the fix/issue-17462-option-zip-none branch from 892c510 to c3ee38c Compare July 27, 2026 11:42
@Amit5601
Amit5601 marked this pull request as draft July 27, 2026 11:50
@rustbot rustbot removed the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Jul 27, 2026
@Amit5601
Amit5601 force-pushed the fix/issue-17462-option-zip-none branch from c3ee38c to 8a32bab Compare July 27, 2026 11:52
@Amit5601
Amit5601 marked this pull request as ready for review July 27, 2026 12:01
@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Jul 27, 2026

@Gri-ffin Gri-ffin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also tried this code:

trait MyZip {
    fn zip(self, other: Option<()>) -> &'static str;
}

impl<T> MyZip for &Option<T> {
    fn zip(self, other: Option<()>) -> &'static str {
        "not Option::zip"
    }
}

fn main() {
    let opt = Some(1);
    let r = (&opt).zip(None::<()>);
}

It lints this case although it shouldn't, (but would anyone do that though?), otherwise looks good.

View changes since this review

Comment thread clippy_lints/src/option_zip_none.rs Outdated
@Amit5601

Copy link
Copy Markdown
Author

@Gri-ffin Is the current logic sufficient?

@Gri-ffin

Copy link
Copy Markdown
Contributor

You can tighten the lint by checking that the method resolves to Option::zip rather than a user defined method using type_dependant_def_id to get the method DefId and then inspecting cx.tcx.associated_item(method_def_id) / its impl container to verify that the method comes from the inherent Option impl.

@Amit5601
Amit5601 force-pushed the fix/issue-17462-option-zip-none branch from 95dd73b to 24c498b Compare July 27, 2026 16:22
@Amit5601
Amit5601 marked this pull request as draft July 27, 2026 16:25
@rustbot rustbot removed the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Jul 27, 2026
@Amit5601
Amit5601 force-pushed the fix/issue-17462-option-zip-none branch 4 times, most recently from bcf352a to aecbce4 Compare July 27, 2026 16:51
@Amit5601
Amit5601 marked this pull request as ready for review July 27, 2026 17:01
@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Jul 27, 2026
@Amit5601
Amit5601 requested a review from Gri-ffin July 27, 2026 17:01

@Gri-ffin Gri-ffin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also add tests for references (since they should also lint). Aside from this LGTM.

View changes since this review

Comment thread tests/ui/option_zip_none.rs Outdated
@Amit5601
Amit5601 force-pushed the fix/issue-17462-option-zip-none branch from aecbce4 to d8a7bff Compare July 27, 2026 17:36
@Amit5601
Amit5601 marked this pull request as draft July 27, 2026 17:39
@rustbot rustbot removed the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Jul 27, 2026
@Amit5601
Amit5601 force-pushed the fix/issue-17462-option-zip-none branch from d8a7bff to 8b8e0a1 Compare July 27, 2026 17:43
@Amit5601
Amit5601 marked this pull request as ready for review July 27, 2026 17:53
@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Jul 27, 2026
@Amit5601
Amit5601 force-pushed the fix/issue-17462-option-zip-none branch from c87d507 to 230e87b Compare July 30, 2026 12:53
@Amit5601
Amit5601 requested a review from samueltardieu July 30, 2026 12:58
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties and removed S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) labels Jul 30, 2026
@Amit5601
Amit5601 force-pushed the fix/issue-17462-option-zip-none branch from 230e87b to 04697cc Compare July 30, 2026 16:54
@Amit5601
Amit5601 requested a review from ada4a July 30, 2026 17:00
Comment thread clippy_lints/src/methods/option_zip_none.rs Outdated
Comment thread clippy_lints/src/methods/option_zip_none.rs Outdated
Comment thread clippy_lints/src/methods/mod.rs
@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties labels Jul 30, 2026
Comment thread { Outdated
@Amit5601

Copy link
Copy Markdown
Author

@ada4a I tried applying your suggested simplification, but it caused the compile-test suite to fail.

@Amit5601
Amit5601 force-pushed the fix/issue-17462-option-zip-none branch from 04697cc to 06cee0b Compare July 30, 2026 18:18
@Amit5601
Amit5601 requested a review from ada4a July 30, 2026 18:23

@ada4a ada4a left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies, that's probably because my suggestions skipped .instantiate_identity().skip_norm_wip(). Try this instead?

View changes since this review

Comment thread clippy_lints/src/methods/option_zip_none.rs Outdated
Comment thread clippy_lints/src/methods/option_zip_none.rs Outdated
Comment thread clippy_lints/src/methods/option_zip_none.rs Outdated
@Amit5601
Amit5601 force-pushed the fix/issue-17462-option-zip-none branch from 06cee0b to 693dbbf Compare July 31, 2026 14:49
@Amit5601
Amit5601 requested a review from ada4a July 31, 2026 15:01
Comment thread clippy_lints/src/methods/mod.rs Outdated
Comment thread clippy_lints/src/methods/mod.rs Outdated
Comment thread tests/ui/manual_option_zip.rs
Comment thread tests/ui/option_zip_none.rs Outdated
Comment thread tests/ui/option_zip_none.rs Outdated
Comment thread tests/ui/option_zip_none.rs Outdated
Comment thread tests/ui/option_zip_none.rs Outdated
@Amit5601
Amit5601 force-pushed the fix/issue-17462-option-zip-none branch from 693dbbf to 4b8f202 Compare August 1, 2026 06:13
Comment thread tests/ui/manual_option_zip.rs
Comment thread clippy_lints/src/methods/option_zip_none.rs Outdated
Comment thread clippy_lints/src/methods/option_zip_none.rs Outdated
@Amit5601

Amit5601 commented Aug 1, 2026

Copy link
Copy Markdown
Author

@ada4a I tried swapping allow to expect, but it causes an unfulfilled_lint_expectations error.

@Amit5601

Amit5601 commented Aug 2, 2026

Copy link
Copy Markdown
Author

@ada4a I have updated the suggestions and adjusted the test suite to ''//@no-rustfix'' to handle the multi-type suggestion overlap. I replaced allow in the other file because it threw no error while it causes an unfulfilled_lint_expectations error in manual_option_rs file.

@ada4a ada4a left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! if let ExprKind::MethodCall(..) = expr.kind is an elegant way of solving this:)

Let's iterate on this a bit more

View changes since this review

Comment thread clippy_lints/src/methods/option_zip_none.rs Outdated
}

let applicability = if suggested_map {
Applicability::Unspecified

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you give the reasoning behind this?

Comment thread clippy_lints/src/methods/option_zip_none.rs Outdated
Comment thread tests/ui/option_zip_none.rs Outdated
Comment thread clippy_lints/src/methods/option_zip_none.rs Outdated
Comment thread clippy_lints/src/methods/option_zip_none.rs Outdated
suggested_map = true;
} else if !recv_is_none
&& arg_is_none
&& let Some(recv_snip) = snippet_opt(cx, recv.span)

@ada4a ada4a Aug 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, in this case you don't even need recv_snip – instead, you could only replace call_span, which is one the fields of ExprKind::MethodCall, and points to everything after the dot (zip(None) in this case), with map(|n| (n, None)).

This has the additional benefit of making the diff more fine-grained: it will only point at the part after the dot.

View changes since the review

Comment thread clippy_lints/src/methods/option_zip_none.rs Outdated
Comment thread tests/ui/option_zip_none_unfixable.rs
Comment thread clippy_lints/src/methods/option_zip_none.rs
Comment on lines +13 to +14
LL | let _: Option<((), i32)> = Option::zip(None::<()>, Some(1));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: otherwise, simplify this to `None`: `None::<()>`

@ada4a ada4a Aug 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the help message still includes otherwise, , for some reason...

View changes since the review

Comment thread tests/ui/option_zip_none.stderr Outdated
LL - let _ = (&standard_opt).zip(None::<()>);
LL + let _ = (&standard_opt).map(|n| (n, None::<()>));
|
help: simplify this to `None`

@ada4a ada4a Aug 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this also doesn't look right..

View changes since the review

Comment on lines +3 to +18
mod edge_case {
trait MyZip {
fn zip(self, other: Option<()>) -> &'static str;
}

impl<T> MyZip for &Option<T> {
fn zip(self, _other: Option<()>) -> &'static str {
"not Option::zip"
}
}

pub fn test_custom_trait() {
let opt = Some(1);
let _ = (&opt).zip(None::<()>);
}
}

@ada4a ada4a Aug 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add a similar case where you define an item (probably an enum) called MyOption, with an inherent (i.e. defined in a regular impl block) method called zip, and check that that doesn't trigger the lint either?

View changes since the review

@rustbot

This comment has been minimized.

@ada4a ada4a left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies, somehow missed the notification.

This is looking great, thank you :) Only some nits remaining

View changes since this review


let applicability = if suggested_map { Applicability::Unspecified } else { app };

diag.span_suggestion(expr.span, fallback_msg, none_snippet.to_string(), applicability);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: span_suggestion takes impl Into<String>, so you can omit .to_string()

Suggested change
diag.span_suggestion(expr.span, fallback_msg, none_snippet.to_string(), applicability);
diag.span_suggestion(expr.span, fallback_msg, none_snippet, applicability);

let mut suggested_map = false;

let none_snippet = if recv_is_none {
snippet_with_context(cx, recv.span, expr.span.ctxt(), "None", &mut app).0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getting the context of a span is a somewhat expensive operation -- could you please get it once, store it in a variable (which we usually call ctxt), and use that everywhere?

Comment on lines +17 to +18
}
enum MyOption {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
enum MyOption {
}
enum MyOption {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point I think everything here is actually fixable? Because we do emit the None suggestion


if let ExprKind::MethodCall(_, _, _, call_span) = expr.kind {
if recv_is_none && !arg_is_none {
let arg_snip = snippet_with_context(cx, arg.span, expr.span.ctxt(), "..", &mut app).0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: since arg is a simple expression, its fallback snippet should be _ (or (_), if you wish, since it's going to be a method call receiver)

@Amit5601

Copy link
Copy Markdown
Author

@ada4a I actually tried consolidating everything into the option_zip_none.rs file, but doing so breaks rustfix with an E0308: mismatched types error. I haven't removed the unfixable.rs file so that tests remain green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lint-nominated Create an FCP-thread on Zulip for this PR needs-fcp PRs that add, remove, or rename lints and need an FCP S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) S-waiting-on-review Status: Awaiting review from the assignee but also interested parties

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.zip(None) is always None so it should be linted

5 participants