From 271f32e80eb28884d6c2759a7581d4f725d12be0 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Mon, 30 Jun 2025 10:21:31 +0100 Subject: [PATCH 01/22] Move missing/multiple calls to init/del queries to folder --- .../CallsToInitDel/MethodCallOrder.qll | 77 +++++++++++++++++++ .../{ => CallsToInitDel}/MissingCallToDel.py | 0 .../MissingCallToDel.qhelp | 0 .../{ => CallsToInitDel}/MissingCallToDel.ql | 0 .../{ => CallsToInitDel}/MissingCallToInit.py | 0 .../MissingCallToInit.qhelp | 0 .../{ => CallsToInitDel}/MissingCallToInit.ql | 0 .../SuperclassDelCalledMultipleTimes.py | 0 .../SuperclassDelCalledMultipleTimes.qhelp | 0 .../SuperclassDelCalledMultipleTimes.ql | 0 .../SuperclassDelCalledMultipleTimes2.py | 0 .../SuperclassInitCalledMultipleTimes.py | 0 .../SuperclassInitCalledMultipleTimes.qhelp | 0 .../SuperclassInitCalledMultipleTimes.ql | 0 .../SuperclassInitCalledMultipleTimes2.py | 0 python/ql/src/Classes/MethodCallOrder.qll | 2 + 16 files changed, 79 insertions(+) create mode 100644 python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll rename python/ql/src/Classes/{ => CallsToInitDel}/MissingCallToDel.py (100%) rename python/ql/src/Classes/{ => CallsToInitDel}/MissingCallToDel.qhelp (100%) rename python/ql/src/Classes/{ => CallsToInitDel}/MissingCallToDel.ql (100%) rename python/ql/src/Classes/{ => CallsToInitDel}/MissingCallToInit.py (100%) rename python/ql/src/Classes/{ => CallsToInitDel}/MissingCallToInit.qhelp (100%) rename python/ql/src/Classes/{ => CallsToInitDel}/MissingCallToInit.ql (100%) rename python/ql/src/Classes/{ => CallsToInitDel}/SuperclassDelCalledMultipleTimes.py (100%) rename python/ql/src/Classes/{ => CallsToInitDel}/SuperclassDelCalledMultipleTimes.qhelp (100%) rename python/ql/src/Classes/{ => CallsToInitDel}/SuperclassDelCalledMultipleTimes.ql (100%) rename python/ql/src/Classes/{ => CallsToInitDel}/SuperclassDelCalledMultipleTimes2.py (100%) rename python/ql/src/Classes/{ => CallsToInitDel}/SuperclassInitCalledMultipleTimes.py (100%) rename python/ql/src/Classes/{ => CallsToInitDel}/SuperclassInitCalledMultipleTimes.qhelp (100%) rename python/ql/src/Classes/{ => CallsToInitDel}/SuperclassInitCalledMultipleTimes.ql (100%) rename python/ql/src/Classes/{ => CallsToInitDel}/SuperclassInitCalledMultipleTimes2.py (100%) diff --git a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll new file mode 100644 index 000000000000..b911ee0183e1 --- /dev/null +++ b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll @@ -0,0 +1,77 @@ +deprecated module; + +import python + +// Helper predicates for multiple call to __init__/__del__ queries. +pragma[noinline] +private predicate multiple_invocation_paths_helper( + FunctionInvocation top, FunctionInvocation i1, FunctionInvocation i2, FunctionObject multi +) { + i1 != i2 and + i1 = top.getACallee+() and + i2 = top.getACallee+() and + i1.getFunction() = multi +} + +pragma[noinline] +private predicate multiple_invocation_paths( + FunctionInvocation top, FunctionInvocation i1, FunctionInvocation i2, FunctionObject multi +) { + multiple_invocation_paths_helper(top, i1, i2, multi) and + i2.getFunction() = multi +} + +/** Holds if `self.name` calls `multi` by multiple paths, and thus calls it more than once. */ +predicate multiple_calls_to_superclass_method(ClassObject self, FunctionObject multi, string name) { + exists(FunctionInvocation top, FunctionInvocation i1, FunctionInvocation i2 | + multiple_invocation_paths(top, i1, i2, multi) and + top.runtime(self.declaredAttribute(name)) and + self.getASuperType().declaredAttribute(name) = multi + | + // Only called twice if called from different functions, + // or if one call-site can reach the other. + i1.getCall().getScope() != i2.getCall().getScope() + or + i1.getCall().strictlyReaches(i2.getCall()) + ) +} + +/** Holds if all attributes called `name` can be inferred to be methods. */ +private predicate named_attributes_not_method(ClassObject cls, string name) { + cls.declaresAttribute(name) and not cls.declaredAttribute(name) instanceof FunctionObject +} + +/** Holds if `f` actually does something. */ +private predicate does_something(FunctionObject f) { + f.isBuiltin() and not f = theObjectType().lookupAttribute("__init__") + or + exists(Stmt s | s = f.getFunction().getAStmt() and not s instanceof Pass) +} + +/** Holds if `meth` looks like it should have a call to `name`, but does not */ +private predicate missing_call(FunctionObject meth, string name) { + exists(CallNode call, AttrNode attr | + call.getScope() = meth.getFunction() and + call.getFunction() = attr and + attr.getName() = name and + not exists(FunctionObject f | f.getACall() = call) + ) +} + +/** Holds if `self.name` does not call `missing`, even though it is expected to. */ +predicate missing_call_to_superclass_method( + ClassObject self, FunctionObject top, FunctionObject missing, string name +) { + missing = self.getASuperType().declaredAttribute(name) and + top = self.lookupAttribute(name) and + /* There is no call to missing originating from top */ + not top.getACallee*() = missing and + /* Make sure that all named 'methods' are objects that we can understand. */ + not exists(ClassObject sup | + sup = self.getAnImproperSuperType() and + named_attributes_not_method(sup, name) + ) and + not self.isAbstract() and + does_something(missing) and + not missing_call(top, name) +} diff --git a/python/ql/src/Classes/MissingCallToDel.py b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.py similarity index 100% rename from python/ql/src/Classes/MissingCallToDel.py rename to python/ql/src/Classes/CallsToInitDel/MissingCallToDel.py diff --git a/python/ql/src/Classes/MissingCallToDel.qhelp b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.qhelp similarity index 100% rename from python/ql/src/Classes/MissingCallToDel.qhelp rename to python/ql/src/Classes/CallsToInitDel/MissingCallToDel.qhelp diff --git a/python/ql/src/Classes/MissingCallToDel.ql b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql similarity index 100% rename from python/ql/src/Classes/MissingCallToDel.ql rename to python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql diff --git a/python/ql/src/Classes/MissingCallToInit.py b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.py similarity index 100% rename from python/ql/src/Classes/MissingCallToInit.py rename to python/ql/src/Classes/CallsToInitDel/MissingCallToInit.py diff --git a/python/ql/src/Classes/MissingCallToInit.qhelp b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.qhelp similarity index 100% rename from python/ql/src/Classes/MissingCallToInit.qhelp rename to python/ql/src/Classes/CallsToInitDel/MissingCallToInit.qhelp diff --git a/python/ql/src/Classes/MissingCallToInit.ql b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql similarity index 100% rename from python/ql/src/Classes/MissingCallToInit.ql rename to python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql diff --git a/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.py b/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.py similarity index 100% rename from python/ql/src/Classes/SuperclassDelCalledMultipleTimes.py rename to python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.py diff --git a/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.qhelp b/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.qhelp similarity index 100% rename from python/ql/src/Classes/SuperclassDelCalledMultipleTimes.qhelp rename to python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.qhelp diff --git a/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.ql b/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql similarity index 100% rename from python/ql/src/Classes/SuperclassDelCalledMultipleTimes.ql rename to python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql diff --git a/python/ql/src/Classes/SuperclassDelCalledMultipleTimes2.py b/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes2.py similarity index 100% rename from python/ql/src/Classes/SuperclassDelCalledMultipleTimes2.py rename to python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes2.py diff --git a/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.py b/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.py similarity index 100% rename from python/ql/src/Classes/SuperclassInitCalledMultipleTimes.py rename to python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.py diff --git a/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.qhelp b/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.qhelp similarity index 100% rename from python/ql/src/Classes/SuperclassInitCalledMultipleTimes.qhelp rename to python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.qhelp diff --git a/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.ql b/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql similarity index 100% rename from python/ql/src/Classes/SuperclassInitCalledMultipleTimes.ql rename to python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql diff --git a/python/ql/src/Classes/SuperclassInitCalledMultipleTimes2.py b/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes2.py similarity index 100% rename from python/ql/src/Classes/SuperclassInitCalledMultipleTimes2.py rename to python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes2.py diff --git a/python/ql/src/Classes/MethodCallOrder.qll b/python/ql/src/Classes/MethodCallOrder.qll index 620cb8028780..b911ee0183e1 100644 --- a/python/ql/src/Classes/MethodCallOrder.qll +++ b/python/ql/src/Classes/MethodCallOrder.qll @@ -1,3 +1,5 @@ +deprecated module; + import python // Helper predicates for multiple call to __init__/__del__ queries. From a2fc14af2e85c2293a738aeb93534e47ece4003c Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Mon, 30 Jun 2025 16:21:17 +0100 Subject: [PATCH 02/22] Update missing call to init --- .../CallsToInitDel/MethodCallOrder.qll | 99 +++++++++++++------ .../CallsToInitDel/MissingCallToInit.ql | 31 +++--- 2 files changed, 86 insertions(+), 44 deletions(-) diff --git a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll index b911ee0183e1..b14c6138015f 100644 --- a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll +++ b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll @@ -1,6 +1,8 @@ -deprecated module; +/** Definitions for reasoning about multiple or missing calls to superclass methods. */ import python +import semmle.python.ApiGraphs +import semmle.python.dataflow.new.internal.DataFlowDispatch // Helper predicates for multiple call to __init__/__del__ queries. pragma[noinline] @@ -36,42 +38,77 @@ predicate multiple_calls_to_superclass_method(ClassObject self, FunctionObject m ) } -/** Holds if all attributes called `name` can be inferred to be methods. */ -private predicate named_attributes_not_method(ClassObject cls, string name) { - cls.declaresAttribute(name) and not cls.declaredAttribute(name) instanceof FunctionObject +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 `f` actually does something. */ -private predicate does_something(FunctionObject f) { - f.isBuiltin() and not f = theObjectType().lookupAttribute("__init__") - or - exists(Stmt s | s = f.getFunction().getAStmt() and not s instanceof Pass) +predicate superCall(DataFlow::MethodCallNode call, string name) { + exists(DataFlow::Node sup | + call.calls(sup, name) and + sup = API::builtin("super").getACall() + ) } -/** Holds if `meth` looks like it should have a call to `name`, but does not */ -private predicate missing_call(FunctionObject meth, string name) { - exists(CallNode call, AttrNode attr | - call.getScope() = meth.getFunction() and - call.getFunction() = attr and - attr.getName() = name and - not exists(FunctionObject f | f.getACall() = call) +predicate callsSuper(Function meth) { + exists(DataFlow::MethodCallNode call | + call.getScope() = meth and + superCall(call, meth.getName()) ) } -/** Holds if `self.name` does not call `missing`, even though it is expected to. */ -predicate missing_call_to_superclass_method( - ClassObject self, FunctionObject top, FunctionObject missing, string name +predicate callsMethodOnClassWithSelf( + Function meth, DataFlow::MethodCallNode call, Class target, string name ) { - missing = self.getASuperType().declaredAttribute(name) and - top = self.lookupAttribute(name) and - /* There is no call to missing originating from top */ - not top.getACallee*() = missing and - /* Make sure that all named 'methods' are objects that we can understand. */ - not exists(ClassObject sup | - sup = self.getAnImproperSuperType() and - named_attributes_not_method(sup, name) - ) and - not self.isAbstract() and - does_something(missing) and - not missing_call(top, 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) + ) +} + +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 exists(Class target | callTarget = classTracker(target)) + ) +} + +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( + 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) + ) } diff --git a/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql index 4f5d3d90e84a..a8f1c5b6179c 100644 --- a/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql +++ b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql @@ -1,5 +1,5 @@ /** - * @name Missing call to `__init__` during object initialization + * @name Missing call to superclass `__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 @@ -14,16 +14,21 @@ import python import MethodCallOrder -from ClassObject self, FunctionObject initializer, FunctionObject missing +predicate missingCallToSuperclassInit(Function base, Function shouldCall, Class mroStart) { + missingCallToSuperclassMethod(base, shouldCall, mroStart, "__init__") +} + +from Function base, Function shouldCall, Class mroStart, string msg 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" + 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 $@, which follows it in the MRO of $@, leaving it partially initialized." + ) +select base, msg, shouldCall, shouldCall.getQualifiedName(), mroStart, mroStart.getName() From 6f9983a431ab8d1ddfd3c690022305da75e564cd Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Mon, 30 Jun 2025 16:59:51 +0100 Subject: [PATCH 03/22] Add missing call to del --- .../CallsToInitDel/MissingCallToDel.ql | 29 ++++++++++++------- .../CallsToInitDel/MissingCallToInit.ql | 2 +- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql index be49dc48b5f4..1c0bf6f6ec4a 100644 --- a/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql +++ b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql @@ -1,6 +1,6 @@ /** - * @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. + * @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 @@ -15,12 +15,21 @@ import python import MethodCallOrder -from ClassObject self, FunctionObject missing +predicate missingCallToSuperclassDel(Function base, Function shouldCall, Class mroStart) { + missingCallToSuperclassMethod(base, shouldCall, mroStart, "__del__") +} + +from Function base, Function shouldCall, Class mroStart, string msg 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() + missingCallToSuperclassDel(base, shouldCall, mroStart) and + ( + // Simple case: the method that should be called is directly overridden + mroStart = base.getScope() and + msg = "This deletion 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 deletion method does not call $@, which follows it in the MRO of $@, leaving it not properly cleaned up." + ) +select base, msg, shouldCall, shouldCall.getQualifiedName(), mroStart, mroStart.getName() diff --git a/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql index a8f1c5b6179c..9adcad3e46a1 100644 --- a/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql +++ b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql @@ -1,6 +1,6 @@ /** * @name Missing call to superclass `__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. + * @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 From adcfdf1da4388cde06ea614f21782cc7595eaf70 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Tue, 1 Jul 2025 11:25:06 +0100 Subject: [PATCH 04/22] Modernize multple calls to init/del --- .../new/internal/DataFlowDispatch.qll | 13 +++-- .../CallsToInitDel/MethodCallOrder.qll | 49 +++++++++---------- .../SuperclassDelCalledMultipleTimes.ql | 27 +++++----- .../SuperclassInitCalledMultipleTimes.ql | 28 +++++------ 4 files changed, 59 insertions(+), 58 deletions(-) diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll index 1a38593bce48..ce778eff85d2 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll @@ -851,9 +851,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) @@ -866,7 +871,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. diff --git a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll index b14c6138015f..253360bf2275 100644 --- a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll +++ b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll @@ -4,37 +4,32 @@ import python import semmle.python.ApiGraphs import semmle.python.dataflow.new.internal.DataFlowDispatch -// Helper predicates for multiple call to __init__/__del__ queries. -pragma[noinline] -private predicate multiple_invocation_paths_helper( - FunctionInvocation top, FunctionInvocation i1, FunctionInvocation i2, FunctionObject multi -) { - i1 != i2 and - i1 = top.getACallee+() and - i2 = top.getACallee+() and - i1.getFunction() = multi -} - -pragma[noinline] -private predicate multiple_invocation_paths( - FunctionInvocation top, FunctionInvocation i1, FunctionInvocation i2, FunctionObject multi -) { - multiple_invocation_paths_helper(top, i1, i2, multi) and - i2.getFunction() = multi +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 + not call1 = call2 and + calledMulti = getASuperCallTarget(cls, meth, call1) and + calledMulti = getASuperCallTarget(cls, meth, call2) and + nonTrivial(calledMulti) + ) } -/** Holds if `self.name` calls `multi` by multiple paths, and thus calls it more than once. */ -predicate multiple_calls_to_superclass_method(ClassObject self, FunctionObject multi, string name) { - exists(FunctionInvocation top, FunctionInvocation i1, FunctionInvocation i2 | - multiple_invocation_paths(top, i1, i2, multi) and - top.runtime(self.declaredAttribute(name)) and - self.getASuperType().declaredAttribute(name) = multi +Function getASuperCallTarget(Class mroBase, Function meth, DataFlow::MethodCallNode call) { + meth = call.getScope() and + getADirectSuperclass*(mroBase) = meth.getScope() and + call.calls(_, meth.getName()) and + exists(Function target, Class nextMroBase | + (result = target or result = getASuperCallTarget(nextMroBase, target, _)) | - // Only called twice if called from different functions, - // or if one call-site can reach the other. - i1.getCall().getScope() != i2.getCall().getScope() + superCall(call, _) and + nextMroBase = mroBase and + target = + findFunctionAccordingToMroKnownStartingClass(getNextClassInMroKnownStartingClass(meth.getScope(), + mroBase), mroBase, meth.getName()) or - i1.getCall().strictlyReaches(i2.getCall()) + callsMethodOnClassWithSelf(meth, call, nextMroBase, _) and + target = findFunctionAccordingToMro(nextMroBase, meth.getName()) ) } diff --git a/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql b/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql index 019da4257aa0..d75d948809dc 100644 --- a/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql +++ b/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql @@ -1,6 +1,6 @@ /** * @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. + * @description A duplicated call to a superclass `__del__` method may lead to class instances not be cleaned up properly. * @kind problem * @tags quality * reliability @@ -14,16 +14,17 @@ import python import MethodCallOrder -from ClassObject self, FunctionObject multi +predicate multipleCallsToSuperclassDel(Function meth, Function calledMulti) { + multipleCallsToSuperclassMethod(meth, calledMulti, "__sel__") +} + +from Function meth, Function calledMulti 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() + multipleCallsToSuperclassDel(meth, calledMulti) and + // Don't alert for multiple calls to a superclass del when a subclass will do. + not exists(Function subMulti | + multipleCallsToSuperclassDel(meth, subMulti) and + calledMulti.getScope() = getADirectSuperclass+(subMulti.getScope()) + ) +select meth, "This delete method calls $@ multiple times.", calledMulti, + calledMulti.getQualifiedName() diff --git a/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql b/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql index 6251ef274dac..1f6f61222bfe 100644 --- a/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql +++ b/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql @@ -1,6 +1,6 @@ /** * @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. + * @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 @@ -14,17 +14,17 @@ import python import MethodCallOrder -from ClassObject self, FunctionObject multi +predicate multipleCallsToSuperclassInit(Function meth, Function calledMulti) { + multipleCallsToSuperclassMethod(meth, calledMulti, "__init__") +} + +from Function meth, Function calledMulti 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() + multipleCallsToSuperclassInit(meth, calledMulti) and + // Don't alert for multiple calls to a superclass init when a subclass will do. + not exists(Function subMulti | + multipleCallsToSuperclassInit(meth, subMulti) and + calledMulti.getScope() = getADirectSuperclass+(subMulti.getScope()) + ) +select meth, "This initializer method calls $@ multiple times.", calledMulti, + calledMulti.getQualifiedName() From caddec474ce6f74fbfd4ee21f5a8f75b44f64e41 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Tue, 1 Jul 2025 11:38:12 +0100 Subject: [PATCH 05/22] Update alert messages --- python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql | 4 ++-- python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql | 2 +- .../CallsToInitDel/SuperclassInitCalledMultipleTimes.ql | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql index 1c0bf6f6ec4a..4cc72ee9facd 100644 --- a/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql +++ b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql @@ -25,11 +25,11 @@ where ( // Simple case: the method that should be called is directly overridden mroStart = base.getScope() and - msg = "This deletion method does not call $@, which may leave $@ not properly cleaned up." + 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 deletion method does not call $@, which follows it in the MRO of $@, leaving it not properly cleaned up." + "This delete method does not call super().__del__, which may cause $@ to be missed during the cleanup of $@." ) select base, msg, shouldCall, shouldCall.getQualifiedName(), mroStart, mroStart.getName() diff --git a/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql index 9adcad3e46a1..56c6bd258cd2 100644 --- a/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql +++ b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql @@ -29,6 +29,6 @@ where // 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 $@, which follows it in the MRO of $@, leaving it partially initialized." + "This initialization method does not call super().__init__, which may cause $@ to be missed during the initialization of $@." ) select base, msg, shouldCall, shouldCall.getQualifiedName(), mroStart, mroStart.getName() diff --git a/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql b/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql index 1f6f61222bfe..4f577dc4a764 100644 --- a/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql +++ b/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql @@ -26,5 +26,5 @@ where multipleCallsToSuperclassInit(meth, subMulti) and calledMulti.getScope() = getADirectSuperclass+(subMulti.getScope()) ) -select meth, "This initializer method calls $@ multiple times.", calledMulti, +select meth, "This initialization method calls $@ multiple times.", calledMulti, calledMulti.getQualifiedName() From 71d1179877a80cdfb574bf52c9533bf95b2f42a8 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Tue, 1 Jul 2025 16:39:12 +0100 Subject: [PATCH 06/22] Fix FPs and typo --- python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll | 2 +- .../Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll index 253360bf2275..5fd28ef9ddf7 100644 --- a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll +++ b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll @@ -8,7 +8,7 @@ predicate multipleCallsToSuperclassMethod(Function meth, Function calledMulti, s exists(DataFlow::MethodCallNode call1, DataFlow::MethodCallNode call2, Class cls | meth.getName() = name and meth.getScope() = cls and - not call1 = call2 and + call1.asExpr() != call2.asExpr() and calledMulti = getASuperCallTarget(cls, meth, call1) and calledMulti = getASuperCallTarget(cls, meth, call2) and nonTrivial(calledMulti) diff --git a/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql b/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql index d75d948809dc..7aca3dee1892 100644 --- a/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql +++ b/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql @@ -15,7 +15,7 @@ import python import MethodCallOrder predicate multipleCallsToSuperclassDel(Function meth, Function calledMulti) { - multipleCallsToSuperclassMethod(meth, calledMulti, "__sel__") + multipleCallsToSuperclassMethod(meth, calledMulti, "__del__") } from Function meth, Function calledMulti From 085df269a327dfc01d4a5dfa29902482914df0cf Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Wed, 2 Jul 2025 10:09:51 +0100 Subject: [PATCH 07/22] Move tests and add inline expectation postprocessing --- .../query-tests/Classes/missing-del/MissingCallToDel.qlref | 3 ++- .../query-tests/Classes/missing-init/MissingCallToInit.qlref | 3 ++- .../Classes/multiple/SuperclassDelCalledMultipleTimes.qlref | 1 - .../Classes/multiple/SuperclassInitCalledMultipleTimes.qlref | 1 - .../SuperclassDelCalledMultipleTimes.expected | 0 .../multiple-del/SuperclassDelCalledMultipleTimes.qlref | 2 ++ .../Classes/multiple/{ => multiple-del}/multiple_del.py | 0 .../SuperclassInitCalledMultipleTimes.expected | 0 .../multiple-init/SuperclassInitCalledMultipleTimes.qlref | 2 ++ .../Classes/multiple/{ => multiple-init}/multiple_init.py | 2 +- 10 files changed, 9 insertions(+), 5 deletions(-) delete mode 100644 python/ql/test/query-tests/Classes/multiple/SuperclassDelCalledMultipleTimes.qlref delete mode 100644 python/ql/test/query-tests/Classes/multiple/SuperclassInitCalledMultipleTimes.qlref rename python/ql/test/query-tests/Classes/multiple/{ => multiple-del}/SuperclassDelCalledMultipleTimes.expected (100%) create mode 100644 python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.qlref rename python/ql/test/query-tests/Classes/multiple/{ => multiple-del}/multiple_del.py (100%) rename python/ql/test/query-tests/Classes/multiple/{ => multiple-init}/SuperclassInitCalledMultipleTimes.expected (100%) create mode 100644 python/ql/test/query-tests/Classes/multiple/multiple-init/SuperclassInitCalledMultipleTimes.qlref rename python/ql/test/query-tests/Classes/multiple/{ => multiple-init}/multiple_init.py (98%) diff --git a/python/ql/test/query-tests/Classes/missing-del/MissingCallToDel.qlref b/python/ql/test/query-tests/Classes/missing-del/MissingCallToDel.qlref index 8bf1811d0fab..b7ff00870542 100644 --- a/python/ql/test/query-tests/Classes/missing-del/MissingCallToDel.qlref +++ b/python/ql/test/query-tests/Classes/missing-del/MissingCallToDel.qlref @@ -1 +1,2 @@ -Classes/MissingCallToDel.ql +query: Classes/CallsToInitDel/MissingCallToDel.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql \ No newline at end of file diff --git a/python/ql/test/query-tests/Classes/missing-init/MissingCallToInit.qlref b/python/ql/test/query-tests/Classes/missing-init/MissingCallToInit.qlref index b8635a5f8d92..86700427963a 100644 --- a/python/ql/test/query-tests/Classes/missing-init/MissingCallToInit.qlref +++ b/python/ql/test/query-tests/Classes/missing-init/MissingCallToInit.qlref @@ -1 +1,2 @@ -Classes/MissingCallToInit.ql +query: Classes/CallsToInitDel/MissingCallToInit.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql \ No newline at end of file diff --git a/python/ql/test/query-tests/Classes/multiple/SuperclassDelCalledMultipleTimes.qlref b/python/ql/test/query-tests/Classes/multiple/SuperclassDelCalledMultipleTimes.qlref deleted file mode 100644 index 560d7b7dc416..000000000000 --- a/python/ql/test/query-tests/Classes/multiple/SuperclassDelCalledMultipleTimes.qlref +++ /dev/null @@ -1 +0,0 @@ -Classes/SuperclassDelCalledMultipleTimes.ql diff --git a/python/ql/test/query-tests/Classes/multiple/SuperclassInitCalledMultipleTimes.qlref b/python/ql/test/query-tests/Classes/multiple/SuperclassInitCalledMultipleTimes.qlref deleted file mode 100644 index 042ddb76904c..000000000000 --- a/python/ql/test/query-tests/Classes/multiple/SuperclassInitCalledMultipleTimes.qlref +++ /dev/null @@ -1 +0,0 @@ -Classes/SuperclassInitCalledMultipleTimes.ql diff --git a/python/ql/test/query-tests/Classes/multiple/SuperclassDelCalledMultipleTimes.expected b/python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.expected similarity index 100% rename from python/ql/test/query-tests/Classes/multiple/SuperclassDelCalledMultipleTimes.expected rename to python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.expected diff --git a/python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.qlref b/python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.qlref new file mode 100644 index 000000000000..8fa7d25474b0 --- /dev/null +++ b/python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.qlref @@ -0,0 +1,2 @@ +query: Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql \ No newline at end of file diff --git a/python/ql/test/query-tests/Classes/multiple/multiple_del.py b/python/ql/test/query-tests/Classes/multiple/multiple-del/multiple_del.py similarity index 100% rename from python/ql/test/query-tests/Classes/multiple/multiple_del.py rename to python/ql/test/query-tests/Classes/multiple/multiple-del/multiple_del.py diff --git a/python/ql/test/query-tests/Classes/multiple/SuperclassInitCalledMultipleTimes.expected b/python/ql/test/query-tests/Classes/multiple/multiple-init/SuperclassInitCalledMultipleTimes.expected similarity index 100% rename from python/ql/test/query-tests/Classes/multiple/SuperclassInitCalledMultipleTimes.expected rename to python/ql/test/query-tests/Classes/multiple/multiple-init/SuperclassInitCalledMultipleTimes.expected diff --git a/python/ql/test/query-tests/Classes/multiple/multiple-init/SuperclassInitCalledMultipleTimes.qlref b/python/ql/test/query-tests/Classes/multiple/multiple-init/SuperclassInitCalledMultipleTimes.qlref new file mode 100644 index 000000000000..9a2b156acd35 --- /dev/null +++ b/python/ql/test/query-tests/Classes/multiple/multiple-init/SuperclassInitCalledMultipleTimes.qlref @@ -0,0 +1,2 @@ +query: Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql \ No newline at end of file diff --git a/python/ql/test/query-tests/Classes/multiple/multiple_init.py b/python/ql/test/query-tests/Classes/multiple/multiple-init/multiple_init.py similarity index 98% rename from python/ql/test/query-tests/Classes/multiple/multiple_init.py rename to python/ql/test/query-tests/Classes/multiple/multiple-init/multiple_init.py index 6a97ef67f6ca..28dd16eeed1e 100644 --- a/python/ql/test/query-tests/Classes/multiple/multiple_init.py +++ b/python/ql/test/query-tests/Classes/multiple/multiple-init/multiple_init.py @@ -2,7 +2,7 @@ class Base(object): def __init__(self): - pass + print("Base init") class C1(Base): From 2faf67d08c8892f45d02388c7a108b0c93e2bbe8 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Wed, 2 Jul 2025 10:53:28 +0100 Subject: [PATCH 08/22] Update test outputs + fix semantics --- .../CallsToInitDel/MethodCallOrder.qll | 11 +++--- .../SuperclassDelCalledMultipleTimes.expected | 4 +- .../multiple/multiple-del/multiple_del.py | 19 ++++++++-- ...SuperclassInitCalledMultipleTimes.expected | 4 +- .../multiple/multiple-init/multiple_init.py | 38 ++++++++----------- 5 files changed, 40 insertions(+), 36 deletions(-) diff --git a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll index 5fd28ef9ddf7..58330eeb9999 100644 --- a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll +++ b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll @@ -19,17 +19,16 @@ Function getASuperCallTarget(Class mroBase, Function meth, DataFlow::MethodCallN meth = call.getScope() and getADirectSuperclass*(mroBase) = meth.getScope() and call.calls(_, meth.getName()) and - exists(Function target, Class nextMroBase | - (result = target or result = getASuperCallTarget(nextMroBase, target, _)) - | + exists(Function target | (result = target or result = getASuperCallTarget(mroBase, target, _)) | superCall(call, _) and - nextMroBase = mroBase and target = findFunctionAccordingToMroKnownStartingClass(getNextClassInMroKnownStartingClass(meth.getScope(), mroBase), mroBase, meth.getName()) or - callsMethodOnClassWithSelf(meth, call, nextMroBase, _) and - target = findFunctionAccordingToMro(nextMroBase, meth.getName()) + exists(Class called | + callsMethodOnClassWithSelf(meth, call, called, _) and + target = findFunctionAccordingToMroKnownStartingClass(called, mroBase, meth.getName()) + ) ) } diff --git a/python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.expected b/python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.expected index 210f24c7525d..ad9858397dff 100644 --- a/python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.expected +++ b/python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.expected @@ -1,2 +1,2 @@ -| multiple_del.py:17:1:17:17 | class Y3 | Class Y3 may not be cleaned up properly as $@ may be called multiple times during destruction. | multiple_del.py:9:5:9:22 | Function __del__ | method Y1.__del__ | -| multiple_del.py:34:1:34:17 | class Z3 | Class Z3 may not be cleaned up properly as $@ may be called multiple times during destruction. | multiple_del.py:26:5:26:22 | Function __del__ | method Z1.__del__ | +| multiple_del.py:21:5:21:22 | Function __del__ | This delete method calls $@ multiple times. | multiple_del.py:9:5:9:22 | Function __del__ | Y1.__del__ | +| multiple_del.py:43:5:43:22 | Function __del__ | This delete method calls $@ multiple times. | multiple_del.py:32:5:32:22 | Function __del__ | Z1.__del__ | diff --git a/python/ql/test/query-tests/Classes/multiple/multiple-del/multiple_del.py b/python/ql/test/query-tests/Classes/multiple/multiple-del/multiple_del.py index 284f6bf6969f..f72e7c5d00ad 100644 --- a/python/ql/test/query-tests/Classes/multiple/multiple-del/multiple_del.py +++ b/python/ql/test/query-tests/Classes/multiple/multiple-del/multiple_del.py @@ -2,37 +2,48 @@ class Base(object): def __del__(self): - pass + print("Base del") class Y1(Base): def __del__(self): + print("Y1 del") super(Y1, self).__del__() class Y2(Base): def __del__(self): + print("Y2 del") super(Y2, self).__del__() #When `type(self) == Y3` this calls `Y1.__del__` class Y3(Y2, Y1): - def __del__(self): + def __del__(self): # $ Alert + print("Y3 del") Y1.__del__(self) Y2.__del__(self) +a = Y3() +del a + #Calling a method multiple times by using explicit calls when a base inherits from other base class Z1(object): def __del__(self): - pass + print("Z1 del") class Z2(Z1): def __del__(self): + print("Z2 del") Z1.__del__(self) class Z3(Z2, Z1): - def __del__(self): + def __del__(self): # $ Alert + print("Z3 del") Z1.__del__(self) Z2.__del__(self) + +b = Z3() +del b diff --git a/python/ql/test/query-tests/Classes/multiple/multiple-init/SuperclassInitCalledMultipleTimes.expected b/python/ql/test/query-tests/Classes/multiple/multiple-init/SuperclassInitCalledMultipleTimes.expected index 04ce8c0f3730..42d019e7f710 100644 --- a/python/ql/test/query-tests/Classes/multiple/multiple-init/SuperclassInitCalledMultipleTimes.expected +++ b/python/ql/test/query-tests/Classes/multiple/multiple-init/SuperclassInitCalledMultipleTimes.expected @@ -1,2 +1,2 @@ -| multiple_init.py:17:1:17:17 | class C3 | Class C3 may not be initialized properly as $@ may be called multiple times during initialization. | multiple_init.py:9:5:9:23 | Function __init__ | method C1.__init__ | -| multiple_init.py:34:1:34:17 | class D3 | Class D3 may not be initialized properly as $@ may be called multiple times during initialization. | multiple_init.py:26:5:26:23 | Function __init__ | method D1.__init__ | +| multiple_init.py:21:5:21:23 | Function __init__ | This initialization method calls $@ multiple times. | multiple_init.py:9:5:9:23 | Function __init__ | C1.__init__ | +| multiple_init.py:42:5:42:23 | Function __init__ | This initialization method calls $@ multiple times. | multiple_init.py:31:5:31:23 | Function __init__ | D1.__init__ | diff --git a/python/ql/test/query-tests/Classes/multiple/multiple-init/multiple_init.py b/python/ql/test/query-tests/Classes/multiple/multiple-init/multiple_init.py index 28dd16eeed1e..cba8b24523fa 100644 --- a/python/ql/test/query-tests/Classes/multiple/multiple-init/multiple_init.py +++ b/python/ql/test/query-tests/Classes/multiple/multiple-init/multiple_init.py @@ -7,70 +7,64 @@ def __init__(self): class C1(Base): def __init__(self): + print("C1 init") super(C1, self).__init__() class C2(Base): def __init__(self): + print("C2 init") super(C2, self).__init__() #When `type(self) == C3` this calls `C1.__init__` class C3(C2, C1): - def __init__(self): + def __init__(self): # $ Alert + print("C3 init") C1.__init__(self) C2.__init__(self) +C3() + #Calling a method multiple times by using explicit calls when a base inherits from other base class D1(object): def __init__(self): - pass + print("D1 init") class D2(D1): def __init__(self): + print("D2 init") D1.__init__(self) class D3(D2, D1): - def __init__(self): + def __init__(self): # $ Alert + print("D3 init") D1.__init__(self) D2.__init__(self) +D3() + #OK to call object.__init__ multiple times class E1(object): def __init__(self): + print("E1 init") super(E1, self).__init__() class E2(object): def __init__(self): + print("E2 init") object.__init__(self) class E3(E2, E1): - def __init__(self): + def __init__(self): # OK: builtin init methods are fine. E1.__init__(self) E2.__init__(self) -#Two invocations, but can only be called once -class F1(Base): - - def __init__(self, cond): - if cond: - Base.__init__(self) - else: - Base.__init__(self) - -#Single call, splitting causes what seems to be multiple invocations. -class F2(Base): - - def __init__(self, cond): - if cond: - pass - if cond: - pass - Base.__init__(self) +E3() From 1b4e2feb60cdb1a3221b305242fe486edeff20bb Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Thu, 3 Jul 2025 14:22:04 +0100 Subject: [PATCH 09/22] Change implenetation of missing calls to use getASuperCallTarget, and change alerts to alert on the class and provide clearer information, using optional location links. --- .../CallsToInitDel/MethodCallOrder.qll | 128 +++++++++++++----- .../CallsToInitDel/MissingCallToDel.ql | 42 ++++-- .../CallsToInitDel/MissingCallToInit.ql | 41 +++--- 3 files changed, 146 insertions(+), 65 deletions(-) diff --git a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll index 58330eeb9999..1ebde88083a8 100644 --- a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll +++ b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll @@ -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( - Function base, Function shouldCall, Class mroStart, string name -) { +predicate missingCallToSuperclassMethod(Class base, Function shouldCall, string name) { base.getName() = name and shouldCall.getName() = name and - not callsSuper(base) and - not callsMethodOnUnknownClassWithSelf(base, name) and + base = getADirectSuperclass*(base.getScope()) and + not shouldCall = getASuperCallTargetFromClass(base, 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) + // "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) + ) +} + +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; + +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() +} diff --git a/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql index 4cc72ee9facd..3d742d45a006 100644 --- a/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql +++ b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql @@ -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() diff --git a/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql index 56c6bd258cd2..1b13fef46c73 100644 --- a/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql +++ b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql @@ -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() From 16b90a176b3fcd00318a1468b8c46c1b364ad8fe Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Thu, 3 Jul 2025 15:32:54 +0100 Subject: [PATCH 10/22] Fixes --- python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll | 3 +-- python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql | 2 +- python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll index 1ebde88083a8..f5853e686734 100644 --- a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll +++ b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll @@ -85,9 +85,8 @@ predicate callsMethodOnUnknownClassWithSelf(Function meth, string name) { } predicate missingCallToSuperclassMethod(Class base, Function shouldCall, string name) { - base.getName() = name and shouldCall.getName() = name and - base = getADirectSuperclass*(base.getScope()) 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 diff --git a/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql index 3d742d45a006..a6ade1f1d312 100644 --- a/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql +++ b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql @@ -38,7 +38,7 @@ where msg = "This class does not call $@ during destruction. ($@ may be missing a call to a base class __del__)" or - not getDelMethod(base) and + not exists(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.)" diff --git a/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql index 1b13fef46c73..9081960a75bb 100644 --- a/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql +++ b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql @@ -21,8 +21,7 @@ where missingCallToSuperclassMethodRestricted(base, shouldCall, "__init__") and possiblyMissingSuper = getPossibleMissingSuperOption(base, shouldCall, "__init__") and ( - not possiblyMissingSuper.isNone() and - possibleIssue = possiblyMissingSuper and + possibleIssue.asSome() = possiblyMissingSuper.asSome() and msg = "This class does not call $@ during initialization. ($@ may be missing a call to super().__init__)" or From b3056fc5a749643f00b5fa8abef72a591e84af21 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Thu, 3 Jul 2025 15:48:52 +0100 Subject: [PATCH 11/22] Update tests for calls to init + fixes --- .../CallsToInitDel/MethodCallOrder.qll | 14 +- .../missing-init/MissingCallToInit.expected | 8 +- .../Classes/missing-init/missing_init.py | 124 +++++++++--------- 3 files changed, 77 insertions(+), 69 deletions(-) diff --git a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll index f5853e686734..191243e03328 100644 --- a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll +++ b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll @@ -95,13 +95,15 @@ predicate missingCallToSuperclassMethod(Class base, Function shouldCall, string 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) + 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 superShouldCall | - superShouldCall.getScope() = getADirectSuperclass+(shouldCall.getScope()) and - missingCallToSuperclassMethod(base, superShouldCall, name) + 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) ) } diff --git a/python/ql/test/query-tests/Classes/missing-init/MissingCallToInit.expected b/python/ql/test/query-tests/Classes/missing-init/MissingCallToInit.expected index 6cb92041a630..90ca4bf49e77 100644 --- a/python/ql/test/query-tests/Classes/missing-init/MissingCallToInit.expected +++ b/python/ql/test/query-tests/Classes/missing-init/MissingCallToInit.expected @@ -1,3 +1,5 @@ -| missing_init.py:12:1:12:13 | class B3 | Class B3 may not be initialized properly as $@ is not called from its $@. | missing_init.py:9:5:9:23 | Function __init__ | method B2.__init__ | missing_init.py:14:5:14:23 | Function __init__ | __init__ method | -| missing_init.py:39:1:39:21 | class IUVT | Class IUVT may not be initialized properly as $@ is not called from its $@. | missing_init.py:30:5:30:23 | Function __init__ | method UT.__init__ | missing_init.py:26:5:26:23 | Function __init__ | __init__ method | -| missing_init.py:72:1:72:13 | class AB | Class AB may not be initialized properly as $@ is not called from its $@. | missing_init.py:69:5:69:23 | Function __init__ | method AA.__init__ | missing_init.py:75:5:75:23 | Function __init__ | __init__ method | +| missing_init.py:14:5:14:23 | Function __init__ | This initialization method does not call $@, which may leave $@ partially initialized. | missing_init.py:9:5:9:23 | Function __init__ | B2.__init__ | missing_init.py:13:1:13:13 | Class B3 | B3 | +| missing_init.py:29:5:29:23 | Function __init__ | This initialization method does not call super().__init__, which may cause $@ to be missed during the initialization of $@. | missing_init.py:33:5:33:23 | Function __init__ | UT.__init__ | missing_init.py:42:1:42:21 | Class IUVT | IUVT | +| missing_init.py:70:5:70:23 | Function __init__ | This initialization method does not call $@, which may leave $@ partially initialized. | missing_init.py:64:5:64:23 | Function __init__ | AA.__init__ | missing_init.py:67:1:67:13 | Class AB | AB | +| missing_init.py:124:9:124:27 | Function __init__ | This initialization method does not call $@, which may leave $@ partially initialized. | missing_init.py:117:5:117:23 | Function __init__ | DA.__init__ | missing_init.py:122:5:122:17 | Class DC | DC | +| missing_init.py:134:5:134:23 | Function __init__ | This initialization method does not call $@, which may leave $@ partially initialized. | missing_init.py:117:5:117:23 | Function __init__ | DA.__init__ | missing_init.py:132:1:132:13 | Class DD | DD | diff --git a/python/ql/test/query-tests/Classes/missing-init/missing_init.py b/python/ql/test/query-tests/Classes/missing-init/missing_init.py index b1651557759f..68c5b1fad7b5 100644 --- a/python/ql/test/query-tests/Classes/missing-init/missing_init.py +++ b/python/ql/test/query-tests/Classes/missing-init/missing_init.py @@ -2,18 +2,21 @@ class B1(object): def __init__(self): - do_something() + print("B1 init") class B2(B1): def __init__(self): + print("B2 init") B1.__init__(self) -class B3(B2): - - def __init__(self): +class B3(B2): # $ Alert + def __init__(self): + print("B3 init") B1.__init__(self) +B3() + #OK if superclass __init__ is builtin as #builtin classes tend to rely on __new__ class MyException(Exception): @@ -23,11 +26,11 @@ def __init__(self): #ODASA-4107 class IUT(object): - def __init__(self): + def __init__(self): print("IUT init") class UT(object): - def __init__(self): + def __init__(self): print("UT init") class PU(object): @@ -36,150 +39,151 @@ class PU(object): class UVT(UT, PU): pass -class IUVT(IUT, UVT): +class IUVT(IUT, UVT): # $ Alert pass -#False positive +print("IUVT") +IUVT() + class M1(object): def __init__(self): - print("A") + print("M1 init") class M2(object): pass class Mult(M2, M1): def __init__(self): - super(Mult, self).__init__() # Calls M1.__init__ - -class X: - def __init__(self): - do_something() - -class Y(X): - @decorated - def __init__(self): - X.__init__(self) + print("Mult init") + super(Mult, self).__init__() # OK - Calls M1.__init__ -class Z(Y): - def __init__(self): - Y.__init__(self) +Mult() class AA(object): def __init__(self): - do_something() + print("AA init") -class AB(AA): +class AB(AA): # $ Alert - #Don't call super class init - def __init__(self): - do_something() + # Doesn't call super class init + def __init__(self): + print("AB init") class AC(AB): def __init__(self): - #Missing call to AA.__init__ but not AC's fault. + # Doesn't call AA init, but we don't alert here as the issue is with AB. + print("AC init") super(AC, self).__init__() +AC() + import six import abc class BA(object): def __init__(self): - do_something() + print("BA init") @six.add_metaclass(abc.ABCMeta) class BB(BA): def __init__(self): + print("BB init") super(BB,self).__init__() +BB() + @six.add_metaclass(abc.ABCMeta) class CA(object): def __init__(self): - do_something() + print("CA init") -class CB(BA): +class CB(CA): def __init__(self): + print("CB init") super(CB,self).__init__() +CB() + #ODASA-5799 class DA(object): def __init__(self): - do_something() + print("DA init") class DB(DA): - class DC(DA): + class DC(DA): # $ SPURIOUS: Alert # We only consider direct super calls, so have an FP here - def __init__(self): + def __init__(self): + print("DC init") sup = super(DB.DC, self) sup.__init__() +DB.DC() + #Simpler variants -class DD(DA): +class DD(DA): # $ SPURIOUS: Alert # We only consider direct super calls, so have an FP here - def __init__(self): + def __init__(self): + print("DD init") sup = super(DD, self) sup.__init__() +DD() + class DE(DA): - class DF(DA): + class DF(DA): # No alert here - def __init__(self): + def __init__(self): + print("DF init") sup = super(DE.DF, self).__init__() +DE.DF() + class FA(object): def __init__(self): - pass + pass # does nothing, thus is considered a trivial method and ok to not call class FB(object): def __init__(self): - do_something() + print("FB init") class FC(FA, FB): def __init__(self): - #OK to skip call to FA.__init__ as that does nothing. + # No alert here - ok to skip call to trivial FA init FB.__init__(self) #Potential false positives. class ConfusingInit(B1): - def __init__(self): + def __init__(self): # We track this correctly and don't alert. super_call = super(ConfusingInit, self).__init__ super_call() -# Library class -import collections - -class G1(collections.Counter): - +class G1: def __init__(self): - collections.Counter.__init__(self) - -class G2(G1): + print("G1 init") +class G2: def __init__(self): - super(G2, self).__init__() + print("G2 init") -class G3(collections.Counter): - - def __init__(self): - super(G3, self).__init__() - -class G4(G3): - - def __init__(self): - G3.__init__(self) +class G3(G1, G2): + def __init__(self): + print("G3 init") + for cls in self.__class__.__bases__: + cls.__init__(self) # We dont track which classes this could refer to, but assume it calls all required init methods and don't alert. From 73057d3b6fbe2667afa18f42559e077e7e78bbf8 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Thu, 3 Jul 2025 15:56:28 +0100 Subject: [PATCH 12/22] Add additional test case + update missing del tests --- .../missing-del/MissingCallToDel.expected | 2 +- .../Classes/missing-del/missing_del.py | 9 +++++++-- .../missing-init/MissingCallToInit.expected | 11 ++++++----- .../Classes/missing-init/missing_init.py | 16 ++++++++++++++++ 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/python/ql/test/query-tests/Classes/missing-del/MissingCallToDel.expected b/python/ql/test/query-tests/Classes/missing-del/MissingCallToDel.expected index 7f080b1d729b..80e02da0e132 100644 --- a/python/ql/test/query-tests/Classes/missing-del/MissingCallToDel.expected +++ b/python/ql/test/query-tests/Classes/missing-del/MissingCallToDel.expected @@ -1 +1 @@ -| missing_del.py:12:1:12:13 | class X3 | Class X3 may not be cleaned up properly as $@ is not called during deletion. | missing_del.py:9:5:9:22 | Function __del__ | method X2.__del__ | +| missing_del.py:13:1:13:13 | Class X3 | This class does not call $@ during destruction. ($@ may be missing a call to a base class __del__) | missing_del.py:9:5:9:22 | Function __del__ | X2.__del__ | missing_del.py:15:5:15:22 | Function __del__ | X3.__del__ | diff --git a/python/ql/test/query-tests/Classes/missing-del/missing_del.py b/python/ql/test/query-tests/Classes/missing-del/missing_del.py index 5d4e30e681d2..6548bb1fa3b4 100644 --- a/python/ql/test/query-tests/Classes/missing-del/missing_del.py +++ b/python/ql/test/query-tests/Classes/missing-del/missing_del.py @@ -2,14 +2,19 @@ class X1(object): def __del__(self): - pass + print("X1 del") class X2(X1): def __del__(self): + print("X2 del") X1.__del__(self) -class X3(X2): +class X3(X2): # $ Alert - skips X2 del def __del__(self): + print("X3 del") X1.__del__(self) + +a = X3() +del a diff --git a/python/ql/test/query-tests/Classes/missing-init/MissingCallToInit.expected b/python/ql/test/query-tests/Classes/missing-init/MissingCallToInit.expected index 90ca4bf49e77..c0f35be3ff9b 100644 --- a/python/ql/test/query-tests/Classes/missing-init/MissingCallToInit.expected +++ b/python/ql/test/query-tests/Classes/missing-init/MissingCallToInit.expected @@ -1,5 +1,6 @@ -| missing_init.py:14:5:14:23 | Function __init__ | This initialization method does not call $@, which may leave $@ partially initialized. | missing_init.py:9:5:9:23 | Function __init__ | B2.__init__ | missing_init.py:13:1:13:13 | Class B3 | B3 | -| missing_init.py:29:5:29:23 | Function __init__ | This initialization method does not call super().__init__, which may cause $@ to be missed during the initialization of $@. | missing_init.py:33:5:33:23 | Function __init__ | UT.__init__ | missing_init.py:42:1:42:21 | Class IUVT | IUVT | -| missing_init.py:70:5:70:23 | Function __init__ | This initialization method does not call $@, which may leave $@ partially initialized. | missing_init.py:64:5:64:23 | Function __init__ | AA.__init__ | missing_init.py:67:1:67:13 | Class AB | AB | -| missing_init.py:124:9:124:27 | Function __init__ | This initialization method does not call $@, which may leave $@ partially initialized. | missing_init.py:117:5:117:23 | Function __init__ | DA.__init__ | missing_init.py:122:5:122:17 | Class DC | DC | -| missing_init.py:134:5:134:23 | Function __init__ | This initialization method does not call $@, which may leave $@ partially initialized. | missing_init.py:117:5:117:23 | Function __init__ | DA.__init__ | missing_init.py:132:1:132:13 | Class DD | DD | +| missing_init.py:13:1:13:13 | Class B3 | This class does not call $@ during initialization. ($@ may be missing a call to a base class __init__) | missing_init.py:9:5:9:23 | Function __init__ | B2.__init__ | missing_init.py:14:5:14:23 | Function __init__ | B3.__init__ | +| missing_init.py:42:1:42:21 | Class IUVT | This class does not call $@ during initialization. (The class lacks an __init__ method to ensure every base class __init__ is called.) | missing_init.py:33:5:33:23 | Function __init__ | UT.__init__ | file://:0:0:0:0 | (none) | | +| missing_init.py:67:1:67:13 | Class AB | This class does not call $@ during initialization. ($@ may be missing a call to a base class __init__) | missing_init.py:64:5:64:23 | Function __init__ | AA.__init__ | missing_init.py:70:5:70:23 | Function __init__ | AB.__init__ | +| missing_init.py:122:5:122:17 | Class DC | This class does not call $@ during initialization. ($@ may be missing a call to a base class __init__) | missing_init.py:117:5:117:23 | Function __init__ | DA.__init__ | missing_init.py:124:9:124:27 | Function __init__ | DB.DC.__init__ | +| missing_init.py:132:1:132:13 | Class DD | This class does not call $@ during initialization. ($@ may be missing a call to a base class __init__) | missing_init.py:117:5:117:23 | Function __init__ | DA.__init__ | missing_init.py:134:5:134:23 | Function __init__ | DD.__init__ | +| missing_init.py:200:1:200:17 | Class H3 | This class does not call $@ during initialization. ($@ may be missing a call to super().__init__) | missing_init.py:197:5:197:23 | Function __init__ | H2.__init__ | missing_init.py:193:5:193:23 | Function __init__ | H1.__init__ | diff --git a/python/ql/test/query-tests/Classes/missing-init/missing_init.py b/python/ql/test/query-tests/Classes/missing-init/missing_init.py index 68c5b1fad7b5..68498765f75f 100644 --- a/python/ql/test/query-tests/Classes/missing-init/missing_init.py +++ b/python/ql/test/query-tests/Classes/missing-init/missing_init.py @@ -187,3 +187,19 @@ def __init__(self): for cls in self.__class__.__bases__: cls.__init__(self) # We dont track which classes this could refer to, but assume it calls all required init methods and don't alert. +G3() + +class H1: + def __init__(self): + print("H1 init") + +class H2: + def __init__(self): + print("H2 init") + +class H3(H1, H2): # $ Alert # The alert should also mention that H1.__init__ may be missing a call to super().__init__ + def __init__(self): + print("H3 init") + super().__init__() + +H3() \ No newline at end of file From 2e6f35b29017a50827558213233375bd534ba71d Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Thu, 3 Jul 2025 16:07:17 +0100 Subject: [PATCH 13/22] Remove case excluding classes with a __new__ method; as it doesn't make much sense (__init__ is still called anyway) --- python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql | 1 - 1 file changed, 1 deletion(-) diff --git a/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql index 9081960a75bb..566d8320a29b 100644 --- a/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql +++ b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql @@ -16,7 +16,6 @@ import MethodCallOrder 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, "__init__") and possiblyMissingSuper = getPossibleMissingSuperOption(base, shouldCall, "__init__") and From c5b79fa622b7e6e690cdb11d0457f861104cd483 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 4 Jul 2025 10:30:20 +0100 Subject: [PATCH 14/22] Update multiple calls queries to include call targets in alert message --- .../CallsToInitDel/MethodCallOrder.qll | 38 +++++++++++++++---- .../SuperclassDelCalledMultipleTimes.ql | 33 ++++++++++++---- .../SuperclassInitCalledMultipleTimes.ql | 33 ++++++++++++---- 3 files changed, 81 insertions(+), 23 deletions(-) diff --git a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll index 191243e03328..02d60ca420e0 100644 --- a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll +++ b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll @@ -5,11 +5,14 @@ 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 | +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 - call1.asExpr() != call2.asExpr() and + call1.getLocation().toString() < call2.getLocation().toString() and calledMulti = getASuperCallTargetFromCall(cls, meth, call1, name) and calledMulti = getASuperCallTargetFromCall(cls, meth, call2, name) and nonTrivial(calledMulti) @@ -18,23 +21,44 @@ predicate multipleCallsToSuperclassMethod(Function meth, Function calledMulti, s 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) + ) +} + +Function getDirectSuperCallTargetFromCall( + Class mroBase, Function meth, DataFlow::MethodCallNode call, string name ) { meth = call.getScope() and getADirectSuperclass*(mroBase) = meth.getScope() and meth.getName() = name and call.calls(_, name) and - exists(Class targetCls | result = getASuperCallTargetFromClass(mroBase, targetCls, name) | + 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) + targetCls = getNextClassInMroKnownStartingClass(meth.getScope(), mroBase) and + result = findFunctionAccordingToMroKnownStartingClass(targetCls, mroBase, name) or - callsMethodOnClassWithSelf(meth, call, targetCls, _) + // 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` will sill be used + // assuming it's 0-arg or is 2-arg with `self` as second arg. + callsMethodOnClassWithSelf(meth, call, targetCls, _) and + result = findFunctionAccordingToMroKnownStartingClass(targetCls, targetCls, name) ) } 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)) + ( + result = target + or + result = getASuperCallTargetFromCall(mroBase, target, _, name) + ) ) } diff --git a/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql b/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql index 7aca3dee1892..7772aa153730 100644 --- a/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql +++ b/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql @@ -14,17 +14,34 @@ import python import MethodCallOrder -predicate multipleCallsToSuperclassDel(Function meth, Function calledMulti) { - multipleCallsToSuperclassMethod(meth, calledMulti, "__del__") +predicate multipleCallsToSuperclassDel( + Function meth, Function calledMulti, DataFlow::MethodCallNode call1, + DataFlow::MethodCallNode call2 +) { + multipleCallsToSuperclassMethod(meth, calledMulti, call1, call2, "__del__") } -from Function meth, Function calledMulti +from + Function meth, Function calledMulti, DataFlow::MethodCallNode call1, + DataFlow::MethodCallNode call2, Function target1, Function target2, string msg where - multipleCallsToSuperclassDel(meth, calledMulti) and - // Don't alert for multiple calls to a superclass del when a subclass will do. + 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 + 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 deletion 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 deletion method calls $@ multiple times, via $@ and $@." ) -select meth, "This delete method calls $@ multiple times.", calledMulti, - calledMulti.getQualifiedName() +select meth, msg, calledMulti, calledMulti.getQualifiedName(), call1, "this call", call2, + "this call", target1, target1.getQualifiedName(), target2, target2.getQualifiedName() diff --git a/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql b/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql index 4f577dc4a764..04c226aa1951 100644 --- a/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql +++ b/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql @@ -14,17 +14,34 @@ import python import MethodCallOrder -predicate multipleCallsToSuperclassInit(Function meth, Function calledMulti) { - multipleCallsToSuperclassMethod(meth, calledMulti, "__init__") +predicate multipleCallsToSuperclassInit( + Function meth, Function calledMulti, DataFlow::MethodCallNode call1, + DataFlow::MethodCallNode call2 +) { + multipleCallsToSuperclassMethod(meth, calledMulti, call1, call2, "__init__") } -from Function meth, Function calledMulti +from + Function meth, Function calledMulti, DataFlow::MethodCallNode call1, + DataFlow::MethodCallNode call2, Function target1, Function target2, string msg where - multipleCallsToSuperclassInit(meth, calledMulti) and - // Don't alert for multiple calls to a superclass init when a subclass will do. + 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 + 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, "This initialization method calls $@ multiple times.", calledMulti, - calledMulti.getQualifiedName() +select meth, msg, calledMulti, calledMulti.getQualifiedName(), call1, "this call", call2, + "this call", target1, target1.getQualifiedName(), target2, target2.getQualifiedName() From 804b9ef941447cbdef5c0a4f8a668b68bdb88163 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 4 Jul 2025 11:08:33 +0100 Subject: [PATCH 15/22] Update tests and add an additional test --- .../SuperclassDelCalledMultipleTimes.expected | 4 ++-- ...SuperclassInitCalledMultipleTimes.expected | 5 +++-- .../multiple/multiple-init/multiple_init.py | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.expected b/python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.expected index ad9858397dff..0df0670b2191 100644 --- a/python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.expected +++ b/python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.expected @@ -1,2 +1,2 @@ -| multiple_del.py:21:5:21:22 | Function __del__ | This delete method calls $@ multiple times. | multiple_del.py:9:5:9:22 | Function __del__ | Y1.__del__ | -| multiple_del.py:43:5:43:22 | Function __del__ | This delete method calls $@ multiple times. | multiple_del.py:32:5:32:22 | Function __del__ | Z1.__del__ | +| multiple_del.py:21:5:21:22 | Function __del__ | This deletion method calls $@ multiple times, via $@ and $@, resolving to $@ and $@ respectively. | multiple_del.py:9:5:9:22 | Function __del__ | Y1.__del__ | multiple_del.py:23:9:23:24 | ControlFlowNode for Attribute() | this call | multiple_del.py:24:9:24:24 | ControlFlowNode for Attribute() | this call | multiple_del.py:9:5:9:22 | Function __del__ | Y1.__del__ | multiple_del.py:15:5:15:22 | Function __del__ | Y2.__del__ | +| multiple_del.py:43:5:43:22 | Function __del__ | This deletion method calls $@ multiple times, via $@ and $@, resolving to $@ and $@ respectively. | multiple_del.py:32:5:32:22 | Function __del__ | Z1.__del__ | multiple_del.py:45:9:45:24 | ControlFlowNode for Attribute() | this call | multiple_del.py:46:9:46:24 | ControlFlowNode for Attribute() | this call | multiple_del.py:32:5:32:22 | Function __del__ | Z1.__del__ | multiple_del.py:37:5:37:22 | Function __del__ | Z2.__del__ | diff --git a/python/ql/test/query-tests/Classes/multiple/multiple-init/SuperclassInitCalledMultipleTimes.expected b/python/ql/test/query-tests/Classes/multiple/multiple-init/SuperclassInitCalledMultipleTimes.expected index 42d019e7f710..e2a0934e9027 100644 --- a/python/ql/test/query-tests/Classes/multiple/multiple-init/SuperclassInitCalledMultipleTimes.expected +++ b/python/ql/test/query-tests/Classes/multiple/multiple-init/SuperclassInitCalledMultipleTimes.expected @@ -1,2 +1,3 @@ -| multiple_init.py:21:5:21:23 | Function __init__ | This initialization method calls $@ multiple times. | multiple_init.py:9:5:9:23 | Function __init__ | C1.__init__ | -| multiple_init.py:42:5:42:23 | Function __init__ | This initialization method calls $@ multiple times. | multiple_init.py:31:5:31:23 | Function __init__ | D1.__init__ | +| multiple_init.py:21:5:21:23 | Function __init__ | This initialization method calls $@ multiple times, via $@ and $@, resolving to $@ and $@ respectively. | multiple_init.py:9:5:9:23 | Function __init__ | C1.__init__ | multiple_init.py:23:9:23:25 | ControlFlowNode for Attribute() | this call | multiple_init.py:24:9:24:25 | ControlFlowNode for Attribute() | this call | multiple_init.py:9:5:9:23 | Function __init__ | C1.__init__ | multiple_init.py:15:5:15:23 | Function __init__ | C2.__init__ | +| multiple_init.py:42:5:42:23 | Function __init__ | This initialization method calls $@ multiple times, via $@ and $@, resolving to $@ and $@ respectively. | multiple_init.py:31:5:31:23 | Function __init__ | D1.__init__ | multiple_init.py:44:9:44:25 | ControlFlowNode for Attribute() | this call | multiple_init.py:45:9:45:25 | ControlFlowNode for Attribute() | this call | multiple_init.py:31:5:31:23 | Function __init__ | D1.__init__ | multiple_init.py:36:5:36:23 | Function __init__ | D2.__init__ | +| multiple_init.py:84:5:84:23 | Function __init__ | This initialization method calls $@ multiple times, via $@ and $@, resolving to $@ and $@ respectively. | multiple_init.py:80:5:80:23 | Function __init__ | F3.__init__ | multiple_init.py:86:9:86:25 | ControlFlowNode for Attribute() | this call | multiple_init.py:87:9:87:25 | ControlFlowNode for Attribute() | this call | multiple_init.py:75:5:75:23 | Function __init__ | F2.__init__ | multiple_init.py:80:5:80:23 | Function __init__ | F3.__init__ | diff --git a/python/ql/test/query-tests/Classes/multiple/multiple-init/multiple_init.py b/python/ql/test/query-tests/Classes/multiple/multiple-init/multiple_init.py index cba8b24523fa..59efb28e691e 100644 --- a/python/ql/test/query-tests/Classes/multiple/multiple-init/multiple_init.py +++ b/python/ql/test/query-tests/Classes/multiple/multiple-init/multiple_init.py @@ -68,3 +68,22 @@ def __init__(self): # OK: builtin init methods are fine. E3() +class F1: + pass + +class F2(F1): + def __init__(self): + print("F2 init") + super().__init__() + +class F3(F1): + def __init__(self): + print("F3 init") + +class F4(F2, F3): + def __init__(self): # $ Alert # F2's super call calls F3 + print("F4 init") + F2.__init__(self) + F3.__init__(self) + +F4() \ No newline at end of file From 6ca4f32ce0702415c0b94ec4a14444393e10f892 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 4 Jul 2025 11:32:39 +0100 Subject: [PATCH 16/22] qhelp: move examples to subfolder --- .../Classes/CallsToInitDel/{ => examples}/MissingCallToDel.py | 0 .../CallsToInitDel/{ => examples}/MissingCallToInit.py | 4 ++-- .../{ => examples}/SuperclassDelCalledMultipleTimes.py | 0 .../{ => examples}/SuperclassDelCalledMultipleTimes2.py | 0 .../{ => examples}/SuperclassInitCalledMultipleTimes.py | 0 .../{ => examples}/SuperclassInitCalledMultipleTimes2.py | 0 6 files changed, 2 insertions(+), 2 deletions(-) rename python/ql/src/Classes/CallsToInitDel/{ => examples}/MissingCallToDel.py (100%) rename python/ql/src/Classes/CallsToInitDel/{ => examples}/MissingCallToInit.py (85%) rename python/ql/src/Classes/CallsToInitDel/{ => examples}/SuperclassDelCalledMultipleTimes.py (100%) rename python/ql/src/Classes/CallsToInitDel/{ => examples}/SuperclassDelCalledMultipleTimes2.py (100%) rename python/ql/src/Classes/CallsToInitDel/{ => examples}/SuperclassInitCalledMultipleTimes.py (100%) rename python/ql/src/Classes/CallsToInitDel/{ => examples}/SuperclassInitCalledMultipleTimes2.py (100%) diff --git a/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.py b/python/ql/src/Classes/CallsToInitDel/examples/MissingCallToDel.py similarity index 100% rename from python/ql/src/Classes/CallsToInitDel/MissingCallToDel.py rename to python/ql/src/Classes/CallsToInitDel/examples/MissingCallToDel.py diff --git a/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.py b/python/ql/src/Classes/CallsToInitDel/examples/MissingCallToInit.py similarity index 85% rename from python/ql/src/Classes/CallsToInitDel/MissingCallToInit.py rename to python/ql/src/Classes/CallsToInitDel/examples/MissingCallToInit.py index 1b3e0e3aee59..14d7c5a80fd3 100644 --- a/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.py +++ b/python/ql/src/Classes/CallsToInitDel/examples/MissingCallToInit.py @@ -10,14 +10,14 @@ def __init__(self): 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): diff --git a/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.py b/python/ql/src/Classes/CallsToInitDel/examples/SuperclassDelCalledMultipleTimes.py similarity index 100% rename from python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.py rename to python/ql/src/Classes/CallsToInitDel/examples/SuperclassDelCalledMultipleTimes.py diff --git a/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes2.py b/python/ql/src/Classes/CallsToInitDel/examples/SuperclassDelCalledMultipleTimes2.py similarity index 100% rename from python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes2.py rename to python/ql/src/Classes/CallsToInitDel/examples/SuperclassDelCalledMultipleTimes2.py diff --git a/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.py b/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimes.py similarity index 100% rename from python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.py rename to python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimes.py diff --git a/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes2.py b/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimes2.py similarity index 100% rename from python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes2.py rename to python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimes2.py From 2e5f470d742dbe36eb6bd4b2945c699bca181c72 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 4 Jul 2025 15:44:37 +0100 Subject: [PATCH 17/22] Update qhelp + alert messages --- .../CallsToInitDel/MissingCallToDel.qhelp | 38 ++++++------ .../CallsToInitDel/MissingCallToDel.ql | 6 +- .../CallsToInitDel/MissingCallToInit.qhelp | 40 ++++++------- .../SuperclassDelCalledMultipleTimes.qhelp | 49 +++++++-------- .../SuperclassDelCalledMultipleTimes.ql | 4 +- .../SuperclassInitCalledMultipleTimes.qhelp | 60 ++++++++++++------- .../examples/MissingCallToDel.py | 4 +- .../SuperclassDelCalledMultipleTimes.py | 21 ++++--- .../SuperclassDelCalledMultipleTimes2.py | 32 ---------- .../SuperclassInitCalledMultipleTimes.py | 36 ----------- .../SuperclassInitCalledMultipleTimes2.py | 38 ------------ .../SuperclassInitCalledMultipleTimesBad1.py | 20 +++++++ .../SuperclassInitCalledMultipleTimesBad3.py | 22 +++++++ .../SuperclassInitCalledMultipleTimesGood2.py | 22 +++++++ .../missing-del/MissingCallToDel.expected | 2 +- .../SuperclassDelCalledMultipleTimes.expected | 4 +- 16 files changed, 186 insertions(+), 212 deletions(-) delete mode 100644 python/ql/src/Classes/CallsToInitDel/examples/SuperclassDelCalledMultipleTimes2.py delete mode 100644 python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimes.py delete mode 100644 python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimes2.py create mode 100644 python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimesBad1.py create mode 100644 python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimesBad3.py create mode 100644 python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimesGood2.py diff --git a/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.qhelp b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.qhelp index 864ddd1b56b8..a9897d3c682e 100644 --- a/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.qhelp +++ b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.qhelp @@ -4,47 +4,49 @@ -

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 __del__ methods that need to be called. +

