Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
bea8502
Move missing/multiple calls to init/del queries to folder
joefarebrother Jun 30, 2025
38af3ac
Update missing call to init
joefarebrother Jun 30, 2025
a02016a
Add missing call to del
joefarebrother Jun 30, 2025
d0daacd
Modernize multple calls to init/del
joefarebrother Jul 1, 2025
4f63528
Update alert messages
joefarebrother Jul 1, 2025
45b5efa
Fix FPs and typo
joefarebrother Jul 1, 2025
732c818
Move tests and add inline expectation postprocessing
joefarebrother Jul 2, 2025
99a05ed
Update test outputs + fix semantics
joefarebrother Jul 2, 2025
3c74e12
Change implenetation of missing calls to use getASuperCallTarget, and…
joefarebrother Jul 3, 2025
9ac9526
Fixes
joefarebrother Jul 3, 2025
c9932e1
Update tests for calls to init + fixes
joefarebrother Jul 3, 2025
9619ae8
Add additional test case + update missing del tests
joefarebrother Jul 3, 2025
18b949c
Remove case excluding classes with a __new__ method; as it doesn't ma…
joefarebrother Jul 3, 2025
b4b20d7
Update multiple calls queries to include call targets in alert message
joefarebrother Jul 4, 2025
daa5525
Update tests and add an additional test
joefarebrother Jul 4, 2025
86bb0e8
qhelp: move examples to subfolder
joefarebrother Jul 4, 2025
ba86584
Update qhelp + alert messages
joefarebrother Jul 4, 2025
74a3127
Update integration test output
joefarebrother Jul 4, 2025
e01519f
Add change note
joefarebrother Jul 4, 2025
5992dc3
Add qldoc
joefarebrother Jul 4, 2025
58f2bd4
Fix changenote formatting
joefarebrother Jul 4, 2025
c9dc54a
Fix typos
joefarebrother Jul 4, 2025
72df584
Update integration test outout and fix qhelp
joefarebrother Jul 7, 2025
cc486dd
Remove tostring
joefarebrother Jul 7, 2025
fb0380b
Inline locationBefore
joefarebrother Jul 14, 2025
ba68fe9
Adress review suggestions - cleanups
joefarebrother Jul 14, 2025
8c9c66c
Fix typo in example
joefarebrother Jul 18, 2025
8545c7d
Fix doc typo
joefarebrother Jul 21, 2025
f709713
Rank multiple calls so only the first 2 calls are alerted
joefarebrother Sep 1, 2025
2dcf3c7
Remove erronous private
joefarebrother Sep 2, 2025
cd6a151
Add missing predicate + update test output
joefarebrother Sep 3, 2025
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
@@ -1,16 +1,16 @@
ql/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql
ql/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql
ql/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql
ql/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql
ql/python/ql/src/Classes/Comparisons/EqualsOrHash.ql
ql/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql
ql/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql
ql/python/ql/src/Classes/ConflictingAttributesInBaseClasses.ql
ql/python/ql/src/Classes/DefineEqualsWhenAddingAttributes.ql
ql/python/ql/src/Classes/InconsistentMRO.ql
ql/python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql
ql/python/ql/src/Classes/MissingCallToDel.ql
ql/python/ql/src/Classes/MissingCallToInit.ql
ql/python/ql/src/Classes/MutatingDescriptor.ql
ql/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql
ql/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.ql
ql/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.ql
ql/python/ql/src/Classes/WrongNameForArgumentInClassInstantiation.ql
ql/python/ql/src/Classes/WrongNumberArgumentsInClassInstantiation.ql
ql/python/ql/src/Exceptions/CatchingBaseException.ql
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
ql/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql
ql/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql
ql/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql
ql/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql
ql/python/ql/src/Classes/Comparisons/EqualsOrHash.ql
ql/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql
ql/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql
ql/python/ql/src/Classes/ConflictingAttributesInBaseClasses.ql
ql/python/ql/src/Classes/DefineEqualsWhenAddingAttributes.ql
ql/python/ql/src/Classes/InconsistentMRO.ql
ql/python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql
ql/python/ql/src/Classes/MissingCallToDel.ql
ql/python/ql/src/Classes/MissingCallToInit.ql
ql/python/ql/src/Classes/MutatingDescriptor.ql
ql/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql
ql/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.ql
ql/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.ql
ql/python/ql/src/Classes/WrongNameForArgumentInClassInstantiation.ql
ql/python/ql/src/Classes/WrongNumberArgumentsInClassInstantiation.ql
ql/python/ql/src/Exceptions/CatchingBaseException.ql
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
ql/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql
ql/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql
ql/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql
ql/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql
ql/python/ql/src/Classes/Comparisons/EqualsOrHash.ql
ql/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql
ql/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql
ql/python/ql/src/Classes/ConflictingAttributesInBaseClasses.ql
ql/python/ql/src/Classes/DefineEqualsWhenAddingAttributes.ql
ql/python/ql/src/Classes/InconsistentMRO.ql
ql/python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql
ql/python/ql/src/Classes/MissingCallToDel.ql
ql/python/ql/src/Classes/MissingCallToInit.ql
ql/python/ql/src/Classes/MutatingDescriptor.ql
ql/python/ql/src/Classes/OverwritingAttributeInSuperClass.ql
ql/python/ql/src/Classes/PropertyInOldStyleClass.ql
ql/python/ql/src/Classes/SlotsInOldStyleClass.ql
ql/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql
ql/python/ql/src/Classes/SuperInOldStyleClass.ql
ql/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.ql
ql/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.ql
ql/python/ql/src/Classes/WrongNameForArgumentInClassInstantiation.ql
ql/python/ql/src/Classes/WrongNumberArgumentsInClassInstantiation.ql
ql/python/ql/src/Diagnostics/ExtractedFiles.ql
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -856,9 +856,14 @@ Class getNextClassInMroKnownStartingClass(Class cls, Class startingClass) {
)
}

