Repository navigation
fix(python): resolve ClassName() calls to __init__ - #2424
Conversation
|
Thanks for opening this — it has been seen, and it is queued. This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence. Current review status: working through a backlog. What that means for this PR, concretely:
Things that will genuinely speed it up whenever review does happen:
If this fixes a bug, a reproduction we can run is worth more than a description of the symptom. Thanks for contributing, and sorry in advance for the wait. |
|
Thanks for keeping the constructor work separate from dunders/callbacks and for documenting the inherited-init limit. The proposed move of the CALLS target from Class to init is an explicit contract choice for maintainer review; I have recorded it without asking for a second-edge implementation now. |
|
Thanks again for the constructor-call proposal and the stated inherited-init limit. Choosing the Class-versus-init CALLS contract remains our decision; no alternate implementation is requested while that is pending. Sorry for the delay—I’ll follow up soon. |
|
A note on the one red CI job on 904ecca: test-unix (macos-15-intel) failed only on |
A Python construction call resolved to the Class node, so a user __init__ never received a caller from ClassName(...) and the dead-code view reported live constructors as unreferenced. When the called name is the class itself, resolve it to the __init__ found on the class or, depth-first, its declared bases (the lookup super().__init__() already uses), with a new lsp_constructor_init strategy that joins the textual call site through its reason. Classes with no user __init__ keep the Class target (lsp_constructor), as does a builtins.* __init__. Calls through an instance (f = Foo(); f()) are unchanged, since they dispatch to __call__. This changes the documented Class-target contract for classes that define __init__; the three tests that pinned it are updated. Refs DeusData#1642 Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
904ecca to
1b64c50
Compare
…tor call
pipeline_python_init_function_shares_submodule_qn pinned the old Class
target for Layout("demo"). Layout defines __init__, so the constructor
call now lands on that method. The other tests that pin the contract
were moved in the previous commit; this one reached main afterwards.
Refs DeusData#1642
Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
|
The red run on 1b64c50 was this PR: two |
A construction call now resolves to __init__ only when the called class declares that __init__ itself. An inherited __init__ is no longer followed: class B(A) without its own __init__ keeps the B() call on the Class B, as it was before this branch. Following the bases bound every subclass construction to the base constructor. On a django index (fb113765) that made Model.__init__ a 583-caller hub (every model instantiation), bound IntegerField() to Field.__init__ and HttpResponseForbidden() to HttpResponse.__init__, and degraded 625 constructor call sites versus main (457 fell back to textual resolution, 168 lost their row; CALLS -161). With own __init__ only, 6016 sites move to the class own __init__, 5468 stay on the Class, none fall back to a textual strategy, and total CALLS is +4 versus main. Tests: the same-file and cross-file inherited cases now pin the Class target, and a new same-file subclass test at the LSP and the graph level covers B(A) without __init__ (stays on B, never A.__init__) and C(A) with its own __init__ (binds to C.__init__, never the class C or A.__init__). The strategy inventory comment in the repro suite is updated to match. Refs DeusData#1642 Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Brings the branch up to date with main edc2301. Main only changed CI configuration since the branch base (CodeQL action, actions/attest and msys2/setup-msys2 pins, Dependabot grouping); no conflicts. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
|
Hi @mvanhorn, thank you very much for this fix and for the care you put into it. Keeping the constructor half of #1642 separate from the dunder and callback work, joining the textual call site through We have now decided the open Class-versus-
Please pull before pushing again so these commits are not lost. We will merge as soon as the required checks are green. Thanks again for a carefully built and well-tested fix! |
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
|
Thank you, @mvanhorn - this was green and ready, but a change that merged into main just before it (the Next.js route support in #2747) also edited tests/test_pipeline.c, so the earlier CI run no longer covered the tree this would merge into. We merged current main into your branch (e87a9ff, a plain signed-off merge commit; your change and our earlier fix-up are byte-for-byte the same) so CI re-runs on the up-to-date tree. It merges as soon as that run is green. If you push again, please pull first so the merge commit is kept. |
|
Merged - thank you very much, @mvanhorn! A Python |
What does this PR do?
Refs #1642 (the constructor half, per your note on 2026-09-25; dunders and callbacks passed by reference are left for the separate work you described).
Embedder(model="y")resolved to theEmbedderClass node, so a user__init__never got a caller from a construction call, and the dead-code view listed live constructors as unreferenced.With this change, when the called name is the class itself, the Python LSP resolves the call to the
__init__found on the class or, depth-first, on its declared bases. That is the same lookupsuper().__init__()already uses, so not full C3 MRO. The new strategylsp_constructor_initjoins the textual call site through itsreason, the waylsp_import_aliasdoes, and is added tocbm_pipeline_invocation_reason_join_strategy. A class with no user__init__, or whose only__init__is abuiltins.*one, keeps today'slsp_constructoredge to the class. A call through an instance (f = Foo(); f()) is left alone, since it dispatches to__call__.One behavior change to confirm: for a class that defines
__init__, the Class node no longer receives the instantiation CALLS edge. The edge now lands on__init__, which matches how Java and C++ constructor calls resolve here. Three tests pinned the old contract, and I updated them:es_usage_crossfile_python,lrp_python_usage_instantiation, and thekPyConstructorrepro (now a class without__init__, plus a sibling repro forlsp_constructor_init). Keeping both edges would need a second emission path in the pipeline, because two different targets at one call site read as ambiguous and fall back to the class. I kept that out of this PR. If you want both edges, I'm happy to follow up.Tests: 7 new
py_lspcases (explicit, inherited, no__init__, callable instance, builtins guard, cross-file import, cross-file base) and the issue's repro as an end-to-end edge test inedge_structural. Withpy_lsp.candlsp_resolve.hreverted to main, 8 of these fail (scripts/test.sh --suites py_lsp,edge_structural,lsp_resolution_probe: 200 passed, 8 failed). With the fix, all 208 pass. The fullscripts/test.shrun passes on macOS arm64 (ASan + UBSan), and so doesmake -f Makefile.cbm lint-ci.AI was used for assistance. I reviewed the change and ran the commands above myself.
Checklist
git commit -s) — required, CI rejectsunsigned commits (DCO, see CONTRIBUTING.md)
make -f Makefile.cbm test)make -f Makefile.cbm lint-ci)