+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 __del__ +methods are called.

-If the __del__ method of a superclass is not called during object destruction it is likely that +Classes with a __del__ method (a finalizer) typically hold some resource such as a file handle that needs to be cleaned up. +If the __del__ method of a superclass is not called during object finalization, it is likely that that resources may be leaked.

-

A call to the __del__ method of a superclass during object destruction may be omitted: +

A call to the __init__ method of a superclass during object initialization may be unintentionally skipped:

    -
  • When a subclass calls the __del__ method of the wrong class.
  • -
  • When a call to the __del__ method of one its base classes is omitted.
  • +
  • If a subclass calls the __del__ method of the wrong class.
  • +
  • If a call to the __del__ method of one its base classes is omitted.
  • +
  • If a call to super().__del__ is used, but not all __del__ methods in the Method Resolution Order (MRO) + chain themselves call super(). This in particular arises more often in cases of multiple inheritance.
-

Either be careful to explicitly call the __del__ of the correct base class, or -use super() throughout the inheritance hierarchy.

- -

Alternatively refactor one or more of the classes to use composition rather than inheritance.

+

Ensure that all superclass __del__ methods are properly called. +Either each base class's finalize method should be explicitly called, or super() calls +should be consistently used throughout the inheritance hierarchy.

-

In this example, explicit calls to __del__ are used, but SportsCar erroneously calls +

