Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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 @@ -1956,3 +1956,161 @@ 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 `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,34 @@
*/

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)
init = DuckTyping::getInit(cls)
select call, "Keyword argument '" + name + "' is not a supported parameter name of $@.", init,
init.getQualifiedName()
Loading