Skip to content

Let the narrower behavior win where both landed - #48

Merged
zmaril merged 3 commits into
mainfrom
narrower-behavior-wins
Aug 26, 2026
Merged

Let the narrower behavior win where both landed#48
zmaril merged 3 commits into
mainfrom
narrower-behavior-wins

Conversation

@zmaril

@zmaril zmaril commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Follow-up to the merged idiom work, from measuring precision off the 13-file
corpus for the first time.

The problem

Option::and_then is match self { Some(x) => f(x), None => None }. The hole
swallows whatever a narrower way of consuming an Option puts there, so it
matches map, filter and ok_or and says less about each than they say about
themselves. On clippy's manual_map test it landed on fifteen of the same
lines Option::map did
— fifteen second, weaker findings on code that already
had a better one.

This is the failure mode notes/todo.txt records twice, most recently as
Option::is_none_or firing 1,390 times across five hundred crates: "both were
correct, both subsumed every narrower behavior".

Why is_reportable does not catch it

It is the existing answer to a behavior that describes too much, and it cannot
reach this one. and_then names two variants, so it clears the anchor floor
while still subsuming everything narrower. What separates the two is not how
much either names on its own but how they stand to each other — and a form
used as a pattern already answers that. A form that matches another behavior's
form accepts everywhere that one does and elsewhere besides.

Per placement, not per pack

The part worth reviewing. Being broader is not being wrong: code that really
does reimplement and_then should hear about it, and a pack-wide rule would
lose that. It is only grounds for standing aside where something narrower has
already landed. tests/subsumption.rs pins both directions.

Measured

corpus before after
13-file clippy corpus 26/201, 36 reported, 36/36 on-target unchanged
wider 18-lint manual_* sample 59 reported, 10 found, 43 off-target 44 reported, 10 found, 28 off-target

Nothing that was right was removed. On-target share goes from 16/59 to 16/44.

The 28 that remain are not this problem:

  • 11 are manual_unwrap_or_default reporting unwrap_or — correct, just
    less sharp than the API clippy names
  • 11 are manual_filter reporting and_then because Option::filter
    derives no behavior at all. Nothing narrower exists to win. That is a recall
    gap, and a separate piece of work.

Also worth knowing

The measure/packs/std pack is stale. Re-derived with current code it is
589 behaviors rather than 483, and Iterator::filter_map comes out as
(sift f0 v1 (call f1 v1)) rather than the unlifted Sequence[Traverse, None]
it is stored as. Re-deriving does not move the score — that fresh form is
size 5 against MINIMUM_REPORTABLE_SIZE 6 with 0 anchors, so it is correctly
refused — but anyone reading that pack should know it predates the one-step lift.

358 tests, clippy clean.

zmaril and others added 3 commits August 25, 2026 21:27
The catalog is not committed and git does not track an empty directory, so
`infact-packs/rust-std/api/` is absent on a fresh checkout — and `--output` does
not create the path it is given. The documented command therefore failed on the
one machine state it exists for, with a bare `No such file or directory` that
names neither the path nor the reason.

Found by running it on a fresh checkout of merged main rather than by reading it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ghYbUJFcVfkKYacbTVJsR
`Option::and_then` is `match self { Some(x) => f(x), None => None }`, and the
hole swallows whatever a narrower way of consuming an `Option` puts there. So it
matches `map`, and `filter`, and `ok_or`, and says less about each than they do
about themselves. On clippy's `manual_map` test it landed on fifteen of the same
lines `Option::map` did; every one of those was a second, weaker finding on code
that already had a better one.

`is_reportable` was the existing answer to a behavior that describes too much,
and it does not reach this: `and_then` names two variants, so it passes the
anchor floor while still subsuming everything narrower. What separates them is
not how much either names on its own but how they stand to EACH OTHER, and a
form used as a pattern already answers that — a form that matches another
behavior's form accepts everywhere that one does and elsewhere besides.

Asked per placement rather than per pack, which is the part that matters. Being
broader is not being wrong: code that really does reimplement `and_then` should
hear about it, and a pack-wide rule would lose that. It is only grounds for
standing aside where something narrower has already landed.

Measured. On the 13-file clippy corpus, unchanged: 26/201, 36 reported, 36/36
on-target — this removes nothing that was right. On a wider sample of 18
`manual_*` lints fetched for this, reported findings fall from 59 to 44 and
off-target from 43 to 28, with all ten on-target findings kept. The on-target
share goes from 16/59 to 16/44.

What is left off-target is not this problem. Eleven are `manual_unwrap_or_default`
reporting `unwrap_or`, which is correct and merely less sharp than the API clippy
names. Eleven are `manual_filter` reporting `and_then` because `Option::filter`
derives no behavior at all — nothing narrower exists to win, and that is a recall
gap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ghYbUJFcVfkKYacbTVJsR
`Iterator::fold` reported that no implementation was found. It has one: a loop
that accumulates, right there in the trait. What went wrong is that `max_by` and
`min_by` each declare their own `fn fold` helper inside their bodies, the
container was carried down into those bodies, and so three callables answered to
`Iterator::fold`. The resolver refuses a name two callables answer to — correctly
— and a method with a perfectly good body fell out of the pack.

The container stops at a function boundary now. A function declared inside
another function's body is a local helper, not a method of whatever type
surrounds them both.

Measured on core: 261 behaviors to 263, and nothing lost. `Iterator::fold` and
`Iterator::count` derive where they did not.

WHAT I PREDICTED AND DID NOT GET: that the whole family built on `fold` would
follow it — `sum`, `max`, `min`, `last`, `reduce`, `product`, `nth`. None did.
They fail for their own separate reasons, which are not this one and are not yet
known. That is the sixth prediction in this file's history to be wrong about
what a correct fix would reach, and it is recorded here because the tally it came
from is only useful if the reach is measured rather than argued.

The clippy corpus is unchanged at 26/201, 36/36 on-target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ghYbUJFcVfkKYacbTVJsR
@zmaril
zmaril merged commit 6c7046f into main Aug 26, 2026
1 check passed
@zmaril
zmaril deleted the narrower-behavior-wins branch August 26, 2026 16:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant