Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion internal/cbm/lsp/py_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -3249,7 +3249,28 @@ 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)) {
py_emit_resolved_call(ctx, qn, "lsp_constructor", 0.85f, call_node);
/* 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 = cbm_registry_lookup_method(ctx->registry, qn, "__init__");
}
if (fi && fi->qualified_name &&
strncmp(fi->qualified_name, "builtins.", sizeof("builtins.") - 1) != 0) {
py_emit_resolved_call_reason(ctx, fi->qualified_name,
"lsp_constructor_init", 0.85f, fname,
call_node);
} else {
py_emit_resolved_call(ctx, qn, "lsp_constructor", 0.85f, call_node);
}
}
}
return;
Expand Down
1 change: 1 addition & 0 deletions src/pipeline/lsp_resolve.h
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ static inline bool cbm_pipeline_invocation_reason_join_strategy(const char *stra
strcmp(strategy, "lsp_method_ref_ctor_synth") == 0 ||
strcmp(strategy, "lsp_dict_dispatch") == 0 ||
strcmp(strategy, "lsp_import_alias") == 0 ||
strcmp(strategy, "lsp_constructor_init") == 0 ||
strcmp(strategy, "php_method_dynamic") == 0);
}

Expand Down
32 changes: 26 additions & 6 deletions tests/repro/repro_lsp_go_py.c
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +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 (py_lsp.c:1624) ClassName() where the name is a
* NAMED type in scope
* 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
Expand Down Expand Up @@ -340,13 +344,24 @@ static const char kPyDirect[] =
"def caller(v):\n"
" return helper(v)\n";

/* lsp_constructor — ClassName() where the name is a NAMED type in scope
* (py_lsp.c:1620-1624: cbm_scope_lookup yields a NAMED type → emit constructor
* edge to the class QN). */
/* lsp_constructor — ClassName() where the class has no user __init__.
* The edge stays on the Class node. A class that defines __init__ is
* lsp_constructor_init below; strstr would otherwise treat that strategy
* as a hit for "lsp_constructor". */
static const char kPyConstructor[] =
"class Widget:\n"
" def label(self):\n"
" return 1\n"
"def caller():\n"
" return Widget()\n";

/* lsp_constructor_init — ClassName() resolves to the user __init__
* (issue #1642). reason carries the class-name leaf so the pipeline join
* still matches the textual call. */
static const char kPyConstructorInit[] =
"class Widget:\n"
" def __init__(self):\n"
" pass\n"
" self.ready = True\n"
"def caller():\n"
" return Widget()\n";

Expand Down Expand Up @@ -538,6 +553,10 @@ TEST(repro_lsp_py_constructor) {
return assert_lsp_strategy("main.py", kPyConstructor, "lsp_constructor");
}

TEST(repro_lsp_py_constructor_init) {
return assert_lsp_strategy("main.py", kPyConstructorInit, "lsp_constructor_init");
}

TEST(repro_lsp_py_method) {
return assert_lsp_strategy("main.py", kPyMethod, "lsp_method");
}
Expand Down Expand Up @@ -623,6 +642,7 @@ SUITE(repro_lsp_go_py) {

RUN_TEST(repro_lsp_py_direct);
RUN_TEST(repro_lsp_py_constructor);
RUN_TEST(repro_lsp_py_constructor_init);
RUN_TEST(repro_lsp_py_method);
RUN_TEST(repro_lsp_py_super);
RUN_TEST(repro_lsp_py_super_init);
Expand Down
22 changes: 20 additions & 2 deletions tests/test_edge_structural.c
Original file line number Diff line number Diff line change
Expand Up @@ -826,14 +826,31 @@ TEST(es_decorates_attribute_csharp) {
* resolves in the registry. pass_usages.c handles this.
* ══════════════════════════════════════════════════════════════════ */

/* Python constructor syntax is a CALLS edge to the materialized Class node. */
/* Python ClassName() with a user __init__ is a CALLS edge to that method.
* The Class node no longer receives the instantiation edge. A class with no
* user __init__ still targets the Class (lsp_constructor). */
TEST(es_usage_crossfile_python) {
static const ES_LangFile f[] = {
{"models.py",
"class User:\n def __init__(self, name):\n self.name = name\n"},
{"main.py",
"from .models import User\n\n\ndef create_user(name):\n return User(name)\n"}};
ASSERT_EQ(es_exact_edge_by_name(f, 2, "CALLS", "create_user", "User"), 1);
ASSERT_EQ(es_exact_edge_by_name(f, 2, "CALLS", "create_user", "__init__"), 1);
ASSERT_EQ(es_exact_edge_by_name(f, 2, "CALLS", "create_user", "User"), 0);
PASS();
}

/* Issue #1642 repro: build() -> Embedder.__init__, not the Embedder class. */
TEST(es_calls_python_constructor_init) {
static const ES_LangFile f[] = {
{"main.py", "class Embedder:\n"
" def __init__(self, model=\"x\"):\n"
" self.model = model\n"
"\n"
"def build():\n"
" return Embedder(model=\"y\")\n"}};
ASSERT_EQ(es_exact_edge_by_name(f, 1, "CALLS", "build", "__init__"), 1);
ASSERT_EQ(es_exact_edge_by_name(f, 1, "CALLS", "build", "Embedder"), 0);
PASS();
}

Expand Down Expand Up @@ -982,6 +999,7 @@ SUITE(edge_structural) {
/* ── FAMILY 7: USAGE cross-file ─────────────────────────── */
/* Uncertain: depends on USAGE extraction for each language. */
RUN_TEST(es_usage_crossfile_python);
RUN_TEST(es_calls_python_constructor_init);
RUN_TEST(es_usage_crossfile_typescript);
RUN_TEST(es_usage_crossfile_go);

Expand Down
51 changes: 49 additions & 2 deletions tests/test_lsp_resolution_probe.c
Original file line number Diff line number Diff line change
Expand Up @@ -1837,16 +1837,62 @@ TEST(lrp_tsx_usage_import_type_with_decoy) {
PASS();
}

/* Python: constructor syntax is an exact call to the materialized Class node. */
/* Python: ClassName() with a user __init__ is an exact CALLS edge to that
* method. The Class node no longer receives the instantiation edge. */
TEST(lrp_python_usage_instantiation) {
static const LRP_File f[] = {
{"model.py", "class User:\n def __init__(self, name):\n self.name = name\n"},
{"main.py", "from .model import User\n\n\ndef create(name):\n return User(name)\n"}};
LRP_Proj lp;
cbm_store_t *store = lrp_index(&lp, f, 2);
int exact = store ? lrp_exact_calls_by_name(store, lp.project, "create", "User") : -1;
int exact = store ? lrp_exact_calls_by_name(store, lp.project, "create", "__init__") : -1;
int class_edge = store ? lrp_exact_calls_by_name(store, lp.project, "create", "User") : -1;
lrp_cleanup(&lp, store);
ASSERT_EQ(exact, 1);
ASSERT_EQ(class_edge, 0);
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();
}

Expand Down Expand Up @@ -1967,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);
}
5 changes: 3 additions & 2 deletions tests/test_pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -21140,8 +21140,9 @@ static int pyn_run_init_collision(int fillers) {
const char *project = cbm_pipeline_project_name(p);
imports[run] = pyn_edge(s, project, "tests.test_app.py.__file__",
"src.rowpkg.layout.Layout", "IMPORTS");
/* Layout defines __init__, so Layout("demo") targets it (#1642). */
calls[run] = pyn_edge(s, project, "tests.test_app.test_build",
"src.rowpkg.layout.Layout", "CALLS");
"src.rowpkg.layout.Layout.__init__", "CALLS");
method[run] = pyn_edge(s, project, "tests.test_app.test_build",
"src.rowpkg.layout.Layout.create", "CALLS");
cbm_store_close(s);
Expand All @@ -21160,7 +21161,7 @@ static int pyn_run_init_collision(int fillers) {
for (int run = 0; run < 2; run++) {
ASSERT_EQ(rc[run], 0);
ASSERT_TRUE(imports[run]); /* the class stays importable */
ASSERT_TRUE(calls[run]); /* Layout("demo") */
ASSERT_TRUE(calls[run]); /* Layout("demo") -> Layout.__init__ */
ASSERT_TRUE(method[run]); /* .create() through the constructed type */
}
PASS();
Expand Down
Loading
Loading