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

[internal] Return statements in finally block point to end block for unreachable (PLW0101) #15276

Merged
merged 3 commits into from
Jan 7, 2025

Conversation

dylwil3
Copy link
Collaborator

@dylwil3 dylwil3 commented Jan 5, 2025

Note: PLW0101 remains in testing rather than preview, so this PR does not modify any public behavior (hence the title beginning with internal rather than pylint, for the sake of the changelog.)

Fixes an error in the processing of try statements in the control flow graph builder.

When processing a try statement, the block following a return was forced to point to the finally block. However, if the return was in the finally block, this caused the block to point to itself. In the case where the whole try-finally statement was also included inside of a loop, this caused an infinite loop in the builder for the control flow graph as it attempted to resolve edges.

Closes #15248

Test function

Source

def l():
    while T:
        try:
            while ():
                if 3:
                    break
        finally:
            return

Control Flow Graph

flowchart TD
  start(("Start"))
  return(("End"))
  block0[["`*(empty)*`"]]
  block1[["Loop continue"]]
  block2["return\n"]
  block3[["Loop continue"]]
  block4["break\n"]
  block5["if 3:
                    break\n"]
  block6["while ():
                if 3:
                    break\n"]
  block7[["Exception raised"]]
  block8["try:
            while ():
                if 3:
                    break
        finally:
            return\n"]
  block9["while T:
        try:
            while ():
                if 3:
                    break
        finally:
            return\n"]
  start --> block9
  block9 -- "T" --> block8
  block9 -- "else" --> block0
  block8 -- "Exception raised" --> block7
  block8 -- "else" --> block6
  block7 --> block2
  block6 -- "()" --> block5
  block6 -- "else" --> block2
  block5 -- "3" --> block4
  block5 -- "else" --> block3
  block4 --> block2
  block3 --> block6
  block2 --> return
  block1 --> block9
  block0 --> return
Loading

@dylwil3 dylwil3 added rule Implementing or modifying a lint rule testing Related to testing Ruff itself bug Something isn't working and removed testing Related to testing Ruff itself labels Jan 5, 2025
@AlexWaygood AlexWaygood added the internal An internal refactor or improvement label Jan 5, 2025
Copy link
Contributor

github-actions bot commented Jan 5, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@MichaReiser
Copy link
Member

Is this overlapping with #15278 or do they fix different issues?

@augustelalande
Copy link
Contributor

augustelalande commented Jan 6, 2025

Yes they are attempting to fix the same issue. Technically, this PR is more of a band-aid because the CFG should not fall through to the finally block in the post_process_try and is only doing so because of the issue I pointed out in #15278. Still I would recommend applying both changes: #15276 and #15278 to make it more robust.

@dylwil3
Copy link
Collaborator Author

dylwil3 commented Jan 6, 2025

I agree we should apply both corrections to the CFG construction, but I think we should keep the rule as "test" for now.

I'm not so worried about discovering more edge cases where the CFG needs improvements, that's fine. However, infinite loops or panics are more disruptive since they cause the whole ruff call to crash. So I would feel more comfortable if we could either prove that the loops in post_process_loop and post_process_try must terminate, or, even better, replace them with some bounded for loop (which we know to be correct for some other reason).

@MichaReiser
Copy link
Member

Thanks for the explanation.

What's the CFG if we wrap the try statement in another try with a finally block?

@dylwil3
Copy link
Collaborator Author

dylwil3 commented Jan 7, 2025

What's the CFG if we wrap the try statement in another try with a finally block?

Like this, which looks correct to me:

def l():
    while T:
        try:
            try:
                while ():
                    if 3:
                        break
            finally:
                return
        finally:
            return
flowchart TD
  start(("Start"))
  return(("End"))
  block0[["`*(empty)*`"]]
  block1[["Loop continue"]]
  block2["return\n"]
  block3["return\n"]
  block4[["Loop continue"]]
  block5["break\n"]
  block6["if 3:
                        break\n"]
  block7["while ():
                    if 3:
                        break\n"]
  block8[["Exception raised"]]
  block9["try:
                while ():
                    if 3:
                        break
            finally:
                return\n"]
  block10[["Exception raised"]]
  block11["try:
            try:
                while ():
                    if 3:
                        break
            finally:
                return
        finally:
            return\n"]
  block12["while T:
        try:
            try:
                while ():
                    if 3:
                        break
            finally:
                return
        finally:
            return\n"]

  start --> block12
  block12 -- "T" --> block11
  block12 -- "else" --> block0
  block11 -- "Exception raised" --> block10
  block11 -- "else" --> block9
  block10 --> block2
  block9 -- "Exception raised" --> block8
  block9 -- "else" --> block7
  block8 --> block3
  block7 -- "()" --> block6
  block7 -- "else" --> block3
  block6 -- "3" --> block5
  block6 -- "else" --> block4
  block5 --> block3
  block4 --> block7
  block3 --> block2
  block2 --> return
  block1 --> block12
  block0 --> return
Loading

// if we are already in a `finally` block, terminate
if Some(idx) == finally_index {
return;
}
// re-route to finally if present and not already re-routed
if let Some(finally_index) = finally_index {
blocks.blocks[idx].next = NextBlock::Always(finally_index);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would probably be a small refactor and is something better left for its own PR but we could have better runtime assertions around preventing loops by introducing a blocks.set_next(idx, next) method that contains an assertion that idx and the next's index aren't equal.

@dylwil3 dylwil3 merged commit a876090 into astral-sh:main Jan 7, 2025
21 checks passed
@dylwil3 dylwil3 deleted the cfg-infloop branch January 7, 2025 17:26
dcreager added a commit that referenced this pull request Jan 8, 2025
* main:
  [`pylint`] Fix `unreachable` infinite loop (`PLW0101`) (#15278)
  fix invalid syntax in workflow file (#15357)
  [`pycodestyle`] Avoid false positives related to type aliases (`E252`) (#15356)
  [`flake8-builtins`] Disapply `A005` to stub files (#15350)
  Improve logging system using `logLevel`, avoid trace value (#15232)
  [`flake8-builtins`] Rename `A005` and improve its error message (#15348)
  Spruce up docs for pydoclint rules (#15325)
  [`flake8-type-checking`] Apply `TC008` more eagerly in `TYPE_CHECKING` blocks and disapply it in stubs (#15180)
  [red-knot] `knot_extensions` Python API (#15103)
  Display Union of Literals as a Literal (#14993)
  [red-knot] all types are assignable to object (#15332)
  [`ruff`] Parenthesize arguments to `int` when removing `int` would change semantics in `unnecessary-cast-to-int` (`RUF046`) (#15277)
  [`eradicate`] Correctly handle metadata blocks directly followed by normal blocks (`ERA001`) (#15330)
  Narrowing for class patterns in match statements (#15223)
  [red-knot] add call checking (#15200)
  Spruce up docs for `slice-to-remove-prefix-or-suffix` (`FURB188`) (#15328)
  [`internal`] Return statements in finally block point to end block for `unreachable` (`PLW0101`) (#15276)
  [`ruff`] Treat `)` as a regex metacharacter (`RUF043`, `RUF055`) (#15318)
  Use uv consistently throughout the documentation (#15302)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working internal An internal refactor or improvement rule Implementing or modifying a lint rule
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Long freeze(>3 minutes - probably infinite) when checking file
4 participants