fixhunt

ruff RUF021

labelled unknown
Runs / 2026-10-17T21-42-13-508Z-lmbw-ruff-RUF021ruff 0.17.12026-10-17 21:42 UTC8 programs × 2 round(s)16 programs, 13 fired, 13 fixed, 0 findings, 3 rejectedmodel cost $0.0035

Analysis Nemotron 3 Super

Flags chains of 'and'/'or' without explicit precedence via parentheses.

Fix: Inserts parentheses around subexpressions to enforce 'and' before 'or' precedence, e.g., rewriting 'a or b and c' as 'a or (b and c)' and 'd and e or f' as '(d and e) or f'.

11 hazards
  • Rewriting 'a or b and c' as 'a or (b and c)' changes evaluation if 'a' has side effects and is truthy, as 'b and c' would no longer be evaluated.
  • Rewriting 'd and e or f' as '(d and e) or f' changes evaluation if 'd' is falsey and 'e' has side effects, as 'e' would now be evaluated in the original but not in the fix due to short-circuiting.
  • If 'b' or 'e' is a property with a getter that raises an exception, moving its evaluation inside parentheses may cause the exception to be raised in different contexts.
  • In lambdas, adding parentheses may alter binding precedence if the lambda body is part of a larger expression, though rare.
  • Comments placed between operators (e.g., 'a # comment\n or b # comment\n and c') may be lost or misplaced during rewrite if the tool does not preserve comment attachment.
  • If the expression is part of a tuple or list unpacking, adding parentheses could be interpreted as a tuple literal, altering structure (e.g., 'x, y = a or b and c' vs 'x, y = a or (b and c)' — same here, but risky in nested contexts).
  • In default argument values, rewriting could affect when side effects occur due to early binding, though the fix doesn't change timing relative to function definition.
  • If 'a', 'b', or 'c' are overloaded operators (e.g., __or__, __and__) in a custom class, adding parentheses does not change semantics as these are still method calls, but evaluation order of operands is preserved — low risk, but noted for completeness.
  • In async contexts, if 'b' or 'e' is an awaitable, rewriting does not change await behavior as the fix doesn't introduce or remove awaits — but if mistakenly wrapped in a non-async context, could cause issues; however, the fix preserves expression structure so this is unlikely.
  • If the code uses a linter or formatter that expects specific spacing around operators, the autofix may produce formatting that conflicts with project style, requiring reformat.
  • In string annotations or forward references (rare for expressions), parentheses could be misinterpreted, though this applies only to annotations, not executable code.

Programs

idresulttitlehazard
r1-p001fix okShort-circuit side effectRewriting 'd and e or f' as '(d and e) or f' changes evaluation if 'd' is falsey and 'e' has side effects, as 'e' would now be evaluated in the original but not in the fix due to short-circuiting.
r1-p002invalidWalrus in conditionwalrus, comprehensions scoping, late binding in closures and lambdas
r1-p003fix okLambda binding precedenceIn lambdas, adding parentheses may alter binding precedence if the lambda body is part of a larger expression, though rare.
r1-p004fix okTruthiness edgetruthiness of custom __bool__/__len__, None vs falsy, 'is' vs '=='
r1-p005fix okBytes vs strbytes vs str, encoding and newline handling, text vs binary modes
r1-p006invalidContext manager write-onlycontext managers that are not files, objects with write() but no close()
r1-p007fix okPathlib vs os.pathpathlib vs os.path behavior on relative paths, symlinks, missing files, bytes paths
r1-p008fix okDefault arg side effectIn default argument values, rewriting could affect when side effects occur due to early binding, though the fix doesn't change timing relative to function definition.
r2-p001fix okSide effects in chained boolean - evaluation order changedevaluation order and side effects when expressions are merged, duplicated or deleted
r2-p002fix okAsync function call inside boolean chain - await safety checkIn async contexts, if 'b' or 'e' is an awaitable, rewriting does not change await behavior as the fix doesn't introduce or remove awaits — but if mistakenly wrapped in a non-async context, could cause
r2-p003fix okComments stripped during boolean rewrite causing confusionComments placed between operators (e.g., 'a # comment\n or b # comment\n and c') may be lost or misplaced during rewrite if the tool does not preserve comment attachment.
r2-p004fix okAttached docstring/comment lost during AST transformationcomments and docstrings attached to the removed or moved code
r2-p005fix okTruthy left operand skips right evaluation after fixRewriting 'a or b and c' as 'a or (b and c)' changes evaluation if 'a' has side effects and is truthy, as 'b and c' would no longer be evaluated.
r2-p006fix okFalsey left operand causes right evaluation in original but not after fixRewriting 'd and e or f' as '(d and e) or f' changes evaluation if 'd' is falsey and 'e' has side effects, as 'e' would now be evaluated in the original but not in the fix due to short-circuiting.
r2-p007invalidProperty descriptor side effects altered by precedence changeclass attributes vs instance attributes, descriptors, properties with side effects
r2-p008fix okIterator consumed prematurely due to changed evaluation ordergenerators vs lists, iterators consumed once, lazy vs eager evaluation

Sources

r1-p001 · Short-circuit side effect
1def f():
2 print('f')
3 return False
4side = []
5def g():
6 side.append(1)
7 return True
8x = f() and g() or True
9print(len(side))
10x = (f() and g()) or True
11print(len(side))
--- orig.py	2026-10-17 17:42:22.656209768 -0400
+++ fixed.py	2026-10-17 17:42:22.825208522 -0400
@@ -5,7 +5,7 @@
 def g():
     side.append(1)
     return True
-x = f() and g() or True
+x = (f() and g()) or True
 print(len(side))
 x = (f() and g()) or True
 print(len(side))
f
0
f
0
r1-p002 · Walrus in condition (generated program does not parse: Traceback (most recent call last):)
1ls = [0]
2if (tmp := ls[0]) == 0 or tmp := ls.pop():
3 pass
4else:
5 tmp = -1
6print(tmp)
7if ((tmp := ls[0]) == 0) or tmp := ls.pop():
8 pass
9else:
10 tmp = -1
11print(tmp)
r1-p003 · Lambda binding precedence
1f = lambda: True and False or True
2result = f() and print('inner') or print('outer')
3f = lambda: (True and False) or True
4result = f() and print('inner') or print('outer')
--- orig.py	2026-10-17 17:42:23.056206819 -0400
+++ fixed.py	2026-10-17 17:42:23.213205662 -0400
@@ -1,4 +1,4 @@
-f = lambda: True and False or True
-result = f() and print('inner') or print('outer')
 f = lambda: (True and False) or True
-result = f() and print('inner') or print('outer')
+result = (f() and print('inner')) or print('outer')
+f = lambda: (True and False) or True
+result = (f() and print('inner')) or print('outer')
inner
outer
inner
outer
r1-p004 · Truthiness edge
1class A:
2 def __bool__(self):
3 print('A bool')
4 return False
5 def __len__(self):
6 return 0
7a = A()
8b = []
9if a or b and True:
10 print('yes')
11else:
12 print('no')
13if a or (b and True):
14 print('yes')
15else:
16 print('no')
--- orig.py	2026-10-17 17:42:23.313204925 -0400
+++ fixed.py	2026-10-17 17:42:23.468203783 -0400
@@ -6,7 +6,7 @@
         return 0
 a = A()
 b = []
-if a or b and True:
+if a or (b and True):
     print('yes')
 else:
     print('no')
A bool
no
A bool
no
r1-p005 · Bytes vs str
1data = b'hello'
2empty = b''
3if data or empty and b'world':
4 res = b'yes'
5else:
6 res = b'no'
7print(res)
8if data or (empty and b'world'):
9 res = b'yes'
10else:
11 res = b'no'
12print(res)
--- orig.py	2026-10-17 17:42:23.571203023 -0400
+++ fixed.py	2026-10-17 17:42:23.733201830 -0400
@@ -1,6 +1,6 @@
 data = b'hello'
 empty = b''
-if data or empty and b'world':
+if data or (empty and b'world'):
     res = b'yes'
 else:
     res = b'no'
b'yes'
b'yes'
r1-p006 · Context manager write-only (generated program does not parse: Traceback (most recent call last):)
1class W:
2 def write(self, s):
3 print(s, end='')
4 def __enter__(self):
5 return self
6 def __exit__(self, *e):
7 pass
8w = W()
9flag = False
10with w if flag or True:
11 w.write('x')
12with w if (flag or True):
13 w.write('y')
14print()
r1-p007 · Pathlib vs os.path
1import os
2try:
3 os.remove('test.txt')
4except FileNotFoundError:
5 pass
6p = 'test.txt'
7if not os.path.exists(p) or open(p, 'w').close() and True:
8 pass
9else:
10 print('fail')
11if (not os.path.exists(p)) or (open(p, 'w').close() and True):
12 pass
13else:
14 print('fail')
15try:
16 os.remove('test.txt')
17except:
18 pass
--- orig.py	2026-10-17 17:42:23.971200076 -0400
+++ fixed.py	2026-10-17 17:42:24.130198905 -0400
@@ -4,7 +4,7 @@
 except FileNotFoundError:
     pass
 p = 'test.txt'
-if not os.path.exists(p) or open(p, 'w').close() and True:
+if not os.path.exists(p) or (open(p, 'w').close() and True):
     pass
 else:
     print('fail')
r1-p008 · Default arg side effect
1side = []
2def log():
3 side.append(1)
4 return 0
5def f(a=log() or True and False):
6 return len(side)
7print(f())
8side = []
9def g(a=(log() or True) and False):
10 return len(side)
11print(g())
--- orig.py	2026-10-17 17:42:24.221198235 -0400
+++ fixed.py	2026-10-17 17:42:24.392196976 -0400
@@ -2,7 +2,7 @@
 def log():
     side.append(1)
     return 0
-def f(a=log() or True and False):
+def f(a=log() or (True and False)):
     return len(side)
 print(f())
 side = []
1
1
r2-p001 · Side effects in chained boolean - evaluation order changed
1def f():
2 print('f called')
3 return False
4def g():
5 print('g called')
6 return True
7def h():
8 print('h called')
9 return True
10x = f() or g() and h()
11print(x)
--- orig.py	2026-10-17 17:42:30.131154810 -0400
+++ fixed.py	2026-10-17 17:42:30.296153600 -0400
@@ -7,5 +7,5 @@
 def h():
     print('h called')
     return True
-x = f() or g() and h()
+x = f() or (g() and h())
 print(x)
f called
g called
h called
True
r2-p002 · Async function call inside boolean chain - await safety check
1import asyncio
2async def get_val(name, val):
3 print(f'{name} computed')
4 return val
5async def main():
6 a = await get_val('a', False)
7 b = await get_val('b', True)
8 c = await get_val('c', True)
9 res = a or b and c
10 print(res)
11asyncio.run(main())
--- orig.py	2026-10-17 17:42:30.388152926 -0400
+++ fixed.py	2026-10-17 17:42:30.666150889 -0400
@@ -6,6 +6,6 @@
     a = await get_val('a', False)
     b = await get_val('b', True)
     c = await get_val('c', True)
-    res = a or b and c
+    res = a or (b and c)
     print(res)
 asyncio.run(main())
a computed
b computed
c computed
True
r2-p003 · Comments stripped during boolean rewrite causing confusion
1# First operand
2a = False # left side
3# Middle
4eval_val = True # middle
5# Last
6last_val = False # right
7result = a or eval_val and last_val
8print(result)
--- orig.py	2026-10-17 17:42:30.840149613 -0400
+++ fixed.py	2026-10-17 17:42:31.006148397 -0400
@@ -4,5 +4,5 @@
 eval_val = True  # middle
 # Last
 last_val = False  # right
-result = a or eval_val and last_val
+result = a or (eval_val and last_val)
 print(result)
False
r2-p004 · Attached docstring/comment lost during AST transformation
1def compute():
2 """This function computes a boolean."""
3 flag_a = False
4 flag_b = True
5 flag_c = False
6 result = flag_a or flag_b and flag_c # Critical path logic
7 return result
8print(compute())
--- orig.py	2026-10-17 17:42:31.102147694 -0400
+++ fixed.py	2026-10-17 17:42:31.262146523 -0400
@@ -3,6 +3,6 @@
     flag_a = False
     flag_b = True
     flag_c = False
-    result = flag_a or flag_b and flag_c  # Critical path logic
+    result = flag_a or (flag_b and flag_c)  # Critical path logic
     return result
 print(compute())
False
r2-p005 · Truthy left operand skips right evaluation after fix
1def get_a():
2 print('A evaluated')
3 return True
4def get_b():
5 print('B evaluated')
6 return False
7def get_c():
8 print('C evaluated')
9 return True
10res = get_a() or get_b() and get_c()
11print('Result:', res)
--- orig.py	2026-10-17 17:42:31.360145805 -0400
+++ fixed.py	2026-10-17 17:42:31.517144655 -0400
@@ -7,5 +7,5 @@
 def get_c():
     print('C evaluated')
     return True
-res = get_a() or get_b() and get_c()
+res = get_a() or (get_b() and get_c())
 print('Result:', res)
A evaluated
Result: True
r2-p006 · Falsey left operand causes right evaluation in original but not after fix
1def get_d():
2 print('D evaluated')
3 return False
4def get_e():
5 print('E evaluated')
6 return True
7def get_f():
8 print('F evaluated')
9 return False
10res = get_d() and get_e() or get_f()
11print('Result:', res)
--- orig.py	2026-10-17 17:42:31.612143959 -0400
+++ fixed.py	2026-10-17 17:42:31.769142810 -0400
@@ -7,5 +7,5 @@
 def get_f():
     print('F evaluated')
     return False
-res = get_d() and get_e() or get_f()
+res = (get_d() and get_e()) or get_f()
 print('Result:', res)
D evaluated
F evaluated
Result: False
r2-p007 · Property descriptor side effects altered by precedence change (generated program is not deterministic)
1class Flag:
2 def __init__(self, name, val):
3 self.name = name
4 self._val = val
5 def __bool__(self):
6 print(f'{self.name} accessed')
7 return self._val
8
9a = Flag('A', True)
10b = Flag('B', False)
11c = Flag('C', True)
12res = a or b and c
13print('Result:', res)
r2-p008 · Iterator consumed prematurely due to changed evaluation order
1def gen():
2 yield 1
3 yield 0
4 yield 1
5it = gen()
6def next_or_default():
7 try:
8 return next(it)
9 except StopIteration:
10 return 0
11res = next_or_default() or next_or_default() and next_or_default()
12print('Result:', res)
13try:
14 next(it)
15 print('Extra value:', next(it))
16except StopIteration:
17 print('Iterator exhausted')
--- orig.py	2026-10-17 17:42:31.949141492 -0400
+++ fixed.py	2026-10-17 17:42:32.110140313 -0400
@@ -8,7 +8,7 @@
         return next(it)
     except StopIteration:
         return 0
-res = next_or_default() or next_or_default() and next_or_default()
+res = next_or_default() or (next_or_default() and next_or_default())
 print('Result:', res)
 try:
     next(it)
Result: 1
Extra value: 1