Skip to content

Python: model exception edges for raise-prone expressions inside try/with#21937

Draft
yoff wants to merge 1 commit into
yoff/python-shared-cfg-dataflow-flipfrom
yoff/python-cfg-modelling-exceptions
Draft

Python: model exception edges for raise-prone expressions inside try/with#21937
yoff wants to merge 1 commit into
yoff/python-shared-cfg-dataflow-flipfrom
yoff/python-cfg-modelling-exceptions

Conversation

@yoff
Copy link
Copy Markdown
Contributor

@yoff yoff commented Jun 4, 2026

Stacked on top of #21925 (the shared-CFG dataflow flip).

Problem

After the flip, the new CFG only modelled exception edges for explicit raise and assert statements. Code reachable only via the implicit exception path of an arbitrary expression (e.g., the body of an except handler following a try-body whose call() could raise) was classified as dead, breaking several exception-related queries:

  • py/stack-trace-exposure (StackTraceExposure)
  • py/file-not-closed (FileNotAlwaysClosed)
  • py/catch-base-exception (CatchingBaseException)
  • py/empty-except (EmptyExcept)
  • py/use-of-exit (UseOfExit)
  • Plus the internal ExceptionInfo test.

Approach

Add a mayThrow predicate over expressions known to be implicit exception sources in Python (calls, attribute access, subscripts, arithmetic/comparison operators, imports, await/yield/yield from) plus from m import * at the statement level, and route them through the shared CFG's beginAbruptCompletion(_, _, ExceptionSuccessor, always=false) hook.

Restricted to nodes inside a try/with statement in the same scope. This mirrors Java's ControlFlowGraph::mayThrow, which only emits exception edges where local handling can observe them. Outside such contexts the edges would add CFG complexity (weakening BarrierGuard precision, breaking SSA continuity around augmented assignments and subscript stores) without analysis benefit, since exceptions just propagate to the function exit anyway.

Effect on the test suite

Improvements:

  • ~100 alerts restored across exception-related query tests (StackTraceExposure +29, ExceptionInfo +17, FileNotAlwaysClosed +52, UseOfExit +1, CatchingBaseException restored).
  • 11 false-positive alerts in FileNotAlwaysClosed (lines 20/30/39/58/69/79/91/182/225/252/275) are suppressed because the query can now correctly trace exception flow to the close.
  • 3 incidental improvements blessed (ViewAliasInExcept now found by FindSubclass; CmpTest narrows gap to legacy; NormalDataflowTest fixes 1 missing result — the MISSING: keyword was dropped from test.py:847 per inline-expectation convention).
  • Regression-guard test file dead_under_no_raise.py updated (its docstring literally predicted "if raise modelling is later added, this file will need to be revisited").

Regressions (documented):

  • FileNotAlwaysClosed re-marks lines 49/141/237 as MISSING:Alert. These were producing alerts on flip and were cleaned up there; with this commit's exception modelling, the query becomes optimistic about close-paths through buggy guards (e.g., finally: if f.closed: f.close()) because it now sees real exception edges into those guards. The test file comments at lines 52/147/244 already note "We don't precisely consider this condition, so this result is MISSING". These are known query limitations, not new bugs.

Zero new dataflow precision regressions — narrowing to in-try/with avoids both the BarrierGuard weakening (is_safe(...) == True-style patterns) and SSA-continuity issues (+=, subscript stores) that an unconditional mayThrow would cause.

CPython CFG consistency: all 11 checks pass.

Full Python suite: 973/975 passing (2 pre-existing failures unrelated: hidden/test.ql and long_path/LongPath.ql).

Stack

@github-actions github-actions Bot added the Python label Jun 4, 2026
@yoff yoff force-pushed the yoff/python-cfg-modelling-exceptions branch from 83c5f33 to 647b976 Compare June 4, 2026 07:47
…with

The new CFG previously only emitted exception edges for explicit `raise`
and `assert` statements. As a result, code that became reachable only
via the exception path of an arbitrary expression (e.g., the body of an
`except` handler following a try-body whose `call()` could raise) was
classified as dead, breaking analyses like StackTraceExposure,
FileNotAlwaysClosed, ExceptionInfo, UseOfExit, and CatchingBaseException.

This commit adds a `mayThrow` predicate over expressions that are known
sources of implicit exceptions in Python (calls, attribute access,
subscripts, arithmetic/comparison operators, imports, await/yield/yield
from) plus `from m import *` at the statement level, and routes them
through the shared CFG's `beginAbruptCompletion(_, _, ExceptionSuccessor,
always=false)` hook.

The set of exception sources is restricted to nodes that are
syntactically inside a `try`/`with` statement in the same scope.
This mirrors Java's `ControlFlowGraph::mayThrow`, which only emits
exception edges where local handling can observe them — outside such
contexts, the edges add CFG complexity (weakening BarrierGuard
precision and breaking SSA continuity around augmented assignments and
subscript stores) without analysis benefit, since exceptions just
propagate to the function exit anyway.

Net effect on the test suite: ~100 alerts restored across the exception-
related query tests (StackTraceExposure +29, ExceptionInfo +17,
FileNotAlwaysClosed +52, UseOfExit +1, CatchingBaseException restored)
with no precision regressions. Affected `.expected` files and the
regression-guard `dead_under_no_raise.py` are updated accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@yoff yoff force-pushed the yoff/python-shared-cfg-dataflow-flip branch from 0b3f28f to db94cd9 Compare June 4, 2026 08:09
@yoff yoff force-pushed the yoff/python-cfg-modelling-exceptions branch from 647b976 to c34dc45 Compare June 4, 2026 08:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant