Skip to content

[Bug]: Unsafe FURB108 when lazy evaluation is required #350

@mthuurne

Description

@mthuurne

Has your issue already been fixed?

  • Have you checked to see if your issue still exists on the master branch? See the docs for instructions on how to setup a local build of Refurb.
  • Have you looked at the open/closed issues to see if anyone has already reported your issue?
  • If reporting a false positive/incorrect suggestion, have you double checked that the suggested fix changes the code semantics?

The Bug

The following code:

from bisect import bisect
from collections.abc import Sequence

def f(events: Sequence[int], threshold: int) -> None:
    cutoff = bisect(events, threshold)
    if cutoff == 0 or events[cutoff - 1] == 0:
        pass

Emits the following error:

$ refurb file.py
file.py:6:8 [FURB108]: Replace `x == y or z == y` with `y in (x, z)`

However, this suggestion is unsafe because if events is empty, cutoff - 1 is not a valid index. With or this is fine, as lazy evaluation halts after it finds that cutoff == 0 is true, but the tuple notation always evaluates both expressions.

I don't expect Refurb to understand the details of bisect(), but it could perhaps detect that cutoff is checked in the left hand side and used in the right hand side of or and as a precaution not issue FURB108.

I'm also not sure the suggested change (de-duplicating the 0) would actually make the code more elegant, but that is subjective, while the unsafe behavior is an objective problem.

Version Info

Refurb: v2.0.0
Mypy: v1.13.0

Python Version

Python 3.12.7

Config File

# N/A

Extra Info

The double check:

from bisect import bisect
from collections.abc import Sequence

def f1(events: Sequence[int], threshold: int) -> None:
    cutoff = bisect(events, threshold)
    if cutoff == 0 or events[cutoff - 1] == 0:
        pass

def f2(events: Sequence[int], threshold: int) -> None:
    cutoff = bisect(events, threshold)
    if 0 in (cutoff, events[cutoff - 1]):
        pass

f1([], 123)
f2([], 123)

Here, the f1() call returns, while the f2() call raises IndexError.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions