Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

E712: Invalid parentheses removal #4925

Closed
Tracked by #4972
addisoncrump opened this issue Jun 7, 2023 · 8 comments · Fixed by #6575
Closed
Tracked by #4972

E712: Invalid parentheses removal #4925

addisoncrump opened this issue Jun 7, 2023 · 8 comments · Fixed by #6575
Assignees
Labels
bug Something isn't working

Comments

@addisoncrump
Copy link
Contributor

E712 fix may remove parentheses in a way which causes invalid syntax:

def a(x):
  return x == 5

if(a(2)) == True:
  print("it's five")

Becomes:

def a(x):
  return x == 5

ifa(2) is True:
  print("it's five")

Discovered by #4822.

@charliermarsh charliermarsh added the bug Something isn't working label Jun 7, 2023
@charliermarsh
Copy link
Member

\cc @MichaReiser - A parenthesized node would solve this (just tabulating).

In the meantime, we'd need to rewrite with LibCST I think.

@MichaReiser
Copy link
Member

\cc @MichaReiser - A parenthesized node would solve this (just tabulating).

In the meantime, we'd need to rewrite with LibCST I think.

Thanks for the ping. Mutating the tree instead of manually creating text edits would solve that too

@charliermarsh
Copy link
Member

Ideally the edit would be "just" replacing == with is, rather than rewriting the entire expression 😂

@addisoncrump
Copy link
Contributor Author

addisoncrump commented Jun 8, 2023

Sadly would not work for the opposite reason:

if a==True:
  ...

would become

if aisTrue:
  ...

@jasikpark
Copy link

Just repro'ed another version of this w/ #4822 in libafl mode 😁

if(True) == TrueElement or x == TrueElement:
    pass

assert (not Soo) in bar
assert {"x": not foo} in bar
cargo run --bin ruff -- --fix ./minimized-from-crash-2c30da3ded030419
    Finished dev [unoptimized + debuginfo] target(s) in 0.11s
     Running `target/debug/ruff --fix ./minimized-from-crash-2c30da3ded030419`
warning: Detected debug build without --no-cache.
error: Autofix introduced a syntax error in `minimized-from-crash-2c30da3ded030419` with rule codes E712: invalid syntax. Got unexpected token Newline at byte offset 42
---
ifTrue is TrueElement or x == TrueElement:
    pass

assert (not Soo) in bar
assert {"x": not foo} in bar

---
minimized-from-crash-2c30da3ded030419:1:4: E712 Comparison to `True` should be `cond is True` or `if cond:`
minimized-from-crash-2c30da3ded030419:1:13: F821 Undefined name `TrueElement`
minimized-from-crash-2c30da3ded030419:1:28: F821 Undefined name `x`
minimized-from-crash-2c30da3ded030419:1:33: F821 Undefined name `TrueElement`
minimized-from-crash-2c30da3ded030419:3:14: F821 Undefined name `Soo`
minimized-from-crash-2c30da3ded030419:3:22: F821 Undefined name `bar`
minimized-from-crash-2c30da3ded030419:4:18: F821 Undefined name `foo`
minimized-from-crash-2c30da3ded030419:4:26: F821 Undefined name `bar`
Found 8 errors.

@addisoncrump
Copy link
Contributor Author

I found another, more exciting example of this; consider the following snippet:

x = range(0, 10)

def y():
  global x

  for i in x:
    if (yield i) == True:
      print("even")
    else:
      print("odd")

v = y()
next(v)

for i in range(1, 10):
  print(v.send(i % 2 == 0))

This is a really dumb program that uses generators wrong, but it gets the point across. Ruff attempts to fix this to:

x = range(0, 10)

def y():
  global x

  for i in x:
    if yield i is True:
      print("even")
    else:
      print("odd")

v = y()
next(v)

for i in range(1, 10):
  print(v.send(i % 2 == 0))

Which is invalid because the yield must be surrounded by parentheses. I think the answer to this is to simply not remove the parentheses, as it seems to be not a totally sane fix.

@dhruvmanila
Copy link
Member

Which is invalid because the yield must be surrounded by parentheses. I think the answer to this is to simply not remove the parentheses, as it seems to be not a totally sane fix.

This seems like a regression as I can only reproduce this on v0.0.279 onwards and not on any previous versions.

@dhruvmanila
Copy link
Member

A bisection search gives me 875e04e.

Right, so the patch generation was moved from Generator-based to Locator-based which is not aware of the parenthesis while the Generator-based is.

@charliermarsh charliermarsh self-assigned this Aug 14, 2023
charliermarsh added a commit that referenced this issue Aug 17, 2023
## Summary

This PR exposes our `is_expression_parenthesized` logic such that we can
use it to expand expressions when autofixing to include their
parenthesized ranges.

This solution has a few drawbacks: (1) we need to compute parenthesized
ranges in more places, which also relies on backwards lexing; and (2) we
need to make use of this in any relevant fixes.

However, I still think it's worth pursuing. On (1), the implementation
is very contained, so IMO we can easily swap this out for a more
performant solution in the future if needed. On (2), this improves
correctness and fixes some bad syntax errors detected by fuzzing, which
means it has value even if it's not as robust as an _actual_
`ParenthesizedExpression` node in the AST itself.

Closes #4925.

## Test Plan

`cargo test` with new cases that previously failed the fuzzer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants