From e772ff820a70778352f772aec4e566ae456cd4c8 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sun, 18 Oct 2026 20:54:58 +0200 Subject: [PATCH 1/2] test(python): methods of a class nested in a class resolve through the LSP RED on F11a alone: py_process_class walks a nested class under the module, so none of its LSP rows join the method nodes. Signed-off-by: Martin Vogel --- tests/test_pipeline.c | 89 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 396c5ed3e6..a5b10e0342 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -15112,6 +15112,93 @@ TEST(pipeline_python_decorated_nested_class_extracted_parallel) { return pyn_run_decorated_nested(52); } +/* A class nested in a class is walked by the Python LSP under its outer + * class (m.Outer.Inner): a typed-parameter call, a self call and a + * constructor call in its methods resolve, and so does a decorated nested + * class. Walked under the module (m.Inner) none of the LSP rows joined the + * method nodes, so the calls fell to name guesses or got no edge. */ +static const char pyn_nested_class_py[] = "class Helper:\n" + " def go(self):\n" + " return 1\n" + "\n" + "def deco(cls):\n" + " return cls\n" + "\n" + "class Outer:\n" + " class Inner:\n" + " def run(self, h: Helper):\n" + " return h.go()\n" + "\n" + " def again(self):\n" + " return self.run(Helper())\n" + "\n" + " @deco\n" + " class Decorated:\n" + " def run(self, h: Helper):\n" + " return h.go()\n"; + +static int pyn_run_nested_class(int fillers) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_nestcls_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + write_temp_file(tmp, "sn.py", pyn_nested_class_py); + for (int i = 0; i < fillers; i++) { + char name[64]; + char body[128]; + snprintf(name, sizeof(name), "filler%d.py", i); + snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i); + write_temp_file(tmp, name, body); + } + char *old_workers = getenv("CBM_WORKERS"); + char *saved = old_workers ? strdup(old_workers) : NULL; + if (fillers > 0) { + cbm_setenv("CBM_WORKERS", "4", 1); + } + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/nestcls.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + int rc = p ? cbm_pipeline_run(p) : -1; + cbm_store_t *s = rc == 0 ? cbm_store_open_path(db_path) : NULL; + const char *project = p ? cbm_pipeline_project_name(p) : NULL; + bool typed = s && pyn_edge_with(s, project, "sn.Outer.Inner.run", "sn.Helper.go", "CALLS", + "\"strategy\":\"lsp_"); + bool self_call = s && pyn_edge_with(s, project, "sn.Outer.Inner.again", "sn.Outer.Inner.run", + "CALLS", "\"strategy\":\"lsp_"); + bool ctor = s && pyn_edge_with(s, project, "sn.Outer.Inner.again", "sn.Helper", "CALLS", + "\"strategy\":\"lsp_"); + bool decorated = s && pyn_edge_with(s, project, "sn.Outer.Decorated.run", "sn.Helper.go", + "CALLS", "\"strategy\":\"lsp_"); + if (s) { + cbm_store_close(s); + } + if (p) { + cbm_pipeline_free(p); + } + if (saved) { + cbm_setenv("CBM_WORKERS", saved, 1); + free(saved); + } else { + cbm_unsetenv("CBM_WORKERS"); + } + th_rmtree(tmp); + ASSERT_EQ(rc, 0); + ASSERT_TRUE(typed); + ASSERT_TRUE(self_call); + ASSERT_TRUE(ctor); + ASSERT_TRUE(decorated); + PASS(); +} + +TEST(pipeline_python_nested_class_methods_resolve) { + return pyn_run_nested_class(0); +} + +TEST(pipeline_python_nested_class_methods_resolve_parallel) { + return pyn_run_nested_class(52); +} + TEST(pipeline_python_project) { /* Port of TestPipelinePythonProject */ const char *files[] = {"main.py", "utils.py"}; @@ -24621,6 +24708,8 @@ SUITE(pipeline) { RUN_TEST(pipeline_python_nested_helper_keeps_tests_parallel_issue1548); RUN_TEST(pipeline_python_decorated_nested_class_extracted); RUN_TEST(pipeline_python_decorated_nested_class_extracted_parallel); + RUN_TEST(pipeline_python_nested_class_methods_resolve); + RUN_TEST(pipeline_python_nested_class_methods_resolve_parallel); RUN_TEST(pipeline_header_include_target_is_independent_of_registration_order); RUN_TEST(pipeline_imports_multi_symbol_edges); RUN_TEST(pipeline_go_cross_package_call); From ace384c91a7f3e4d8b07fa677444674311bf43e0 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sun, 18 Oct 2026 21:02:28 +0200 Subject: [PATCH 2/2] fix(python): walk a class nested in a class under its outer class py_process_class qualified every class under the module, so a class nested in a class (proj.m.Outer.Inner, the QN extraction gives it) was walked as proj.m.Inner: its methods' LSP rows carried callers no node has and never joined. A typed-parameter call in such a method got no edge at all, a self call fell to unique_name and a constructor to same_module. A nested class, decorated or not, is now walked under its enclosing class through the same py_process_class_in the function-local classes use. Tests: pipeline_python_nested_class_methods_resolve(_parallel). Signed-off-by: Martin Vogel --- internal/cbm/lsp/py_lsp.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/cbm/lsp/py_lsp.c b/internal/cbm/lsp/py_lsp.c index 18384d82e0..d9e8306353 100644 --- a/internal/cbm/lsp/py_lsp.c +++ b/internal/cbm/lsp/py_lsp.c @@ -4706,9 +4706,15 @@ static void py_process_class_in(PyLSPContext *ctx, TSNode class_node, const char strcmp(ts_node_type(def), "function_definition") == 0 && !py_is_init_method(ctx, def)) { py_process_function(ctx, def, ctx->enclosing_class_qn); + } else if (!ts_node_is_null(def) && + strcmp(ts_node_type(def), "class_definition") == 0) { + py_process_class_in(ctx, def, ctx->enclosing_class_qn); } } else if (strcmp(ck, "class_definition") == 0) { - py_process_class(ctx, c); + /* A class nested in a class is qualified by its outer class + * (proj.m.Outer.Inner), the QN extraction gives it; under the + * module its methods' calls never joined their callers. */ + py_process_class_in(ctx, c, ctx->enclosing_class_qn); } } }