fixhunt

ruff PLR1714

labelled unknown
Runs / 2026-10-17T21-01-25-797Z-1i8b-ruff-PLR1714ruff 0.17.12026-10-17 21:01 UTC8 programs × 2 round(s)15 programs, 8 fired, 0 fixed, 0 findings, 0 rejectedmodel cost $0.0030

Analysis Nemotron 3 Super

Flags repeated equality comparisons like 'x == a or x == b' that can be rewritten using a membership test.

Fix: Rewrites repeated equality comparisons of the form 'expr == val1 or expr == val2 ...' into 'expr in {val1, val2, ...}' when all values are hashable, otherwise into 'expr in (val1, val2, ...)'.

8 hazards
  • The expression on the left-hand side may have side effects (e.g., a function call), causing it to be evaluated multiple times in the original but only once in the fix.
  • If any compared value is unhashable (e.g., a list or dict), the fix uses a tuple, but the original may rely on short-circuiting to avoid evaluating later values after a match.
  • The fix evaluates all values in the set/tuple upfront, which can raise exceptions during construction (e.g., division by zero in a literal) that would not occur in the original if earlier comparisons succeeded.
  • Comments attached to individual comparison operands may be lost or misplaced during the rewrite into a single expression.
  • If the left-hand expression is mutable and changed between comparisons (e.g., via aliasing), the original may see different values while the fix uses a single evaluated value.
  • In rare cases, custom __eq__ implementations may behave differently when invoked multiple times vs. once due to state changes.
  • The fix may alter operator precedence interactions if the original expression was part of a larger boolean expression relying on short-circuiting for control flow.
  • If the compared values include duplicates, the deduplication in a set may change behavior if equality is not idempotent or if duplicates had semantic meaning (though unlikely).

Programs

idresulttitlehazard
r1-p001fired, no fixComments inside chained equality stripped by IN conversioncomments and docstrings attached to the removed or moved code
r1-p002fired, no fixInline comments on operands lost during rewriteComments attached to individual comparison operands may be lost or misplaced during the rewrite into a single expression.
r1-p004silent prediction offPath comparison broken by IN rewrite assuming same typepathlib vs os.path behavior on relative paths, symlinks, missing files, bytes paths
r1-p005silent prediction offSide effects in expr duplicated by IN rewriteevaluation order and side effects when expressions are merged, duplicated or deleted
r1-p006silent prediction offDuplicate values in OR chain deduplicated by set changing meaningIf the compared values include duplicates, the deduplication in a set may change behavior if equality is not idempotent or if duplicates had semantic meaning (though unlikely).
r1-p007fired, no fixType annotation at runtime affected by missing name after rewritetype annotations evaluated at runtime, typing-only imports
r1-p008silent prediction offDict key order and duplicate keys changed by set conversiondict ordering, duplicate keys, __hash__/__eq__ overrides, sets
r2-p001silent prediction offGenerators exhausted after first iterationgenerators vs lists, iterators consumed once, lazy vs eager evaluation
r2-p002fired, no fixKeyword-only argument default evaluated oncekeyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once
r2-p003fired, no fixShort-circuiting in larger boolean expressionThe fix may alter operator precedence interactions if the original expression was part of a larger boolean expression relying on short-circuiting for control flow.
r2-p004fired, no fixRuntime evaluation of type annotationtype annotations evaluated at runtime, typing-only imports
r2-p005silent prediction offBytes vs str equality with encodingbytes vs str, encoding and newline handling, text vs binary modes
r2-p006fired, no fixAsync function awaited twiceasync def/await ordering, async generators
r2-p007fired, no fixProperty with side effect accessed twiceclass attributes vs instance attributes, descriptors, properties with side effects
r2-p008silent prediction offPathlib vs os.path symlink resolutionpathlib vs os.path behavior on relative paths, symlinks, missing files, bytes paths

Sources

r1-p001 · Comments inside chained equality stripped by IN conversion
1# Setup
2x = 1
3# First check: magic number one
4if x == 1 or x == 2: # Second: magic number two
5 print('matched')
6else:
7 print('no match')
matched
r1-p002 · Inline comments on operands lost during rewrite
1value = 'alpha'
2# First option: primary
3if value == 'alpha' or value == 'beta': # Second: fallback
4 print('got alpha or beta')
5else:
6 print('other')
got alpha or beta
r1-p004 · Path comparison broken by IN rewrite assuming same type
1import os
2from pathlib import Path
3p = Path('tmp')
4p.write_text('test')
5try:
6 if str(p) == 'tmp' or os.fsencode(p) == b'tmp':
7 print('path matched')
8 else:
9 print('no match')
10except Exception as e:
11 print(type(e).__name__, e)
12finally:
13 p.unlink(missing_ok=True)
path matched
r1-p005 · Side effects in expr duplicated by IN rewrite
1counter = 0
2def getx():
3 global counter
4 counter += 1
5 return counter
6if getx() == 1 or getx() == 2:
7 print('first or second call matched')
8print('total calls:', counter)
first or second call matched
total calls: 1
r1-p006 · Duplicate values in OR chain deduplicated by set changing meaning
1class EqCounter:
2 def __init__(self, val):
3 self.val = val
4 self.eq_count = 0
5 def __eq__(self, other):
6 self.eq_count += 1
7 return self.val == other
8obj = EqCounter(5)
9if obj == 5 or obj == 5:
10 print('matched,', obj.eq_count, 'comparisons')
11else:
12 print('no match,', obj.eq_count, 'comparisons')
matched, 1 comparisons
r1-p007 · Type annotation at runtime affected by missing name after rewrite
1T = int
2if 1 == T or 2 == T: # T is int at runtime
3 print('matched')
4else:
5 print('no match')
6print('T is', T)
no match
T is <class 'int'>
r1-p008 · Dict key order and duplicate keys changed by set conversion
1class BadHash:
2 def __init__(self, v):
3 self.v = v
4 def __hash__(self):
5 return hash(self.v)
6 def __eq__(self, other):
7 return isinstance(other, BadHash) and self.v == other.v
8d = {BadHash(1): 'a', BadHash(2): 'b'}
9k = BadHash(1)
10if k in d or k == BadHash(2):
11 print('found via dict or equality')
12else:
13 print('not found')
found via dict or equality
r2-p001 · Generators exhausted after first iteration
1def gen():
2 yield 1
3 yield 2
4
5g = gen()
6a = []
7for _ in range(2):
8 if next(g) == 1 or next(g) == 2:
9 a.append('yes')
10 else:
11 a.append('no')
12print(a)
r2-p002 · Keyword-only argument default evaluated once
1L = []
2def f(x, *, y=L.append('default') or 0):
3 return x == 1 or x == 2
4print(f(1))
5print(f(2))
6print(L)
True
True
['default']
r2-p003 · Short-circuiting in larger boolean expression
1def side():
2 print('side')
3 return True
4
5if side() and (x == 1 or x == 2):
6 print('taken')
7else:
8 print('skipped')
9x = 1
side
r2-p004 · Runtime evaluation of type annotation
1T = int
2def f(x) -> 'T':
3 return x == 1 or x == 2
4print(f(1))
5print(f(2))
True
True
r2-p005 · Bytes vs str equality with encoding
1b = b'hello'
2s = 'hello'
3if b == b'hello' or s == 'hello':
4 print('match')
5else:
6 print('no match')
match
r2-p006 · Async function awaited twice
1async def f():
2 return 1
3
4async def main():
5 r = await f()
6 if r == 1 or r == 2:
7 print('yes')
8 else:
9 print('no')
10import asyncio
11asyncio.run(main())
yes
r2-p007 · Property with side effect accessed twice
1class C:
2 def __init__(self):
3 self._x = 0
4 @property
5 def x(self):
6 self._x += 1
7 return self._x
8
9c = C()
10if c.x == 1 or c.x == 2:
11 print(c._x)
12else:
13 print('none')
1
r2-p008 · Pathlib vs os.path symlink resolution
1import os
2import pathlib
3os.symlink('tmp', 'link')
4try:
5 p = pathlib.Path('link')
6 o = os.path
7 if p.exists() or o.exists('link'):
8 print('exists')
9 else:
10 print('missing')
11except Exception as e:
12 print(type(e).__name__, e)
13finally:
14 try: os.unlink('link')
15 except: pass
missing