Skip to content

Python: Modernize the init-calls-subclass query #19709

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

joefarebrother
Copy link
Contributor

No description provided.

@Copilot Copilot AI review requested due to automatic review settings June 10, 2025 08:29
@joefarebrother joefarebrother requested a review from a team as a code owner June 10, 2025 08:29
@joefarebrother joefarebrother marked this pull request as draft June 10, 2025 08:30
Copy link
Contributor

github-actions bot commented Jun 10, 2025

QHelp previews:

python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.qhelp

__init__ method calls overridden method

When initializing an instance of the class in the class' __init__ method, calls tha are made using the instance may receive an instance of the class that is not yet fully initialized. When a method called in an initializer is overridden in a subclass, the subclass method receives the instance in a potentially unexpected state. Fields that would be initialized after the call, including potentially in the subclass' __init__ method, will not be initialized. This may lead to runtime errors, as well as make the code more difficult to maintain, as future changes may not be aware of which fields would not be initialized.

Recommendation

If possible, refactor the initializer method such that initialization is complete before calling any overridden methods. For helper methods used as part of initialization, avoid overriding them, and instead call any additional logic required in the subclass' __init__ method.

If the overridden method does not depend on the instance self, and only on its class, consider making it a @classmethod or @staticmethod instead.

If calling an overridden method is absolutely required, consider marking it as an internal method (by using an _ prefix) to discourage external users of the library from overriding it and observing partially initialized state, and ensure that the fact it is called during initialization is mentioned in the documentation.

Example

In the following case, the __init__ method of Super calls the set_up method that is overridden by Sub. This results in Sub.set_up being called with a partially initialized instance of Super which may be unexpected.

class Super(object):

    def __init__(self, arg):
        self._state = "Not OK"
        self.set_up(arg) # BAD: This method is overridden, so `Sub.set_up` receives a partially initialized instance.
        self._state = "OK"

    def set_up(self, arg):
        "Do some setup"
        self.a = 2

class Sub(Super):

    def __init__(self, arg):
        super().__init__(arg)
        self.important_state = "OK"

    def set_up(self, arg):
        super().set_up(arg)
        "Do some more setup"
        # BAD: at this point `self._state` is set to `"Not OK"`, and `self.important_state` is not initialized.
        if self._state == "OK":
            self.b = self.a + 2

In the following case, the initialization methods are separate between the superclass and the subclass.

class Super(object):

    def __init__(self, arg):
        self._state = "Not OK"
        self.super_set_up(arg) # GOOD: This isn't overriden. Instead, additional setup the subclass needs is called by the subclass' `__init__ method.`
        self._state = "OK"

    def super_set_up(self, arg):
        "Do some setup"
        self.a = 2


class Sub(Super):

    def __init__(self, arg):
        super().__init__(arg)
        self.sub_set_up(self, arg)
        self.important_state = "OK"


    def sub_set_up(self, arg):
        "Do some more setup"
        if self._state == "OK":
            self.b = self.a + 2

References

Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR modernizes the py/init-calls-subclass QL query by replacing the old implementation with a new dataflow-based approach.

  • Removes the legacy InitCallsSubclassMethod.ql.
  • Adds a new directory and file under InitCallsSubclass/ using DataFlow predicates.
  • Defines two predicates (initSelfCall and initSelfCallOverridden) to detect subclass-overridden calls in __init__.

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
python/ql/src/Classes/InitCallsSubclassMethod.ql Deleted the legacy query file
python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql Added modernized dataflow-based query implementation
Comments suppressed due to low confidence (4)

python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql:28

  • [nitpick] The parameter name override is ambiguous and matches a language keyword. Consider renaming it to something like overridingMethod for clarity.
predicate initSelfCallOverridden(Function init, DataFlow::MethodCallNode call, Function override) {

python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql:18

  • [nitpick] It would be helpful to add a brief doc comment above this predicate explaining its purpose and parameters for future maintainers.
predicate initSelfCall(Function init, DataFlow::MethodCallNode call) {

python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql:38

  • The new dataflow-based implementation should be accompanied by tests to validate that overridden __init__ calls are correctly detected; consider adding or updating QL tests.
from Function init, DataFlow::MethodCallNode call, Function override

python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql:16

  • This import isn't used in the new query. Consider removing it to keep dependencies minimal.
import semmle.python.dataflow.new.internal.DataFlowDispatch

@@ -1 +1 @@
Classes/InitCallsSubclassMethod.ql
Classes/InitCallsSubclass/InitCallsSubclassMethod.ql

Check warning

Code scanning / CodeQL

Query test without inline test expectations Warning test

Query test does not use inline test expectations.
@joefarebrother joefarebrother force-pushed the python-qual-init-call-subclass branch 2 times, most recently from 0f7a24e to 2aad607 Compare June 12, 2025 10:20
@joefarebrother joefarebrother changed the title [Draft] Python: Modernize the init-calls-subclass query Python: Modernize the init-calls-subclass query Jun 13, 2025
@joefarebrother joefarebrother marked this pull request as ready for review June 13, 2025 09:45
@joefarebrother joefarebrother added the no-change-note-required This PR does not need a change note label Jun 13, 2025
tausbn
tausbn previously approved these changes Jun 16, 2025
Copy link
Contributor

@tausbn tausbn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments/suggestions, but otherwise this looks good to me.
(Feel free to postpone my suggestion about self parameters to a later PR if you just want to merge the present one.)

Also, in future PRs could you make sure that you don't rename/move and also modify files in the same commit? This really confuses the diff (as it doesn't know the two files should be linked up) and makes it difficult to see what the actual differences are.

@joefarebrother joefarebrother force-pushed the python-qual-init-call-subclass branch from 89f0843 to d1bd722 Compare June 17, 2025 12:59
@joefarebrother joefarebrother requested a review from tausbn June 17, 2025 22:19
@joefarebrother joefarebrother merged commit 4ae72db into github:main Jun 18, 2025
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation no-change-note-required This PR does not need a change note Python
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants