Files
codeql/python/ql/src/Expressions/IsComparisons.qll
Copilot 717ff62d70 Python: deprecate AstNode.getAFlowNode() and rewrite internal callers
Preparatory refactor for the shared-CFG dataflow migration.

Deprecates the AstNode.getAFlowNode() cached predicate on the public
Python QL API and rewrites all ~140 internal callers across lib/, src/,
test/, and tools/ from `expr.getAFlowNode() = cfgNode` to
`cfgNode.getNode() = expr`, using ControlFlowNode.getNode() which
already exists in Flow.qll.

The predicate itself is preserved (with a deprecation note pointing at
the new pattern) so external users do not experience churn — they can
migrate at their own pace and the AST/CFG hierarchies still get the
intended untangling once the deprecation eventually elapses.

Semantic noop verified by:
- All 361 lib/ + src/ queries compile clean.
- All 122 ControlFlow + PointsTo library-tests pass.
- All 64 dataflow library-tests pass.
- All 113 Variables/Exceptions/Expressions/Statements/Functions/Imports/
  Security/CWE-798/ModificationOfParameterWithDefault query-tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-22 14:55:19 +02:00

150 lines
5.5 KiB
Plaintext

/** INTERNAL - Helper predicates for queries that inspect the comparison of objects using `is`. */
import python
private import LegacyPointsTo
/** Holds if the comparison `comp` uses `is` or `is not` (represented as `op`) to compare its `left` and `right` arguments. */
predicate comparison_using_is(Compare comp, ControlFlowNode left, Cmpop op, ControlFlowNode right) {
exists(CompareNode fcomp | fcomp.getNode() = comp |
fcomp.operands(left, op, right) and
(op instanceof Is or op instanceof IsNot)
)
}
/** Holds if the class `c` overrides the default notion of equality or comparison. */
predicate overrides_eq_or_cmp(ClassValue c) {
major_version() = 2 and c.hasAttribute("__eq__")
or
c.declaresAttribute("__eq__") and not c = Value::named("object")
or
exists(ClassValue sup | sup = c.getASuperType() and not sup = Value::named("object") |
sup.declaresAttribute("__eq__")
)
or
major_version() = 2 and c.hasAttribute("__cmp__")
}
/** Holds if the class `cls` is likely to only have a single instance throughout the program. */
predicate probablySingleton(ClassValue cls) {
strictcount(Value inst | inst.getClass() = cls) = 1
or
cls = Value::named("None").getClass()
}
/** Holds if using `is` to compare instances of the class `c` is likely to cause unexpected behavior. */
predicate invalid_to_use_is_portably(ClassValue c) {
overrides_eq_or_cmp(c) and
// Exclude type/builtin-function/bool as it is legitimate to compare them using 'is' but they implement __eq__
not c = Value::named("type") and
not c = ClassValue::builtinFunction() and
not c = Value::named("bool") and
// OK to compare with 'is' if a singleton
not probablySingleton(c)
}
/** Holds if the control flow node `f` points to either `True`, `False`, or `None`. */
predicate simple_constant(ControlFlowNodeWithPointsTo f) {
exists(Value val | f.pointsTo(val) |
val = Value::named("True") or val = Value::named("False") or val = Value::named("None")
)
}
private predicate cpython_interned_value(Expr e) {
exists(string text | text = e.(StringLiteral).getText() |
text.length() = 0
or
text.length() = 1 and text.regexpMatch("[U+0000-U+00ff]")
)
or
exists(int i | i = e.(IntegerLiteral).getN().toInt() | -5 <= i and i <= 256)
or
exists(Tuple t | t = e and not exists(t.getAnElt()))
}
/**
* The set of values that can be expected to be interned across
* the main implementations of Python. PyPy, Jython, etc tend to
* follow CPython, but it varies, so this is a best guess.
*/
private predicate universally_interned_value(Expr e) {
e.(IntegerLiteral).getN().toInt() = 0
or
exists(Tuple t | t = e and not exists(t.getAnElt()))
or
e.(StringLiteral).getText() = ""
}
/** Holds if the expression `e` points to an interned constant in CPython. */
predicate cpython_interned_constant(ExprWithPointsTo e) {
exists(Expr const | e.pointsTo(_, const) | cpython_interned_value(const))
}
/** Holds if the expression `e` points to a value that can be reasonably expected to be interned across all implementations of Python. */
predicate universally_interned_constant(ExprWithPointsTo e) {
exists(Expr const | e.pointsTo(_, const) | universally_interned_value(const))
}
private predicate comparison_both_types(Compare comp, Cmpop op, ClassValue cls1, ClassValue cls2) {
exists(ControlFlowNodeWithPointsTo op1, ControlFlowNodeWithPointsTo op2 |
comparison_using_is(comp, op1, op, op2) or comparison_using_is(comp, op2, op, op1)
|
op1.inferredValue().getClass() = cls1 and
op2.inferredValue().getClass() = cls2
)
}
private predicate comparison_one_type(Compare comp, Cmpop op, ClassValue cls) {
not comparison_both_types(comp, _, _, _) and
exists(ControlFlowNodeWithPointsTo operand |
comparison_using_is(comp, operand, op, _) or comparison_using_is(comp, _, op, operand)
|
operand.inferredValue().getClass() = cls
)
}
/**
* Holds if using `is` or `is not` as the operator `op` in the comparison `comp` would be invalid when applied to the class `cls`.
*/
predicate invalid_portable_is_comparison(Compare comp, Cmpop op, ClassValue cls) {
// OK to use 'is' when defining '__eq__'
not exists(Function eq | eq.getName() = "__eq__" or eq.getName() = "__ne__" |
eq = comp.getScope().getScope*()
) and
(
comparison_one_type(comp, op, cls) and invalid_to_use_is_portably(cls)
or
exists(ClassValue other | comparison_both_types(comp, op, cls, other) |
invalid_to_use_is_portably(cls) and
invalid_to_use_is_portably(other)
)
) and
// OK to use 'is' when comparing items from a known set of objects
not exists(ExprWithPointsTo left, ExprWithPointsTo right, Value val |
comp.compares(left, op, right) and
exists(ImmutableLiteral il | il = val.(ConstantObjectInternal).getLiteral())
|
left.pointsTo(val) and right.pointsTo(val)
or
// Simple constant in module, probably some sort of sentinel
exists(AstNode origin |
not left.pointsTo(_) and
right.pointsTo(val, origin) and
origin.getScope().getEnclosingModule() = comp.getScope().getEnclosingModule()
)
) and
// OK to use 'is' when comparing with a member of an enum
not exists(ExprWithPointsTo left, ExprWithPointsTo right, AstNode origin |
comp.compares(left, op, right) and
enum_member(origin)
|
left.pointsTo(_, origin) or right.pointsTo(_, origin)
)
}
private predicate enum_member(AstNode obj) {
exists(ClassValue cls, AssignStmt asgn | cls.getASuperType().getName() = "Enum" |
cls.getScope() = asgn.getScope() and
asgn.getValue() = obj
)
}