Merge pull request #19932 from joefarebrother/python-qual-init-del-calls

Python: Modernize 4 queries for missing/multiple calls to init/del methods
This commit is contained in:
Joe Farebrother
2025-09-08 09:29:38 +01:00
committed by GitHub
49 changed files with 956 additions and 608 deletions

View File

@@ -0,0 +1,207 @@
/** Definitions for reasoning about multiple or missing calls to superclass methods. */
import python
import semmle.python.ApiGraphs
import semmle.python.dataflow.new.internal.DataFlowDispatch
import codeql.util.Option
/** Holds if `meth` is a method named `name` that transitively calls `calledMulti` of the same name via the calls `call1` and `call2`. */
predicate multipleCallsToSuperclassMethod(
Function meth, Function calledMulti, DataFlow::MethodCallNode call1,
DataFlow::MethodCallNode call2, string name
) {
exists(Class cls |
meth.getName() = name and
meth.getScope() = cls and
locationBefore(call1.getLocation(), call2.getLocation()) and
rankedSuperCallByLocation(1, cls, meth, call1, name, calledMulti) and
rankedSuperCallByLocation(2, cls, meth, call2, name, calledMulti) and
nonTrivial(calledMulti)
)
}
predicate rankedSuperCallByLocation(
int i, Class mroBase, Function meth, DataFlow::MethodCallNode call, string name, Function target
) {
call =
rank[i](DataFlow::MethodCallNode calli |
target = getASuperCallTargetFromCall(mroBase, meth, calli, name)
|
calli order by calli.getLocation().getStartLine(), calli.getLocation().getStartColumn()
)
}
/** Holds if l1 comes before l2, assuming they're in the same file. */
pragma[inline]
private predicate locationBefore(Location l1, Location l2) {
l1.getStartLine() < l2.getStartLine()
or
l1.getStartLine() = l2.getStartLine() and
l1.getStartColumn() < l2.getStartColumn()
}
/** Gets a method transitively called by `meth` named `name` with `call` that it overrides, with `mroBase` as the type determining the MRO to search. */
Function getASuperCallTargetFromCall(
Class mroBase, Function meth, DataFlow::MethodCallNode call, string name
) {
exists(Function target | target = getDirectSuperCallTargetFromCall(mroBase, meth, call, name) |
result = target
or
result = getASuperCallTargetFromCall(mroBase, target, _, name)
)
}
/** Gets the method called by `meth` named `name` with `call`, with `mroBase` as the type determining the MRO to search. */
Function getDirectSuperCallTargetFromCall(
Class mroBase, Function meth, DataFlow::MethodCallNode call, string name
) {
meth = call.getScope() and
meth.getName() = name and
call.calls(_, name) and
mroBase = getADirectSubclass*(meth.getScope()) and
exists(Class targetCls |
// the differences between 0-arg and 2-arg super is not considered; we assume each super uses the mro of the instance `self`
superCall(call, _) and
targetCls = getNextClassInMroKnownStartingClass(meth.getScope(), mroBase) and
result = findFunctionAccordingToMroKnownStartingClass(targetCls, mroBase, name)
or
// targetCls is the mro base for this lookup.
// note however that if the call we find uses super(), that still uses the mro of the instance `self`
// assuming it's 0-arg or is 2-arg with `self` as second arg.
callsMethodOnClassWithSelf(meth, call, targetCls, _) and
result = findFunctionAccordingToMroKnownStartingClass(targetCls, targetCls, name)
)
}
/** Gets a method that is transitively called by a call to `cls.<name>`, with `mroBase` as the type determining the MRO to search. */
Function getASuperCallTargetFromClass(Class mroBase, Class cls, string name) {
exists(Function target |
target = findFunctionAccordingToMroKnownStartingClass(cls, mroBase, name) and
(
result = target
or
result = getASuperCallTargetFromCall(mroBase, target, _, name)
)
)
}
/** Holds if `meth` does something besides calling a superclass method. */
predicate nonTrivial(Function meth) {
exists(Stmt s | s = meth.getAStmt() |
not s instanceof Pass and
not exists(DataFlow::Node call | call.asExpr() = s.(ExprStmt).getValue() |
superCall(call, meth.getName())
or
callsMethodOnClassWithSelf(meth, call, _, meth.getName())
)
) and
exists(meth.getANormalExit()) // doesn't always raise an exception
}
/** Holds if `call` is a call to `super().<name>`. No distinction is made between 0- and 2- arg super calls. */
predicate superCall(DataFlow::MethodCallNode call, string name) {
exists(DataFlow::Node sup |
call.calls(sup, name) and
sup = API::builtin("super").getACall()
)
}
/** Holds if `meth` calls a `super()` method of the same name. */
predicate callsSuper(Function meth) {
exists(DataFlow::MethodCallNode call |
call.getScope() = meth and
superCall(call, meth.getName())
)
}
/** Holds if `meth` calls `target.<name>(self, ...)` with the call `call`. */
predicate callsMethodOnClassWithSelf(
Function meth, DataFlow::MethodCallNode call, Class target, string name
) {
exists(DataFlow::Node callTarget, DataFlow::ParameterNode self |
call.calls(callTarget, name) and
self.getParameter() = meth.getArg(0) and
self.(DataFlow::LocalSourceNode).flowsTo(call.getArg(0)) and
callTarget = classTracker(target)
)
}
/** Holds if `meth` calls a method named `name` passing its `self` argument as its first parameter, but the class it refers to is unknown. */
predicate callsMethodOnUnknownClassWithSelf(Function meth, string name) {
exists(DataFlow::MethodCallNode call, DataFlow::Node callTarget, DataFlow::ParameterNode self |
call.calls(callTarget, name) and
self.getParameter() = meth.getArg(0) and
self.(DataFlow::LocalSourceNode).flowsTo(call.getArg(0)) and
not callTarget = classTracker(any(Class target))
)
}
/** Holds if `base` does not call a superclass method `shouldCall` named `name` when it appears it should. */
predicate missingCallToSuperclassMethod(Class base, Function shouldCall, string name) {
shouldCall.getName() = name and
shouldCall.getScope() = getADirectSuperclass+(base) and
not shouldCall = getASuperCallTargetFromClass(base, base, name) and
nonTrivial(shouldCall) and
// "Benefit of the doubt" - if somewhere in the chain we call an unknown superclass, assume all the necessary parent methods are called from it
not callsMethodOnUnknownClassWithSelf(getASuperCallTargetFromClass(base, base, name), name)
}
/**
* Holds if `base` does not call a superclass method `shouldCall` named `name` when it appears it should.
* Results are restricted to hold only for the highest `base` class and the lowest `shouldCall` method in the hierarchy for which this applies.
*/
predicate missingCallToSuperclassMethodRestricted(Class base, Function shouldCall, string name) {
missingCallToSuperclassMethod(base, shouldCall, name) and
not exists(Class superBase |
// Alert only on the highest base class that has the issue
superBase = getADirectSuperclass+(base) and
missingCallToSuperclassMethod(superBase, shouldCall, name)
) and
not exists(Function subShouldCall |
// Mention in the alert only the lowest method we're missing the call to
subShouldCall.getScope() = getADirectSubclass+(shouldCall.getScope()) and
missingCallToSuperclassMethod(base, subShouldCall, name)
)
}
/**
* If `base` contains a `super()` call, gets a method in the inheritance hierarchy of `name` in the MRO of `base`
* that does not contain a `super()` call, but would call `shouldCall` if it did, which does not otherwise get called
* during a call to `base.<name>`.
*/
Function getPossibleMissingSuper(Class base, Function shouldCall, string name) {
missingCallToSuperclassMethod(base, shouldCall, name) and
exists(Function baseMethod |
baseMethod.getScope() = base and
baseMethod.getName() = name and
// the base method calls super, so is presumably expecting every method called in the MRO chain to do so
callsSuper(baseMethod) and
// result is something that does get called in the chain
result = getASuperCallTargetFromClass(base, base, name) and
// it doesn't call super
not callsSuper(result) and
// if it did call super, it would resolve to the missing method
shouldCall =
findFunctionAccordingToMroKnownStartingClass(getNextClassInMroKnownStartingClass(result
.getScope(), base), base, name)
)
}
/** An optional `Function`. */
class FunctionOption extends LocatableOption<Location, Function>::Option {
/** Gets the qualified name of this function, or the empty string if it is None. */
string getQualifiedName() {
this.isNone() and result = ""
or
result = this.asSome().getQualifiedName()
}
}
/** Gets the result of `getPossibleMissingSuper`, or None if none exists. */
bindingset[name]
FunctionOption getPossibleMissingSuperOption(Class base, Function shouldCall, string name) {
result.asSome() = getPossibleMissingSuper(base, shouldCall, name)
or
not exists(getPossibleMissingSuper(base, shouldCall, name)) and
result.isNone()
}

View File

@@ -0,0 +1,52 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Python, unlike some other object-oriented languages such as Java, allows the developer complete freedom in
when and how superclass finalizers are called during object finalization.
However, the developer has responsibility for ensuring that objects are properly cleaned up, and that all superclass <code>__del__</code>
methods are called.
</p>
<p>
Classes with a <code>__del__</code> method (a finalizer) typically hold some resource such as a file handle that needs to be cleaned up.
If the <code>__del__</code> method of a superclass is not called during object finalization, it is likely that
resources may be leaked.
</p>
<p>A call to the <code>__del__</code> method of a superclass during object initialization may be unintentionally skipped:
</p>
<ul>
<li>If a subclass calls the <code>__del__</code> method of the wrong class.</li>
<li>If a call to the <code>__del__</code> method of one its base classes is omitted.</li>
<li>If a call to <code>super().__del__</code> is used, but not all <code>__del__</code> methods in the Method Resolution Order (MRO)
chain themselves call <code>super()</code>. This in particular arises more often in cases of multiple inheritance. </li>
</ul>
</overview>
<recommendation>
<p>Ensure that all superclass <code>__del__</code> methods are properly called.
Either each base class's finalize method should be explicitly called, or <code>super()</code> calls
should be consistently used throughout the inheritance hierarchy.</p>
</recommendation>
<example>
<p>In the following example, explicit calls to <code>__del__</code> are used, but <code>SportsCar</code> erroneously calls
<code>Vehicle.__del__</code>. This is fixed in <code>FixedSportsCar</code> by calling <code>Car.__del__</code>.
</p>
<sample src="examples/MissingCallToDel.py" />
</example>
<references>
<li>Python Reference: <a href="https://docs.python.org/3/reference/datamodel.html#object.__del__">__del__</a>.</li>
<li>Python Standard Library: <a href="https://docs.python.org/3/library/functions.html#super">super</a>.</li>
<li>Python Glossary: <a href="https://docs.python.org/3/glossary.html#term-method-resolution-order">Method resolution order</a>.</li>
</references>
</qhelp>

