mirror of
https://github.com/github/codeql.git
synced 2026-01-01 00:27:24 +01:00
Will need subsequent PRs fixing up test failures (due to deprecated methods moving around), but other than that everything should be straight-forward.
149 lines
5.3 KiB
Plaintext
149 lines
5.3 KiB
Plaintext
/** INTERNAL - Helper predicates for queries that inspect the comparison of objects using `is`. */
|
|
|
|
import python
|
|
|
|
/** 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 = comp.getAFlowNode() |
|
|
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(ControlFlowNode 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.(StrConst).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.(StrConst).getText() = ""
|
|
}
|
|
|
|
/** Holds if the expression `e` points to an interned constant in CPython. */
|
|
predicate cpython_interned_constant(Expr 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(Expr 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(ControlFlowNode op1, ControlFlowNode 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(ControlFlowNode 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(Expr left, Expr right, Value val |
|
|
comp.compares(left, op, right) and
|
|
exists(ImmutableLiteral il | il.getLiteralValue() = val)
|
|
|
|
|
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(Expr left, Expr 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
|
|
)
|
|
}
|