fix(python): bind ClassName() only to the class own __init__

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 #1642

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-11-01 21:29:22 +01:00
1 parent aa02511cc7
commit 054719044a
4 files changed
+105 -27

No files matched your search

+7 -5
View File
@@ -3249,17 +3249,19 @@ static void py_emit_call_for(PyLSPContext *ctx, TSNode call_node) {
py_emit_resolved_call_reason(ctx, qn, "lsp_import_alias", 0.85f, fname,
call_node);
} else if (qn && cbm_registry_lookup_type(ctx->registry, qn)) {
/* ClassName() runs a user __init__ found on the class or,
* depth-first, a declared base (the same walk
* super().__init__ uses, not C3 MRO). Retarget only when
* this binding IS the class being called. An instance
/* ClassName() binds to the __init__ the class declares
* itself. An inherited __init__ is not followed: B(A)
* without its own __init__ keeps the call on B, so a base
* constructor does not collect every subclass
* construction as its caller. Retarget only when this
* binding IS the class being called. An instance
* (`f = Foo(); f()`) keeps the class row: that call
* dispatches to __call__, and a reason-join would invent
* a Foo.__init__ edge. A builtins.* __init__ is not a
* project constructor, so the class target stays. */
const CBMRegisteredFunc *fi = NULL;
if (qn_short && strcmp(qn_short, fname) == 0) {
fi = py_lookup_attribute(ctx, qn, "__init__");
fi = cbm_registry_lookup_method(ctx->registry, qn, "__init__");
}
if (fi && fi->qualified_name &&
strncmp(fi->qualified_name, "builtins.", sizeof("builtins.") - 1) != 0) {
+6 -4
View File
@@ -64,10 +64,12 @@
* PYTHON STRATEGY INVENTORY — every literal "lsp_..." emitted by py_lsp.c
* (grep '"lsp_' internal/cbm/lsp/py_lsp.c), with its keying site:
* lsp_direct (py_lsp.c:1631) module-local f()
* lsp_constructor ClassName() with no user __init__ → the Class
* lsp_constructor_init ClassName() with a user __init__ (on the class
* or a declared base) → that __init__. The textual
* callee leaf stays the class name; reason joins it.
* lsp_constructor ClassName() where the class declares no __init__
* of its own (an inherited one is not followed)
* → the Class
* lsp_constructor_init ClassName() where the class declares its own
* __init__ → that __init__. The textual callee
* leaf stays the class name; reason joins it.
* lsp_method (py_lsp.c:1731) obj.method() on a NAMED-typed
* receiver (covers self.other())
* lsp_super (py_lsp.c:1693) super().method() resolved on a
+44
View File
@@ -1853,6 +1853,49 @@ TEST(lrp_python_usage_instantiation) {
PASS();
}
/* Python same-file subclasses in the graph: ClassName() binds only to an
* __init__ the class declares itself. B(A) declares none, so make_b keeps
* its CALLS edge on the class B and the inherited A.__init__ gets none.
* C(A) declares its own __init__, so make_c calls C.__init__, not C and not
* the inherited A.__init__. */
TEST(lrp_python_subclass_constructor_own_init_only) {
static const LRP_File f[] = {{"main.py", "class A:\n"
" def __init__(self):\n"
" self.ready = True\n"
"\n"
"class B(A):\n"
" def go(self):\n"
" return 1\n"
"\n"
"class C(A):\n"
" def __init__(self):\n"
" self.ready = False\n"
"\n"
"def make_b():\n"
" return B()\n"
"\n"
"def make_c():\n"
" return C()\n"}};
LRP_Proj lp;
cbm_store_t *store = lrp_index(&lp, f, 1);
const char *p = store ? lp.project : NULL;
int b_class = lrp_exact_edge_by_qn_suffix(store, p, "CALLS", "main.make_b", "main.B");
int b_base_init =
lrp_exact_edge_by_qn_suffix(store, p, "CALLS", "main.make_b", "main.A.__init__");
int c_own_init =
lrp_exact_edge_by_qn_suffix(store, p, "CALLS", "main.make_c", "main.C.__init__");
int c_class = lrp_exact_edge_by_qn_suffix(store, p, "CALLS", "main.make_c", "main.C");
int c_base_init =
lrp_exact_edge_by_qn_suffix(store, p, "CALLS", "main.make_c", "main.A.__init__");
lrp_cleanup(&lp, store);
ASSERT_EQ(b_class, 1);
ASSERT_EQ(b_base_init, 0);
ASSERT_EQ(c_own_init, 1);
ASSERT_EQ(c_class, 0);
ASSERT_EQ(c_base_init, 0);
PASS();
}
/* ══════════════════════════════════════════════════════════════════════
* SUITE registration
* ══════════════════════════════════════════════════════════════════════ */
@@ -1970,4 +2013,5 @@ SUITE(lsp_resolution_probe) {
RUN_TEST(lrp_ts_usage_import_type_with_decoy);
RUN_TEST(lrp_tsx_usage_import_type_with_decoy);
RUN_TEST(lrp_python_usage_instantiation);
RUN_TEST(lrp_python_subclass_constructor_own_init_only);
}
+48 -18
View File
@@ -403,26 +403,55 @@ TEST(pylsp_constructor_call_targets_init) {
PASS();
}
/* class B(A) with only A.__init__ resolves construction of B to A.__init__. */
TEST(pylsp_constructor_inherited_init) {
/* Same-file subclasses: only an __init__ the class declares itself is a
* construction target. B(A) inherits A.__init__ but declares none, so B()
* stays on the class B (never A.__init__). C(A) declares its own __init__,
* so C() resolves to C.__init__ (never the inherited A.__init__). */
static int count_build_rows(const CBMFileResult *r, const char *caller_leaf,
const char *callee_qn, const char *strategy) {
int n = 0;
for (int i = 0; i < r->resolved_calls.count; i++) {
const CBMResolvedCall *rc = &r->resolved_calls.items[i];
if (rc->kind != CBM_RESOLVED_INVOCATION || !rc->caller_qn || !rc->callee_qn ||
!rc->strategy)
continue;
const char *dot = strrchr(rc->caller_qn, '.');
const char *leaf = dot ? dot + 1 : rc->caller_qn;
if (strcmp(leaf, caller_leaf) == 0 && strcmp(rc->callee_qn, callee_qn) == 0 &&
strcmp(rc->strategy, strategy) == 0)
n++;
}
return n;
}
TEST(pylsp_constructor_same_file_subclass_own_init_only) {
CBMFileResult *r = extract_py("class A:\n"
" def __init__(self):\n"
" self.ready = True\n"
"class B(A):\n"
" def go(self):\n"
" return 1\n"
"def build():\n"
" return B()\n");
"class C(A):\n"
" def __init__(self):\n"
" self.ready = False\n"
"def build_b():\n"
" return B()\n"
"def build_c():\n"
" return C()\n");
ASSERT_NOT_NULL(r);
int idx = require_resolved(r, "build", "A.__init__");
/* B() keeps the class target; the inherited A.__init__ gets no row. */
ASSERT_EQ(count_build_rows(r, "build_b", "test.main.B", "lsp_constructor"), 1);
ASSERT_EQ(find_resolved(r, "build_b", "__init__"), -1);
/* C() binds to the __init__ C declares itself. */
int idx = require_resolved(r, "build_c", "test.main.C.__init__");
ASSERT_GTE(idx, 0);
if (idx >= 0) {
const CBMResolvedCall *rc = &r->resolved_calls.items[idx];
ASSERT_STR_EQ(rc->strategy, "lsp_constructor_init");
ASSERT(strstr(rc->callee_qn, "B.__init__") == NULL);
ASSERT(strncmp(rc->callee_qn, "builtins.", 9) != 0);
ASSERT_STR_EQ(rc->reason, "C");
}
ASSERT_EQ(find_resolved(r, "build", "B.__init__"), -1);
ASSERT_EQ(find_resolved(r, "build_c", "test.main.A.__init__"), -1);
ASSERT_EQ(count_build_rows(r, "build_c", "test.main.C", "lsp_constructor"), 0);
cbm_free_result(r);
PASS();
}
@@ -1126,10 +1155,11 @@ TEST(pylsp_crossfile_constructor_targets_init) {
PASS();
}
/* class B(A) where A's __init__ lives in another module. The cross registry
* records the real base QN; per-file registration would have stored
* test.main.A and missed the base. */
TEST(pylsp_crossfile_inherited_init) {
/* class B(A) where A's __init__ lives in another module and B declares no
* __init__ of its own. Construction binds only to an __init__ the class
* declares itself, so B() stays on the class B and the inherited
* base_mod.A.__init__ gets no row. */
TEST(pylsp_crossfile_inherited_init_stays_on_class) {
const char *source = "from base_mod import A\n"
"class B(A):\n"
" def go(self):\n"
@@ -1168,12 +1198,12 @@ TEST(pylsp_crossfile_inherited_init) {
cbm_run_py_lsp_cross_with_registry(&arena, source, (int)strlen(source), "test.main", reg,
imp_names, imp_qns, 1, NULL, &out, NULL);
int idx = find_resolved_arr(&out, "build", "base_mod.A.__init__");
ASSERT_EQ(find_resolved_arr(&out, "build", "__init__"), -1);
int idx = find_resolved_arr(&out, "build", "test.main.B");
ASSERT_GTE(idx, 0);
if (idx >= 0) {
ASSERT_STR_EQ(out.items[idx].strategy, "lsp_constructor_init");
ASSERT_STR_EQ(out.items[idx].reason, "B");
ASSERT(strstr(out.items[idx].callee_qn, "B.__init__") == NULL);
ASSERT_STR_EQ(out.items[idx].callee_qn, "test.main.B");
ASSERT_STR_EQ(out.items[idx].strategy, "lsp_constructor");
}
cbm_arena_destroy(&arena);
PASS();
@@ -5266,7 +5296,7 @@ SUITE(py_lsp) {
RUN_TEST(pylsp_method_via_self);
RUN_TEST(pylsp_constructor_call_returns_instance);
RUN_TEST(pylsp_constructor_call_targets_init);
RUN_TEST(pylsp_constructor_inherited_init);
RUN_TEST(pylsp_constructor_same_file_subclass_own_init_only);
RUN_TEST(pylsp_constructor_without_init_stays_on_class);
RUN_TEST(pylsp_callable_instance_call_skips_init);
RUN_TEST(pylsp_constructor_skips_builtin_init);
@@ -5289,7 +5319,7 @@ SUITE(py_lsp) {
RUN_TEST(pylsp_crossfile_classmethod_on_class_issue228);
RUN_TEST(pylsp_crossfile_inheritance);
RUN_TEST(pylsp_crossfile_constructor_targets_init);
RUN_TEST(pylsp_crossfile_inherited_init);
RUN_TEST(pylsp_crossfile_inherited_init_stays_on_class);
RUN_TEST(pylsp_batch_two_files);
RUN_TEST(pylsp_from_import_alias_equal_module_leaf_targets_imported_member);
RUN_TEST(pylsp_project_prefixed_direct_alias_same_tail_is_not_from_import_reference);