From 271f32e80eb28884d6c2759a7581d4f725d12be0 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Mon, 30 Jun 2025 10:21:31 +0100 Subject: [PATCH 01/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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 | From e8a65b8555738570c3205ad6585117e4ad99c169 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Mon, 7 Jul 2025 09:49:23 +0100 Subject: [PATCH 23/27] Update integration test outout and fix qhelp --- .../query-suite/python-security-and-quality.qls.expected | 8 ++++---- .../ql/src/Classes/CallsToInitDel/MissingCallToDel.qhelp | 2 +- .../CallsToInitDel/SuperclassDelCalledMultipleTimes.qhelp | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected b/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected index 170d9f442f92..564304a7b539 100644 --- a/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected +++ b/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected @@ -1,3 +1,7 @@ +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 @@ -5,16 +9,12 @@ ql/python/ql/src/Classes/EqualsOrNotEquals.ql ql/python/ql/src/Classes/IncompleteOrdering.ql ql/python/ql/src/Classes/InconsistentMRO.ql ql/python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql -ql/python/ql/src/Classes/MissingCallToDel.ql -ql/python/ql/src/Classes/MissingCallToInit.ql ql/python/ql/src/Classes/MutatingDescriptor.ql ql/python/ql/src/Classes/OverwritingAttributeInSuperClass.ql ql/python/ql/src/Classes/PropertyInOldStyleClass.ql ql/python/ql/src/Classes/SlotsInOldStyleClass.ql ql/python/ql/src/Classes/SubclassShadowing.ql ql/python/ql/src/Classes/SuperInOldStyleClass.ql -ql/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.ql -ql/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.ql ql/python/ql/src/Classes/WrongNameForArgumentInClassInstantiation.ql ql/python/ql/src/Classes/WrongNumberArgumentsInClassInstantiation.ql ql/python/ql/src/Diagnostics/ExtractedFiles.ql diff --git a/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.qhelp b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.qhelp index a9897d3c682e..e461aebdc028 100644 --- a/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.qhelp +++ b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.qhelp @@ -13,7 +13,7 @@ methods are called.

    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. +resources may be leaked.

    A call to the __init__ method of a superclass during object initialization may be unintentionally skipped: diff --git a/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.qhelp b/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.qhelp index f828cfb8e648..df9c073fcceb 100644 --- a/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.qhelp +++ b/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.qhelp @@ -30,7 +30,7 @@ cleaned up properly as expected.

    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. +This can be ensured by calling super().__del__ for each finalizer method in the inheritance chain.

    @@ -41,7 +41,7 @@ being called twice. FixedSportsCar.__del__ fixes this by using super() consistently with the other delete methods.

    - + From f5066c7050b4d970fd6eab49e5f88e3cc546768f Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Mon, 7 Jul 2025 11:02:48 +0100 Subject: [PATCH 24/27] Remove tostring --- .../ql/src/Classes/CallsToInitDel/MethodCallOrder.qll | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll index 6506cb1a1933..6bb5a5862535 100644 --- a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll +++ b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll @@ -13,13 +13,21 @@ predicate multipleCallsToSuperclassMethod( exists(Class cls | meth.getName() = name and meth.getScope() = cls and - call1.getLocation().toString() < call2.getLocation().toString() and + locationBefore(call1.getLocation(), call2.getLocation()) and calledMulti = getASuperCallTargetFromCall(cls, meth, call1, name) and calledMulti = getASuperCallTargetFromCall(cls, meth, call2, name) and nonTrivial(calledMulti) ) } +/** Holds if l1 comes before l2, assuming they're in the same file. */ +private predicate locationBefore(Location l1, Location l2) { + l1.getStartLine() < l2.getStartLine() + or + l1.getStartLine() = l2.getStartLine() and + l1.getStartColumn() < l2.getStartColumn() +} + /** Gets a method transitively called by `meth` named `name` with `call` that it overrides, with `mroBase` as the type determining the MRO to search. */ Function getASuperCallTargetFromCall( Class mroBase, Function meth, DataFlow::MethodCallNode call, string name From 2c93e2c3697f962b63d8b60bee483e6e841b06b4 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Mon, 14 Jul 2025 13:18:59 +0100 Subject: [PATCH 25/27] Inline locationBefore --- python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll | 1 + 1 file changed, 1 insertion(+) diff --git a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll index 6bb5a5862535..98a1d70915b2 100644 --- a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll +++ b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll @@ -21,6 +21,7 @@ predicate multipleCallsToSuperclassMethod( } /** Holds if l1 comes before l2, assuming they're in the same file. */ +pragma[inline] private predicate locationBefore(Location l1, Location l2) { l1.getStartLine() < l2.getStartLine() or From 7dad89f44b75560fbc6c0c4faa6359f7ef7d0467 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Mon, 14 Jul 2025 13:30:56 +0100 Subject: [PATCH 26/27] Adress review suggestions - cleanups --- python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll index 98a1d70915b2..bd98f3fb0fb7 100644 --- a/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll +++ b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll @@ -45,7 +45,6 @@ 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 mroBase = getADirectSubclass*(meth.getScope()) and @@ -56,7 +55,7 @@ Function getDirectSuperCallTargetFromCall( result = findFunctionAccordingToMroKnownStartingClass(targetCls, mroBase, name) or // targetCls is the mro base for this lookup. - // note however that if the call we find uses super(), that still uses the mro of the instance `self` will sill be used + // note however that if the call we find uses super(), that still uses the mro of the instance `self` // assuming it's 0-arg or is 2-arg with `self` as second arg. callsMethodOnClassWithSelf(meth, call, targetCls, _) and result = findFunctionAccordingToMroKnownStartingClass(targetCls, targetCls, name) @@ -96,7 +95,7 @@ predicate superCall(DataFlow::MethodCallNode call, string name) { ) } -/** Holds if `meth` calls `super().` where `name` is the name of the method. */ +/** Holds if `meth` calls a `super()` method of the same name. */ predicate callsSuper(Function meth) { exists(DataFlow::MethodCallNode call | call.getScope() = meth and @@ -122,7 +121,7 @@ predicate callsMethodOnUnknownClassWithSelf(Function meth, string name) { 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)) + not callTarget = classTracker(any(Class target)) ) } From d2a8e5d65174e36dd6f2c4a4d50305cf3083cdb1 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 18 Jul 2025 14:18:03 +0100 Subject: [PATCH 27/27] Fix typo in example --- .../examples/SuperclassInitCalledMultipleTimesGood2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimesGood2.py b/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimesGood2.py index ab8d98116aaf..9e4bbdea0584 100644 --- a/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimesGood2.py +++ b/python/ql/src/Classes/CallsToInitDel/examples/SuperclassInitCalledMultipleTimesGood2.py @@ -14,7 +14,7 @@ def __init__(self): 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". + def __init__(self): # GOOD: Each method calls super, so each init method runs once. self.state will be set to "B". super().__init__() self.d = 1 pFad - Phonifier reborn

    Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

    Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


    Alternative Proxies:

    Alternative Proxy

    pFad Proxy

    pFad v3 Proxy

    pFad v4 Proxy