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/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/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..bd98f3fb0fb7 --- /dev/null +++ b/python/ql/src/Classes/CallsToInitDel/MethodCallOrder.qll @@ -0,0 +1,221 @@ +/** Definitions for reasoning about multiple or missing calls to superclass methods. */ + +import python +import semmle.python.ApiGraphs +import semmle.python.dataflow.new.internal.DataFlowDispatch +import codeql.util.Option + +/** Holds if `meth` is a method named `name` that transitively calls `calledMulti` of the same name via the calls `call1` and `call2`. */ +predicate multipleCallsToSuperclassMethod( + Function meth, Function calledMulti, DataFlow::MethodCallNode call1, + DataFlow::MethodCallNode call2, string name +) { + exists(Class cls | + meth.getName() = name and + meth.getScope() = cls and + locationBefore(call1.getLocation(), call2.getLocation()) and + 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. */ +pragma[inline] +private predicate locationBefore(Location l1, Location l2) { + l1.getStartLine() < l2.getStartLine() + or + l1.getStartLine() = l2.getStartLine() and + l1.getStartColumn() < l2.getStartColumn() +} + +/** Gets a method transitively called by `meth` named `name` with `call` that it overrides, with `mroBase` as the type determining the MRO to search. */ +Function getASuperCallTargetFromCall( + Class mroBase, Function meth, DataFlow::MethodCallNode call, string name +) { + exists(Function target | target = getDirectSuperCallTargetFromCall(mroBase, meth, call, name) | + result = target + or + result = getASuperCallTargetFromCall(mroBase, target, _, name) + ) +} + +/** Gets the method called by `meth` named `name` with `call`, with `mroBase` as the type determining the MRO to search. */ +Function getDirectSuperCallTargetFromCall( + Class mroBase, Function meth, DataFlow::MethodCallNode call, string name +) { + meth = call.getScope() and + meth.getName() = name and + call.calls(_, name) and + mroBase = getADirectSubclass*(meth.getScope()) and + exists(Class targetCls | + // the differences between 0-arg and 2-arg super is not considered; we assume each super uses the mro of the instance `self` + superCall(call, _) and + targetCls = getNextClassInMroKnownStartingClass(meth.getScope(), mroBase) and + result = findFunctionAccordingToMroKnownStartingClass(targetCls, mroBase, name) + or + // targetCls is the mro base for this lookup. + // note however that if the call we find uses super(), that still uses the mro of the instance `self` + // assuming it's 0-arg or is 2-arg with `self` as second arg. + callsMethodOnClassWithSelf(meth, call, targetCls, _) and + result = findFunctionAccordingToMroKnownStartingClass(targetCls, targetCls, name) + ) +} + +/** Gets a method that is transitively called by a call to `cls.`, 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 a `super()` method of the same name. */ +predicate callsSuper(Function meth) { + exists(DataFlow::MethodCallNode call | + call.getScope() = meth and + superCall(call, meth.getName()) + ) +} + +/** Holds if `meth` calls `target.(self, ...)` with the call `call`. */ +predicate callsMethodOnClassWithSelf( + Function meth, DataFlow::MethodCallNode call, Class target, string name +) { + exists(DataFlow::Node callTarget, DataFlow::ParameterNode self | + call.calls(callTarget, name) and + self.getParameter() = meth.getArg(0) and + self.(DataFlow::LocalSourceNode).flowsTo(call.getArg(0)) and + callTarget = classTracker(target) + ) +} + +/** Holds if `meth` calls a method named `name` passing its `self` argument as its first parameter, but the class it refers to is unknown. */ +predicate callsMethodOnUnknownClassWithSelf(Function meth, string name) { + exists(DataFlow::MethodCallNode call, DataFlow::Node callTarget, DataFlow::ParameterNode self | + call.calls(callTarget, name) and + self.getParameter() = meth.getArg(0) and + self.(DataFlow::LocalSourceNode).flowsTo(call.getArg(0)) and + not callTarget = classTracker(any(Class target)) + ) +} + +/** Holds if `base` does not call a superclass method `shouldCall` named `name` when it appears it should. */ +predicate missingCallToSuperclassMethod(Class base, Function shouldCall, string name) { + shouldCall.getName() = name and + shouldCall.getScope() = getADirectSuperclass+(base) and + not shouldCall = getASuperCallTargetFromClass(base, base, name) and + nonTrivial(shouldCall) and + // "Benefit of the doubt" - if somewhere in the chain we call an unknown superclass, assume all the necessary parent methods are called from it + not callsMethodOnUnknownClassWithSelf(getASuperCallTargetFromClass(base, base, name), name) +} + +/** + * Holds if `base` does not call a superclass method `shouldCall` named `name` when it appears it should. + * Results are restricted to hold only for the highest `base` class and the lowest `shouldCall` method in the hierarchy for which this applies. + */ +predicate missingCallToSuperclassMethodRestricted(Class base, Function shouldCall, string name) { + missingCallToSuperclassMethod(base, shouldCall, name) and + not exists(Class superBase | + // Alert only on the highest base class that has the issue + superBase = getADirectSuperclass+(base) and + missingCallToSuperclassMethod(superBase, shouldCall, name) + ) and + not exists(Function subShouldCall | + // Mention in the alert only the lowest method we're missing the call to + subShouldCall.getScope() = getADirectSubclass+(shouldCall.getScope()) and + missingCallToSuperclassMethod(base, subShouldCall, name) + ) +} + +/** + * If `base` contains a `super()` call, gets a method in the inheritance hierarchy of `name` in the MRO of `base` + * that does not contain a `super()` call, but would call `shouldCall` if it did, which does not otherwise get called + * during a call to `base.`. + */ +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..e461aebdc028 --- /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 +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..df9c073fcceb --- /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 method 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..9e4bbdea0584 --- /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.state 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 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