Change implenetation of missing calls to use getASuperCallTarget, and change alerts to alert on the class and provide clearer information, using optional location links.

This commit is contained in:
Joe Farebrother
2025-07-03 14:22:04 +01:00
parent 99a05ed5a4
commit 3c74e12b9c
3 changed files with 146 additions and 65 deletions

View File

@@ -3,32 +3,38 @@
import python
import semmle.python.ApiGraphs
import semmle.python.dataflow.new.internal.DataFlowDispatch
import codeql.util.Option
predicate multipleCallsToSuperclassMethod(Function meth, Function calledMulti, string name) {
exists(DataFlow::MethodCallNode call1, DataFlow::MethodCallNode call2, Class cls |
meth.getName() = name and
meth.getScope() = cls and
call1.asExpr() != call2.asExpr() and
calledMulti = getASuperCallTarget(cls, meth, call1) and
calledMulti = getASuperCallTarget(cls, meth, call2) and
calledMulti = getASuperCallTargetFromCall(cls, meth, call1, name) and
calledMulti = getASuperCallTargetFromCall(cls, meth, call2, name) and
nonTrivial(calledMulti)
)
}
Function getASuperCallTarget(Class mroBase, Function meth, DataFlow::MethodCallNode call) {
Function getASuperCallTargetFromCall(
Class mroBase, Function meth, DataFlow::MethodCallNode call, string name
) {
meth = call.getScope() and
getADirectSuperclass*(mroBase) = meth.getScope() and
call.calls(_, meth.getName()) and
exists(Function target | (result = target or result = getASuperCallTarget(mroBase, target, _)) |
meth.getName() = name and
call.calls(_, name) and
exists(Class targetCls | result = getASuperCallTargetFromClass(mroBase, targetCls, name) |
superCall(call, _) and
target =
findFunctionAccordingToMroKnownStartingClass(getNextClassInMroKnownStartingClass(meth.getScope(),
mroBase), mroBase, meth.getName())
targetCls = getNextClassInMroKnownStartingClass(meth.getScope(), mroBase)
or
exists(Class called |
callsMethodOnClassWithSelf(meth, call, called, _) and
target = findFunctionAccordingToMroKnownStartingClass(called, mroBase, meth.getName())
)
callsMethodOnClassWithSelf(meth, call, targetCls, _)
)
}
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))
)
}
@@ -78,31 +84,83 @@ predicate callsMethodOnUnknownClassWithSelf(Function meth, string name) {
)
}
predicate mayProceedInMro(Class a, Class b, Class mroStart) {
b = getNextClassInMroKnownStartingClass(a, mroStart)
or
exists(Class mid |
mid = getNextClassInMroKnownStartingClass(a, mroStart) and
mayProceedInMro(mid, b, mroStart)
predicate missingCallToSuperclassMethod(Class base, Function shouldCall, string name) {
base.getName() = name and
shouldCall.getName() = name and
base = getADirectSuperclass*(base.getScope()) 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)
}
predicate missingCallToSuperclassMethodRestricted(Class base, Function shouldCall, string name) {
missingCallToSuperclassMethod(base, shouldCall, name) and
not exists(Class subBase |
subBase = getADirectSubclass+(base) and
missingCallToSuperclassMethod(subBase, shouldCall, name)
) and
not exists(Function superShouldCall |
superShouldCall.getScope() = getADirectSuperclass+(shouldCall.getScope()) and
missingCallToSuperclassMethod(base, superShouldCall, name)
)
}
predicate missingCallToSuperclassMethod(
Function base, Function shouldCall, Class mroStart, string name
) {
base.getName() = name and
shouldCall.getName() = name and
not callsSuper(base) and
not callsMethodOnUnknownClassWithSelf(base, name) and
nonTrivial(shouldCall) and
base.getScope() = getADirectSuperclass*(mroStart) and
mayProceedInMro(base.getScope(), shouldCall.getScope(), mroStart) and
not exists(Class called |
(
callsMethodOnClassWithSelf(base, _, called, name)
or
callsMethodOnClassWithSelf(findFunctionAccordingToMro(mroStart, name), _, called, name)
) and
shouldCall.getScope() = getADirectSuperclass*(called)
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)
)
}
private module FunctionOption = Option<Function>;
class FunctionOption extends FunctionOption::Option {
/**
* Holds if this element is at the specified location.
* The location spans column `startcolumn` of line `startline` to
* column `endcolumn` of line `endline` in file `filepath`.
* For more information, see
* [Locations](https://codeql.github.com/docs/writing-codeql-queries/providing-locations-in-codeql-queries/).
*/
predicate hasLocationInfo(
string filepath, int startline, int startcolumn, int endline, int endcolumn
) {
this.asSome()
.getLocation()
.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
or
this.isNone() and
filepath = "" and
startline = 0 and
startcolumn = 0 and
endline = 0 and
endcolumn = 0
}
string getQualifiedName() {
result = this.asSome().getQualifiedName()
or
this.isNone() and
result = ""
}
}
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

