Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
497a441
Python: Use API graphs instead of points-to for simple built-ins
tausbn Feb 18, 2026
63b497d
Python: Port `py/print-during-import`
tausbn Feb 19, 2026
f6e53f6
Python: Introduce `DuckTyping` module
tausbn Feb 20, 2026
34837ea
Python: Port ContainsNonContainer.ql
tausbn Feb 20, 2026
ff86c1d
Python: Port NonIteratorInForLoop.ql
tausbn Feb 20, 2026
5f0bccd
Python: Port ShouldUseWithStatement.ql
tausbn Feb 20, 2026
714dfcc
Python: Port UnusedExceptionObject.ql
tausbn Feb 20, 2026
2df8973
Python: Add `DuckTyping::isNewStyle`
tausbn Feb 20, 2026
b15ceeb
Python: Add declares/getAttribute API
tausbn Feb 20, 2026
e5a0ae8
Python: Port SlotsInOldStyleClass.ql
tausbn Feb 20, 2026
e595b5b
Python: Port SuperInOldStyleClass.ql
tausbn Feb 20, 2026
aa39dca
Python: Port PropertyInOldStyleClass.ql
tausbn Feb 20, 2026
75bbb51
Python: Port InconsistentMRO.ql
tausbn Feb 20, 2026
811e547
Python: Port HashedButNoHash.ql
tausbn Feb 20, 2026
11fe15b
Python: Port UselessClass.ql
tausbn Feb 20, 2026
8a6567f
Python: Port ShouldBeContextManager.ql
tausbn Feb 20, 2026
62433fe
Python: Port WrongNameForArgumentInClassInstantiation.ql
tausbn Feb 23, 2026
f4a7316
Python: Port WrongNumberArgumentsInClassInstantiation.ql
tausbn Feb 23, 2026
dd18c6e
Python: Remove missing results
tausbn Feb 23, 2026
4a3310f
Python: Extend DuckTyping module
tausbn Feb 23, 2026
e4e39f3
Python: Port DeprecatedSliceMethod.ql
tausbn Feb 23, 2026
f2d98b3
Python: Port DocStrings.ql
tausbn Feb 23, 2026
31b6438
Python: Add `DuckTyping::hasUnreliableMro`
tausbn Mar 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ module Builtins {
"UnicodeDecodeError", "UnicodeEncodeError", "UnicodeError", "UnicodeTranslateError",
"UnicodeWarning", "UserWarning", "ValueError", "Warning", "ZeroDivisionError",
// Added for compatibility
"exec"
"exec",
// Added by the `site` module (available by default unless `-S` is used)
"copyright", "credits", "exit", "quit"
]
or
// Built-in constants shared between Python 2 and 3
Expand All @@ -51,8 +53,8 @@ module Builtins {
or
// Python 2 only
result in [
"basestring", "cmp", "execfile", "file", "long", "raw_input", "reduce", "reload", "unichr",
"unicode", "xrange"
"apply", "basestring", "cmp", "execfile", "file", "long", "raw_input", "reduce", "reload",
"unichr", "unicode", "xrange"
]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1977,3 +1977,174 @@ private module OutNodes {
* `kind`.
*/
OutNode getAnOutNode(DataFlowCall call, ReturnKind kind) { call = result.getCall(kind) }

/**
* Provides predicates for approximating type properties of user-defined classes
* based on their structure (method declarations, base classes).
*
* This module should _not_ be used in the call graph computation itself, as parts of it may depend
* on layers that themselves build upon the call graph (e.g. API graphs).
*/
module DuckTyping {
private import semmle.python.ApiGraphs

/**
* Holds if `cls` or any of its resolved superclasses declares a method with the given `name`.
*/
predicate hasMethod(Class cls, string name) {
cls.getAMethod().getName() = name
or
hasMethod(getADirectSuperclass(cls), name)
}

/**
* Holds if `cls` has a base class that cannot be resolved to a user-defined class
* and is not just `object`, meaning it may inherit methods from an unknown class.
*/
predicate hasUnresolvedBase(Class cls) {
exists(Expr base | base = cls.getABase() |
not base = classTracker(_).asExpr() and
not base = API::builtin("object").getAValueReachableFromSource().asExpr()
)
}

/**
* Holds if `cls` supports the container protocol, i.e. it declares
* `__contains__`, `__iter__`, or `__getitem__`.
*/
predicate isContainer(Class cls) {
hasMethod(cls, "__contains__") or
hasMethod(cls, "__iter__") or
hasMethod(cls, "__getitem__")
}

/**
* Holds if `cls` supports the iterable protocol, i.e. it declares
* `__iter__` or `__getitem__`.
*/
predicate isIterable(Class cls) {
hasMethod(cls, "__iter__") or
hasMethod(cls, "__getitem__")
}

/**
* Holds if `cls` supports the iterator protocol, i.e. it declares
* both `__iter__` and `__next__`.
*/
predicate isIterator(Class cls) {
hasMethod(cls, "__iter__") and
hasMethod(cls, "__next__")
}

/**
* Holds if `cls` supports the context manager protocol, i.e. it declares
* both `__enter__` and `__exit__`.
*/
predicate isContextManager(Class cls) {
hasMethod(cls, "__enter__") and
hasMethod(cls, "__exit__")
}

/**
* Holds if `cls` supports the descriptor protocol, i.e. it declares
* `__get__`, `__set__`, or `__delete__`.
*/
predicate isDescriptor(Class cls) {
hasMethod(cls, "__get__") or
hasMethod(cls, "__set__") or
hasMethod(cls, "__delete__")
}

/**
* Holds if `cls` directly assigns to an attribute named `name` in its class body.
* This covers attribute assignments like `x = value`, but not method definitions.
*/
predicate declaresAttribute(Class cls, string name) { exists(getAnAttributeValue(cls, name)) }

/**
* Gets the value expression assigned to attribute `name` directly in the class body of `cls`.
*/
Expr getAnAttributeValue(Class cls, string name) {
exists(Assign a |
a.getScope() = cls and
a.getATarget().(Name).getId() = name and
result = a.getValue()
)
}

/**
* Holds if `cls` is callable, i.e. it declares `__call__`.
*/
predicate isCallable(Class cls) { hasMethod(cls, "__call__") }

/**
* Holds if `cls` supports the mapping protocol, i.e. it declares
* `__getitem__` and `keys`, or `__getitem__` and `__iter__`.
*/
predicate isMapping(Class cls) {
hasMethod(cls, "__getitem__") and
(hasMethod(cls, "keys") or hasMethod(cls, "__iter__"))
}

/**
* Holds if `cls` is a new-style class. In Python 3, all classes are new-style.
* In Python 2, a class is new-style if it (transitively) inherits from `object`,
* or has a declared `__metaclass__`, or has an unresolved base class.
*/
predicate isNewStyle(Class cls) {
major_version() = 3
or
major_version() = 2 and
(
cls.getABase() = API::builtin("object").getAValueReachableFromSource().asExpr()
or
isNewStyle(getADirectSuperclass(cls))
or
hasUnresolvedBase(cls)
or
exists(cls.getMetaClass())
or
// Module-level __metaclass__ = type makes all classes in the module new-style
exists(Assign a |
a.getScope() = cls.getEnclosingModule() and
a.getATarget().(Name).getId() = "__metaclass__"
)
)
}

/**
* Gets the `__init__` function that will be invoked when `cls` is constructed,
* resolved according to the MRO.
*/
Function getInit(Class cls) { result = invokedFunctionFromClassConstruction(cls, "__init__") }

/**
* Holds if `cls` or any of its superclasses uses multiple inheritance, or
* has an unresolved base class. In these cases, our MRO approximation may
* resolve to the wrong `__init__`, so we should not flag argument mismatches.
*/
predicate hasUnreliableMro(Class cls) {
exists(Class sup | sup = getADirectSuperclass*(cls) |
exists(sup.getBase(1))
or
hasUnresolvedBase(sup)
)
}

/**
* Holds if `f` overrides a method in a superclass with the same name.
*/
predicate overridesMethod(Function f) {
exists(Class cls | f.getScope() = cls | hasMethod(getADirectSuperclass(cls), f.getName()))
}

/**
* Holds if `f` is a property accessor (decorated with `@property`, `@name.setter`,
* or `@name.deleter`).
*/
predicate isPropertyAccessor(Function f) {
exists(Attribute a | a = f.getADecorator() | a.getName() = "setter" or a.getName() = "deleter")
or
f.getADecorator().(Name).getId() = "property"
}
}
19 changes: 12 additions & 7 deletions python/ql/src/Classes/InconsistentMRO.ql
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,24 @@
*/

import python
private import LegacyPointsTo
private import semmle.python.dataflow.new.internal.DataFlowDispatch

ClassObject left_base(ClassObject type, ClassObject base) {
exists(int i | i > 0 and type.getBaseType(i) = base and result = type.getBaseType(i - 1))
/**
* Gets the `i`th base class of `cls`, if it can be resolved to a user-defined class.
*/
Class getBaseType(Class cls, int i) { cls.getBase(i) = classTracker(result).asExpr() }

Class left_base(Class type, Class base) {
exists(int i | i > 0 and getBaseType(type, i) = base and result = getBaseType(type, i - 1))
}

predicate invalid_mro(ClassObject t, ClassObject left, ClassObject right) {
t.isNewStyle() and
predicate invalid_mro(Class t, Class left, Class right) {
DuckTyping::isNewStyle(t) and
left = left_base(t, right) and
left = right.getAnImproperSuperType()
left = getADirectSuperclass*(right)
}

from ClassObject t, ClassObject left, ClassObject right
from Class t, Class left, Class right
where invalid_mro(t, left, right)
select t,
"Construction of class " + t.getName() +
Expand Down
9 changes: 6 additions & 3 deletions python/ql/src/Classes/PropertyInOldStyleClass.ql
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
*/

import python
private import LegacyPointsTo
private import semmle.python.dataflow.new.internal.DataFlowDispatch

from PropertyObject prop, ClassObject cls
where cls.declaredAttribute(_) = prop and not cls.failedInference() and not cls.isNewStyle()
from Function prop, Class cls
where
prop.getScope() = cls and
prop.getADecorator().(Name).getId() = "property" and
not DuckTyping::isNewStyle(cls)
select prop,
"Property " + prop.getName() + " will not work properly, as class " + cls.getName() +
" is an old-style class."
8 changes: 5 additions & 3 deletions python/ql/src/Classes/ShouldBeContextManager.ql
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
*/

import python
private import LegacyPointsTo
private import semmle.python.dataflow.new.internal.DataFlowDispatch

from ClassValue c
where not c.isBuiltin() and not c.isContextManager() and exists(c.declaredAttribute("__del__"))
from Class c
where
not DuckTyping::isContextManager(c) and
DuckTyping::hasMethod(c, "__del__")
select c,
"Class " + c.getName() +
" implements __del__ (presumably to release some resource). Consider making it a context manager."
8 changes: 5 additions & 3 deletions python/ql/src/Classes/SlotsInOldStyleClass.ql
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
*/

import python
private import LegacyPointsTo
private import semmle.python.dataflow.new.internal.DataFlowDispatch

from ClassObject c
where not c.isNewStyle() and c.declaresAttribute("__slots__") and not c.failedInference()
from Class c
where
not DuckTyping::isNewStyle(c) and
DuckTyping::declaresAttribute(c, "__slots__")
select c,
"Using '__slots__' in an old style class just creates a class attribute called '__slots__'."
9 changes: 4 additions & 5 deletions python/ql/src/Classes/SuperInOldStyleClass.ql
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,13 @@
*/

import python
private import LegacyPointsTo
private import semmle.python.dataflow.new.internal.DataFlowDispatch

predicate uses_of_super_in_old_style_class(Call s) {
exists(Function f, ClassObject c |
exists(Function f, Class c |
s.getScope() = f and
f.getScope() = c.getPyClass() and
not c.failedInference() and
not c.isNewStyle() and
f.getScope() = c and
not DuckTyping::isNewStyle(c) and
s.getFunc().(Name).getId() = "super"
)
}
Expand Down
11 changes: 3 additions & 8 deletions python/ql/src/Classes/UselessClass.ql
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/

import python
private import LegacyPointsTo
private import semmle.python.dataflow.new.internal.DataFlowDispatch

predicate fewer_than_two_public_methods(Class cls, int methods) {
(methods = 0 or methods = 1) and
Expand All @@ -25,13 +25,8 @@ predicate does_not_define_special_method(Class cls) {
}

predicate no_inheritance(Class c) {
not exists(ClassValue cls, ClassValue other |
cls.getScope() = c and
other != ClassValue::object()
|
other.getABaseType() = cls or
cls.getABaseType() = other
) and
not exists(getADirectSubclass(c)) and
not exists(getADirectSuperclass(c)) and
not exists(Expr base | base = c.getABase() |
not base instanceof Name or base.(Name).getId() != "object"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,35 @@
*/

import python
import Expressions.CallArgs
private import LegacyPointsTo
private import semmle.python.dataflow.new.internal.DataFlowDispatch

from Call call, ClassValue cls, string name, FunctionValue init
/**
* Holds if `name` is a legal argument name for calling `init`.
*/
bindingset[name]
predicate isLegalArgumentName(Function init, string name) {
exists(init.getArgByName(name))
or
init.hasKwArg()
}

/**
* Holds if `call` constructs class `cls` and passes a keyword argument `name`
* that does not correspond to any parameter of `cls.__init__`.
*/
predicate illegally_named_parameter(Call call, Class cls, string name) {
exists(Function init |
resolveClassCall(call.getAFlowNode(), cls) and
init = DuckTyping::getInit(cls) and
name = call.getANamedArgumentName() and
not isLegalArgumentName(init, name)
)
}

from Call call, Class cls, string name, Function init
where
illegally_named_parameter(call, cls, name) and
init = get_function_or_initializer(cls)
not DuckTyping::hasUnreliableMro(cls) and
init = DuckTyping::getInit(cls)
select call, "Keyword argument '" + name + "' is not a supported parameter name of $@.", init,
init.getQualifiedName()
Loading