In the following example, explicit calls to __del__ are used, but SportsCar erroneously calls Vehicle.__del__. This is fixed in FixedSportsCar by calling Car.__del__.

- +
-
  • Python Tutorial: Classes.
  • -
  • Python Standard Library: super.
  • -
  • Artima Developer: Things to Know About Python Super.
  • -
  • Wikipedia: Composition over inheritance.
  • +
  • Python Reference: __del__.
  • +
  • Python Standard Library: super.
  • +
  • Python Glossary: Method resolution order.
  • diff --git a/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql index a6ade1f1d312..7d8e11b569d7 100644 --- a/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql +++ b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql @@ -30,18 +30,18 @@ where not possiblyMissingSuper.isNone() and possibleIssue = possiblyMissingSuper and msg = - "This class does not call $@ during destruction. ($@ may be missing a call to super().__del__)" + "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 destruction. ($@ may be missing a call to a base class __del__)" + "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 destruction. (The class lacks an __del__ method to ensure every base class __del__ is called.)" + "This class does not call $@ during finalization. (The class lacks an __del__ method to ensure every base class __del__ is called.)" ) ) ) diff --git a/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.qhelp b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.qhelp index 31ad3d693a5d..76121446f99e 100644 --- a/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.qhelp +++ b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.qhelp @@ -4,49 +4,47 @@ -

    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 __init__ methods that need to be called. +

    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 __init__ +methods are called.

    -If the __init__ method of a superclass is not called during object initialization it is likely that -that object will end up in an incorrect state. +If the __init__ 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.

    -

    A call to the __init__ method of a superclass during object initialization may be omitted: +

    A call to the __init__ method of a superclass during object initialization may be unintentionally skipped:

      -
    • When a subclass calls the __init__ method of the wrong class.
    • -
    • When a call to the __init__ method of one its base classes is omitted.
    • -
    • When multiple inheritance is used and a class inherits from several base classes, - and at least one of those does not use super() in its own __init__ method.
    • +
    • If a subclass calls the __init__ method of the wrong class.
    • +
    • If a call to the __init__ method of one its base classes is omitted.
    • +
    • If a call to super().__init__ is used, but not all __init__ methods in the Method Resolution Order (MRO) + chain themselves call super(). This in particular arises more often in cases of multiple inheritance.
    -

    Either be careful to explicitly call the __init__ of the correct base class, or -use super() throughout the inheritance hierarchy.

    - -

    Alternatively refactor one or more of the classes to use composition rather than inheritance.

    +

    Ensure that all superclass __init__ methods are properly called. +Either each base class's initialize method should be explicitly called, or super() calls +should be consistently used throughout the inheritance hierarchy.

    -

    In this example, explicit calls to __init__ are used, but SportsCar erroneously calls +

    In the following example, explicit calls to __init__ are used, but SportsCar erroneously calls Vehicle.__init__. This is fixed in FixedSportsCar by calling Car.__init__.

    - +
    -
  • Python Tutorial: Classes.
  • -
  • Python Standard Library: super.
  • -
  • Artima Developer: Things to Know About Python Super.
  • -
  • Wikipedia: Composition over inheritance.
  • +
  • Python Reference: __init__.
  • +
  • Python Standard Library: super.
  • +
  • Python Glossary: Method resolution order.
  • diff --git a/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.qhelp b/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.qhelp index d9514b2c68c4..f828cfb8e648 100644 --- a/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.qhelp +++ b/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.qhelp @@ -4,55 +4,52 @@ -

    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 __del__ methods that need to be called. +

    +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.

    -Calling a __del__ method more than once during object destruction risks resources being released multiple -times. The relevant __del__ method may not be designed to be called more than once. +Objects with a __del__ 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.

    There are a number of ways that a __del__ method may be be called more than once.

    • There may be more than one explicit call to the method in the hierarchy of __del__ methods.
    • -
    • A class using multiple inheritance directly calls the __del__ methods of its base types. - One or more of those base types uses super() to pass down the inheritance chain.
    • +
    • 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)
    • +
    • Another situation involving multiple inheritance arises when a subclass calls the __del__ methods of each of its base classes, + one of which calls super().__del__. 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.
    -

    Either be careful not to explicitly call a __del__ method more than once, or -use super() throughout the inheritance hierarchy.

    - -

    Alternatively refactor one or more of the classes to use composition rather than inheritance.

    +

    Ensure that each finalizer method is called exactly once during finalization. +This can be ensured by calling super().__del__ for each finalizer methid in the inheritance chain. +

    -

    In the first example, explicit calls to __del__ are used, but SportsCar erroneously calls -both Vehicle.__del__ and Car.__del__. -This can be fixed by removing the call to Vehicle.__del__, as shown in FixedSportsCar. -

    - - -

    In the second example, there is a mixture of explicit calls to __del__ and calls using super(). -To fix this example, super() should be used throughout. +

    In the following example, there is a mixture of explicit calls to __del__ and calls using super(), resulting in Vehicle.__del__ +being called twice. +FixedSportsCar.__del__ fixes this by using super() consistently with the other delete methods.

    - -
  • Python Tutorial: Classes.
  • -
  • Python Standard Library: super.
  • -
  • Artima Developer: Things to Know About Python Super.
  • -
  • Wikipedia: Composition over inheritance.
  • - + +
  • Python Reference: __del__.
  • +
  • Python Standard Library: super.
  • +
  • Python Glossary: Method resolution order.
  • +
  • Wikipedia: The Diamond Problem.
  • diff --git a/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql b/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql index 7772aa153730..dfdc3545e64f 100644 --- a/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql +++ b/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.ql @@ -36,12 +36,12 @@ where ( target1 != target2 and msg = - "This deletion method calls $@ multiple times, via $@ and $@, resolving to $@ and $@ respectively." + "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 deletion method calls $@ multiple times, via $@ and $@." + 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() diff --git a/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.qhelp b/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.qhelp index f1140d68b891..d7060adef8d9 100644 --- a/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.qhelp +++ b/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.qhelp @@ -4,54 +4,70 @@ -

    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 __init__ methods that need to be called. +

    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.

    -Calling an __init__ method more than once during object initialization risks the object being incorrectly initialized. -It is unlikely that the relevant __init__ method is designed to be called more than once. +Calling an __init__ 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.

    There are a number of ways that an __init__ method may be be called more than once.

    • There may be more than one explicit call to the method in the hierarchy of __init__ methods.
    • -
    • A class using multiple inheritance directly calls the __init__ methods of its base types. - One or more of those base types uses super() to pass down the inheritance chain.
    • +
    • 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)
    • +
    • Another situation involving multiple inheritance arises when a subclass calls the __init__ methods of each of its base classes, + one of which calls super().__init__. 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.
    -

    Either be careful not to explicitly call an __init__ method more than once, or -use super() throughout the inheritance hierarchy.

    +

    +Take care whenever possible not to call an an initializer multiple times. If each __init__ method in the hierarchy +calls super().__init__(), 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 super() calls in the base classes. +

    +

    +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. +

    -

    Alternatively refactor one or more of the classes to use composition rather than inheritance.

    -

    In the first example, explicit calls to __init__ are used, but SportsCar erroneously calls -both Vehicle.__init__ and Car.__init__. -This can be fixed by removing the call to Vehicle.__init__, as shown in FixedSportsCar. +

    In the following (BAD) example, the class D calls B.__init__ and C.__init__, +which each call A.__init__. This results in self.state being set to None as +A.__init__ is called again after B.__init__ had finished. This may lead to unexpected results.

    - + -

    In the second example, there is a mixture of explicit calls to __init__ and calls using super(). -To fix this example, super() should be used throughout. +

    In the following (GOOD) example, a call to super().__init__ is made in each class +in the inheritance hierarchy, ensuring each initializer is called exactly once.

    - + + +

    In the following (BAD) example, explicit base class calls are mixed with super() calls, and C.__init__ is called twice.

    +
    -
  • Python Tutorial: Classes.
  • -
  • Python Standard Library: super.
  • -
  • Artima Developer: Things to Know About Python Super.
  • -
  • Wikipedia: Composition over inheritance.
  • +
  • Python Reference: __init__.
  • +
  • Python Standard Library: super.
  • +
  • Python Glossary: Method resolution order.
  • +
  • Wikipedia: The Diamond Problem.
  • diff --git a/python/ql/src/Classes/CallsToInitDel/examples/MissingCallToDel.py b/python/ql/src/Classes/CallsToInitDel/examples/MissingCallToDel.py index 37520071b3ae..296d36be7d8f 100644 --- a/python/ql/src/Classes/CallsToInitDel/examples/MissingCallToDel.py +++ b/python/ql/src/Classes/CallsToInitDel/examples/MissingCallToDel.py @@ -10,14 +10,14 @@ def __del__(self): 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): diff --git a/python/ql/src/Classes/CallsToInitDel/examples/SuperclassDelCalledMultipleTimes.py b/python/ql/src/Classes/CallsToInitDel/examples/SuperclassDelCalledMultipleTimes.py index 0ee6e61bcb1a..f48f325f8b57 100644 --- a/python/ql/src/Classes/CallsToInitDel/examples/SuperclassDelCalledMultipleTimes.py +++ b/python/ql/src/Classes/CallsToInitDel/examples/SuperclassDelCalledMultipleTimes.py @@ -1,29 +1,32 @@ -#Calling a method multiple times by using explicit calls when a base inherits from other base + +#Calling a method multiple times by using explicit calls when a base uses super() class Vehicle(object): - + def __del__(self): recycle(self.base_parts) - + super(Vehicle, self).__del__() class Car(Vehicle): def __del__(self): recycle(self.car_parts) - Vehicle.__del__(self) - - + super(Car, self).__del__() + + 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 only calling Car.__del__ +# GOOD: super() is used ensuring each del method is called once. class FixedSportsCar(Car, Vehicle): def __del__(self): recycle(self.sports_car_parts) - Car.__del__(self) + super(SportsCar, self).__del__() + + diff --git a/python/ql/src/Classes/CallsToInitDel/examples/SuperclassDelCalledMultipleTimes2.py b/python/ql/src/Classes/CallsToInitDel/examples/SuperclassDelCalledMultipleTimes2.py deleted file mode 100644 index 8cb1433ac0c4..000000000000 --- a/python/ql/src/Classes/CallsToInitDel/examples/SuperclassDelCalledMultipleTimes2.py +++ /dev/null @@ -1,32 +0,0 @@ - -#Calling a method multiple times by using explicit calls when a base uses super() -class Vehicle(object): - - def __del__(self): - recycle(self.base_parts) - super(Vehicle, self).__del__() - -class Car(Vehicle): - - def __del__(self): - recycle(self.car_parts) - super(Car, self).__del__() - - -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 using super() -class FixedSportsCar(Car, Vehicle): - - def __del__(self): - recycle(self.sports_car_parts) - super(SportsCar, self).__del__() - - diff --git a/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimes.py b/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimes.py deleted file mode 100644 index 050d5d389d61..000000000000 --- a/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimes.py +++ /dev/null @@ -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 - diff --git a/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimes2.py b/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimes2.py deleted file mode 100644 index e12e86a079ee..000000000000 --- a/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimes2.py +++ /dev/null @@ -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 - diff --git a/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimesBad1.py b/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimesBad1.py new file mode 100644 index 000000000000..0f595a534dac --- /dev/null +++ b/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimesBad1.py @@ -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. + diff --git a/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimesBad3.py b/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimesBad3.py new file mode 100644 index 000000000000..9a70876e7a85 --- /dev/null +++ b/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimesBad3.py @@ -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 \ No newline at end of file diff --git a/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimesGood2.py b/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimesGood2.py new file mode 100644 index 000000000000..ab8d98116aaf --- /dev/null +++ b/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimesGood2.py @@ -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.stat =e will be set to "B". + super().__init__() + self.d = 1 + + + diff --git a/python/ql/test/query-tests/Classes/missing-del/MissingCallToDel.expected b/python/ql/test/query-tests/Classes/missing-del/MissingCallToDel.expected index 80e02da0e132..2ec5e1352584 100644 --- a/python/ql/test/query-tests/Classes/missing-del/MissingCallToDel.expected +++ b/python/ql/test/query-tests/Classes/missing-del/MissingCallToDel.expected @@ -1 +1 @@ -| missing_del.py:13:1:13:13 | Class X3 | This class does not call $@ during destruction. ($@ may be missing a call to a base class __del__) | missing_del.py:9:5:9:22 | Function __del__ | X2.__del__ | missing_del.py:15:5:15:22 | Function __del__ | X3.__del__ | +| missing_del.py:13:1:13:13 | Class X3 | This class does not call $@ during finalization. ($@ may be missing a call to a base class __del__) | missing_del.py:9:5:9:22 | Function __del__ | X2.__del__ | missing_del.py:15:5:15:22 | Function __del__ | X3.__del__ | diff --git a/python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.expected b/python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.expected index 0df0670b2191..b7ee48feba78 100644 --- a/python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.expected +++ b/python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.expected @@ -1,2 +1,2 @@ -| multiple_del.py:21:5:21:22 | Function __del__ | This deletion method calls $@ multiple times, via $@ and $@, resolving to $@ and $@ respectively. | multiple_del.py:9:5:9:22 | Function __del__ | Y1.__del__ | multiple_del.py:23:9:23:24 | ControlFlowNode for Attribute() | this call | multiple_del.py:24:9:24:24 | ControlFlowNode for Attribute() | this call | multiple_del.py:9:5:9:22 | Function __del__ | Y1.__del__ | multiple_del.py:15:5:15:22 | Function __del__ | Y2.__del__ | -| multiple_del.py:43:5:43:22 | Function __del__ | This deletion method calls $@ multiple times, via $@ and $@, resolving to $@ and $@ respectively. | multiple_del.py:32:5:32:22 | Function __del__ | Z1.__del__ | multiple_del.py:45:9:45:24 | ControlFlowNode for Attribute() | this call | multiple_del.py:46:9:46:24 | ControlFlowNode for Attribute() | this call | multiple_del.py:32:5:32:22 | Function __del__ | Z1.__del__ | multiple_del.py:37:5:37:22 | Function __del__ | Z2.__del__ | +| multiple_del.py:21:5:21:22 | Function __del__ | This finalization method calls $@ multiple times, via $@ and $@, resolving to $@ and $@ respectively. | multiple_del.py:9:5:9:22 | Function __del__ | Y1.__del__ | multiple_del.py:23:9:23:24 | ControlFlowNode for Attribute() | this call | multiple_del.py:24:9:24:24 | ControlFlowNode for Attribute() | this call | multiple_del.py:9:5:9:22 | Function __del__ | Y1.__del__ | multiple_del.py:15:5:15:22 | Function __del__ | Y2.__del__ | +| multiple_del.py:43:5:43:22 | Function __del__ | This finalization method calls $@ multiple times, via $@ and $@, resolving to $@ and $@ respectively. | multiple_del.py:32:5:32:22 | Function __del__ | Z1.__del__ | multiple_del.py:45:9:45:24 | ControlFlowNode for Attribute() | this call | multiple_del.py:46:9:46:24 | ControlFlowNode for Attribute() | this call | multiple_del.py:32:5:32:22 | Function __del__ | Z1.__del__ | multiple_del.py:37:5:37:22 | Function __del__ | Z2.__del__ | From d2c68dedc9f09aaf6a5b1881abd3d2a133e2092e Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 4 Jul 2025 15:53:02 +0100 Subject: [PATCH 18/22] Update integration test output --- .../query-suite/python-code-quality-extended.qls.expected | 8 ++++---- .../query-suite/python-code-quality.qls.expected | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected b/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected index 960972c508c8..efd34ac3db63 100644 --- a/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected +++ b/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected @@ -1,14 +1,14 @@ +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/ConflictingAttributesInBaseClasses.ql ql/python/ql/src/Classes/DefineEqualsWhenAddingAttributes.ql ql/python/ql/src/Classes/EqualsOrHash.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.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 diff --git a/python/ql/integration-tests/query-suite/python-code-quality.qls.expected b/python/ql/integration-tests/query-suite/python-code-quality.qls.expected index 960972c508c8..efd34ac3db63 100644 --- a/python/ql/integration-tests/query-suite/python-code-quality.qls.expected +++ b/python/ql/integration-tests/query-suite/python-code-quality.qls.expected @@ -1,14 +1,14 @@ +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/ConflictingAttributesInBaseClasses.ql ql/python/ql/src/Classes/DefineEqualsWhenAddingAttributes.ql ql/python/ql/src/Classes/EqualsOrHash.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.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 From f1026e436a14f10d8c48fdb28b0760f0a3de4601 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 4 Jul 2025 16:03:50 +0100 Subject: [PATCH 19/22] Add change note --- .../2025-06-04-missing-multiple-calls-to-init-del.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 python/ql/src/change-notes/2025-06-04-missing-multiple-calls-to-init-del.md diff --git a/python/ql/src/change-notes/2025-06-04-missing-multiple-calls-to-init-del.md b/python/ql/src/change-notes/2025-06-04-missing-multiple-calls-to-init-del.md new file mode 100644 index 000000000000..fe9a0cc110df --- /dev/null +++ b/python/ql/src/change-notes/2025-06-04-missing-multiple-calls-to-init-del.md @@ -0,0 +1,4 @@ +--- +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, and producing more precise results with more descriptive alert messages, and improved documentation. \ No newline at end of file From c47e6e3a0f3c249dab0a547a52c96c0adf07f93e Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 4 Jul 2025 16:26:07 +0100 Subject: [PATCH 20/22] Add qldoc --- .../CallsToInitDel/MethodCallOrder.qll | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll index 02d60ca420e0..321da002fdc8 100644 --- a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll +++ b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll @@ -5,6 +5,7 @@ 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 @@ -19,6 +20,7 @@ predicate multipleCallsToSuperclassMethod( ) } +/** 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 ) { @@ -29,6 +31,7 @@ Function getASuperCallTargetFromCall( ) } +/** 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 ) { @@ -51,6 +54,7 @@ Function getDirectSuperCallTargetFromCall( ) } +/** Gets a method that is transitively called by a call to `cls.`, 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 @@ -62,6 +66,7 @@ Function getASuperCallTargetFromClass(Class mroBase, Class cls, string 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 @@ -74,6 +79,7 @@ predicate nonTrivial(Function meth) { exists(meth.getANormalExit()) // doesn't always raise an exception } +/** Holds if `call` is a call to `super().`. No distinction is made btween 0- and 2- arg super calls. */ predicate superCall(DataFlow::MethodCallNode call, string name) { exists(DataFlow::Node sup | call.calls(sup, name) and @@ -81,6 +87,7 @@ predicate superCall(DataFlow::MethodCallNode call, string name) { ) } +/** Holds if `meth` calls `super().` where `name` is the name of the method. */ predicate callsSuper(Function meth) { exists(DataFlow::MethodCallNode call | call.getScope() = meth and @@ -88,6 +95,7 @@ predicate callsSuper(Function meth) { ) } +/** Holds if `meth` calls `target.(self, ...)` with the call `call`. */ predicate callsMethodOnClassWithSelf( Function meth, DataFlow::MethodCallNode call, Class target, string name ) { @@ -99,6 +107,7 @@ predicate callsMethodOnClassWithSelf( ) } +/** 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 @@ -108,6 +117,7 @@ predicate callsMethodOnUnknownClassWithSelf(Function meth, string name) { ) } +/** 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 @@ -117,6 +127,9 @@ predicate missingCallToSuperclassMethod(Class base, Function shouldCall, string 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 heirarchy for which this applies. + */ predicate missingCallToSuperclassMethodRestricted(Class base, Function shouldCall, string name) { missingCallToSuperclassMethod(base, shouldCall, name) and not exists(Class superBase | @@ -131,6 +144,11 @@ predicate missingCallToSuperclassMethodRestricted(Class base, Function shouldCal ) } +/** + * If `base` contains a `super()` call, gets a method in the inheritence heirarchy 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.`. + * */ Function getPossibleMissingSuper(Class base, Function shouldCall, string name) { missingCallToSuperclassMethod(base, shouldCall, name) and exists(Function baseMethod | @@ -151,6 +169,7 @@ Function getPossibleMissingSuper(Class base, Function shouldCall, string name) { private module FunctionOption = Option; +/** An optional `Function`. */ class FunctionOption extends FunctionOption::Option { /** * Holds if this element is at the specified location. @@ -174,6 +193,7 @@ class FunctionOption extends FunctionOption::Option { endcolumn = 0 } + /** Gets the qualified name of this function. */ string getQualifiedName() { result = this.asSome().getQualifiedName() or @@ -182,6 +202,7 @@ class FunctionOption extends FunctionOption::Option { } } +/** 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) From 4b49ac3c3ef794a7211e0d601cb3f979a84c35ea Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 4 Jul 2025 16:32:49 +0100 Subject: [PATCH 21/22] Fix changenote formatting --- .../2025-06-04-missing-multiple-calls-to-init-del.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/ql/src/change-notes/2025-06-04-missing-multiple-calls-to-init-del.md b/python/ql/src/change-notes/2025-06-04-missing-multiple-calls-to-init-del.md index fe9a0cc110df..5dfe5c2b8413 100644 --- a/python/ql/src/change-notes/2025-06-04-missing-multiple-calls-to-init-del.md +++ b/python/ql/src/change-notes/2025-06-04-missing-multiple-calls-to-init-del.md @@ -1,4 +1,4 @@ --- -minorAnalysis +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, and producing more precise results with more descriptive alert messages, and improved documentation. \ No newline at end of file +* 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. \ No newline at end of file From d163bdf981b6956c7953d9bb66ef5be47cc67ffd Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 4 Jul 2025 16:35:52 +0100 Subject: [PATCH 22/22] Fix typos --- .../CallsToInitDel/MethodCallOrder.qll | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll index 321da002fdc8..6506cb1a1933 100644 --- a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll +++ b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll @@ -31,7 +31,7 @@ Function getASuperCallTargetFromCall( ) } -/** Gets the method called by `meth` named `name` with `call`, with `mroBase` as the type determining the MRO to search. */ +/** 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 ) { @@ -54,7 +54,7 @@ Function getDirectSuperCallTargetFromCall( ) } -/** Gets a method that is transitively called by a call to `cls.`, with `mroBase` as the type determining the MRO to search. */ +/** Gets a method that is transitively called by a call to `cls.`, 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 @@ -79,7 +79,7 @@ predicate nonTrivial(Function meth) { exists(meth.getANormalExit()) // doesn't always raise an exception } -/** Holds if `call` is a call to `super().`. No distinction is made btween 0- and 2- arg super calls. */ +/** Holds if `call` is a call to `super().`. 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 @@ -127,8 +127,9 @@ predicate missingCallToSuperclassMethod(Class base, Function shouldCall, string 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 heirarchy for which this applies. +/** + * 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 @@ -144,11 +145,11 @@ predicate missingCallToSuperclassMethodRestricted(Class base, Function shouldCal ) } -/** - * If `base` contains a `super()` call, gets a method in the inheritence heirarchy of `name` in the MRO of `base` +/** + * 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.`. - * */ + * during a call to `base.`. + */ Function getPossibleMissingSuper(Class base, Function shouldCall, string name) { missingCallToSuperclassMethod(base, shouldCall, name) and exists(Function baseMethod |