View File

@@ -0,0 +1,49 @@
/**
* @name Missing call to superclass `__del__` during object destruction
* @description An omitted call to a superclass `__del__` method may lead to class instances not being cleaned up properly.
* @kind problem
* @tags quality
* reliability
* correctness
* performance
* @problem.severity error
* @sub-severity low
* @precision high
* @id py/missing-call-to-delete
*/
import python
import MethodCallOrder
Function getDelMethod(Class c) {
result = c.getAMethod() and
result.getName() = "__del__"
}
from Class base, Function shouldCall, FunctionOption possibleIssue, string msg
where
not exists(Function newMethod | newMethod = base.getAMethod() and newMethod.getName() = "__new__") and
exists(FunctionOption possiblyMissingSuper |
missingCallToSuperclassMethodRestricted(base, shouldCall, "__del__") and
possiblyMissingSuper = getPossibleMissingSuperOption(base, shouldCall, "__del__") and
(
not possiblyMissingSuper.isNone() and
possibleIssue = possiblyMissingSuper and
msg =
"This class does not call $@ during finalization. ($@ may be missing a call to super().__del__)"
or
possiblyMissingSuper.isNone() and
(
possibleIssue.asSome() = getDelMethod(base) and
msg =
"This class does not call $@ during finalization. ($@ may be missing a call to a base class __del__)"
or
not exists(getDelMethod(base)) and
possibleIssue.isNone() and
msg =
"This class does not call $@ during finalization. (The class lacks an __del__ method to ensure every base class __del__ is called.)"
)
)
)
select base, msg, shouldCall, shouldCall.getQualifiedName(), possibleIssue,
possibleIssue.getQualifiedName()

View File

@@ -0,0 +1,50 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>Python, unlike some other object-oriented languages such as Java, allows the developer complete freedom in
when and how superclass initializers are called during object initialization.
However, the developer has responsibility for ensuring that objects are properly initialized, and that all superclass <code>__init__</code>
methods are called.
</p>
<p>
If the <code>__init__</code> method of a superclass is not called during object initialization, this can lead to errors due to
the object not being fully initialized, such as having missing attributes.
</p>
<p>A call to the <code>__init__</code> method of a superclass during object initialization may be unintentionally skipped:
</p>
<ul>
<li>If a subclass calls the <code>__init__</code> method of the wrong class.</li>
<li>If a call to the <code>__init__</code> method of one its base classes is omitted.</li>
<li>If a call to <code>super().__init__</code> is used, but not all <code>__init__</code> methods in the Method Resolution Order (MRO)
chain themselves call <code>super()</code>. This in particular arises more often in cases of multiple inheritance. </li>
</ul>
</overview>
<recommendation>
<p>Ensure that all superclass <code>__init__</code> methods are properly called.
Either each base class's initialize method should be explicitly called, or <code>super()</code> calls
should be consistently used throughout the inheritance hierarchy.</p>
</recommendation>
<example>
<p>In the following example, explicit calls to <code>__init__</code> are used, but <code>SportsCar</code> erroneously calls
<code>Vehicle.__init__</code>. This is fixed in <code>FixedSportsCar</code> by calling <code>Car.__init__</code>.
</p>
<sample src="examples/MissingCallToInit.py" />
</example>
<references>
<li>Python Reference: <a href="https://docs.python.org/3/reference/datamodel.html#object.__init__">__init__</a>.</li>
<li>Python Standard Library: <a href="https://docs.python.org/3/library/functions.html#super">super</a>.</li>
<li>Python Glossary: <a href="https://docs.python.org/3/glossary.html#term-method-resolution-order">Method resolution order</a>.</li>
</references>
</qhelp>

View File

@@ -0,0 +1,41 @@
/**
* @name Missing call to superclass `__init__` during object initialization
* @description An omitted call to a superclass `__init__` method may lead to objects of this class not being fully initialized.
* @kind problem
* @tags quality
* reliability
* correctness
* @problem.severity error
* @sub-severity low
* @precision high
* @id py/missing-call-to-init
*/
import python
import MethodCallOrder
from Class base, Function shouldCall, FunctionOption possibleIssue, string msg
where
exists(FunctionOption possiblyMissingSuper |
missingCallToSuperclassMethodRestricted(base, shouldCall, "__init__") and
possiblyMissingSuper = getPossibleMissingSuperOption(base, shouldCall, "__init__") and
(
possibleIssue.asSome() = possiblyMissingSuper.asSome() and
msg =
"This class does not call $@ during initialization. ($@ may be missing a call to super().__init__)"
or
possiblyMissingSuper.isNone() and
(
possibleIssue.asSome() = base.getInitMethod() and
msg =
"This class does not call $@ during initialization. ($@ may be missing a call to a base class __init__)"
or
not exists(base.getInitMethod()) and
possibleIssue.isNone() and
msg =
"This class does not call $@ during initialization. (The class lacks an __init__ method to ensure every base class __init__ is called.)"
)
)
)
select base, msg, shouldCall, shouldCall.getQualifiedName(), possibleIssue,
possibleIssue.getQualifiedName()

View File

@@ -0,0 +1,55 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Python, unlike some other object-oriented languages such as Java, allows the developer complete freedom in
when and how superclass finalizers are called during object finalization.
However, the developer has responsibility for ensuring that objects are properly cleaned up.
</p>
<p>
Objects with a <code>__del__</code> method (a finalizer) often hold resources such as file handles that need to be cleaned up.
If a superclass finalizer is called multiple times, this may lead to errors such as closing an already closed file, and lead to objects not being
cleaned up properly as expected.
</p>
<p>There are a number of ways that a <code>__del__</code> method may be be called more than once.</p>
<ul>
<li>There may be more than one explicit call to the method in the hierarchy of <code>__del__</code> methods.</li>
<li>In situations involving multiple inheritance, an finalization method may call the finalizers of each of its base types,
which themselves both call the finalizer of a shared base type. (This is an example of the Diamond Inheritance problem)</li>
<li>Another situation involving multiple inheritance arises when a subclass calls the <code>__del__</code> methods of each of its base classes,
one of which calls <code>super().__del__</code>. This super call resolves to the next class in the Method Resolution Order (MRO) of the subclass,
which may be another base class that already has its initializer explicitly called.</li>
</ul>
</overview>
<recommendation>
<p>Ensure that each finalizer method is called exactly once during finalization.
This can be ensured by calling <code>super().__del__</code> for each finalizer method in the inheritance chain.
</p>
</recommendation>
<example>
<p>In the following example, there is a mixture of explicit calls to <code>__del__</code> and calls using <code>super()</code>, resulting in <code>Vehicle.__del__</code>
being called twice.
<code>FixedSportsCar.__del__</code> fixes this by using <code>super()</code> consistently with the other delete methods.
</p>
<sample src="examples/SuperclassDelCalledMultipleTimes.py" />
</example>
<references>
<li>Python Reference: <a href="https://docs.python.org/3/reference/datamodel.html#object.__del__">__del__</a>.</li>
<li>Python Standard Library: <a href="https://docs.python.org/3/library/functions.html#super">super</a>.</li>
<li>Python Glossary: <a href="https://docs.python.org/3/glossary.html#term-method-resolution-order">Method resolution order</a>.</li>
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem">The Diamond Problem</a>.</li>
</references>
</qhelp>

View File

@@ -0,0 +1,47 @@
/**
* @name Multiple calls to `__del__` during object destruction
* @description A duplicated call to a superclass `__del__` method may lead to class instances not be cleaned up properly.
* @kind problem
* @tags quality
* reliability
* correctness
* @problem.severity warning
* @sub-severity high
* @precision very-high
* @id py/multiple-calls-to-delete
*/
import python
import MethodCallOrder
predicate multipleCallsToSuperclassDel(
Function meth, Function calledMulti, DataFlow::MethodCallNode call1,
DataFlow::MethodCallNode call2
) {
multipleCallsToSuperclassMethod(meth, calledMulti, call1, call2, "__del__")
}
from
Function meth, Function calledMulti, DataFlow::MethodCallNode call1,
DataFlow::MethodCallNode call2, Function target1, Function target2, string msg
where
multipleCallsToSuperclassDel(meth, calledMulti, call1, call2) and
// Only alert for the lowest method in the hierarchy that both calls will call.
not exists(Function subMulti |
multipleCallsToSuperclassDel(meth, subMulti, _, _) and
calledMulti.getScope() = getADirectSuperclass+(subMulti.getScope())
) and
target1 = getDirectSuperCallTargetFromCall(meth.getScope(), meth, call1, _) and
target2 = getDirectSuperCallTargetFromCall(meth.getScope(), meth, call2, _) and
(
target1 != target2 and
msg =
"This finalization method calls $@ multiple times, via $@ and $@, resolving to $@ and $@ respectively."
or
target1 = target2 and
// The targets themselves are called multiple times (either is calledMulti, or something earlier in the MRO)
// Mentioning them again would be redundant.
msg = "This finalization method calls $@ multiple times, via $@ and $@."
)
select meth, msg, calledMulti, calledMulti.getQualifiedName(), call1, "this call", call2,
"this call", target1, target1.getQualifiedName(), target2, target2.getQualifiedName()

View File

@@ -0,0 +1,74 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>Python, unlike some other object-oriented languages such as Java, allows the developer complete freedom in
when and how superclass initializers are called during object initialization.
However, the developer has responsibility for ensuring that objects are properly initialized.
</p>
<p>
Calling an <code>__init__</code> method more than once during object initialization risks the object being incorrectly
initialized, as the method and the rest of the inheritance chain may not have been written with the expectation
that it could be called multiple times. For example, it may set attributes to a default value in a way that unexpectedly overwrites
values setting those attributes in a subclass.
</p>
<p>There are a number of ways that an <code>__init__</code> method may be be called more than once.</p>
<ul>
<li>There may be more than one explicit call to the method in the hierarchy of <code>__init__</code> methods.</li>
<li>In situations involving multiple inheritance, an initialization method may call the initializers of each of its base types,
which themselves both call the initializer of a shared base type. (This is an example of the Diamond Inheritance problem)</li>
<li>Another situation involving multiple inheritance arises when a subclass calls the <code>__init__</code> methods of each of its base classes,
one of which calls <code>super().__init__</code>. This super call resolves to the next class in the Method Resolution Order (MRO) of the subclass,
which may be another base class that already has its initializer explicitly called.</li>
</ul>
</overview>
<recommendation>
<p>
Take care whenever possible not to call an an initializer multiple times. If each <code>__init__</code> method in the hierarchy
calls <code>super().__init__()</code>, then each initializer will be called exactly once according to the MRO of the subclass.
When explicitly calling base class initializers (such as to pass different arguments to different initializers),
ensure this is done consistently throughout, rather than using <code>super()</code> calls in the base classes.
</p>
<p>
In some cases, it may not be possible to avoid calling a base initializer multiple times without significant refactoring.
In this case, carefully check that the initializer does not interfere with subclass initializers
when called multiple times (such as by overwriting attributes), and ensure this behavior is documented.
</p>
</recommendation>
<example>
<p>In the following (BAD) example, the class <code>D</code> calls <code>B.__init__</code> and <code>C.__init__</code>,
which each call <code>A.__init__</code>. This results in <code>self.state</code> being set to <code>None</code> as
<code>A.__init__</code> is called again after <code>B.__init__</code> had finished. This may lead to unexpected results.
</p>
<sample src="examples/SuperclassInitCalledMultipleTimesBad1.py" />
<p>In the following (GOOD) example, a call to <code>super().__init__</code> is made in each class
in the inheritance hierarchy, ensuring each initializer is called exactly once.
</p>
<sample src="examples/SuperclassInitCalledMultipleTimesGood2.py" />
<p>In the following (BAD) example, explicit base class calls are mixed with <code>super()</code> calls, and <code>C.__init__</code> is called twice.</p>
<sample src="examples/SuperclassInitCalledMultipleTimesBad3.py" />
</example>
<references>
<li>Python Reference: <a href="https://docs.python.org/3/reference/datamodel.html#object.__init__">__init__</a>.</li>
<li>Python Standard Library: <a href="https://docs.python.org/3/library/functions.html#super">super</a>.</li>
<li>Python Glossary: <a href="https://docs.python.org/3/glossary.html#term-method-resolution-order">Method resolution order</a>.</li>
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem">The Diamond Problem</a>.</li>
</references>
</qhelp>

View File

@@ -0,0 +1,47 @@
/**
* @name Multiple calls to `__init__` during object initialization
* @description A duplicated call to a superclass `__init__` method may lead to objects of this class not being properly initialized.
* @kind problem
* @tags quality
* reliability
* correctness
* @problem.severity warning
* @sub-severity high
* @precision very-high
* @id py/multiple-calls-to-init
*/
import python
import MethodCallOrder
predicate multipleCallsToSuperclassInit(
Function meth, Function calledMulti, DataFlow::MethodCallNode call1,
DataFlow::MethodCallNode call2
) {
multipleCallsToSuperclassMethod(meth, calledMulti, call1, call2, "__init__")
}
from
Function meth, Function calledMulti, DataFlow::MethodCallNode call1,
DataFlow::MethodCallNode call2, Function target1, Function target2, string msg
where
multipleCallsToSuperclassInit(meth, calledMulti, call1, call2) and
// Only alert for the lowest method in the hierarchy that both calls will call.
not exists(Function subMulti |
multipleCallsToSuperclassInit(meth, subMulti, _, _) and
calledMulti.getScope() = getADirectSuperclass+(subMulti.getScope())
) and
target1 = getDirectSuperCallTargetFromCall(meth.getScope(), meth, call1, _) and
target2 = getDirectSuperCallTargetFromCall(meth.getScope(), meth, call2, _) and
(
target1 != target2 and
msg =
"This initialization method calls $@ multiple times, via $@ and $@, resolving to $@ and $@ respectively."
or
target1 = target2 and
// The targets themselves are called multiple times (either is calledMulti, or something earlier in the MRO)
// Mentioning them again would be redundant.
msg = "This initialization method calls $@ multiple times, via $@ and $@."
)
select meth, msg, calledMulti, calledMulti.getQualifiedName(), call1, "this call", call2,
"this call", target1, target1.getQualifiedName(), target2, target2.getQualifiedName()

View File

@@ -10,14 +10,14 @@ class Car(Vehicle):
recycle(self.car_parts)
Vehicle.__del__(self)
#Car.__del__ is missed out.
#BAD: Car.__del__ is not called.
class SportsCar(Car, Vehicle):
def __del__(self):
recycle(self.sports_car_parts)
Vehicle.__del__(self)
#Fix SportsCar by calling Car.__del__
#GOOD: Car.__del__ is called correctly.
class FixedSportsCar(Car, Vehicle):
def __del__(self):

View File

@@ -10,14 +10,14 @@ class Car(Vehicle):
Vehicle.__init__(self)
self.car_init()
#Car.__init__ is missed out.
# BAD: Car.__init__ is not called.
class SportsCar(Car, Vehicle):
def __init__(self):
Vehicle.__init__(self)
self.sports_car_init()
#Fix SportsCar by calling Car.__init__
# GOOD: Car.__init__ is called correctly.
class FixedSportsCar(Car, Vehicle):
def __init__(self):

View File

@@ -15,14 +15,14 @@ class Car(Vehicle):
class SportsCar(Car, Vehicle):
# Vehicle.__del__ will get called twice
# BAD: Vehicle.__del__ will get called twice
def __del__(self):
recycle(self.sports_car_parts)
Car.__del__(self)
Vehicle.__del__(self)
#Fix SportsCar by using super()
# GOOD: super() is used ensuring each del method is called once.
class FixedSportsCar(Car, Vehicle):
def __del__(self):

View File

@@ -0,0 +1,20 @@
class A:
def __init__(self):
self.state = None
class B(A):
def __init__(self):
A.__init__(self)
self.state = "B"
self.b = 3
class C(A):
def __init__(self):
A.__init__(self)
self.c = 2
class D(B,C):
def __init__(self):
B.__init__(self)
C.__init__(self) # BAD: This calls A.__init__ a second time, setting self.state to None.

View File

@@ -0,0 +1,22 @@
class A:
def __init__(self):
print("A")
self.state = None
class B(A):
def __init__(self):
print("B")
super().__init__() # When called from D, this calls C.__init__
self.state = "B"
self.b = 3
class C(A):
def __init__(self):
print("C")
super().__init__()
self.c = 2
class D(B,C):
def __init__(self):
B.__init__(self)
C.__init__(self) # BAD: C.__init__ is called a second time

View File

@@ -0,0 +1,22 @@
class A:
def __init__(self):
self.state = None
class B(A):
def __init__(self):
super().__init__()
self.state = "B"
self.b = 3
class C(A):
def __init__(self):
super().__init__()
self.c = 2
class D(B,C):
def __init__(self): # GOOD: Each method calls super, so each init method runs once. self.state will be set to "B".
super().__init__()
self.d = 1

View File

@@ -1,3 +1,5 @@
deprecated module;
import python
// Helper predicates for multiple call to __init__/__del__ queries.

View File

@@ -1,50 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>Python, unlike statically typed languages such as Java, allows complete freedom when calling methods during object destruction.
However, standard object-oriented principles apply to Python classes using deep inheritance hierarchies.
Therefore the developer has responsibility for ensuring that objects are properly cleaned up when
there are multiple <code>__del__</code> methods that need to be called.
</p>
<p>
If the <code>__del__</code> method of a superclass is not called during object destruction it is likely that
that resources may be leaked.
</p>
<p>A call to the <code>__del__</code> method of a superclass during object destruction may be omitted:
</p>
<ul>
<li>When a subclass calls the <code>__del__</code> method of the wrong class.</li>
<li>When a call to the <code>__del__</code> method of one its base classes is omitted.</li>
</ul>
</overview>
<recommendation>
<p>Either be careful to explicitly call the <code>__del__</code> of the correct base class, or
use <code>super()</code> throughout the inheritance hierarchy.</p>
<p>Alternatively refactor one or more of the classes to use composition rather than inheritance.</p>
</recommendation>
<example>
<p>In this example, explicit calls to <code>__del__</code> are used, but <code>SportsCar</code> erroneously calls
<code>Vehicle.__del__</code>. This is fixed in <code>FixedSportsCar</code> by calling <code>Car.__del__</code>.
</p>
<sample src="MissingCallToDel.py" />
</example>
<references>
<li>Python Tutorial: <a href="https://docs.python.org/2/tutorial/classes.html">Classes</a>.</li>
<li>Python Standard Library: <a href="https://docs.python.org/2/library/functions.html#super">super</a>.</li>
<li>Artima Developer: <a href="http://www.artima.com/weblogs/viewpost.jsp?thread=236275">Things to Know About Python Super</a>.</li>
<li>Wikipedia: <a href="http://en.wikipedia.org/wiki/Composition_over_inheritance">Composition over inheritance</a>.</li>
</references>
</qhelp>

View File

@@ -1,26 +0,0 @@
/**
* @name Missing call to `__del__` during object destruction
* @description An omitted call to a super-class `__del__` method may lead to class instances not being cleaned up properly.
* @kind problem
* @tags quality
* reliability
* correctness
* performance
* @problem.severity error
* @sub-severity low
* @precision high
* @id py/missing-call-to-delete
*/
import python
import MethodCallOrder
from ClassObject self, FunctionObject missing
where
missing_call_to_superclass_method(self, _, missing, "__del__") and
not missing.neverReturns() and
not self.failedInference() and
not missing.isBuiltin()
select self,
"Class " + self.getName() + " may not be cleaned up properly as $@ is not called during deletion.",
missing, missing.descriptiveString()

View File

@@ -1,52 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>Python, unlike statically typed languages such as Java, allows complete freedom when calling methods during object initialization.
However, standard object-oriented principles apply to Python classes using deep inheritance hierarchies.
Therefore the developer has responsibility for ensuring that objects are properly initialized when
there are multiple <code>__init__</code> methods that need to be called.
</p>
<p>
If the <code>__init__</code> method of a superclass is not called during object initialization it is likely that
that object will end up in an incorrect state.
</p>
<p>A call to the <code>__init__</code> method of a superclass during object initialization may be omitted:
</p>
<ul>
<li>When a subclass calls the <code>__init__</code> method of the wrong class.</li>
<li>When a call to the <code>__init__</code> method of one its base classes is omitted.</li>
<li>When multiple inheritance is used and a class inherits from several base classes,
and at least one of those does not use <code>super()</code> in its own <code>__init__</code> method.</li>
</ul>
</overview>
<recommendation>
<p>Either be careful to explicitly call the <code>__init__</code> of the correct base class, or
use <code>super()</code> throughout the inheritance hierarchy.</p>
<p>Alternatively refactor one or more of the classes to use composition rather than inheritance.</p>
</recommendation>
<example>
<p>In this example, explicit calls to <code>__init__</code> are used, but <code>SportsCar</code> erroneously calls
<code>Vehicle.__init__</code>. This is fixed in <code>FixedSportsCar</code> by calling <code>Car.__init__</code>.
</p>
<sample src="MissingCallToInit.py" />
</example>
<references>
<li>Python Tutorial: <a href="https://docs.python.org/2/tutorial/classes.html">Classes</a>.</li>
<li>Python Standard Library: <a href="https://docs.python.org/2/library/functions.html#super">super</a>.</li>
<li>Artima Developer: <a href="http://www.artima.com/weblogs/viewpost.jsp?thread=236275">Things to Know About Python Super</a>.</li>
<li>Wikipedia: <a href="http://en.wikipedia.org/wiki/Composition_over_inheritance">Composition over inheritance</a>.</li>
</references>
</qhelp>

