| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent b4ff876 commit 4c8e6e3
2 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1553,6 +1553,89 @@ private module Input implements InputSig1, InputSig2 { | |||
| 1553 | 1553 | ||
| 1554 | 1554 | private string assertThrowTag() { result = "[assert-throw]" } | |
| 1555 | 1555 | ||
| 1556 | + /** | ||
| 1557 | + * Holds if the AST node `n` may raise an exception at runtime as part of | ||
| 1558 | + * its normal evaluation (not via an explicit `raise`/`assert`, which are | ||
| 1559 | + * modelled separately). | ||
| 1560 | + * | ||
| 1561 | + * The set mirrors what the legacy CFG used to flag implicitly: function | ||
| 1562 | + * calls (anything can raise), attribute access (`AttributeError`), | ||
| 1563 | + * subscript access (`IndexError`/`KeyError`/`TypeError`), arithmetic and | ||
| 1564 | + * comparison operators (`TypeError`/`ZeroDivisionError`), imports | ||
| 1565 | + * (`ImportError`/`ModuleNotFoundError`), and generator/coroutine | ||
| 1566 | + * suspension points (`await`/`yield`/`yield from`). | ||
| 1567 | + * | ||
| 1568 | + * Bare `Name` reads are intentionally excluded — modelling every name | ||
| 1569 | + * read as `mayThrow` would explode CFG edge count for negligible | ||
| 1570 | + * analysis value. `BoolExpr`/`IfExp` containers are also excluded; the | ||
| 1571 | + * operands they evaluate contribute their own exception edges. | ||
| 1572 | + */ | ||
| 1573 | + private predicate exprMayThrow(Py::Expr e) { | ||
| 1574 | + e instanceof Py::Call | ||
| 1575 | + or | ||
| 1576 | + e instanceof Py::Attribute | ||
| 1577 | + or | ||
| 1578 | + e instanceof Py::Subscript | ||
| 1579 | + or | ||
| 1580 | + e instanceof Py::BinaryExpr | ||
| 1581 | + or | ||
| 1582 | + e instanceof Py::UnaryExpr | ||
| 1583 | + or | ||
| 1584 | + e instanceof Py::Compare | ||
| 1585 | + or | ||
| 1586 | + e instanceof Py::ImportExpr | ||
| 1587 | + or | ||
| 1588 | + e instanceof Py::ImportMember | ||
| 1589 | + or | ||
| 1590 | + e instanceof Py::Await | ||
| 1591 | + or | ||
| 1592 | + e instanceof Py::Yield | ||
| 1593 | + or | ||
| 1594 | + e instanceof Py::YieldFrom | ||
| 1595 | + } | ||
| 1596 | + | ||
| 1597 | + /** | ||
| 1598 | + * Holds if the statement `s` may raise an exception at runtime as part | ||
| 1599 | + * of its normal evaluation. Currently restricted to `from m import *` | ||
| 1600 | + * (which performs the import as a statement-level side effect). | ||
| 1601 | + */ | ||
| 1602 | + private predicate stmtMayThrow(Py::Stmt s) { s instanceof Py::ImportStar } | ||
| 1603 | + | ||
| 1604 | + /** | ||
| 1605 | + * Holds if `n` is syntactically inside the body, handlers, `else`, or | ||
| 1606 | + * `finally` of a `try` statement (or the body of a `with` statement, | ||
| 1607 | + * which compiles to an implicit try/finally for `__exit__`) in the | ||
| 1608 | + * same scope. | ||
| 1609 | + * | ||
| 1610 | + * This mirrors Java's `ControlFlowGraph::mayThrow`, which only emits | ||
| 1611 | + * exception edges when there is local exception handling that would | ||
| 1612 | + * observe them. Outside such contexts, exception edges would add CFG | ||
| 1613 | + * complexity (weakening BarrierGuard precision and breaking SSA | ||
| 1614 | + * continuity around augmented assignments and subscript stores) | ||
| 1615 | + * without any analysis benefit, since exceptions just propagate to | ||
| 1616 | + * the function exit anyway. | ||
| 1617 | + */ | ||
| 1618 | + private predicate inExceptionContext(Py::AstNode py) { | ||
| 1619 | + exists(Py::Try t | t.containsInScope(py)) | ||
| 1620 | + or | ||
| 1621 | + exists(Py::With w | w.containsInScope(py)) | ||
| 1622 | + } | ||
| 1623 | + | ||
| 1624 | + /** | ||
| 1625 | + * Holds if `n` may raise an exception during normal evaluation. See | ||
| 1626 | + * `exprMayThrow` and `stmtMayThrow` for the included AST classes. | ||
| 1627 | + * | ||
| 1628 | + * Restricted to nodes inside a `try`/`with` statement: matches Java's | ||
| 1629 | + * approach of only modelling exception flow where it can be observed | ||
| 1630 | + * by local handling. | ||
| 1631 | + */ | ||
| 1632 | + private predicate mayThrow(Ast::AstNode n) { | ||
| 1633 | + exists(Py::AstNode py | py = n.asExpr() or py = n.asStmt() | | ||
| 1634 | + (exprMayThrow(py) or stmtMayThrow(py)) and | ||
| 1635 | + inExceptionContext(py) | ||
| 1636 | + ) | ||
| 1637 | + } | ||
| 1638 | + | ||
| 1556 | 1639 | predicate additionalNode(Ast::AstNode n, string tag, NormalSuccessor t) { | |
| 1557 | 1640 | n instanceof Ast::AssertStmt and tag = assertThrowTag() and t instanceof DirectSuccessor | |
| 1558 | 1641 | } | |
@@ -1564,6 +1647,11 @@ private module Input implements InputSig1, InputSig2 { | |||
| 1564 | 1647 | n.isAdditional(ast, assertThrowTag()) and | |
| 1565 | 1648 | c.asSimpleAbruptCompletion() instanceof ExceptionSuccessor and | |
| 1566 | 1649 | always = true | |
| 1650 | + or | ||
| 1651 | + mayThrow(ast) and | ||
| 1652 | + n.isIn(ast) and | ||
| 1653 | + c.asSimpleAbruptCompletion() instanceof ExceptionSuccessor and | ||
| 1654 | + always = false | ||
| 1567 | 1655 | } | |
| 1568 | 1656 | ||
| 1569 | 1657 | predicate endAbruptCompletion(Ast::AstNode ast, PreControlFlowNode n, AbruptCompletion c) { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,15 +1,15 @@ | |||
| 1 | - # Dead bindings under the "no expressions raise" CFG abstraction. | ||
| 1 | + # Reachability of code following a try whose body always returns. | ||
| 2 | 2 | # | |
| 3 | - # The new CFG does not currently model raise edges from arbitrary | ||
| 4 | - # expressions. As a consequence, code that is only reachable through | ||
| 5 | - # exception flow is (correctly) classified as dead and has no CFG node. | ||
| 6 | - # Variable bindings in dead code do not need CFG nodes - SSA / dataflow | ||
| 7 | - # over dead code is moot. | ||
| 3 | + # The new CFG models exception edges for raise-prone expressions when | ||
| 4 | + # they appear inside a `try` (or `with`) statement, mirroring Java's | ||
| 5 | + # `mayThrow`. This means the body of a `try` has both a normal | ||
| 6 | + # completion edge and an exception edge to its handlers, so code | ||
| 7 | + # following the try-statement is reachable via the except-handler path | ||
| 8 | + # even when the try-body would otherwise always return. | ||
| 8 | 9 | # | |
| 9 | - # These tests act as a regression guard: the bindings below intentionally | ||
| 10 | - # have no `cfgdefines=` annotations. If raise modelling is later added, | ||
| 11 | - # the BindingsTest infrastructure will surface the new CFG nodes as | ||
| 12 | - # unexpected results, and this file will need to be revisited. | ||
| 10 | + # Code that is not reachable under either normal or exception flow | ||
| 11 | + # (for example, the `else` clause of a try whose body unconditionally | ||
| 12 | + # raises) remains correctly classified as dead. | ||
| 13 | 13 | ||
| 14 | 14 | ||
| 15 | 15 | def f(obj): # $ cfgdefines=f cfgdefines=obj | |
@@ -18,12 +18,12 @@ def f(obj): # $ cfgdefines=f cfgdefines=obj | |||
| 18 | 18 | except TypeError: | |
| 19 | 19 | pass | |
| 20 | 20 | ||
| 21 | - # The first try's body always returns; its except handler does not | ||
| 22 | - # raise or otherwise transfer control, so under "no expressions | ||
| 23 | - # raise" the only paths out of the try-statement are dead. Everything | ||
| 24 | - # below is unreachable. | ||
| 21 | + # The try-body always returns, but `len(obj)` can raise (it is | ||
| 22 | + # inside the try, so we model its exception edge). The | ||
| 23 | + # `except TypeError: pass` handler falls through to here, making | ||
| 24 | + # the code below reachable. | ||
| 25 | 25 | try: | |
| 26 | - hint = type(obj).__length_hint__ | ||
| 26 | + hint = type(obj).__length_hint__ # $ cfgdefines=hint | ||
| 27 | 27 | except AttributeError: | |
| 28 | 28 | return None | |
| 29 | 29 | return hint | |
@@ -35,7 +35,8 @@ def g(): # $ cfgdefines=g | |||
| 35 | 35 | except: | |
| 36 | 36 | raise Exception("outer") | |
| 37 | 37 | else: | |
| 38 | - # Unreachable: the inner try body always raises, so the `else:` | ||
| 38 | + # Unreachable: the inner try body always raises (via an explicit | ||
| 39 | + # `raise`, which is modelled unconditionally), so the `else:` | ||
| 39 | 40 | # clause never runs. | |
| 40 | 41 | hit_inner_else = True | |
| 41 | 42 | ||
@@ -46,7 +47,7 @@ def h(cache, key): # $ cfgdefines=h cfgdefines=cache cfgdefines=key | |||
| 46 | 47 | except KeyError: | |
| 47 | 48 | pass | |
| 48 | 49 | ||
| 49 | - # Same pattern as `f`: dead under "no expressions raise". | ||
| 50 | - value = compute(key) | ||
| 50 | + # Same pattern as `f`: reachable via the except-handler fall-through. | ||
| 51 | + value = compute(key) # $ cfgdefines=value | ||
| 51 | 52 | cache[key] = value | |
| 52 | 53 | return value | |
| Back | FazBrowse Home | New Git URL |
0 commit comments