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 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 new file mode 100644 index 000000000000..6506cb1a1933 --- /dev/null +++ b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll @@ -0,0 +1,213 @@ +/** Definitions for reasoning about multiple or missing calls to superclass methods. */ + +import python +import semmle.python.ApiGraphs +import semmle.python.dataflow.new.internal.DataFlowDispatch +import codeql.util.Option + +/** Holds if `meth` is a method named `name` that transitively calls `calledMulti` of the same name via the calls `call1` and `call2`. */ +predicate multipleCallsToSuperclassMethod( + Function meth, Function calledMulti, DataFlow::MethodCallNode call1, + DataFlow::MethodCallNode call2, string name +) { + exists(Class cls | + meth.getName() = name and + meth.getScope() = cls and + call1.getLocation().toString() < call2.getLocation().toString() and + calledMulti = getASuperCallTargetFromCall(cls, meth, call1, name) and + calledMulti = getASuperCallTargetFromCall(cls, meth, call2, name) and + nonTrivial(calledMulti) + ) +} + +/** Gets a method transitively called by `meth` named `name` with `call` that it overrides, with `mroBase` as the type determining the MRO to search. */ +Function getASuperCallTargetFromCall( + Class mroBase, Function meth, DataFlow::MethodCallNode call, string name +) { + exists(Function target | target = getDirectSuperCallTargetFromCall(mroBase, meth, call, name) | + result = target + or + result = getASuperCallTargetFromCall(mroBase, target, _, name) + ) +} + +/** Gets the method called by `meth` named `name` with `call`, with `mroBase` as the type determining the MRO to search. */ +Function getDirectSuperCallTargetFromCall( + Class mroBase, Function meth, DataFlow::MethodCallNode call, string name +) { + meth = call.getScope() and + getADirectSuperclass*(mroBase) = meth.getScope() and + meth.getName() = name and + call.calls(_, name) and + mroBase = getADirectSubclass*(meth.getScope()) and + exists(Class targetCls | + // the differences between 0-arg and 2-arg super is not considered; we assume each super uses the mro of the instance `self` + superCall(call, _) and + targetCls = getNextClassInMroKnownStartingClass(meth.getScope(), mroBase) and + result = findFunctionAccordingToMroKnownStartingClass(targetCls, mroBase, name) + or + // targetCls is the mro base for this lookup. + // note however that if the call we find uses super(), that still uses the mro of the instance `self` 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) + ) +} + +/** 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 + ( + result = target + or + result = getASuperCallTargetFromCall(mroBase, target, _, name) + ) + ) +} + +/** Holds if `meth` does something besides calling a superclass method. */ +predicate nonTrivial(Function meth) { + exists(Stmt s | s = meth.getAStmt() | + not s instanceof Pass and + not exists(DataFlow::Node call | call.asExpr() = s.(ExprStmt).getValue() | + superCall(call, meth.getName()) + or + callsMethodOnClassWithSelf(meth, call, _, meth.getName()) + ) + ) and + exists(meth.getANormalExit()) // doesn't always raise an exception +} + +/** Holds if `call` is a call to `super().`. No distinction is made between 0- and 2- arg super calls. */ +predicate superCall(DataFlow::MethodCallNode call, string name) { + exists(DataFlow::Node sup | + call.calls(sup, name) and + sup = API::builtin("super").getACall() + ) +} + +/** Holds if `meth` calls `super().` where `name` is the name of the method. */ +predicate callsSuper(Function meth) { + exists(DataFlow::MethodCallNode call | + call.getScope() = meth and + superCall(call, meth.getName()) + ) +} + +/** Holds if `meth` calls `target.(self, ...)` with the call `call`. */ +predicate callsMethodOnClassWithSelf( + Function meth, DataFlow::MethodCallNode call, Class target, string name +) { + exists(DataFlow::Node callTarget, DataFlow::ParameterNode self | + call.calls(callTarget, name) and + self.getParameter() = meth.getArg(0) and + self.(DataFlow::LocalSourceNode).flowsTo(call.getArg(0)) and + callTarget = classTracker(target) + ) +} + +/** Holds if `meth` calls a method named `name` passing its `self` argument as its first parameter, but the class it refers to is unknown. */ +predicate callsMethodOnUnknownClassWithSelf(Function meth, string name) { + exists(DataFlow::MethodCallNode call, DataFlow::Node callTarget, DataFlow::ParameterNode self | + call.calls(callTarget, name) and + self.getParameter() = meth.getArg(0) and + self.(DataFlow::LocalSourceNode).flowsTo(call.getArg(0)) and + not exists(Class target | callTarget = classTracker(target)) + ) +} + +/** Holds if `base` does not call a superclass method `shouldCall` named `name` when it appears it should. */ +predicate missingCallToSuperclassMethod(Class base, Function shouldCall, string name) { + shouldCall.getName() = name and + shouldCall.getScope() = getADirectSuperclass+(base) and + not shouldCall = getASuperCallTargetFromClass(base, base, name) and + nonTrivial(shouldCall) and + // "Benefit of the doubt" - if somewhere in the chain we call an unknown superclass, assume all the necessary parent methods are called from it + not callsMethodOnUnknownClassWithSelf(getASuperCallTargetFromClass(base, base, name), name) +} + +/** + * Holds if `base` does not call a superclass method `shouldCall` named `name` when it appears it should. + * Results are restricted to hold only for the highest `base` class and the lowest `shouldCall` method in the hierarchy for which this applies. + */ +predicate missingCallToSuperclassMethodRestricted(Class base, Function shouldCall, string name) { + missingCallToSuperclassMethod(base, shouldCall, name) and + not exists(Class superBase | + // Alert only on the highest base class that has the issue + superBase = getADirectSuperclass+(base) and + missingCallToSuperclassMethod(superBase, shouldCall, name) + ) and + not exists(Function subShouldCall | + // Mention in the alert only the lowest method we're missing the call to + subShouldCall.getScope() = getADirectSubclass+(shouldCall.getScope()) and + missingCallToSuperclassMethod(base, subShouldCall, name) + ) +} + +/** + * If `base` contains a `super()` call, gets a method in the inheritance hierarchy of `name` in the MRO of `base` + * that does not contain a `super()` call, but would call `shouldCall` if it did, which does not otherwise get called + * during a call to `base.`. + */ +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; + +/** An optional `Function`. */ +class FunctionOption extends FunctionOption::Option { + /** + * Holds if this element is at the specified location. + * The location spans column `startcolumn` of line `startline` to + * column `endcolumn` of line `endline` in file `filepath`. + * For more information, see + * [Locations](https://codeql.github.com/docs/writing-codeql-queries/providing-locations-in-codeql-queries/). + */ + predicate hasLocationInfo( + string filepath, int startline, int startcolumn, int endline, int endcolumn + ) { + this.asSome() + .getLocation() + .hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn) + or + this.isNone() and + filepath = "" and + startline = 0 and + startcolumn = 0 and + endline = 0 and + endcolumn = 0 + } + + /** Gets the qualified name of this function. */ + string getQualifiedName() { + result = this.asSome().getQualifiedName() + or + this.isNone() and + result = "" + } +} + +/** Gets the result of `getPossibleMissingSuper`, or None if none exists. */ +bindingset[name] +FunctionOption getPossibleMissingSuperOption(Class base, Function shouldCall, string name) { + result.asSome() = getPossibleMissingSuper(base, shouldCall, name) + or + not exists(getPossibleMissingSuper(base, shouldCall, name)) and + result.isNone() +} diff --git a/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.qhelp b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.qhelp new file mode 100644 index 000000000000..a9897d3c682e --- /dev/null +++ b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.qhelp @@ -0,0 +1,52 @@ + + + + +

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

+

+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 __init__ method of a superclass during object initialization may be unintentionally skipped: +

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

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 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 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 new file mode 100644 index 000000000000..7d8e11b569d7 --- /dev/null +++ b/python/ql/src/Classes/CallsToInitDel/MissingCallToDel.ql @@ -0,0 +1,49 @@ +/** + * @name Missing call to superclass `__del__` during object destruction + * @description An omitted call to a superclass `__del__` method may lead to class instances not being cleaned up properly. + * @kind problem + * @tags quality + * reliability + * correctness + * performance + * @problem.severity error + * @sub-severity low + * @precision high + * @id py/missing-call-to-delete + */ + +import python +import MethodCallOrder + +Function getDelMethod(Class c) { + result = c.getAMethod() and + result.getName() = "__del__" +} + +from Class base, Function shouldCall, FunctionOption possibleIssue, string msg +where + not exists(Function newMethod | newMethod = base.getAMethod() and newMethod.getName() = "__new__") and + exists(FunctionOption possiblyMissingSuper | + missingCallToSuperclassMethodRestricted(base, shouldCall, "__del__") and + possiblyMissingSuper = getPossibleMissingSuperOption(base, shouldCall, "__del__") and + ( + not possiblyMissingSuper.isNone() and + possibleIssue = possiblyMissingSuper and + msg = + "This class does not call $@ during finalization. ($@ may be missing a call to super().__del__)" + or + possiblyMissingSuper.isNone() and + ( + possibleIssue.asSome() = getDelMethod(base) and + msg = + "This class does not call $@ during finalization. ($@ may be missing a call to a base class __del__)" + or + not exists(getDelMethod(base)) and + possibleIssue.isNone() and + msg = + "This class does not call $@ during finalization. (The class lacks an __del__ method to ensure every base class __del__ is called.)" + ) + ) + ) +select base, msg, shouldCall, shouldCall.getQualifiedName(), possibleIssue, + possibleIssue.getQualifiedName() diff --git a/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.qhelp b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.qhelp new file mode 100644 index 000000000000..76121446f99e --- /dev/null +++ b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.qhelp @@ -0,0 +1,50 @@ + + + + +

    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, 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 unintentionally skipped: +

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

    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 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 Reference: __init__.
  • +
  • Python Standard Library: super.
  • +
  • Python Glossary: Method resolution order.
  • + +
    +
    diff --git a/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql new file mode 100644 index 000000000000..566d8320a29b --- /dev/null +++ b/python/ql/src/Classes/CallsToInitDel/MissingCallToInit.ql @@ -0,0 +1,41 @@ +/** + * @name Missing call to superclass `__init__` during object initialization + * @description An omitted call to a superclass `__init__` method may lead to objects of this class not being fully initialized. + * @kind problem + * @tags quality + * reliability + * correctness + * @problem.severity error + * @sub-severity low + * @precision high + * @id py/missing-call-to-init + */ + +import python +import MethodCallOrder + +from Class base, Function shouldCall, FunctionOption possibleIssue, string msg +where + exists(FunctionOption possiblyMissingSuper | + missingCallToSuperclassMethodRestricted(base, shouldCall, "__init__") and + possiblyMissingSuper = getPossibleMissingSuperOption(base, shouldCall, "__init__") and + ( + possibleIssue.asSome() = possiblyMissingSuper.asSome() and + msg = + "This class does not call $@ during initialization. ($@ may be missing a call to super().__init__)" + or + possiblyMissingSuper.isNone() and + ( + possibleIssue.asSome() = base.getInitMethod() and + msg = + "This class does not call $@ during initialization. ($@ may be missing a call to a base class __init__)" + or + not exists(base.getInitMethod()) and + possibleIssue.isNone() and + msg = + "This class does not call $@ during initialization. (The class lacks an __init__ method to ensure every base class __init__ is called.)" + ) + ) + ) +select base, msg, shouldCall, shouldCall.getQualifiedName(), possibleIssue, + possibleIssue.getQualifiedName() diff --git a/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.qhelp b/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.qhelp new file mode 100644 index 000000000000..f828cfb8e648 --- /dev/null +++ b/python/ql/src/Classes/CallsToInitDel/SuperclassDelCalledMultipleTimes.qhelp @@ -0,0 +1,55 @@ + + + + +

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

    + +

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

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

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

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

    + + +
    + +

    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 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 Reference: __init__.
  • +
  • Python Standard Library: super.
  • +
  • Python Glossary: Method resolution order.
  • +
  • Wikipedia: The Diamond Problem.
  • + + +
    +
    diff --git a/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql b/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql new file mode 100644 index 000000000000..04c226aa1951 --- /dev/null +++ b/python/ql/src/Classes/CallsToInitDel/SuperclassInitCalledMultipleTimes.ql @@ -0,0 +1,47 @@ +/** + * @name Multiple calls to `__init__` during object initialization + * @description A duplicated call to a superclass `__init__` method may lead to objects of this class not being properly initialized. + * @kind problem + * @tags quality + * reliability + * correctness + * @problem.severity warning + * @sub-severity high + * @precision very-high + * @id py/multiple-calls-to-init + */ + +import python +import MethodCallOrder + +predicate multipleCallsToSuperclassInit( + Function meth, Function calledMulti, DataFlow::MethodCallNode call1, + DataFlow::MethodCallNode call2 +) { + multipleCallsToSuperclassMethod(meth, calledMulti, call1, call2, "__init__") +} + +from + Function meth, Function calledMulti, DataFlow::MethodCallNode call1, + DataFlow::MethodCallNode call2, Function target1, Function target2, string msg +where + multipleCallsToSuperclassInit(meth, calledMulti, call1, call2) and + // Only alert for the lowest method in the hierarchy that both calls will call. + not exists(Function subMulti | + multipleCallsToSuperclassInit(meth, subMulti, _, _) and + calledMulti.getScope() = getADirectSuperclass+(subMulti.getScope()) + ) and + target1 = getDirectSuperCallTargetFromCall(meth.getScope(), meth, call1, _) and + target2 = getDirectSuperCallTargetFromCall(meth.getScope(), meth, call2, _) and + ( + target1 != target2 and + msg = + "This initialization method calls $@ multiple times, via $@ and $@, resolving to $@ and $@ respectively." + or + target1 = target2 and + // The targets themselves are called multiple times (either is calledMulti, or something earlier in the MRO) + // Mentioning them again would be redundant. + msg = "This initialization method calls $@ multiple times, via $@ and $@." + ) +select meth, msg, calledMulti, calledMulti.getQualifiedName(), call1, "this call", call2, + "this call", target1, target1.getQualifiedName(), target2, target2.getQualifiedName() diff --git a/python/ql/src/Classes/MissingCallToDel.py b/python/ql/src/Classes/CallsToInitDel/examples/MissingCallToDel.py similarity index 86% rename from python/ql/src/Classes/MissingCallToDel.py rename to python/ql/src/Classes/CallsToInitDel/examples/MissingCallToDel.py index 37520071b3ae..296d36be7d8f 100644 --- a/python/ql/src/Classes/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/MissingCallToInit.py b/python/ql/src/Classes/CallsToInitDel/examples/MissingCallToInit.py similarity index 85% rename from python/ql/src/Classes/MissingCallToInit.py rename to python/ql/src/Classes/CallsToInitDel/examples/MissingCallToInit.py index 1b3e0e3aee59..14d7c5a80fd3 100644 --- a/python/ql/src/Classes/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/SuperclassDelCalledMultipleTimes2.py b/python/ql/src/Classes/CallsToInitDel/examples/SuperclassDelCalledMultipleTimes.py similarity index 85% rename from python/ql/src/Classes/SuperclassDelCalledMultipleTimes2.py rename to python/ql/src/Classes/CallsToInitDel/examples/SuperclassDelCalledMultipleTimes.py index 8cb1433ac0c4..f48f325f8b57 100644 --- a/python/ql/src/Classes/SuperclassDelCalledMultipleTimes2.py +++ b/python/ql/src/Classes/CallsToInitDel/examples/SuperclassDelCalledMultipleTimes.py @@ -15,14 +15,14 @@ def __del__(self): class SportsCar(Car, Vehicle): - # Vehicle.__del__ will get called twice + # BAD: Vehicle.__del__ will get called twice def __del__(self): recycle(self.sports_car_parts) Car.__del__(self) Vehicle.__del__(self) -#Fix SportsCar by using super() +# GOOD: super() is used ensuring each del method is called once. class FixedSportsCar(Car, Vehicle): def __del__(self): 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/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. diff --git a/python/ql/src/Classes/MissingCallToDel.qhelp b/python/ql/src/Classes/MissingCallToDel.qhelp deleted file mode 100644 index 864ddd1b56b8..000000000000 --- a/python/ql/src/Classes/MissingCallToDel.qhelp +++ /dev/null @@ -1,50 +0,0 @@ - - - - -

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

    -

    -If the __del__ method of a superclass is not called during object destruction it is likely that -that resources may be leaked. -

    - -

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

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

    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.

    - - -
    - -

    In this 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.
  • - -
    -
    diff --git a/python/ql/src/Classes/MissingCallToDel.ql b/python/ql/src/Classes/MissingCallToDel.ql deleted file mode 100644 index be49dc48b5f4..000000000000 --- a/python/ql/src/Classes/MissingCallToDel.ql +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @name Missing call to `__del__` during object destruction - * @description An omitted call to a super-class `__del__` method may lead to class instances not being cleaned up properly. - * @kind problem - * @tags quality - * reliability - * correctness - * performance - * @problem.severity error - * @sub-severity low - * @precision high - * @id py/missing-call-to-delete - */ - -import python -import MethodCallOrder - -from ClassObject self, FunctionObject missing -where - missing_call_to_superclass_method(self, _, missing, "__del__") and - not missing.neverReturns() and - not self.failedInference() and - not missing.isBuiltin() -select self, - "Class " + self.getName() + " may not be cleaned up properly as $@ is not called during deletion.", - missing, missing.descriptiveString() diff --git a/python/ql/src/Classes/MissingCallToInit.qhelp b/python/ql/src/Classes/MissingCallToInit.qhelp deleted file mode 100644 index 31ad3d693a5d..000000000000 --- a/python/ql/src/Classes/MissingCallToInit.qhelp +++ /dev/null @@ -1,52 +0,0 @@ - - - - -

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

    -

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

    - -

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

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

    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.

    - - -
    - -

    In this 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.
  • - -
    -
    diff --git a/python/ql/src/Classes/MissingCallToInit.ql b/python/ql/src/Classes/MissingCallToInit.ql deleted file mode 100644 index 4f5d3d90e84a..000000000000 --- a/python/ql/src/Classes/MissingCallToInit.ql +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @name Missing call to `__init__` during object initialization - * @description An omitted call to a super-class `__init__` method may lead to objects of this class not being fully initialized. - * @kind problem - * @tags quality - * reliability - * correctness - * @problem.severity error - * @sub-severity low - * @precision high - * @id py/missing-call-to-init - */ - -import python -import MethodCallOrder - -from ClassObject self, FunctionObject initializer, FunctionObject missing -where - self.lookupAttribute("__init__") = initializer and - missing_call_to_superclass_method(self, initializer, missing, "__init__") and - // If a superclass is incorrect, don't flag this class as well. - not missing_call_to_superclass_method(self.getASuperType(), _, missing, "__init__") and - not missing.neverReturns() and - not self.failedInference() and - not missing.isBuiltin() and - not self.isAbstract() -select self, - "Class " + self.getName() + " may not be initialized properly as $@ is not called from its $@.", - missing, missing.descriptiveString(), initializer, "__init__ method" diff --git a/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.py b/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.py deleted file mode 100644 index 0ee6e61bcb1a..000000000000 --- a/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.py +++ /dev/null @@ -1,29 +0,0 @@ -#Calling a method multiple times by using explicit calls when a base inherits from other base -class Vehicle(object): - - def __del__(self): - recycle(self.base_parts) - - -class Car(Vehicle): - - def __del__(self): - recycle(self.car_parts) - Vehicle.__del__(self) - - -class SportsCar(Car, Vehicle): - - # Vehicle.__del__ will get called twice - def __del__(self): - recycle(self.sports_car_parts) - Car.__del__(self) - Vehicle.__del__(self) - - -#Fix SportsCar by only calling Car.__del__ -class FixedSportsCar(Car, Vehicle): - - def __del__(self): - recycle(self.sports_car_parts) - Car.__del__(self) diff --git a/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.qhelp b/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.qhelp deleted file mode 100644 index d9514b2c68c4..000000000000 --- a/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.qhelp +++ /dev/null @@ -1,58 +0,0 @@ - - - - -

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

    - -

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

    - -

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

    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.

    - -
    - -

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

    - - - -
    - - -
  • Python Tutorial: Classes.
  • -
  • Python Standard Library: super.
  • -
  • Artima Developer: Things to Know About Python Super.
  • -
  • Wikipedia: Composition over inheritance.
  • - - -
    -
    diff --git a/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.ql b/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.ql deleted file mode 100644 index 019da4257aa0..000000000000 --- a/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.ql +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @name Multiple calls to `__del__` during object destruction - * @description A duplicated call to a super-class `__del__` method may lead to class instances not be cleaned up properly. - * @kind problem - * @tags quality - * reliability - * correctness - * @problem.severity warning - * @sub-severity high - * @precision very-high - * @id py/multiple-calls-to-delete - */ - -import python -import MethodCallOrder - -from ClassObject self, FunctionObject multi -where - multiple_calls_to_superclass_method(self, multi, "__del__") and - not multiple_calls_to_superclass_method(self.getABaseType(), multi, "__del__") and - not exists(FunctionObject better | - multiple_calls_to_superclass_method(self, better, "__del__") and - better.overrides(multi) - ) and - not self.failedInference() -select self, - "Class " + self.getName() + - " may not be cleaned up properly as $@ may be called multiple times during destruction.", multi, - multi.descriptiveString() diff --git a/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.py b/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.py deleted file mode 100644 index 050d5d389d61..000000000000 --- a/python/ql/src/Classes/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/SuperclassInitCalledMultipleTimes.qhelp b/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.qhelp deleted file mode 100644 index f1140d68b891..000000000000 --- a/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.qhelp +++ /dev/null @@ -1,58 +0,0 @@ - - - - -

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

    - -

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

    - -

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

    Either be careful not to explicitly call an __init__ 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.

    - -
    - -

    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 second example, there is a mixture of explicit calls to __init__ and calls using super(). -To fix this example, super() should be used throughout. -

    - - - -
    - - -
  • Python Tutorial: Classes.
  • -
  • Python Standard Library: super.
  • -
  • Artima Developer: Things to Know About Python Super.
  • -
  • Wikipedia: Composition over inheritance.
  • - - -
    -
    diff --git a/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.ql b/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.ql deleted file mode 100644 index 6251ef274dac..000000000000 --- a/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.ql +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @name Multiple calls to `__init__` during object initialization - * @description A duplicated call to a super-class `__init__` method may lead to objects of this class not being properly initialized. - * @kind problem - * @tags quality - * reliability - * correctness - * @problem.severity warning - * @sub-severity high - * @precision very-high - * @id py/multiple-calls-to-init - */ - -import python -import MethodCallOrder - -from ClassObject self, FunctionObject multi -where - multi != theObjectType().lookupAttribute("__init__") and - multiple_calls_to_superclass_method(self, multi, "__init__") and - not multiple_calls_to_superclass_method(self.getABaseType(), multi, "__init__") and - not exists(FunctionObject better | - multiple_calls_to_superclass_method(self, better, "__init__") and - better.overrides(multi) - ) and - not self.failedInference() -select self, - "Class " + self.getName() + - " may not be initialized properly as $@ may be called multiple times during initialization.", - multi, multi.descriptiveString() diff --git a/python/ql/src/Classes/SuperclassInitCalledMultipleTimes2.py b/python/ql/src/Classes/SuperclassInitCalledMultipleTimes2.py deleted file mode 100644 index e12e86a079ee..000000000000 --- a/python/ql/src/Classes/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/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..5dfe5c2b8413 --- /dev/null +++ b/python/ql/src/change-notes/2025-06-04-missing-multiple-calls-to-init-del.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* The queries `py/missing-call-to-init`, `py/missing-calls-to-del`, `py/multiple-calls-to-init`, and `py/multiple-calls-to-del` queries have been modernized; no longer relying on outdated libraries, producing more precise results with more descriptive alert messages, and improved documentation. \ No newline at end of file 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..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: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 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/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-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 6cb92041a630..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,3 +1,6 @@ -| 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: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/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/missing-init/missing_init.py b/python/ql/test/query-tests/Classes/missing-init/missing_init.py index b1651557759f..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 @@ -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,167 @@ 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__ + print("Mult init") + super(Mult, self).__init__() # OK - Calls M1.__init__ -class X: - def __init__(self): - do_something() - -class Y(X): - @decorated - def __init__(self): - X.__init__(self) - -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): +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. - def __init__(self): - super(G3, self).__init__() +G3() -class G4(G3): +class H1: + def __init__(self): + print("H1 init") +class H2: def __init__(self): - G3.__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 diff --git a/python/ql/test/query-tests/Classes/multiple/SuperclassDelCalledMultipleTimes.expected b/python/ql/test/query-tests/Classes/multiple/SuperclassDelCalledMultipleTimes.expected deleted file mode 100644 index 210f24c7525d..000000000000 --- a/python/ql/test/query-tests/Classes/multiple/SuperclassDelCalledMultipleTimes.expected +++ /dev/null @@ -1,2 +0,0 @@ -| 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__ | 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.expected b/python/ql/test/query-tests/Classes/multiple/SuperclassInitCalledMultipleTimes.expected deleted file mode 100644 index 04ce8c0f3730..000000000000 --- a/python/ql/test/query-tests/Classes/multiple/SuperclassInitCalledMultipleTimes.expected +++ /dev/null @@ -1,2 +0,0 @@ -| 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__ | 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/multiple-del/SuperclassDelCalledMultipleTimes.expected b/python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.expected new file mode 100644 index 000000000000..b7ee48feba78 --- /dev/null +++ b/python/ql/test/query-tests/Classes/multiple/multiple-del/SuperclassDelCalledMultipleTimes.expected @@ -0,0 +1,2 @@ +| 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__ | 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 71% 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 index 284f6bf6969f..f72e7c5d00ad 100644 --- a/python/ql/test/query-tests/Classes/multiple/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 new file mode 100644 index 000000000000..e2a0934e9027 --- /dev/null +++ b/python/ql/test/query-tests/Classes/multiple/multiple-init/SuperclassInitCalledMultipleTimes.expected @@ -0,0 +1,3 @@ +| 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/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 60% 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..59efb28e691e 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,75 +2,88 @@ class Base(object): def __init__(self): - pass + print("Base init") 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): +E3() + - def __init__(self, cond): - if cond: - Base.__init__(self) - else: - Base.__init__(self) +class F1: + pass -#Single call, splitting causes what seems to be multiple invocations. -class F2(Base): +class F2(F1): + def __init__(self): + print("F2 init") + super().__init__() - def __init__(self, cond): - if cond: - pass - if cond: - pass - Base.__init__(self) +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