View File

@@ -1,29 +0,0 @@
/**
* @name Missing call to `__init__` during object initialization
* @description An omitted call to a super-class `__init__` method may lead to objects of this class not being fully initialized.
* @kind problem
* @tags quality
* reliability
* correctness
* @problem.severity error
* @sub-severity low
* @precision high
* @id py/missing-call-to-init
*/
import python
import MethodCallOrder
from ClassObject self, FunctionObject initializer, FunctionObject missing
where
self.lookupAttribute("__init__") = initializer and
missing_call_to_superclass_method(self, initializer, missing, "__init__") and
// If a superclass is incorrect, don't flag this class as well.
not missing_call_to_superclass_method(self.getASuperType(), _, missing, "__init__") and
not missing.neverReturns() and
not self.failedInference() and
not missing.isBuiltin() and
not self.isAbstract()
select self,
"Class " + self.getName() + " may not be initialized properly as $@ is not called from its $@.",
missing, missing.descriptiveString(), initializer, "__init__ method"

View File

@@ -1,29 +0,0 @@
#Calling a method multiple times by using explicit calls when a base inherits from other base
class Vehicle(object):
def __del__(self):
recycle(self.base_parts)
class Car(Vehicle):
def __del__(self):
recycle(self.car_parts)
Vehicle.__del__(self)
class SportsCar(Car, Vehicle):
# Vehicle.__del__ will get called twice
def __del__(self):
recycle(self.sports_car_parts)
Car.__del__(self)
Vehicle.__del__(self)
#Fix SportsCar by only calling Car.__del__
class FixedSportsCar(Car, Vehicle):
def __del__(self):
recycle(self.sports_car_parts)
Car.__del__(self)

View File

@@ -1,58 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>Python, unlike statically typed languages such as Java, allows complete freedom when calling methods during object destruction.
However, standard object-oriented principles apply to Python classes using deep inheritance hierarchies.
Therefore the developer has responsibility for ensuring that objects are properly cleaned up when
there are multiple <code>__del__</code> methods that need to be called.
</p>
<p>
Calling a <code>__del__</code> method more than once during object destruction risks resources being released multiple
times. The relevant <code>__del__</code> method may not be designed to be called more than once.
</p>
<p>There are a number of ways that a <code>__del__</code> method may be be called more than once.</p>
<ul>
<li>There may be more than one explicit call to the method in the hierarchy of <code>__del__</code> methods.</li>
<li>A class using multiple inheritance directly calls the <code>__del__</code> methods of its base types.
One or more of those base types uses <code>super()</code> to pass down the inheritance chain.</li>
</ul>
</overview>
<recommendation>
<p>Either be careful not to explicitly call a <code>__del__</code> method more than once, or
use <code>super()</code> throughout the inheritance hierarchy.</p>
<p>Alternatively refactor one or more of the classes to use composition rather than inheritance.</p>
</recommendation>
<example>
<p>In the first example, explicit calls to <code>__del__</code> are used, but <code>SportsCar</code> erroneously calls
both <code>Vehicle.__del__</code> and <code>Car.__del__</code>.
This can be fixed by removing the call to <code>Vehicle.__del__</code>, as shown in <code>FixedSportsCar</code>.
</p>
<sample src="SuperclassDelCalledMultipleTimes.py" />
<p>In the second example, there is a mixture of explicit calls to <code>__del__</code> and calls using <code>super()</code>.
To fix this example, <code>super()</code> should be used throughout.
</p>
<sample src="SuperclassInitCalledMultipleTimes2.py" />
</example>
<references>
<li>Python Tutorial: <a href="https://docs.python.org/2/tutorial/classes.html">Classes</a>.</li>
<li>Python Standard Library: <a href="https://docs.python.org/2/library/functions.html#super">super</a>.</li>
<li>Artima Developer: <a href="http://www.artima.com/weblogs/viewpost.jsp?thread=236275">Things to Know About Python Super</a>.</li>
<li>Wikipedia: <a href="http://en.wikipedia.org/wiki/Composition_over_inheritance">Composition over inheritance</a>.</li>
</references>
</qhelp>

View File

@@ -1,29 +0,0 @@
/**
* @name Multiple calls to `__del__` during object destruction
* @description A duplicated call to a super-class `__del__` method may lead to class instances not be cleaned up properly.
* @kind problem
* @tags quality
* reliability
* correctness
* @problem.severity warning
* @sub-severity high
* @precision very-high
* @id py/multiple-calls-to-delete
*/
import python
import MethodCallOrder
from ClassObject self, FunctionObject multi
where
multiple_calls_to_superclass_method(self, multi, "__del__") and
not multiple_calls_to_superclass_method(self.getABaseType(), multi, "__del__") and
not exists(FunctionObject better |
multiple_calls_to_superclass_method(self, better, "__del__") and
better.overrides(multi)
) and
not self.failedInference()
select self,
"Class " + self.getName() +
" may not be cleaned up properly as $@ may be called multiple times during destruction.", multi,
multi.descriptiveString()

View File

@@ -1,36 +0,0 @@
#Calling a method multiple times by using explicit calls when a base inherits from other base
class Vehicle(object):
def __init__(self):
self.mobile = True
class Car(Vehicle):
def __init__(self):
Vehicle.__init__(self)
self.car_init()
def car_init(self):
pass
class SportsCar(Car, Vehicle):
# Vehicle.__init__ will get called twice
def __init__(self):
Vehicle.__init__(self)
Car.__init__(self)
self.sports_car_init()
def sports_car_init(self):
pass
#Fix SportsCar by only calling Car.__init__
class FixedSportsCar(Car, Vehicle):
def __init__(self):
Car.__init__(self)
self.sports_car_init()
def sports_car_init(self):
pass

View File

@@ -1,58 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>Python, unlike statically typed languages such as Java, allows complete freedom when calling methods during object initialization.
However, standard object-oriented principles apply to Python classes using deep inheritance hierarchies.
Therefore the developer has responsibility for ensuring that objects are properly initialized when
there are multiple <code>__init__</code> methods that need to be called.
</p>
<p>
Calling an <code>__init__</code> method more than once during object initialization risks the object being incorrectly initialized.
It is unlikely that the relevant <code>__init__</code> method is designed to be called more than once.
</p>
<p>There are a number of ways that an <code>__init__</code> method may be be called more than once.</p>
<ul>
<li>There may be more than one explicit call to the method in the hierarchy of <code>__init__</code> methods.</li>
<li>A class using multiple inheritance directly calls the <code>__init__</code> methods of its base types.
One or more of those base types uses <code>super()</code> to pass down the inheritance chain.</li>
</ul>
</overview>
<recommendation>
<p>Either be careful not to explicitly call an <code>__init__</code> method more than once, or
use <code>super()</code> throughout the inheritance hierarchy.</p>
<p>Alternatively refactor one or more of the classes to use composition rather than inheritance.</p>
</recommendation>
<example>
<p>In the first example, explicit calls to <code>__init__</code> are used, but <code>SportsCar</code> erroneously calls
both <code>Vehicle.__init__</code> and <code>Car.__init__</code>.
This can be fixed by removing the call to <code>Vehicle.__init__</code>, as shown in <code>FixedSportsCar</code>.
</p>
<sample src="SuperclassInitCalledMultipleTimes.py" />
<p>In the second example, there is a mixture of explicit calls to <code>__init__</code> and calls using <code>super()</code>.
To fix this example, <code>super()</code> should be used throughout.
</p>
<sample src="SuperclassInitCalledMultipleTimes2.py" />
</example>
<references>
<li>Python Tutorial: <a href="https://docs.python.org/2/tutorial/classes.html">Classes</a>.</li>
<li>Python Standard Library: <a href="https://docs.python.org/2/library/functions.html#super">super</a>.</li>
<li>Artima Developer: <a href="http://www.artima.com/weblogs/viewpost.jsp?thread=236275">Things to Know About Python Super</a>.</li>
<li>Wikipedia: <a href="http://en.wikipedia.org/wiki/Composition_over_inheritance">Composition over inheritance</a>.</li>
</references>
</qhelp>

View File

@@ -1,30 +0,0 @@
/**
* @name Multiple calls to `__init__` during object initialization
* @description A duplicated call to a super-class `__init__` method may lead to objects of this class not being properly initialized.
* @kind problem
* @tags quality
* reliability
* correctness
* @problem.severity warning
* @sub-severity high
* @precision very-high
* @id py/multiple-calls-to-init
*/
import python
import MethodCallOrder
from ClassObject self, FunctionObject multi
where
multi != theObjectType().lookupAttribute("__init__") and
multiple_calls_to_superclass_method(self, multi, "__init__") and
not multiple_calls_to_superclass_method(self.getABaseType(), multi, "__init__") and
not exists(FunctionObject better |
multiple_calls_to_superclass_method(self, better, "__init__") and
better.overrides(multi)
) and
not self.failedInference()
select self,
"Class " + self.getName() +
" may not be initialized properly as $@ may be called multiple times during initialization.",
multi, multi.descriptiveString()

View File

@@ -1,38 +0,0 @@
#Calling a method multiple times by using explicit calls when a base uses super()
class Vehicle(object):
def __init__(self):
super(Vehicle, self).__init__()
self.mobile = True
class Car(Vehicle):
def __init__(self):
super(Car, self).__init__()
self.car_init()
def car_init(self):
pass
class SportsCar(Car, Vehicle):
# Vehicle.__init__ will get called twice
def __init__(self):
Vehicle.__init__(self)
Car.__init__(self)
self.sports_car_init()
def sports_car_init(self):
pass
#Fix SportsCar by using super()
class FixedSportsCar(Car, Vehicle):
def __init__(self):
super(SportsCar, self).__init__()
self.sports_car_init()
def sports_car_init(self):
pass

View File

@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* The queries `py/missing-call-to-init`, `py/missing-calls-to-del`, `py/multiple-calls-to-init`, and `py/multiple-calls-to-del` queries have been modernized; no longer relying on outdated libraries, producing more precise results with more descriptive alert messages, and improved documentation.