private Function findFunctionAccordingToMroKnownStartingClass(
Class cls, Class startingClass, string name
) {
/**
* Gets a potential definition of the function `name` of the class `cls` according to our approximation of
* MRO for the class `startingCls` (see `getNextClassInMroKnownStartingClass` for more information).
*
* Note: this is almost the same as `findFunctionAccordingToMro`, except we know the
* `startingClass`, which can give slightly more precise results.
*/
Function findFunctionAccordingToMroKnownStartingClass(Class cls, Class startingClass, string name) {
result = cls.getAMethod() and
result.getName() = name and
cls = getADirectSuperclass*(startingClass)
Expand All @@ -871,7 +876,7 @@ private Function findFunctionAccordingToMroKnownStartingClass(

/**
* Gets a potential definition of the function `name` according to our approximation of
* MRO for the class `cls` (see `getNextClassInMroKnownStartingClass` for more information).
* MRO for the class `startingCls` (see `getNextClassInMroKnownStartingClass` for more information).
*
* Note: this is almost the same as `findFunctionAccordingToMro`, except we know the
* `startingClass`, which can give slightly more precise results.
Expand Down
207 changes: 207 additions & 0 deletions python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll
Original file line number Diff line number Diff line change
@@ -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)
)
)
}
Comment on lines +77 to +86
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This MRO business is a bit awkward. I wonder if we could clean it up by creating a new IPA type representing "the MRO starting at a particular base class", and thus avoid having to thread mroBase through everything (which might mean we could use the built-in transitive closure operators).

This is just some musing on my part -- not necessarily an immediately actionable suggestion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could be a useful component of a public-facing dataflow dispatch / call graph resolution API


/** 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()
}
52 changes: 52 additions & 0 deletions python/ql/src/Classes/CallsToInitDel/MissingCallToDel.qhelp
Original file line number Diff line number Diff line change
@@ -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>
Loading