Python: use private-abstract + final-alias pattern for AstNode

Convert AstNode from a concrete class with empty default predicates into
a private abstract class plus a final alias, matching the pattern used
in cpp/.../EdgeKind.qll and cpp/.../IRVariable.qll:

  abstract private class AstNodeImpl extends TAstNode {
    abstract string toString();
    abstract Py::Location getLocation();
    abstract Callable getEnclosingCallable();
    ...
  }

  final class AstNode = AstNodeImpl;

This makes the compiler enforce that every concrete subclass implements
toString/getLocation/getEnclosingCallable, replacing the brittle
'empty default + per-branch override' arrangement. Sister classes
inside the module now extend AstNodeImpl instead of AstNode (which is
final and cannot be extended).

The empty Parameter stub gains explicit none() overrides for the
three abstract members, since QL requires them statically even when
the class has no instances.

No behaviour change: all 24 NewCfg evaluation-order tests pass; all
11 shared-CFG consistency queries report 0 violations on CPython.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Copilot
2026-05-07 16:58:43 +00:00
parent 00c742f5ff
commit ed1709eb4a

View File

@@ -69,16 +69,24 @@ module Ast implements AstSig<Py::Location> {
sl = any(Py::ExceptGroupStmt p).getBody()
}
/** An AST node visible to the shared CFG. */
class AstNode extends TAstNode {
/**
* An AST node visible to the shared CFG.
*
* This is the abstract implementation class. It enforces that each
* concrete subclass provides `toString`, `getLocation`, and
* `getEnclosingCallable` (one subclass per `TAstNode` newtype branch).
* The public alias `AstNode` is what users (and the `AstSig` signature)
* see; subclasses inside this module extend `AstNodeImpl` directly.
*/
abstract private class AstNodeImpl extends TAstNode {
/** Gets a textual representation of this AST node. */
string toString() { none() }
abstract string toString();
/** Gets the location of this AST node. */
Py::Location getLocation() { none() }
abstract Py::Location getLocation();
/** Gets the enclosing callable that contains this node, if any. */
Callable getEnclosingCallable() { none() }
abstract Callable getEnclosingCallable();
/** Gets the underlying Python `Stmt`, if this node wraps one. */
Py::Stmt asStmt() { this = TStmt(result) }
@@ -109,6 +117,9 @@ module Ast implements AstSig<Py::Location> {
AstNode getChild(int index) { none() }
}
/** An AST node visible to the shared CFG. */
final class AstNode = AstNodeImpl;
/** Gets the immediately enclosing callable that contains `node`. */
Callable getEnclosingCallable(AstNode node) { result = node.getEnclosingCallable() }
@@ -117,7 +128,7 @@ module Ast implements AstSig<Py::Location> {
*
* In Python, all three are executable scopes with statement bodies.
*/
class Callable extends AstNode, TScope {
class Callable extends AstNodeImpl, TScope {
private Py::Scope sc;
Callable() { this = TScope(sc) }
@@ -137,9 +148,15 @@ module Ast implements AstSig<Py::Location> {
*
* TODO: Implement in order to include parameters in the CFG.
*/
class Parameter extends AstNode {
class Parameter extends AstNodeImpl {
Parameter() { none() }
override string toString() { none() }
override Py::Location getLocation() { none() }
override Callable getEnclosingCallable() { none() }
Expr getDefaultValue() { none() }
}
@@ -147,7 +164,7 @@ module Ast implements AstSig<Py::Location> {
Parameter callableGetParameter(Callable c, int index) { none() }
/** A statement. */
class Stmt extends AstNode {
class Stmt extends AstNodeImpl {
Stmt() { this instanceof TStmt or this instanceof TBlockStmt }
// For `TStmt` instances, delegate to the wrapped Python statement.
@@ -160,7 +177,7 @@ module Ast implements AstSig<Py::Location> {
}
/** An expression. */
class Expr extends AstNode {
class Expr extends AstNodeImpl {
Expr() { this instanceof TExpr or this instanceof TBoolExprPair }
// For `TExpr` instances, delegate to the wrapped Python expression.
@@ -173,7 +190,7 @@ module Ast implements AstSig<Py::Location> {
}
/** A pattern in a `match` statement. */
additional class Pattern extends AstNode, TPattern {
additional class Pattern extends AstNodeImpl, TPattern {
private Py::Pattern p;
Pattern() { this = TPattern(p) }