@@ -15,21 +15,35 @@
import python
import MethodCallOrder
predicate missingCallToSuperclassDel(Function base, Function shouldCall, Class mroStart) {
missingCallToSuperclassMethod(base, shouldCall, mroStart, "__del__")
Function getDelMethod(Class c) {
result = c.getAMethod() and
result.getName() = "__del__"
}
from Function base, Function shouldCall, Class mroStart, string msg
from Class base, Function shouldCall, FunctionOption possibleIssue, string msg
where
missingCallToSuperclassDel(base, shouldCall, mroStart) and
(
// Simple case: the method that should be called is directly overridden
mroStart = base.getScope() and
msg = "This delete method does not call $@, which may leave $@ not properly cleaned up."
or
// Only alert for a different mro base if there are no alerts for direct overrides
not missingCallToSuperclassDel(base, _, base.getScope()) and
msg =
"This delete method does not call super().__del__, which may cause $@ to be missed during the cleanup of $@."
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 destruction. ($@ may be missing a call to super().__del__)"
or
possiblyMissingSuper.isNone() and
(
possibleIssue.asSome() = getDelMethod(base) and
msg =
"This class does not call $@ during destruction. ($@ may be missing a call to a base class __del__)"
or
not getDelMethod(base) and
possibleIssue.isNone() and
msg =
"This class does not call $@ during destruction. (The class lacks an __del__ method to ensure every base class __del__ is called.)"
)
)
)
select base, msg, shouldCall, shouldCall.getQualifiedName(), mroStart, mroStart.getName()
select base, msg, shouldCall, shouldCall.getQualifiedName(), possibleIssue,
possibleIssue.getQualifiedName()

View File

@@ -14,21 +14,30 @@
import python
import MethodCallOrder
predicate missingCallToSuperclassInit(Function base, Function shouldCall, Class mroStart) {
missingCallToSuperclassMethod(base, shouldCall, mroStart, "__init__")
}
from Function base, Function shouldCall, Class mroStart, string msg
from Class base, Function shouldCall, FunctionOption possibleIssue, string msg
where
missingCallToSuperclassInit(base, shouldCall, mroStart) and
(
// Simple case: the method that should be called is directly overridden
mroStart = base.getScope() and
msg = "This initialization method does not call $@, which may leave $@ partially initialized."
or
// Only alert for a different mro base if there are no alerts for direct overrides
not missingCallToSuperclassInit(base, _, base.getScope()) and
msg =
"This initialization method does not call super().__init__, which may cause $@ to be missed during the initialization of $@."
not exists(Function newMethod | newMethod = base.getAMethod() and newMethod.getName() = "__new__") and
exists(FunctionOption possiblyMissingSuper |
missingCallToSuperclassMethodRestricted(base, shouldCall, "__init__") and
possiblyMissingSuper = getPossibleMissingSuperOption(base, shouldCall, "__init__") and
(
not possiblyMissingSuper.isNone() and
possibleIssue = possiblyMissingSuper 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(), mroStart, mroStart.getName()
select base, msg, shouldCall, shouldCall.getQualifiedName(), possibleIssue,
possibleIssue.getQualifiedName()