Merge remote-tracking branch 'origin/main' into fix/rust-generic-impl-call-scope

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>

# Conflicts:
#	tests/test_pipeline.c
This commit is contained in:
Martin Vogel committed 2026-10-18 21:42:15 +02:00
commit 166167384d
27 files changed
+2917 -118

No files matched your search

+1 -1
View File
@@ -846,7 +846,7 @@ TEST_PY_LSP_SCALE_SRCS = tests/test_py_lsp_scale.c
TEST_TS_LSP_SRCS = tests/test_ts_lsp.c
TEST_JAVA_LSP_SRCS = tests/test_java_lsp.c tests/test_java_lsp_coverage.c
TEST_KOTLIN_LSP_SRCS = tests/test_kotlin_lsp.c
TEST_KOTLIN_LSP_SRCS = tests/test_kotlin_lsp.c tests/test_kotlin_typed_calls.c
TEST_RUST_LSP_SRCS = tests/test_rust_lsp.c
TEST_INTEGRATION_SRCS = tests/test_integration.c tests/test_incremental.c tests/test_lang_contract.c tests/test_edge_imports.c tests/test_edge_structural.c tests/test_lsp_resolution_probe.c tests/test_node_creation_probe.c tests/test_edge_types_probe.c tests/test_convergence_probe.c tests/test_matrix_known_classes.c tests/test_matrix_new_constructs.c tests/test_grammar_probe_a.c tests/test_grammar_probe_b.c tests/test_grammar_probe_c.c tests/test_grammar_probe_d.c tests/test_grammar_probe_e.c tests/test_grammar_probe_f.c tests/test_grammar_probe_g.c tests/test_conditional_variants.c tests/test_spawns.c
+65 -2
View File
@@ -6624,11 +6624,16 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
* scope. Note the call-scope side (compute_func_qn in extract_unified.c) is
* NOT language-gated — it qualifies whenever a scope is pushed — so this gate
* and the scope-push rule must move together, or a call QN names a def QN that
* was never minted and the edge is dropped at write. */
* was never minted and the edge is dropped at write.
*
* Python: the only enclosing scope that reaches here is the def a nested
* def sits in (py_push_def_body), so `def index()` inside create_app is
* proj.mod.create_app.index. */
if (ctx->enclosing_class_qn &&
(ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA ||
ctx->language == CBM_LANG_TYPESCRIPT || ctx->language == CBM_LANG_TSX ||
ctx->language == CBM_LANG_ARKTS || ctx->language == CBM_LANG_NIX)) {
ctx->language == CBM_LANG_ARKTS || ctx->language == CBM_LANG_NIX ||
ctx->language == CBM_LANG_PYTHON)) {
def.qualified_name = cbm_arena_sprintf(a, "%s.%s", ctx->enclosing_class_qn, qn_name);
}
def.label = "Function";
@@ -11667,6 +11672,51 @@ static void push_class_body_children(TSNode node, const CBMLangSpec *spec, wd_st
}
}
/* Python: a def's body is a scope of its own, and a `def` or `class` statement
* in it is a definition like any other -- closures, decorator wrappers, the
* route handlers of an app factory (`@app.route` inside create_app), local
* classes. Walk the body with the def's QN as the enclosing scope, so a nested
* def is qualified by the def it sits in: proj.mod.create_app.index. That is
* the QN the Python LSP already gives the calls inside it (py_process_function)
* and the one the call-scope walk computes (extract_unified.c
* py_nearest_scope_qn). Only the body is walked: parameters,
* annotations and decorators hold expressions, never statements. A lambda or
* a comprehension holds no statements either, so neither becomes a def. */
static void py_push_def_body(wd_stack_t *s, TSNode def_node, const char *def_qn) {
TSNode body = ts_node_child_by_field_name(def_node, TS_FIELD("body"));
if (def_qn && !ts_node_is_null(body)) {
wd_push(s, body, def_qn);
}
}
/* Python methods are extracted by extract_class_methods, not by the walk, so
* their bodies are pushed here, under the QN push_method_def gave the method
* (class_qn.name), in source order. */
static void py_push_method_bodies(CBMExtractCtx *ctx, TSNode class_node, const char *class_qn,
wd_stack_t *s) {
TSNode body = find_class_member_body(class_node, ctx->language);
if (ts_node_is_null(body) || !class_qn) {
return;
}
for (int i = (int)ts_node_named_child_count(body) - SKIP_CHAR; i >= 0; i--) {
TSNode method = ts_node_named_child(body, (uint32_t)i);
if (strcmp(ts_node_type(method), "decorated_definition") == 0) {
method = ts_node_child_by_field_name(method, TS_FIELD("definition"));
}
if (ts_node_is_null(method) || strcmp(ts_node_type(method), "function_definition") != 0) {
continue;
}
TSNode name_node = resolve_method_name(method, ctx->language);
char *name =
ts_node_is_null(name_node)
? NULL
: cbm_func_name_node_text(ctx->arena, name_node, ctx->source, ctx->language);
if (name && name[0]) {
py_push_def_body(s, method, cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, name));
}
}
}
// ASCII case-insensitive equality vs a lowercase literal (CFML attribute names
// are case-insensitive, e.g. NAME / Name / name).
static bool ascii_ci_equals(const char *s, const char *lower_lit) {
@@ -12211,6 +12261,16 @@ static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec,
}
if (cbm_kind_in_set(node, spec->function_node_types)) {
if (!is_template_class_node(node, ctx->language)) {
if (ctx->language == CBM_LANG_PYTHON) {
int defs_before = ctx->result->defs.count;
extract_func_def(ctx, node, spec);
if (ctx->result->defs.count > defs_before) {
py_push_def_body(&s, node,
ctx->result->defs.items[ctx->result->defs.count - SKIP_ONE]
.qualified_name);
}
continue;
}
extract_func_def(ctx, node, spec);
// Most languages stop here. JS/TS (and Wolfram) descend into the
// function body so NESTED named definitions are also captured —
@@ -12268,6 +12328,9 @@ static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec,
extract_class_def(ctx, node, spec);
const char *new_enclosing = compute_class_qn(ctx, node, frame.enclosing_class_qn);
push_class_body_children(node, spec, &s, new_enclosing, ctx);
if (ctx->language == CBM_LANG_PYTHON && new_enclosing != frame.enclosing_class_qn) {
py_push_method_bodies(ctx, node, new_enclosing, &s);
}
continue;
}
+28 -5
View File
@@ -916,6 +916,21 @@ static const char *objectscript_get_method_qn(CBMExtractCtx *ctx, TSNode node,
return NULL;
}
/* Python: the nearest named scope around the node -- a def or a class. A def
* or class nested in a def is qualified by that def (proj.mod.create_app.index),
* exactly as extract_defs.c mints it; enclosing_class_qn alone would skip the
* def and hand a def nested in a method its method's QN. Lambdas push no named
* frame, so they stay transparent. */
static const char *py_nearest_scope_qn(const WalkState *state) {
for (int i = state->scope_top - SKIP_ONE; i >= 0; i--) {
const CBMWalkScope *f = &state->scopes[i];
if ((f->kind == SCOPE_FUNC || f->kind == SCOPE_CLASS) && f->qn) {
return f->qn;
}
}
return NULL;
}
// Compute function QN for scope tracking (mirrors cbm_enclosing_func_qn logic).
static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
WalkState *state) {
@@ -1107,8 +1122,10 @@ static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLan
}
}
if (state->enclosing_class_qn) {
return cbm_arena_sprintf(ctx->arena, "%s.%s", state->enclosing_class_qn, qn_name);
const char *outer_qn =
ctx->language == CBM_LANG_PYTHON ? py_nearest_scope_qn(state) : state->enclosing_class_qn;
if (outer_qn) {
return cbm_arena_sprintf(ctx->arena, "%s.%s", outer_qn, qn_name);
}
/* Java/Go: directory-based module so this enclosing-func QN matches the def
* QN and the LSP caller_qn (the lsp_resolve join keys on exact equality). */
@@ -1176,9 +1193,15 @@ static const char *compute_class_qn(CBMExtractCtx *ctx, TSNode node, const WalkS
/* Nested class: prefix with the enclosing class QN (Outer.Inner) so this
* scope QN matches the def-side class QN (extract_defs.c compute_class_qn /
* extract_class_def), which the lsp_resolve join requires for nested types. */
if (state && state->enclosing_class_qn) {
return cbm_arena_sprintf(ctx->arena, "%s.%s", state->enclosing_class_qn, name);
* extract_class_def), which the lsp_resolve join requires for nested types.
* Python: a class inside a def is qualified by the def (py_nearest_scope_qn). */
const char *outer_qn = NULL;
if (state) {
outer_qn = ctx->language == CBM_LANG_PYTHON ? py_nearest_scope_qn(state)
: state->enclosing_class_qn;
}
if (outer_qn) {
return cbm_arena_sprintf(ctx->arena, "%s.%s", outer_qn, name);
}
/* Java/Go: directory-based module (see compute_func_qn). */
+156 -14
View File
@@ -395,9 +395,47 @@ static const CBMType *kt_unwrap_nullable(const CBMType *t) {
return t;
}
/* A kotlin.Any member (toString / equals / hashCode) resolved on a stdlib or
* JDK type -- `count.toString()` on `val count: Int` binds kotlin.Int.toString
* from the stdlib seed. That target has no graph node, so the call join fell
* back to a name guess and bound the call to whichever project class declares
* a same-named override (#1555: detekt `defaultValue.toString()` ->
* Entity.toString). The member is the universal kotlin.Any one, an external
* builtin node: emit that, no project edge. `kotlin.` and `java.` are reserved
* packages (the compilers reject project declarations in them), so the prefix
* proves the owner is not a project type; project QNs in this resolver are
* package-spelled and keep their declared override. */
static bool kt_is_any_member(const char *name) {
return strcmp(name, "toString") == 0 || strcmp(name, "equals") == 0 ||
strcmp(name, "hashCode") == 0;
}
static const char *kt_external_any_member(KotlinLSPContext *ctx, const char *callee_qn) {
const char *dot = strrchr(callee_qn, '.');
if (!dot || !kt_is_any_member(dot + 1)) {
return NULL;
}
if (strncmp(callee_qn, "kotlin.Any.", strlen("kotlin.Any.")) == 0) {
return NULL; /* already the builtin */
}
if (strncmp(callee_qn, "kotlin.", strlen("kotlin.")) != 0 &&
strncmp(callee_qn, "java.", strlen("java.")) != 0) {
return NULL; /* a project override: a real node */
}
return kt_join_dot(ctx->arena, "kotlin.Any", dot + 1);
}
/* Append a typed resolution result to the output array. */
static void kt_emit_resolved_kind(KotlinLSPContext *ctx, const char *callee_qn,
const char *strategy, float confidence, CBMResolvedKind kind) {
if (kind == CBM_RESOLVED_INVOCATION && callee_qn) {
const char *any_qn = kt_external_any_member(ctx, callee_qn);
if (any_qn) {
callee_qn = any_qn;
strategy = "lsp_kt_any";
confidence = confidence < KT_CONF_PARTIAL ? confidence : KT_CONF_PARTIAL;
}
}
if (ctx->debug) {
fprintf(stderr, "[kotlin_lsp] EMIT %s -> %s [%s %.2f] (resolved_calls=%p enc=%s)\n",
ctx->enclosing_func_qn ? ctx->enclosing_func_qn : "(null)",
@@ -1127,20 +1165,26 @@ const char *kotlin_resolve_function_name(KotlinLSPContext *ctx, const char *name
* package/import/bare/default-import lookups miss, so it never overrides a
* more specific match; in the per-file pass the registry holds just this
* file's defs, so the candidate is the file's own sole top-level fun. */
if (ctx->registry && ctx->registry->funcs) {
/* The cross-file walk runs on an own-file overlay chained to the cross
* registry (#1555), so the candidates span the whole chain; a function
* registered in both layers under one QN is one candidate. */
{
const char *only_qn = NULL;
int matches = 0;
for (int i = 0; i < ctx->registry->func_count && matches < 2; i++) {
const CBMRegisteredFunc *f = &ctx->registry->funcs[i];
if (!f->qualified_name || !f->short_name) {
continue;
}
if (f->receiver_type) { /* method / extension — not a bare top-level fun */
continue;
}
if (strcmp(f->short_name, name) == 0) {
only_qn = f->qualified_name;
matches++;
for (const CBMTypeRegistry *r = ctx->registry; r && matches < 2; r = r->fallback) {
for (int i = 0; r->funcs && i < r->func_count && matches < 2; i++) {
const CBMRegisteredFunc *f = &r->funcs[i];
if (!f->qualified_name || !f->short_name) {
continue;
}
if (f->receiver_type) { /* method / extension — not a bare top-level fun */
continue;
}
if (strcmp(f->short_name, name) == 0 &&
!(only_qn && strcmp(only_qn, f->qualified_name) == 0)) {
only_qn = f->qualified_name;
matches++;
}
}
}
if (matches == 1 && only_qn) {
@@ -1335,6 +1379,22 @@ const CBMRegisteredFunc *kotlin_lookup_method(KotlinLSPContext *ctx, const char
const CBMRegisteredFunc *rf =
cbm_registry_lookup_method_aliased(ctx->registry, cur_qn, method_name);
if (rf) {
/* A member wins over an extension of the same name (Kotlin
* resolution). The file's own extension functions carry the
* extended class as receiver_type, so the receiver lookup can
* return `fun Builder.add(..)` for a call meant for the member
* `Builder.add`; the member's QN nests under the class. */
size_t cur_len = strlen(cur_qn);
bool is_member = rf->qualified_name &&
strncmp(rf->qualified_name, cur_qn, cur_len) == 0 &&
rf->qualified_name[cur_len] == '.';
if (!is_member) {
const CBMRegisteredFunc *member = cbm_registry_lookup_func(
ctx->registry, kt_join_dot(ctx->arena, cur_qn, method_name));
if (member) {
return member;
}
}
return rf;
}
@@ -2907,6 +2967,37 @@ out:
return result ? result : cbm_type_unknown();
}
/* `handshake()` inside a class whose constructor declares `private val
* handshake: Handshake?` next to `override fun handshake()` (okhttp
* RealConnection): the property is in the method scope (class fields are bound
* there), but a value of a known type without an `invoke` operator is no call
* candidate, so Kotlin resolves the call to the member function. Once #1555
* typed the class's constructor vals, the lexical-shadow rule below failed
* such calls closed and they fell to a name guess. An unknown or callable
* binding still shadows, as before. */
static bool kt_value_binding_yields_to_member(KotlinLSPContext *ctx, const CBMType *bound,
const char *name) {
if (!bound || cbm_type_is_unknown(bound) || bound->kind == CBM_TYPE_CALLABLE ||
bound->kind == CBM_TYPE_FUNC || !ctx->this_type || cbm_type_is_unknown(ctx->this_type)) {
return false;
}
const CBMType *value = kt_unwrap_nullable(bound);
const char *value_qn = kt_type_qn_of(value);
if (value_qn && kotlin_lookup_method(ctx, value_qn, "invoke")) {
return false; /* an invokable value: the call may be `value.invoke()` */
}
const char *this_qn = kt_type_qn_of(ctx->this_type);
if (!this_qn) {
return false;
}
const CBMRegisteredFunc *rf = kotlin_lookup_method(ctx, this_qn, name);
if (!rf || !rf->qualified_name) {
return false;
}
size_t len = strlen(this_qn);
return strncmp(rf->qualified_name, this_qn, len) == 0 && rf->qualified_name[len] == '.';
}
static const CBMType *kt_eval_constructor_or_func_call(KotlinLSPContext *ctx, TSNode call_node,
const char *callee_text) {
/* callee_text is a bare identifier — it may be:
@@ -2944,7 +3035,10 @@ static const CBMType *kt_eval_constructor_or_func_call(KotlinLSPContext *ctx, TS
bound->data.func.return_types[0]) {
return bound->data.func.return_types[0];
}
return cbm_type_unknown();
if (!kt_value_binding_yields_to_member(ctx, bound, callee_text)) {
return cbm_type_unknown();
}
/* A known non-invokable value yields to the member function: step 1. */
}
/* 1. `this`-method dispatch: if we're inside a class method or DSL
@@ -2967,6 +3061,37 @@ static const CBMType *kt_eval_constructor_or_func_call(KotlinLSPContext *ctx, TS
}
}
/* 1b. Implicit receivers form a chain, innermost first: inside a lambda
* whose receiver (or `it` type, which kt_process_lambda also installs as
* `this`) is another type, and inside a member extension whose extension
* receiver is another type, the enclosing class's members stay callable
* bare. Once #1555 typed more receivers, `repo.let { ping() }` resolved
* against Repo only and fell to a name guess. A call naming a class is a
* constructor call, left to step 2 (a secondary constructor is registered
* as a same-named member and would otherwise be taken for a method). */
const char *ctor_cls = kotlin_resolve_class_name(ctx, callee_text);
bool is_ctor_call =
ctor_cls && ctx->registry && cbm_registry_lookup_type(ctx->registry, ctor_cls) != NULL;
if (ctx->enclosing_class_qn && !is_ctor_call) {
const char *this_qn = (ctx->this_type && !cbm_type_is_unknown(ctx->this_type))
? kt_type_qn_of(ctx->this_type)
: NULL;
if (!this_qn || strcmp(this_qn, ctx->enclosing_class_qn) != 0) {
const CBMRegisteredFunc *rf =
kotlin_lookup_method(ctx, ctx->enclosing_class_qn, callee_text);
if (rf && rf->qualified_name) {
kt_emit_resolved_at(ctx, rf->qualified_name, "lsp_kt_this", KT_CONF_THIS,
call_node);
if (rf->signature && rf->signature->kind == CBM_TYPE_FUNC &&
rf->signature->data.func.return_types &&
rf->signature->data.func.return_types[0]) {
return rf->signature->data.func.return_types[0];
}
return cbm_type_unknown();
}
}
}
/* 2. Class? */
const char *cls_qn = kotlin_resolve_class_name(ctx, callee_text);
if (cls_qn && ctx->registry) {
@@ -4533,6 +4658,11 @@ void kotlin_lsp_process_file(KotlinLSPContext *ctx, TSNode root) {
/* 2. Collect top-level definitions for intra-file resolution. */
kt_collect_top_level_decls(ctx, root);
/* Cross-file: index the own-file overlay now that its declarations are
* in; later additions stay visible through the post-finalize tail. */
if (ctx->overlay_index_arena && ctx->registry) {
cbm_registry_finalize_into((CBMTypeRegistry *)ctx->registry, ctx->overlay_index_arena);
}
/* 3. Walk for call resolution. */
kt_walk_top_level_for_resolution(ctx, root, NULL);
@@ -5436,12 +5566,24 @@ void cbm_run_kotlin_lsp_cross(CBMArena *arena, const char *source, int source_le
project_name = module_qn;
}
/* Own-file overlay (#1555). The cross registry above already holds this
* file's classes, built from extraction defs whose properties carry no
* type (primary-constructor vals none at all). The walk re-registers each
* class from the AST with typed fields; registered into `reg` under the
* same QN that entry was shadowed by the earlier one, so every
* `repository.inserir()` on an injected property fell to the name guess.
* In an overlay checked first, the AST entry wins. */
CBMTypeRegistry overlay;
cbm_registry_init(&overlay, arena);
overlay.fallback = &reg;
KotlinLSPContext ctx;
/* Match the per-file pass: until a source package declaration overrides
* it, the path-derived module is the callable-QN prefix. This preserves
* exact caller identity for default-package functions across files. */
kotlin_lsp_init(&ctx, arena, source, source_len, &reg, module_qn ? module_qn : "",
kotlin_lsp_init(&ctx, arena, source, source_len, &overlay, module_qn ? module_qn : "",
module_qn ? module_qn : "", project_name, /*rel_path=*/NULL, out);
ctx.overlay_index_arena = &idx_arena;
/* Unique short type name -> registered QN, for receiver annotations that
* name a type defined in another file without an import. Two types
+7
View File
@@ -147,6 +147,13 @@ typedef struct KotlinLSPContext {
* each field's REAL def QN here (kotlin class properties are minted with
* module-level QNs, so composing <class>.<member> would miss the node). */
const void *cross_field_map;
/* Cross-file only (NULL per-file): when set, `registry` is a per-file
* overlay chained to the cross registry, and kotlin_lsp_process_file
* indexes the overlay into this scratch arena once the file's own
* declarations are collected. The overlay holds the AST registrations
* (typed primary-constructor and body properties), so they are found
* before the extraction-built entries of the same QN (#1555). */
CBMArena *overlay_index_arena;
} KotlinLSPContext;
/* Initialize a KotlinLSPContext for processing one file. */
+140 -17
View File
@@ -246,6 +246,11 @@ static void py_register_lambda(PyLSPContext *ctx, const char *name, TSNode lambd
static void py_register_dict_literal(PyLSPContext *ctx, const char *name, TSNode dict_node);
static TSNode py_lookup_lambda(PyLSPContext *ctx, const char *name);
static const char *py_lookup_dict_dispatch(PyLSPContext *ctx, const char *var, const char *key);
static void py_process_function(PyLSPContext *ctx, TSNode func_node, const char *container_qn);
static void py_process_class(PyLSPContext *ctx, TSNode class_node);
static void py_process_class_in(PyLSPContext *ctx, TSNode class_node, const char *container_qn);
typedef enum { PY_LOCAL_DEFS_BIND, PY_LOCAL_DEFS_PROCESS } PyLocalDefsMode;
static void py_visit_local_defs(PyLSPContext *ctx, TSNode block, PyLocalDefsMode mode, int depth);
/* ── Scope mutation wrappers ─────────────────────────────────────────────
*
@@ -4362,6 +4367,101 @@ static void py_bind_parameters(PyLSPContext *ctx, TSNode params) {
}
}
/* Bind a def or class statement of the current function body as a local of
* that function (#1548): its graph QN extends the function's
* (enclosing_func_qn.name), the QN extraction gives it. An undecorated def
* registered with that QN becomes an exact callable alias; anything else
* (decorated, overloaded, not registered) is a hard local shadow, so no call
* falls through to a same-named module symbol. A class is bound by its type
* when registered, else shadowed. A parameter of the same name is already a
* local and keeps its own binding. */
static void py_bind_local_def(PyLSPContext *ctx, TSNode def, bool is_class) {
TSNode name_node = ts_node_child_by_field_name(def, "name", 4);
char *name = ts_node_is_null(name_node) ? NULL : py_node_text(ctx, name_node);
if (!name || !name[0] || !ctx->enclosing_func_qn) {
py_disable_callable_value_proof(ctx);
return;
}
if (cbm_scope_lookup_local(ctx->current_scope, name))
return;
const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->enclosing_func_qn, name);
if (!qn) {
py_disable_callable_value_proof(ctx);
py_scope_bind(ctx, name, cbm_type_unknown());
return;
}
if (is_class) {
py_scope_bind(ctx, name,
cbm_registry_lookup_type(ctx->registry, qn) ? cbm_type_named(ctx->arena, qn)
: cbm_type_unknown());
return;
}
const CBMRegisteredFunc *func = cbm_registry_lookup_func(ctx->registry, qn);
if (py_func_is_exact_callable_value(func)) {
py_scope_bind_callable(ctx, name, cbm_type_unknown(), func->qualified_name);
} else {
py_scope_bind(ctx, name, cbm_type_unknown());
}
}
/* Statements a function body nests further statements in. A def inside any of
* them is still a local of the function; a nested def, class or lambda body
* is a scope of its own and is never entered. */
static bool py_is_statement_block(const char *kind) {
static const char *const kinds[] = {"block",
"if_statement",
"elif_clause",
"else_clause",
"for_statement",
"while_statement",
"try_statement",
"except_clause",
"except_group_clause",
"finally_clause",
"with_statement",
"match_statement",
"case_clause",
NULL};
for (int i = 0; kinds[i]; i++) {
if (strcmp(kind, kinds[i]) == 0)
return true;
}
return false;
}
/* Visit the def/class statements of a function body, including those inside
* its compound statements: BIND binds each as a local (py_bind_local_def),
* PROCESS walks each nested def's own body (py_process_function). */
static void py_visit_local_defs(PyLSPContext *ctx, TSNode block, PyLocalDefsMode mode, int depth) {
if (!ctx || ctx->eval_failure || ts_node_is_null(block) || depth >= cbm_lsp_max_walk_depth())
return;
PyKids kids = py_kids(ctx->arena, block);
for (uint32_t i = 0; i < kids.n && !ctx->eval_failure; i++) {
TSNode c = py_kid(&kids, i);
const char *ck = ts_node_type(c);
TSNode def = c;
if (strcmp(ck, "decorated_definition") == 0) {
def = ts_node_child_by_field_name(c, "definition", 10);
if (ts_node_is_null(def))
continue;
ck = ts_node_type(def);
}
if (strcmp(ck, "function_definition") == 0) {
if (mode == PY_LOCAL_DEFS_BIND)
py_bind_local_def(ctx, def, false);
else
py_process_function(ctx, def, ctx->enclosing_func_qn);
} else if (strcmp(ck, "class_definition") == 0) {
if (mode == PY_LOCAL_DEFS_BIND)
py_bind_local_def(ctx, def, true);
else
py_process_class_in(ctx, def, ctx->enclosing_func_qn);
} else if (py_is_statement_block(ck)) {
py_visit_local_defs(ctx, c, mode, depth + 1);
}
}
}
static void py_process_function(PyLSPContext *ctx, TSNode func_node, const char *container_qn) {
if (!ctx || ctx->eval_failure)
return;
@@ -4391,22 +4491,13 @@ static void py_process_function(PyLSPContext *ctx, TSNode func_node, const char
TSNode body = ts_node_child_by_field_name(func_node, "body", 4);
if (!ts_node_is_null(body)) {
/* Python resolves a bare name in the function's own scope first: every
* def/class statement of the body binds a local for the whole body
* (LEGB), whatever a module-level symbol of the same spelling says. */
py_visit_local_defs(ctx, body, PY_LOCAL_DEFS_BIND, 0);
py_resolve_calls_in(ctx, body);
// Also descend into nested function/class definitions in the body.
PyKids bk = py_kids(ctx->arena, body);
for (uint32_t i = 0; i < bk.n; i++) {
TSNode c = py_kid(&bk, i);
const char *ck = ts_node_type(c);
if (strcmp(ck, "function_definition") == 0) {
py_process_function(ctx, c, ctx->enclosing_func_qn);
} else if (strcmp(ck, "decorated_definition") == 0) {
TSNode def = ts_node_child_by_field_name(c, "definition", 10);
if (!ts_node_is_null(def) &&
strcmp(ts_node_type(def), "function_definition") == 0) {
py_process_function(ctx, def, ctx->enclosing_func_qn);
}
}
}
// Also descend into nested function definitions in the body.
py_visit_local_defs(ctx, body, PY_LOCAL_DEFS_PROCESS, 0);
}
py_scope_restore(ctx, saved);
@@ -4536,7 +4627,10 @@ static void py_exec_class_definition(PyLSPContext *ctx, TSNode class_node) {
py_scope_restore(ctx, saved_scope);
}
static void py_process_class(PyLSPContext *ctx, TSNode class_node) {
/* Walk a class's methods with the class qualified under `container_qn`: the
* module for a module-level class, the enclosing def for a class declared in
* a function body (#1548: extraction gives it the def's QN as prefix). */
static void py_process_class_in(PyLSPContext *ctx, TSNode class_node, const char *container_qn) {
if (!ctx || ctx->eval_failure)
return;
TSNode name_node = ts_node_child_by_field_name(class_node, "name", 4);
@@ -4547,7 +4641,8 @@ static void py_process_class(PyLSPContext *ctx, TSNode class_node) {
return;
const char *prev_class = ctx->enclosing_class_qn;
ctx->enclosing_class_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, cname);
ctx->enclosing_class_qn =
cbm_arena_sprintf(ctx->arena, "%s.%s", container_qn ? container_qn : ctx->module_qn, cname);
TSNode body = ts_node_child_by_field_name(class_node, "body", 4);
if (!ts_node_is_null(body)) {
@@ -4621,6 +4716,10 @@ static void py_process_class(PyLSPContext *ctx, TSNode class_node) {
ctx->enclosing_class_qn = prev_class;
}
static void py_process_class(PyLSPContext *ctx, TSNode class_node) {
py_process_class_in(ctx, class_node, NULL);
}
/* Module bindings are replayed in source order before deferred function bodies
* are analyzed. Class definitions always replace an earlier callable identity;
* the registry decides whether the resulting class type is known precisely. */
@@ -4687,6 +4786,28 @@ static char *py_module_prefix(CBMArena *scratch, const char *module_qn, size_t l
return prefix;
}
/* A class declared inside a def -- django's per-test `class MyModelAdmin`, an
* app factory's local class, and anything nested in one -- is a local of that
* def, not a global of the module: bound here by its short name, every
* same-named local class of the file collapsed onto whichever registered last.
* Its QN extends a registered function's (proj.mod.Tests.test_x.MyModelAdmin),
* which tells it apart from a class nested in a class (proj.mod.Outer.Inner).
* Only prefixes longer than the module are probed. */
static bool py_type_is_function_local(const PyLSPContext *ctx, const char *qn, size_t module_len) {
char scope[512];
size_t len = strlen(qn);
if (len >= sizeof(scope))
return false;
memcpy(scope, qn, len + 1);
for (char *dot = strrchr(scope, '.'); dot && (size_t)(dot - scope) > module_len;
dot = strrchr(scope, '.')) {
*dot = '\0';
if (cbm_registry_lookup_func(ctx->registry, scope))
return true;
}
return false;
}
/* A project registry can contain another file's class in the same logical
* Python module. Keep those cross-file globals available, but do not prebind
* classes declared by this source: their binding epoch belongs in the ordered
@@ -4734,6 +4855,8 @@ static void py_bind_external_module_classes(PyLSPContext *ctx, const PyKids *rk)
continue;
if (package_scope && strchr(qn + prefix_len + 1, '.'))
continue; /* submodule/nested classes are not package globals */
if (py_type_is_function_local(ctx, qn, prefix_len))
continue; /* a def-local class is no module global (#1548) */
py_scope_bind(ctx, name, cbm_type_named(ctx->arena, qn));
}
if (prefix)
+56 -19
View File
@@ -4144,6 +4144,32 @@ static bool rust_qn_has_suffix(const char *qualified_name, const char *suffix) {
(qn_len == suffix_len || qualified_name[qn_len - suffix_len - 1U] == '.');
}
/* True when the segment that qualifies a call path's tail names a TYPE:
* `Store::new`, `Self::helper`, `T::default`, `<T as Tr>::f` (turbofish is
* already stripped, so a `>` before the last `::` closes a qualified path).
* Such a call is an associated function, never a free function, so the
* free-function short-name fallbacks must not bind it: with a project-wide
* unique free `new`/`write`, `Store::new()` landed on that function and the
* LSP row pre-empted the registry's correct `Store.new`. Module segments are
* snake_case, so an ASCII-uppercase initial is a type. `tail` must point just
* past the path's last `::`, or equal `path` when there is none. */
static bool rust_path_qualifier_is_type(const char *path, const char *tail) {
if (!path || !tail || tail - path < 3) {
return false;
}
const char *seg_end = tail - 2; /* the last "::" */
if (seg_end[-1] == '>') {
return true;
}
const char *seg = path;
for (const char *p = path; p + 1 < seg_end; p++) {
if (p[0] == ':' && p[1] == ':') {
seg = p + 2;
}
}
return seg < seg_end && seg[0] >= 'A' && seg[0] <= 'Z';
}
/* Resolve only an exact identifier/scoped-path value. No branch, closure, or
* other composite expression enters this helper. A suffix fallback is admitted
* only for a qualified import path and only when it identifies one free func. */
@@ -4530,6 +4556,9 @@ static void rust_resolve_call_expression_inner(RustLSPContext *ctx, TSNode node)
} else {
tail = path;
}
/* `Type::f` is an associated function: the two free-function
* fallbacks below never bind it (see rust_path_qualifier_is_type). */
const bool assoc_call = rust_path_qualifier_is_type(path, tail);
/* Cross-crate workspace-member resolution (#56): when the call
* path's head is a declared Cargo workspace member (e.g.
@@ -4540,7 +4569,7 @@ static void rust_resolve_call_expression_inner(RustLSPContext *ctx, TSNode node)
* MEMBER crate by matching the registered QN's `.<member>.`
* path segment plus the call tail. Requires a parsed manifest
* (threaded through pass_lsp_cross.c); NULL manifest skips this. */
if (ctx->cargo_manifest && tail && *tail) {
if (!assoc_call && ctx->cargo_manifest && tail && *tail) {
const char *head_sep = strstr(path, "::");
if (head_sep && head_sep > path) {
char *head = cbm_arena_strndup(ctx->arena, path, (size_t)(head_sep - path));
@@ -4584,7 +4613,7 @@ static void rust_resolve_call_expression_inner(RustLSPContext *ctx, TSNode node)
* foo; use foo::bar; bar()` a chance to resolve when the
* intermediate module wasn't tracked through an explicit
* use-map entry. */
if (tail && *tail && ctx->module_qn) {
if (!assoc_call && tail && *tail && ctx->module_qn) {
/* Crate prefix is the first dotted segment of module_qn after
* the project name, but for simplicity we just match on
* "starts with first dot-segment". */
@@ -6341,7 +6370,10 @@ static void rust_resolve_against_registry(CBMArena *arena, const char *source, i
* read-only, and shared across every Rust file's resolve (mirrors C/py/cs/ts).
* Converts the pipeline's CBMLSPDef into CBMRustLSPDef inline (same field copy as
* pass_lsp_cross.c's pxc_lspdefs_to_rust, including proven trait provenance).
* module_qn=NULL is byte-identical because all_defs always carry def_module_qn. */
* module_qn=NULL is byte-identical because all_defs always carry def_module_qn.
* all_defs is mixed-language: only Rust defs are registered, exactly like the
* per-file conversion, so a Perl `new` or a C function can never become the
* target of a Rust call (the Go builder filters the same way). */
CBMTypeRegistry *cbm_rust_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, int def_count) {
if (!arena)
return NULL;
@@ -6349,29 +6381,34 @@ CBMTypeRegistry *cbm_rust_build_cross_registry(CBMArena *arena, CBMLSPDef *defs,
if (!reg)
return NULL;
CBMRustLSPDef *rdefs = NULL;
int n = 0;
if (def_count > 0) {
rdefs = (CBMRustLSPDef *)cbm_arena_alloc(arena, (size_t)def_count * sizeof(CBMRustLSPDef));
if (!rdefs)
return NULL;
for (int i = 0; i < def_count; i++) {
rdefs[i].qualified_name = defs[i].qualified_name;
rdefs[i].short_name = defs[i].short_name;
rdefs[i].label = defs[i].label;
rdefs[i].receiver_type = defs[i].receiver_type;
rdefs[i].def_module_qn = defs[i].def_module_qn;
rdefs[i].return_types = defs[i].return_types;
rdefs[i].embedded_types = defs[i].embedded_types;
rdefs[i].field_defs = defs[i].field_defs;
rdefs[i].method_names_str = defs[i].method_names_str;
rdefs[i].signature_param_types = defs[i].signature_param_types;
rdefs[i].signature_param_count = defs[i].signature_param_count;
rdefs[i].trait_qn = defs[i].trait_qn;
rdefs[i].is_interface = defs[i].is_interface;
rdefs[i].is_rust_impl_relation = defs[i].is_rust_impl_relation;
rdefs[i].is_abstract = defs[i].is_abstract;
if (defs[i].lang != CBM_LANG_RUST) {
continue;
}
rdefs[n].qualified_name = defs[i].qualified_name;
rdefs[n].short_name = defs[i].short_name;
rdefs[n].label = defs[i].label;
rdefs[n].receiver_type = defs[i].receiver_type;
rdefs[n].def_module_qn = defs[i].def_module_qn;
rdefs[n].return_types = defs[i].return_types;
rdefs[n].embedded_types = defs[i].embedded_types;
rdefs[n].field_defs = defs[i].field_defs;
rdefs[n].method_names_str = defs[i].method_names_str;
rdefs[n].signature_param_types = defs[i].signature_param_types;
rdefs[n].signature_param_count = defs[i].signature_param_count;
rdefs[n].trait_qn = defs[i].trait_qn;
rdefs[n].is_interface = defs[i].is_interface;
rdefs[n].is_rust_impl_relation = defs[i].is_rust_impl_relation;
rdefs[n].is_abstract = defs[i].is_abstract;
n++;
}
}
rust_populate_cross_registry(reg, arena, rdefs, def_count, /*module_qn=*/NULL);
rust_populate_cross_registry(reg, arena, n > 0 ? rdefs : NULL, n, /*module_qn=*/NULL);
reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */
return reg;
}
+24
View File
@@ -355,6 +355,30 @@ char *cbm_pipeline_resolve_relative_import(const char *source_rel, const char *m
return resolve_js_relative(buf, sizeof(buf), module_path);
}
size_t cbm_pipeline_fqn_symbol_prefix_len(const char *project, const char *rel_path) {
if (!project) {
return 0;
}
char path[FQN_PATH_BUF];
int n = snprintf(path, sizeof(path), "%s", rel_path ? rel_path : "");
if (n < 0 || (size_t)n >= sizeof(path)) {
return 0;
}
cbm_normalize_path_sep(path);
strip_file_extension(path);
const char *segments[CBM_SZ_256];
int seg_count = 0;
segments[seg_count++] = project;
seg_count += tokenize_path(path, segments + seg_count, FQN_MAX_PATH_SEGS);
/* Any symbol name: the prefix cbm_pipeline_fqn_compute puts before it. */
strip_init_or_index(segments, &seg_count, "symbol");
size_t total = 0;
for (int i = 0; i < seg_count; i++) {
total += strlen(segments[i]) + (i > 0 ? FQN_SEP_LEN : 0);
}
return total;
}
char *cbm_pipeline_fqn_folder(const char *project, const char *rel_dir) {
if (!project) {
return strdup("");
+18
View File
@@ -223,6 +223,24 @@ static inline bool cbm_pipeline_node_is_callable_target(const cbm_gbuf_node_t *n
strcmp(node->label, "Constructor") == 0 || strcmp(node->label, "Class") == 0;
}
/* The Kotlin resolver claims a property READ on a typed receiver with an exact
* row (lsp_kt_property_ref) so that the raw-name fallback cannot borrow a
* same-named function. That row names a property, never a function value. Its
* QN is not always materialized under that spelling (a property of a nested
* class); the graph lookup then finds nothing, or its unique-callable step
* picks a same-named function elsewhere (#1555: okhttp `server.webSocket` -> a
* test function `webSocket`, a fabricated CALL_REFERENCE). Such a row is not
* admissible: the caller falls back by name, restricted to non-callable
* targets, which binds the property's own node as before the receiver was
* typed and can never borrow a function. */
static inline bool cbm_pipeline_reference_target_admissible(const CBMResolvedCall *ref,
const cbm_gbuf_node_t *tgt) {
if (!ref || !ref->strategy || strcmp(ref->strategy, "lsp_kt_property_ref") != 0) {
return true;
}
return tgt && !cbm_pipeline_node_is_callable_target(tgt);
}
static inline int cbm_pipeline_qn_class_method_tail_eq(const char *qn, const char *tail) {
const char *qt = cbm_pipeline_qn_class_method_tail(qn);
return qt && tail && strcmp(qt, tail) == 0;
+16 -5
View File
@@ -587,9 +587,18 @@ static void handle_route_registration(cbm_pipeline_ctx_t *ctx, const CBMCall *ca
esc_fa);
cbm_gbuf_insert_edge(ctx->gbuf, source_node->id, route_id, "CALLS", props);
if (call->second_arg_name != NULL && call->second_arg_name[0] != '\0') {
cbm_resolution_t hres =
cbm_registry_resolve_handler(ctx->registry, call->second_arg_name, module_qn, imp_keys,
imp_vals, imp_count, ctx->gbuf, lang);
/* Python: an app factory registers its local defs
* (`app.add_url_rule("/", "index", index)` inside create_app); only the
* lexical lookup sees a function's locals. MUST match pass_parallel.c. */
cbm_resolution_t hres = {0};
if (lang == CBM_LANG_PYTHON) {
hres = cbm_registry_resolve_lexical(ctx->registry, call->second_arg_name,
call->enclosing_func_qn, module_qn);
}
if (!hres.qualified_name) {
hres = cbm_registry_resolve_handler(ctx->registry, call->second_arg_name, module_qn,
imp_keys, imp_vals, imp_count, ctx->gbuf, lang);
}
if (hres.qualified_name != NULL && hres.qualified_name[0] != '\0') {
const cbm_gbuf_node_t *handler = cbm_gbuf_find_by_qn(ctx->gbuf, hres.qualified_name);
if (handler != NULL) {
@@ -967,8 +976,10 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
lang, lsp->strategy, lsp->callee_qn, ctx->project_name);
cbm_resolution_t res = {0};
if (!rust_external && !php_vendor_call) {
res = cbm_registry_resolve_call_lang(ctx->registry, call->callee_name, module_qn, imp_keys,
imp_vals, imp_count, lang);
/* Python LEGB fallback first (MUST match pass_parallel.c). */
res =
cbm_registry_resolve_call_scoped(ctx->registry, call->callee_name, module_qn, imp_keys,
imp_vals, imp_count, lang, call->enclosing_func_qn);
/* Cross-language veto: a name-only guess never binds another
* language's symbol (a JS app.get must not become Python's
* builtins.dict.get). The vetoed answer becomes EMPTY here, before
+11 -4
View File
@@ -432,7 +432,8 @@ static void link_def_parents(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def,
* `link_parents` links its DEFINES_METHOD / diagram parent right away; with
* the result cache the pass links them once every file is registered. */
static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const char *rel,
const char *file_doc, CBMLanguage lang, bool link_parents) {
const char *file_doc, CBMLanguage lang, size_t module_prefix_len,
bool link_parents) {
if (!def->qualified_name || !def->name) {
return;
}
@@ -458,7 +459,8 @@ static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const
* (SQL FROM/JOIN lineage). pass_parallel.c and pipeline_incremental.c seed
* through the same predicate, so the three registries cannot diverge. */
if (node_id > 0 && cbm_label_is_registry_symbol(def->label)) {
cbm_registry_add_lang(ctx->registry, def->name, def->qualified_name, def->label, lang);
cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name, def->label,
lang, module_prefix_len);
}
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__");
const cbm_gbuf_node_t *file_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn);
@@ -1061,10 +1063,15 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
"test_declarations");
}
/* Create nodes for each definition */
/* Create nodes for each definition. A Python file's module QN bounds
* the registry's function-local detection to this file. */
const char *file_doc = result->module_doc; /* goes with the first def */
size_t py_prefix_len = lang == CBM_LANG_PYTHON
? cbm_pipeline_fqn_symbol_prefix_len(ctx->project_name, rel)
: 0;
for (int d = 0; d < result->defs.count; d++) {
process_def(ctx, &result->defs.items[d], rel, file_doc, lang, !local_cache);
process_def(ctx, &result->defs.items[d], rel, file_doc, lang, py_prefix_len,
!local_cache);
file_doc = NULL;
total_defs++;
}
+159 -19
View File
@@ -238,6 +238,67 @@ typedef struct {
CBMLanguage lang;
} pxc_base_scope_t;
/* A type-like graph node, or NULL. */
static const cbm_gbuf_node_t *pxc_type_node(const cbm_gbuf_node_t *node) {
return node && node->qualified_name && cbm_label_is_type_like(node->label) ? node : NULL;
}
/* Python `import a.b.c` binds the ROOT name `a`, so a base spelled through it
* (`class X(a.b.c.Base)`) is module a.b.c's Base. The import map holds the
* imported MODULE under that root local (the IMPORTS edge target), and the
* registry's import_map step appends the whole remaining spelling to it
* (a.b.c + .b.c.Base), so a dotted base never resolved and every member
* inherited through it was invisible to the LSP. Resolve the spelling against
* the module the import names: the import map's module when that local is
* unique there, else the dotted module path under the project's QN rule.
* Import-backed and exact -- no short-name step -- and only a type-like
* project node is accepted. `import a.b as m` and `from a import b` bind the
* module itself and keep resolving through the import map. */
static const char *pxc_python_dotted_base_qn(const pxc_base_scope_t *bs, const char *raw) {
if (bs->lang != CBM_LANG_PYTHON || !bs->imports || !bs->gbuf || !strchr(raw, '.')) {
return NULL;
}
size_t root_len = strcspn(raw, ".");
for (int i = 0; i < bs->imports->count; i++) {
const CBMImport *imp = &bs->imports->items[i];
const char *local = imp->local_name;
const char *path = imp->module_path;
if (!local || !path || strlen(local) != root_len || strncmp(raw, local, root_len) != 0 ||
strncmp(path, local, root_len) != 0 ||
(path[root_len] != '\0' && path[root_len] != '.')) {
continue; /* not a root binding of this base's first segment */
}
size_t path_len = strlen(path);
if (strncmp(raw, path, path_len) != 0 || raw[path_len] != '.') {
continue; /* the base does not extend this import's module path */
}
const char *module_qn = NULL;
int hits = 0;
for (int k = 0; k < bs->imp_count; k++) {
if (bs->imp_keys[k] && bs->imp_vals[k] && strcmp(bs->imp_keys[k], local) == 0) {
module_qn = bs->imp_vals[k];
hits++;
}
}
const cbm_gbuf_node_t *node = NULL;
if (hits == 1) {
char candidate[CBM_SZ_512];
int n = snprintf(candidate, sizeof(candidate), "%s%s", module_qn, raw + path_len);
if (n > 0 && (size_t)n < sizeof(candidate)) {
node = pxc_type_node(cbm_gbuf_find_by_qn(bs->gbuf, candidate));
}
}
if (!node) {
node =
pxc_type_node(cbm_pipeline_lsp_target_node(bs->gbuf, bs->project_name, raw, false));
}
if (node) {
return node->qualified_name;
}
}
return NULL;
}
/* Resolve one base-class spelling to a project QN. Mirrors
* pass_semantic.c::resolve_as_class — same registry, same type-like veto —
* then additionally rejects weak short-name strategies (see above).
@@ -248,6 +309,10 @@ static const char *pxc_resolve_base_qn(const pxc_base_scope_t *bs, const char *r
if (!bs || !bs->reg || !raw || !raw[0]) {
return NULL;
}
const char *dotted = pxc_python_dotted_base_qn(bs, raw);
if (dotted) {
return dotted;
}
cbm_resolution_t res = cbm_registry_resolve_lang(bs->reg, raw, module_qn, bs->imp_keys,
bs->imp_vals, bs->imp_count, bs->lang);
if (!res.qualified_name || !res.qualified_name[0]) {
@@ -1285,6 +1350,68 @@ static void pxc_supersede_occurrence(CBMArena *dst_arena, CBMResolvedCallArray *
dst_calls->count = kept;
}
/* The Kotlin universal-member placeholder: `x.hashCode()` / `toString()` /
* `equals()` on a receiver the resolver could not type is emitted on kotlin.Any
* (strategy lsp_kt_any, reduced confidence) -- the "no narrower receiver known"
* answer. */
static bool pxc_is_kt_any_placeholder(const CBMResolvedCall *rc) {
return rc->strategy && strcmp(rc->strategy, "lsp_kt_any") == 0;
}
static char *pxc_occurrence_key(CBMArena *keys, const CBMResolvedCall *rc) {
return cbm_arena_sprintf(keys, "%u\x1f%s\x1f%u:%u\x1f%u", (unsigned)rc->kind, rc->caller_qn,
rc->site_start_byte, rc->site_end_byte, (unsigned)rc->source_origin);
}
/* #1555: the per-file pass sees only the file's own declarations, so a receiver
* typed by another file (`val type: ProtoType`) is unknown there and
* `type.hashCode()` gets the kotlin.Any placeholder. The cross pass types the
* receiver and answers with the declared override (`ProtoType.hashCode`), or
* with kotlin.Any again when no class on the receiver's chain declares one. Two
* distinct targets at one occurrence are a tombstone in the call join
* (pass_parallel.c LSP_IDX_AMBIGUOUS), so the occurrence fell to the name-based
* registry guess (wire: `type.hashCode` -> AllTypes.hashCode). A placeholder is
* therefore dropped when a cross row resolves the same occurrence; the cross row
* is then the only evidence. One hash pass per side keeps this linear. */
static void pxc_drop_superseded_kt_any(CBMArena *keys, CBMResolvedCallArray *dst_calls,
const CBMResolvedCallArray *src_out) {
bool has_placeholder = false;
for (int i = 0; i < dst_calls->count && !has_placeholder; i++) {
has_placeholder = pxc_is_kt_any_placeholder(&dst_calls->items[i]);
}
if (!has_placeholder) {
return;
}
CBMHashTable *resolved = cbm_ht_create((uint32_t)src_out->count + 1);
for (int j = 0; j < src_out->count; j++) {
const CBMResolvedCall *src = &src_out->items[j];
if (!src->caller_qn || !src->callee_qn || pxc_is_kt_any_placeholder(src) ||
src->site_end_byte <= src->site_start_byte) {
continue;
}
char *k = pxc_occurrence_key(keys, src);
if (k) {
cbm_ht_set(resolved, k, (void *)(uintptr_t)1);
}
}
int kept = 0;
for (int i = 0; i < dst_calls->count; i++) {
const CBMResolvedCall *cur = &dst_calls->items[i];
if (pxc_is_kt_any_placeholder(cur) && cur->caller_qn) {
char *k = pxc_occurrence_key(keys, cur);
if (k && cbm_ht_get(resolved, k)) {
continue;
}
}
if (kept != i) {
dst_calls->items[kept] = *cur;
}
kept++;
}
dst_calls->count = kept;
cbm_ht_free(resolved);
}
static void pxc_append_results(CBMArena *dst_arena, CBMResolvedCallArray *dst_calls,
const CBMResolvedCallArray *src_out) {
if (!dst_calls || !src_out)
@@ -1312,6 +1439,7 @@ static void pxc_append_results(CBMArena *dst_arena, CBMResolvedCallArray *dst_ca
}
pxc_supersede_occurrence(dst_arena, dst_calls, src);
}
pxc_drop_superseded_kt_any(&keys, dst_calls, src_out);
for (int i = 0; i < dst_calls->count; i++) {
const CBMResolvedCall *rc = &dst_calls->items[i];
@@ -1470,31 +1598,42 @@ const struct CBMCargoManifest *cbm_pxc_get_rust_manifest(void) {
/* Convert a CBMLSPDef array (the pipeline's lingua franca, go_lsp.h:73)
* into a CBMRustLSPDef array (rust_lsp.h) inside `arena`. The two structs
* have similar fields but different layouts; CBMLSPDef adds
* language/namespace metadata, so a memcpy is unsafe — copy field-by-field. */
static CBMRustLSPDef *pxc_lspdefs_to_rust(CBMArena *arena, const CBMLSPDef *defs, int def_count) {
* language/namespace metadata, so a memcpy is unsafe — copy field-by-field.
* Only Rust defs are kept: the def set is mixed-language, and a Perl or C
* function must never become a Rust call target (the Go registrar filters
* the same way). *out_count gets the number kept. */
static CBMRustLSPDef *pxc_lspdefs_to_rust(CBMArena *arena, const CBMLSPDef *defs, int def_count,
int *out_count) {
*out_count = 0;
if (!defs || def_count <= 0)
return NULL;
CBMRustLSPDef *out =
(CBMRustLSPDef *)cbm_arena_alloc(arena, (size_t)def_count * sizeof(CBMRustLSPDef));
if (!out)
return NULL;
int n = 0;
for (int i = 0; i < def_count; i++) {
out[i].qualified_name = defs[i].qualified_name;
out[i].short_name = defs[i].short_name;
out[i].label = defs[i].label;
out[i].receiver_type = defs[i].receiver_type;
out[i].def_module_qn = defs[i].def_module_qn;
out[i].return_types = defs[i].return_types;
out[i].embedded_types = defs[i].embedded_types;
out[i].field_defs = defs[i].field_defs;
out[i].method_names_str = defs[i].method_names_str;
out[i].signature_param_types = defs[i].signature_param_types;
out[i].signature_param_count = defs[i].signature_param_count;
out[i].trait_qn = defs[i].trait_qn;
out[i].is_interface = defs[i].is_interface;
out[i].is_rust_impl_relation = defs[i].is_rust_impl_relation;
out[i].is_abstract = defs[i].is_abstract;
if (defs[i].lang != CBM_LANG_RUST) {
continue;
}
out[n].qualified_name = defs[i].qualified_name;
out[n].short_name = defs[i].short_name;
out[n].label = defs[i].label;
out[n].receiver_type = defs[i].receiver_type;
out[n].def_module_qn = defs[i].def_module_qn;
out[n].return_types = defs[i].return_types;
out[n].embedded_types = defs[i].embedded_types;
out[n].field_defs = defs[i].field_defs;
out[n].method_names_str = defs[i].method_names_str;
out[n].signature_param_types = defs[i].signature_param_types;
out[n].signature_param_count = defs[i].signature_param_count;
out[n].trait_qn = defs[i].trait_qn;
out[n].is_interface = defs[i].is_interface;
out[n].is_rust_impl_relation = defs[i].is_rust_impl_relation;
out[n].is_abstract = defs[i].is_abstract;
n++;
}
*out_count = n;
return out;
}
@@ -1581,9 +1720,10 @@ void cbm_pxc_run_one_with_php_bindings(CBMLanguage lang, CBMFileResult *r, const
* but diverge after, so convert into the scratch arena. The
* workspace manifest (set once by the sequential driver) lets
* `crate_a::foo` route across the crate boundary (#56). */
CBMRustLSPDef *rdefs = pxc_lspdefs_to_rust(&scratch, defs, def_count);
int rust_def_count = 0;
CBMRustLSPDef *rdefs = pxc_lspdefs_to_rust(&scratch, defs, def_count, &rust_def_count);
cbm_run_rust_lsp_cross_with_manifest(&scratch, source, source_len, module_qn, rdefs,
def_count, imp_names, imp_qns, imp_count, tree,
rust_def_count, imp_names, imp_qns, imp_count, tree,
g_pxc_rust_manifest, &out, &synthetic_calls);
break;
}
+38 -8
View File
@@ -1773,7 +1773,8 @@ int cbm_parallel_extract(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
* once per file by the caller: computing the file QN and finding its node for
* every definition was 700 k allocations and lookups on the Go corpus. */
static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def,
int64_t file_node_id, CBMLanguage lang, int *reg_entries) {
int64_t file_node_id, CBMLanguage lang, size_t module_prefix_len,
int *reg_entries) {
int edges = 0;
if (!def->name || !def->qualified_name || !def->label) {
return 0;
@@ -1781,7 +1782,8 @@ static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *d
/* Registry membership is defined ONCE by cbm_label_is_registry_symbol
* (helpers.c) — see pass_definitions.c for the per-label rationale. */
if (cbm_label_is_registry_symbol(def->label)) {
cbm_registry_add_lang(ctx->registry, def->name, def->qualified_name, def->label, lang);
cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name, def->label,
lang, module_prefix_len);
(*reg_entries)++;
}
char marked[CBM_SZ_4K];
@@ -1988,9 +1990,14 @@ int cbm_build_registry_from_cache(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
int64_t file_node_id = file_node ? file_node->id : 0;
free(file_qn);
pp_add_file_doc(file_node, result->module_doc);
/* Bounds the function-local detection to this file (pass_definitions.c). */
size_t py_prefix_len = files[i].language == CBM_LANG_PYTHON
? cbm_pipeline_fqn_symbol_prefix_len(ctx->project_name, rel)
: 0;
for (int d = 0; d < result->defs.count; d++) {
defines_edges += register_and_link_def(ctx, &result->defs.items[d], file_node_id,
files[i].language, &reg_entries);
defines_edges +=
register_and_link_def(ctx, &result->defs.items[d], file_node_id,
files[i].language, py_prefix_len, &reg_entries);
}
}
@@ -2410,8 +2417,18 @@ static void emit_route_registration(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *sou
esc_rp);
cbm_gbuf_insert_edge(gbuf, source->id, rid, "CALLS", props);
if (handler_ref && handler_ref[0] != '\0') {
cbm_resolution_t hres = cbm_registry_resolve_handler(registry, handler_ref, module_qn, ik,
iv, ic, main_gbuf, lang);
/* Python: a handler an app factory registers is usually its local def
* (`app.add_url_rule("/", "index", index)` inside create_app); only the
* lexical lookup sees a function's locals. MUST match pass_calls.c. */
cbm_resolution_t hres = {0};
if (lang == CBM_LANG_PYTHON) {
hres = cbm_registry_resolve_lexical(registry, handler_ref, call->enclosing_func_qn,
module_qn);
}
if (!hres.qualified_name) {
hres = cbm_registry_resolve_handler(registry, handler_ref, module_qn, ik, iv, ic,
main_gbuf, lang);
}
if (hres.qualified_name && hres.qualified_name[0] != '\0') {
const cbm_gbuf_node_t *h = cbm_gbuf_find_by_qn(main_gbuf, hres.qualified_name);
if (h) {
@@ -3267,8 +3284,10 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
lang, lsp->strategy, lsp->callee_qn, rc->project_name);
if ((!res.qualified_name || !res.qualified_name[0]) && !call->requires_lsp_resolution &&
!rust_external && !php_vendor_call) {
res = cbm_registry_resolve_call_lang(rc->registry, call->callee_name, module_qn,
imp_keys, imp_vals, imp_count, lang);
/* Python LEGB fallback first (MUST match pass_calls.c). */
res = cbm_registry_resolve_call_scoped(rc->registry, call->callee_name, module_qn,
imp_keys, imp_vals, imp_count, lang,
call->enclosing_func_qn);
}
/* `p->open(fd)`: the registry does not split a callee on the arrow.
* The object's type decides, as for the dot call handled further
@@ -3616,6 +3635,7 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
const cbm_gbuf_node_t *tgt = NULL;
bool precise_call_reference = false;
const CBMResolvedCall *semantic_reference = NULL;
bool property_read = false;
if (cbm_pipeline_usage_semantic_reference_candidate(usage)) {
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
semantic_reference = cbm_pipeline_find_lsp_reference_indexed_in_graph(
@@ -3628,6 +3648,13 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
if (semantic_reference) {
tgt = cbm_pipeline_lsp_target_node(rc->main_gbuf, rc->project_name,
semantic_reference->callee_qn, allow_tail);
if (!cbm_pipeline_reference_target_admissible(semantic_reference, tgt)) {
/* a property read: the name fallback below may still bind the
* property's own node, never a callable */
tgt = NULL;
semantic_reference = NULL;
property_read = true;
}
precise_call_reference = cbm_pipeline_node_is_callable_target(tgt);
}
}
@@ -3683,6 +3710,9 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
cbm_pipeline_node_is_callable_target(tgt))) {
continue;
}
if (property_read && cbm_pipeline_node_is_callable_target(tgt)) {
continue;
}
}
if (!tgt || src->id == tgt->id) {
continue;
+36
View File
@@ -1819,6 +1819,38 @@ bool cbm_import_symbol_fallback_allowed(CBMLanguage lang) {
return lang != CBM_LANG_GO;
}
/* A Python def or class declared inside a function body -- a closure, a
* decorator's `wrapper`, a test's local class -- is a local of that function:
* no import can name it. Its QN extends a Function/Method node's
* (proj.tests.mock_inputs.inner.wrapper.mock_getpass.getpass), which is how it
* is recognised; the symbol-name fallback bound `import getpass` to exactly
* that def. Only an enclosing def of the SAME file counts: a package
* __init__.py function can share a submodule's QN (poetry: layouts/__init__.py
* `def layout` vs layouts/layout.py `class Layout`). Mirrors
* cbm_registry_add_lang_in_module, which keeps the same symbols out of the call
* resolver's short-name index. */
static bool import_candidate_is_python_local(const cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *cand) {
const char *qn = cand->qualified_name;
const char *slash = cand->file_path ? strrchr(cand->file_path, '/') : NULL;
char scope[CBM_SZ_512];
size_t len = qn ? strlen(qn) : 0;
if (!cand->file_path || len == 0 || len >= sizeof(scope) ||
cbm_language_for_filename(slash ? slash + SKIP_ONE : cand->file_path) != CBM_LANG_PYTHON) {
return false;
}
memcpy(scope, qn, len + SKIP_ONE);
for (char *dot = strrchr(scope, '.'); dot; dot = strrchr(scope, '.')) {
*dot = '\0';
const cbm_gbuf_node_t *outer = cbm_gbuf_find_by_qn(gbuf, scope);
if (outer && outer->label && outer->file_path &&
strcmp(outer->file_path, cand->file_path) == 0 &&
(strcmp(outer->label, "Function") == 0 || strcmp(outer->label, "Method") == 0)) {
return true;
}
}
return false;
}
static const char *path_leaf(const char *path) {
const char *leaf = path;
for (const char *p = path; p && *p; p++) {
@@ -2516,6 +2548,10 @@ static const cbm_gbuf_node_t *resolve_import_strategies(
!import_targetable_label(cand->label)) {
continue;
}
/* A Python def-local (#1548) is no importable name. */
if (import_candidate_is_python_local(ctx->gbuf, cand)) {
continue;
}
if (source_file_qn && cand->qualified_name &&
strcmp(cand->qualified_name, source_file_qn) == 0) {
continue; /* self */
+49 -2
View File
@@ -28,6 +28,7 @@ enum {
#include "pipeline/pipeline.h"
#include "pipeline/pipeline_internal.h"
#include "graph_buffer/graph_buffer.h"
#include "discover/discover.h"
#include "foundation/log.h"
#include <stdio.h>
@@ -208,6 +209,48 @@ static char *test_to_prod_path(const char *test_path) {
return NULL;
}
/* #1548: a def declared inside a Python test function (a closure, a local
* helper, a method of a test-local class) is that test's own code, extracted
* as a node of its own. Its calls exercise the target on the test's behalf, so
* TESTS is attributed to the nearest enclosing test function of the same file
* (QN prefix walk). Before #1548 those calls hung on the test FILE node, whose
* name passes the test-name check. NULL when no enclosing test function exists
* (a helper nested in a non-test function stays without TESTS, like a
* module-level helper). */
static const cbm_gbuf_node_t *enclosing_python_test_func(const cbm_gbuf_t *gb,
const cbm_gbuf_node_t *src) {
if (!src->qualified_name || !src->file_path || !src->label ||
(strcmp(src->label, "Function") != 0 && strcmp(src->label, "Method") != 0)) {
return NULL;
}
const char *slash = strrchr(src->file_path, '/');
if (cbm_language_for_filename(slash ? slash + SKIP_ONE : src->file_path) != CBM_LANG_PYTHON) {
return NULL;
}
char scope[CBM_SZ_512];
size_t len = strlen(src->qualified_name);
if (len >= sizeof(scope)) {
return NULL;
}
memcpy(scope, src->qualified_name, len + SKIP_ONE);
for (char *dot = strrchr(scope, '.'); dot; dot = strrchr(scope, '.')) {
*dot = '\0';
const cbm_gbuf_node_t *outer = cbm_gbuf_find_by_qn(gb, scope);
if (!outer) {
continue;
}
if (!outer->file_path || strcmp(outer->file_path, src->file_path) != 0) {
return NULL; /* left the file: no enclosing test function */
}
if (outer->label &&
(strcmp(outer->label, "Function") == 0 || strcmp(outer->label, "Method") == 0) &&
cbm_is_test_func_name(outer->name)) {
return outer;
}
}
return NULL;
}
/* Create TESTS edges from test functions to production functions via CALLS edges. */
static int create_tests_edges(cbm_pipeline_ctx_t *ctx) {
int count = 0;
@@ -238,11 +281,15 @@ static int create_tests_edges(cbm_pipeline_ctx_t *ctx) {
continue;
}
const cbm_gbuf_node_t *test_src = src;
if (!cbm_is_test_func_name(src->name)) {
continue;
test_src = enclosing_python_test_func(ctx->gbuf, src);
if (!test_src) {
continue;
}
}
cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, "TESTS", "{}");
cbm_gbuf_insert_edge(ctx->gbuf, test_src->id, tgt->id, "TESTS", "{}");
count++;
}
return count;
+11
View File
@@ -213,6 +213,7 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
const cbm_gbuf_node_t *tgt = NULL;
bool precise_call_reference = false;
const CBMResolvedCall *semantic_reference = NULL;
bool property_read = false;
if (cbm_pipeline_usage_semantic_reference_candidate(usage)) {
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
semantic_reference = cbm_pipeline_find_lsp_reference_indexed_in_graph(
@@ -225,6 +226,13 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
if (semantic_reference) {
tgt = cbm_pipeline_lsp_target_node(ctx->gbuf, ctx->project_name,
semantic_reference->callee_qn, allow_tail);
if (!cbm_pipeline_reference_target_admissible(semantic_reference, tgt)) {
/* a property read: the name fallback below may still bind the
* property's own node, never a callable */
tgt = NULL;
semantic_reference = NULL;
property_read = true;
}
precise_call_reference = cbm_pipeline_node_is_callable_target(tgt);
}
}
@@ -285,6 +293,9 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
cbm_pipeline_node_is_callable_target(tgt))) {
continue;
}
if (property_read && cbm_pipeline_node_is_callable_target(tgt)) {
continue;
}
}
if (!tgt || src->id == tgt->id) {
continue;
+35 -1
View File
@@ -200,6 +200,12 @@ char *cbm_pipeline_fqn_compute(const char *project, const char *rel_path, const
/* Module QN: project.dir.parts (no name). Caller must free(). */
char *cbm_pipeline_fqn_module(const char *project, const char *rel_path);
/* Length of the prefix cbm_pipeline_fqn_compute(project, rel_path, name) puts
* before a symbol `name` (the joining dot excluded): the QN prefix every
* definition of that file carries, __init__ / index already dropped. Computed
* without allocating; 0 when unavailable. */
size_t cbm_pipeline_fqn_symbol_prefix_len(const char *project, const char *rel_path);
/* Language-aware module QN. When `module_is_dir` is true (Java/Go package
* semantics) the module is derived from the CONTAINING DIRECTORY (the filename
* stem is dropped), so it agrees with the extraction-side def QNs; when false
@@ -253,10 +259,24 @@ void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified
/* As cbm_registry_add, recording `lang`: the DETECTED language of the file
* that defines the symbol (CBM_LANG_COUNT = unknown), which the cross-language
* veto (cbm_registry_name_guess_vetoed) checks against the caller. */
* veto (cbm_registry_name_guess_vetoed) checks against the caller. Detects no
* function-local symbols: that needs the defining file's module QN, see
* cbm_registry_add_lang_in_module. */
void cbm_registry_add_lang(cbm_registry_t *r, const char *name, const char *qualified_name,
const char *label, CBMLanguage lang);
/* As cbm_registry_add_lang. A Python symbol declared inside a function body of
* its own file (cbm_registry_qn_is_function_local, bounded by
* `module_prefix_len`: cbm_pipeline_fqn_symbol_prefix_len of the defining file)
* is registered for exact lookups only, never offered to the short-name
* strategies. The three graph
* registration sites (pass_definitions.c, pass_parallel.c,
* pipeline_incremental.c) all register through this, so full and incremental
* indexes agree. 0 = no local detection. */
void cbm_registry_add_lang_in_module(cbm_registry_t *r, const char *name,
const char *qualified_name, const char *label,
CBMLanguage lang, size_t module_prefix_len);
/* True when code in `caller` can call a symbol defined in `target` by bare
* name: same language, same interop family (JS/TS + script hosts, C/C++/CUDA/
* ObjC, JVM, .NET, ...; the table lives in registry.c), or Swift with the C
@@ -268,6 +288,20 @@ bool cbm_lang_resolution_compatible(CBMLanguage caller, CBMLanguage target);
* CBM_LANG_COUNT for anything else. */
CBMLanguage cbm_registry_synthetic_path_language(const char *file_path);
/* True when a proper prefix of `qualified_name` longer than
* `module_prefix_len` (cbm_pipeline_fqn_symbol_prefix_len of the defining file)
* is a registered Function or Method, i.e. the symbol was declared inside a
* function body of its own file. False when module_prefix_len is 0. */
bool cbm_registry_qn_is_function_local(const cbm_registry_t *r, const char *qualified_name,
size_t module_prefix_len);
/* Python bare-name lexical lookup: the def or class named `callee_name` bound
* in the nearest function scope enclosing `scope_qn` (the caller's QN),
* skipping class bodies, stopping at the caller's module (`module_qn`).
* Strategy "lexical_scope"; qualified_name is NULL when no such local exists. */
cbm_resolution_t cbm_registry_resolve_lexical(const cbm_registry_t *r, const char *callee_name,
const char *scope_qn, const char *module_qn);
/* Resolve a callee name using prioritized strategies.
* import_map: NULL-terminated array of {local_name, resolved_qn} pairs, or NULL.
* Returns result with qualified_name="" if unresolved.
+16 -6
View File
@@ -1327,7 +1327,10 @@ static bool incr_label_is_registry_symbol(const char *label) {
/* Callback for cbm_gbuf_foreach_node: seed the registry with the existing
* project's definition symbols so the resolver can match cross-file symbols
* during incremental. Mirrors the full-index registry contents exactly so an
* incremental re-resolve picks the same nodes a full reindex would. */
* incremental re-resolve picks the same nodes a full reindex would -- including
* a Python function's locals, which cbm_registry_add_lang keeps out of the
* short-name index there too (nodes load ORDER BY id and a file's defs are
* inserted parent first, so the enclosing function is seeded before its locals). */
/* Each symbol is seeded with its file's DETECTED language, exactly what the
* full index recorded (pass_definitions.c / pass_parallel.c), so the cross-
* language veto judges an incremental re-resolve the same way. Stored nodes carry no
@@ -1337,6 +1340,7 @@ static bool incr_label_is_registry_symbol(const char *label) {
typedef struct {
cbm_registry_t *registry;
CBMHashTable *lang_by_path; /* rel_path -> (CBMLanguage + 1); keys borrowed */
const char *project; /* bounds Python function-local detection per file */
} registry_seed_t;
static void registry_visitor(const cbm_gbuf_node_t *node, void *userdata) {
@@ -1351,14 +1355,20 @@ static void registry_visitor(const cbm_gbuf_node_t *node, void *userdata) {
lang = (CBMLanguage)((uintptr_t)v - SKIP_ONE);
}
}
cbm_registry_add_lang(seed->registry, node->name, node->qualified_name, node->label, lang);
/* Same per-file bound as the full index (pass_definitions.c). */
size_t prefix_len = lang == CBM_LANG_PYTHON
? cbm_pipeline_fqn_symbol_prefix_len(seed->project, node->file_path)
: 0;
cbm_registry_add_lang_in_module(seed->registry, node->name, node->qualified_name, node->label,
lang, prefix_len);
}
/* Seed the registry from every registry symbol in the graph buffer, with
* languages taken from the discovered files (see registry_visitor). */
static void registry_seed_from_gbuf(cbm_registry_t *registry, const cbm_gbuf_t *gbuf,
const cbm_file_info_t *files, int file_count) {
registry_seed_t seed = {.registry = registry, .lang_by_path = NULL};
const char *project, const cbm_file_info_t *files,
int file_count) {
registry_seed_t seed = {.registry = registry, .lang_by_path = NULL, .project = project};
if (files && file_count > 0) {
seed.lang_by_path = cbm_ht_create((size_t)file_count * PAIR_LEN);
}
@@ -3302,7 +3312,7 @@ static int run_closure_delta(cbm_pipeline_t *p, const char *db_path, const char
if (!registry) {
goto out;
}
registry_seed_from_gbuf(registry, gbuf, files, file_count);
registry_seed_from_gbuf(registry, gbuf, project, files, file_count);
cbm_log_info("delta.preseed_done", "registry", itoa_buf(cbm_registry_size(registry)),
"elapsed_ms", itoa_buf((int)elapsed_ms(t)));
@@ -4152,7 +4162,7 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
/* Step 3-5: Registry + extract + resolve */
cbm_registry_t *registry = cbm_registry_new();
cbm_clock_gettime(CLOCK_MONOTONIC, &t);
registry_seed_from_gbuf(registry, existing, files, file_count);
registry_seed_from_gbuf(registry, existing, project, files, file_count);
cbm_log_info("incremental.registry_seed", "symbols", itoa_buf(cbm_registry_size(registry)),
"elapsed_ms", itoa_buf((int)elapsed_ms(t)));
+10
View File
@@ -260,6 +260,16 @@ cbm_resolution_t cbm_registry_resolve_call_lang(const cbm_registry_t *r, const c
const char *module_qn, const char **import_map_keys,
const char **import_map_vals, int import_map_count,
CBMLanguage lang);
/* The registry step of BOTH call resolvers once the LSP has not answered:
* for Python, the def or class `callee_name` binds in the nearest def enclosing
* `scope_qn` (cbm_registry_resolve_lexical, "lexical_scope"); otherwise, or
* when there is no such local, cbm_registry_resolve_call_lang. */
cbm_resolution_t cbm_registry_resolve_call_scoped(const cbm_registry_t *r, const char *callee_name,
const char *module_qn,
const char **import_map_keys,
const char **import_map_vals,
int import_map_count, CBMLanguage lang,
const char *scope_qn);
#ifdef CBM_ENABLE_TEST_SEAMS
void cbm_pxc_test_import_probe_fail_after(int successful_allocations);
#endif
+165 -11
View File
@@ -62,6 +62,8 @@ const char *cbm_confidence_band(double score) {
#define CONF_IMPORT_MAP_SUFFIX 0.85
/* Strategy 2: same_module — same file/package → high confidence */
#define CONF_SAME_MODULE 0.90
/* Python lexical scope — a def bound in an enclosing function (LEGB) */
#define CONF_LEXICAL_SCOPE 0.95
/* Strategy 3: unique_name — only one candidate project-wide */
#define CONF_UNIQUE_NAME 0.75
/* Strategy 4: suffix_match — multiple candidates, filtered */
@@ -1181,20 +1183,64 @@ static void index_under_name(cbm_registry_t *r, const char *key, const char *own
}
}
void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified_name,
const char *label) {
cbm_registry_add_lang(r, name, qualified_name, label, CBM_LANG_COUNT);
static bool registry_label_is_callable(const char *label) {
return label && (strcmp(label, "Function") == 0 || strcmp(label, "Method") == 0);
}
void cbm_registry_add_lang(cbm_registry_t *r, const char *name, const char *qualified_name,
const char *label, CBMLanguage lang) {
if (!r || !qualified_name || !label) {
return;
/* Length of the QN prefix the defs of the caller's file carry: its module QN
* (cbm_pipeline_fqn_module), minus the trailing `__init__` / `index` segment
* that symbol QNs drop (pkg/__init__.py defines proj.pkg.f). 0 when there is
* no module QN or `qualified_name` does not extend that prefix. */
static size_t python_def_prefix_len(const char *module_qn, const char *qualified_name) {
static const char *const dropped[] = {".__init__", ".index"};
size_t n = module_qn ? strlen(module_qn) : 0;
for (size_t i = 0; i < sizeof(dropped) / sizeof(dropped[0]); i++) {
size_t k = strlen(dropped[i]);
if (n > k && strcmp(module_qn + n - k, dropped[i]) == 0) {
n -= k;
break;
}
}
if (n == 0 || !qualified_name || strncmp(qualified_name, module_qn, n) != 0 ||
qualified_name[n] != '.') {
return 0;
}
return n;
}
/* Whether `qualified_name` sits inside a def of its OWN file: only prefixes
* strictly longer than `module_prefix_len` (cbm_pipeline_fqn_symbol_prefix_len
* of the defining file) count. A package's __init__.py can define a function
* whose QN equals a submodule's (poetry: layouts/__init__.py `def layout` vs
* layouts/layout.py), so an unbounded walk read the submodule's classes as
* that function's locals -- and the answer depended on which file registered
* first. */
bool cbm_registry_qn_is_function_local(const cbm_registry_t *r, const char *qualified_name,
size_t module_prefix_len) {
char scope[CBM_SZ_512];
size_t len = qualified_name ? strlen(qualified_name) : 0;
size_t floor = module_prefix_len;
if (!r || floor == 0 || len <= floor || len >= sizeof(scope) || qualified_name[floor] != '.') {
return false;
}
memcpy(scope, qualified_name, len + SKIP_ONE);
for (char *dot = strrchr(scope, '.'); dot && (size_t)(dot - scope) > floor;
dot = strrchr(scope, '.')) {
*dot = '\0';
if (registry_label_is_callable(cbm_ht_get(r->exact, scope))) {
return true;
}
}
return false;
}
/* Record `qualified_name` in the exact map. Returns the registry's owned copy
* of the QN, or NULL when it is already registered or cannot be recorded. */
static const char *registry_add_exact(cbm_registry_t *r, const char *qualified_name,
const char *label) {
/* Check for duplicate */
if (cbm_ht_get(r->exact, qualified_name)) {
return;
return NULL;
}
/* Intern the label (bounded set; linear scan is fine at this size). */
@@ -1211,14 +1257,55 @@ void cbm_registry_add_lang(cbm_registry_t *r, const char *name, const char *qual
r->label_pool_n++;
}
if (!interned) {
return; /* pool exhausted (cannot happen with sane label sets) */
return NULL; /* pool exhausted (cannot happen with sane label sets) */
}
/* Store in exact map: QN → interned label. The key is the registry's ONE
* owned copy of the QN; by_name below borrows it (same lifetime) instead
* owned copy of the QN; by_name borrows it (same lifetime) instead
* of a second strdup — this pair of copies was ~280 MB on the kernel. */
cbm_ht_set(r->exact, strdup(qualified_name), (void *)interned);
const char *owned_qn = cbm_ht_get_key(r->exact, qualified_name);
return cbm_ht_get_key(r->exact, qualified_name);
}
void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified_name,
const char *label) {
cbm_registry_add_lang_in_module(r, name, qualified_name, label, CBM_LANG_COUNT, 0);
}
void cbm_registry_add_lang(cbm_registry_t *r, const char *name, const char *qualified_name,
const char *label, CBMLanguage lang) {
cbm_registry_add_lang_in_module(r, name, qualified_name, label, lang, 0);
}
/* Python nests defs: a def or class declared in a function body -- a closure,
* an app factory's route handler, a local class and its methods -- is a local
* of that function, and its QN extends the function's
* (proj.mod.create_app.index). No code outside the function can name it, so it
* stays out of the short-name index: the weak name strategies (unique_name,
* suffix_match) must not offer django's per-test `class MyModelAdmin` or a
* decorator's `def wrapper` to a caller in another function or file. The exact
* map still holds it, for LSP targets and cbm_registry_resolve_lexical. One
* file's defs register parent first, so the enclosing function is already in
* the exact map when its locals arrive; the walk never leaves the symbol's own
* file (`module_prefix_len`, cbm_pipeline_fqn_symbol_prefix_len of the defining
* file; 0 = no local detection).
* Gated to Python: probing every prefix of every symbol would cost a kernel
* index seconds for nothing. */
void cbm_registry_add_lang_in_module(cbm_registry_t *r, const char *name,
const char *qualified_name, const char *label,
CBMLanguage lang, size_t module_prefix_len) {
if (!r || !qualified_name || !label) {
return;
}
if (lang == CBM_LANG_PYTHON &&
cbm_registry_qn_is_function_local(r, qualified_name, module_prefix_len)) {
(void)registry_add_exact(r, qualified_name, label);
return;
}
const char *owned_qn = registry_add_exact(r, qualified_name, label);
if (!owned_qn) {
return;
}
/* Index the symbol under the QN's last dot segment, and additionally under
* the name its caller passed when that segment carries a '#' fence.
@@ -2123,6 +2210,73 @@ cbm_resolution_t cbm_registry_resolve_lineage(const cbm_registry_t *r, const cha
import_map_count, module_qn ? strlen(module_qn) : 0, false);
}
/* Python looks a bare name up in the calling def's own scope, then in each
* enclosing def's scope, and only then in the module and the builtins; a class
* body is never among them. So a bare `index()` in create_app, or `visit(...)`
* in a def nested in method Tree.visit, names the def bound in the nearest
* enclosing def -- create_app.index, Tree.visit.visit -- before anything a
* module-wide strategy would find. Walks scope_qn outward while its prefixes
* are registered defs or classes (skipping the classes) and stops at the
* caller's module (`module_qn`), which the other strategies own -- a
* same-QN function of a package __init__.py is never an enclosing scope.
* Empty unless such a local exists. Uncached: the answer depends on the
* caller, not only on the name. */
cbm_resolution_t cbm_registry_resolve_lexical(const cbm_registry_t *r, const char *callee_name,
const char *scope_qn, const char *module_qn) {
char scope[CBM_SZ_512];
char candidate[CBM_SZ_512];
size_t len = scope_qn ? strlen(scope_qn) : 0;
size_t floor = python_def_prefix_len(module_qn, scope_qn);
if (!r || !callee_name || !callee_name[0] || strchr(callee_name, '.') ||
strstr(callee_name, "::") || len == 0 || len >= sizeof(scope) || floor == 0) {
return empty_result();
}
memcpy(scope, scope_qn, len + SKIP_ONE);
while (strlen(scope) > floor) {
const char *label = cbm_ht_get(r->exact, scope);
if (!label) {
break;
}
if (registry_label_is_callable(label)) {
int n = snprintf(candidate, sizeof(candidate), "%s.%s", scope, callee_name);
const char *stored_key =
n > 0 && (size_t)n < sizeof(candidate) ? cbm_ht_get_key(r->exact, candidate) : NULL;
if (stored_key) {
return (cbm_resolution_t){stored_key, "lexical_scope", CONF_LEXICAL_SCOPE,
REG_RESOLVED};
}
}
char *dot = strrchr(scope, '.');
if (!dot) {
break;
}
*dot = '\0';
}
return empty_result();
}
/* The registry step both call resolvers (pass_calls.c, pass_parallel.c) run
* once the LSP has not answered: a Python bare name bound in an enclosing def
* first (the LSP binds those locals itself; this covers the sites it did not
* resolve), then the language-aware chain. One entry point, so the two
* resolvers cannot diverge on the rule. */
cbm_resolution_t cbm_registry_resolve_call_scoped(const cbm_registry_t *r, const char *callee_name,
const char *module_qn,
const char **import_map_keys,
const char **import_map_vals,
int import_map_count, CBMLanguage lang,
const char *scope_qn) {
if (lang == CBM_LANG_PYTHON) {
cbm_resolution_t lexical =
cbm_registry_resolve_lexical(r, callee_name, scope_qn, module_qn);
if (lexical.qualified_name && lexical.qualified_name[0]) {
return lexical;
}
}
return cbm_registry_resolve_call_lang(r, callee_name, module_qn, import_map_keys,
import_map_vals, import_map_count, lang);
}
/* ── Fuzzy Resolve ──────────────────────────────────────────────── */
/* Filter candidates by import reachability. Returns count of reachable. */
+299
View File
@@ -4723,6 +4723,298 @@ TEST(python_init_nested_module_qn) {
PASS();
}
/* ── #1548: Python defs nested in a function body ─────────────────── */
/* How many defs carry exactly this QN. */
static int pyn_def_count(const CBMFileResult *r, const char *qn) {
int n = 0;
for (int i = 0; i < r->defs.count; i++) {
const char *q = r->defs.items[i].qualified_name;
if (q && strcmp(q, qn) == 0) {
n++;
}
}
return n;
}
/* The one def with this QN, or NULL when there is none (or more than one). */
static const CBMDefinition *pyn_def(const CBMFileResult *r, const char *qn) {
if (pyn_def_count(r, qn) != 1) {
return NULL;
}
for (int i = 0; i < r->defs.count; i++) {
const char *q = r->defs.items[i].qualified_name;
if (q && strcmp(q, qn) == 0) {
return &r->defs.items[i];
}
}
return NULL;
}
/* Calls of `callee` whose enclosing function is exactly `caller_qn`. */
static int pyn_calls(const CBMFileResult *r, const char *callee, const char *caller_qn) {
int n = 0;
for (int i = 0; i < r->calls.count; i++) {
const CBMCall *c = &r->calls.items[i];
if (c->callee_name && strcmp(c->callee_name, callee) == 0 && c->enclosing_func_qn &&
strcmp(c->enclosing_func_qn, caller_qn) == 0) {
n++;
}
}
return n;
}
/* No def QN is minted twice: a nested def must not also surface as a
* module-level one, and walking a def body must not re-extract anything. */
static bool pyn_defs_unique(const CBMFileResult *r) {
for (int i = 0; i < r->defs.count; i++) {
const char *q = r->defs.items[i].qualified_name;
if (q && pyn_def_count(r, q) != 1) {
return false;
}
}
return true;
}
/* The issue's reproducer: `def nested()` inside `def factory()` was never
* extracted. It is now a Function qualified by the def it sits in, the same
* QN the Python LSP gives its calls. Module-level defs, and defs in a
* module-level `if` block, keep their QNs. */
TEST(extract_python_nested_def_qualified_by_enclosing_def_issue1548) {
CBMFileResult *r = extract("def target():\n"
" return 0\n"
"\n"
"\n"
"def module_level():\n"
" return target()\n"
"\n"
"\n"
"if True:\n"
" def inside_if():\n"
" return target()\n"
"\n"
"\n"
"def factory():\n"
" def nested():\n"
" return target()\n"
" return nested\n"
"\n"
"\n"
"def outer():\n"
" def middle():\n"
" def inner():\n"
" return target()\n"
" return inner\n"
" return middle\n",
CBM_LANG_PYTHON, "t", "app.py");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
const CBMDefinition *nested = pyn_def(r, "t.app.factory.nested");
ASSERT_NOT_NULL(nested);
ASSERT_STR_EQ(nested->label, "Function");
ASSERT_EQ(nested->start_line, 15);
ASSERT_NOT_NULL(pyn_def(r, "t.app.outer.middle"));
ASSERT_NOT_NULL(pyn_def(r, "t.app.outer.middle.inner"));
/* Never minted flat, where it would collide with a module-level name. */
ASSERT_EQ(pyn_def_count(r, "t.app.nested"), 0);
ASSERT_EQ(pyn_def_count(r, "t.app.inner"), 0);
/* Controls: unchanged. */
ASSERT_NOT_NULL(pyn_def(r, "t.app.target"));
ASSERT_NOT_NULL(pyn_def(r, "t.app.module_level"));
ASSERT_NOT_NULL(pyn_def(r, "t.app.inside_if"));
ASSERT_NOT_NULL(pyn_def(r, "t.app.factory"));
ASSERT_TRUE(pyn_defs_unique(r));
cbm_free_result(r);
PASS();
}
/* A call inside a nested def belongs to that def. It used to carry the flat
* `t.app.nested`, a QN no node has, so the edge was pinned on the file node:
* inbound traces of target() lost the caller. It is not the outer def's
* either. */
TEST(extract_python_nested_def_calls_attributed_to_it_issue1548) {
CBMFileResult *r = extract("def target():\n"
" return 0\n"
"\n"
"def factory():\n"
" def nested():\n"
" return target()\n"
" return nested\n"
"\n"
"def outer():\n"
" def middle():\n"
" def inner():\n"
" return target()\n"
" return inner\n"
" return middle\n",
CBM_LANG_PYTHON, "t", "app.py");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT_EQ(pyn_calls(r, "target", "t.app.factory.nested"), 1);
ASSERT_EQ(pyn_calls(r, "target", "t.app.outer.middle.inner"), 1);
ASSERT_EQ(pyn_calls(r, "target", "t.app.factory"), 0);
ASSERT_EQ(pyn_calls(r, "target", "t.app.nested"), 0);
ASSERT_EQ(pyn_calls(r, "target", "t.app.inner"), 0);
cbm_free_result(r);
PASS();
}
/* The app-factory shape: route handlers are decorated defs inside
* create_app. Each is extracted with its QN and carries the route its
* decorator declares, which is what mints the Route node and its HANDLES
* edge (pipeline_python_app_factory_routes_issue1548 checks the graph). */
TEST(extract_python_decorated_nested_route_handler_issue1548) {
CBMFileResult *r = extract("from flask import Flask\n"
"\n"
"def create_app():\n"
" app = Flask(__name__)\n"
"\n"
" @app.route(\"/users/<uid>\")\n"
" def show_user(uid):\n"
" return render(uid)\n"
"\n"
" @app.get(\"/health\")\n"
" async def health():\n"
" return render(\"ok\")\n"
"\n"
" return app\n",
CBM_LANG_PYTHON, "t", "web.py");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
const CBMDefinition *show = pyn_def(r, "t.web.create_app.show_user");
ASSERT_NOT_NULL(show);
ASSERT_STR_EQ(show->route_path, "/users/<uid>");
ASSERT_NOT_NULL(show->decorators);
const CBMDefinition *health = pyn_def(r, "t.web.create_app.health");
ASSERT_NOT_NULL(health);
ASSERT_STR_EQ(health->route_path, "/health");
ASSERT_STR_EQ(health->route_method, "GET");
ASSERT_EQ(pyn_calls(r, "render", "t.web.create_app.show_user"), 1);
ASSERT_EQ(pyn_calls(r, "render", "t.web.create_app.health"), 1);
ASSERT_TRUE(pyn_defs_unique(r));
cbm_free_result(r);
PASS();
}
/* A class declared in a def is qualified by the def; its methods are Methods
* of it, and a call in such a method is the method's. */
TEST(extract_python_nested_class_issue1548) {
CBMFileResult *r = extract("def create_app():\n"
" class Settings:\n"
" debug = False\n"
"\n"
" def value(self):\n"
" return load(self)\n"
"\n"
" return Settings().value()\n",
CBM_LANG_PYTHON, "t", "web.py");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
const CBMDefinition *cls = pyn_def(r, "t.web.create_app.Settings");
ASSERT_NOT_NULL(cls);
ASSERT_STR_EQ(cls->label, "Class");
const CBMDefinition *value = pyn_def(r, "t.web.create_app.Settings.value");
ASSERT_NOT_NULL(value);
ASSERT_STR_EQ(value->label, "Method");
ASSERT_STR_EQ(value->parent_class, "t.web.create_app.Settings");
ASSERT_EQ(pyn_calls(r, "load", "t.web.create_app.Settings.value"), 1);
ASSERT_EQ(pyn_def_count(r, "t.web.Settings"), 0);
ASSERT_TRUE(pyn_defs_unique(r));
cbm_free_result(r);
PASS();
}
/* A def nested in a method, named like the method (django's
* AbstractBaseUser.check_password holds a `def setter`; Tree.visit here holds
* a `def visit`). It used to share the method's QN, so its calls -- its
* recursion included -- were the method's. It is its own Function now: the
* recursive call and render() are its, only visit(node) is the method's, and
* the recursion seed flags the nested def. */
TEST(extract_python_nested_def_named_like_its_method_issue1548) {
CBMFileResult *r = extract("class Tree:\n"
" def visit(self, node):\n"
" def visit(n):\n"
" if n:\n"
" visit(n.child)\n"
" render(n)\n"
"\n"
" visit(node)\n",
CBM_LANG_PYTHON, "t", "m.py");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
const CBMDefinition *method = pyn_def(r, "t.m.Tree.visit");
ASSERT_NOT_NULL(method);
ASSERT_STR_EQ(method->label, "Method");
const CBMDefinition *nested = pyn_def(r, "t.m.Tree.visit.visit");
ASSERT_NOT_NULL(nested);
ASSERT_STR_EQ(nested->label, "Function");
ASSERT_EQ(nested->start_line, 3);
ASSERT_EQ(pyn_calls(r, "visit", "t.m.Tree.visit.visit"), 1);
ASSERT_EQ(pyn_calls(r, "render", "t.m.Tree.visit.visit"), 1);
ASSERT_EQ(pyn_calls(r, "visit", "t.m.Tree.visit"), 1);
ASSERT_EQ(pyn_calls(r, "render", "t.m.Tree.visit"), 0);
ASSERT_TRUE(nested->is_recursive);
ASSERT_TRUE(pyn_defs_unique(r));
cbm_free_result(r);
PASS();
}
/* Controls: a lambda and the comprehensions are expressions, not defs. They
* mint nothing, and the calls inside them stay the enclosing def's. */
TEST(extract_python_lambda_and_comprehension_stay_in_enclosing_def_issue1548) {
CBMFileResult *r = extract("def work(items):\n"
" double = lambda x: scale(x)\n"
" squares = [scale(i) for i in items]\n"
" table = {k: scale(k) for k in items}\n"
" return double(squares), table\n",
CBM_LANG_PYTHON, "t", "c.py");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT_NOT_NULL(pyn_def(r, "t.c.work"));
for (int i = 0; i < r->defs.count; i++) {
const char *q = r->defs.items[i].qualified_name;
ASSERT_FALSE(q && strncmp(q, "t.c.work.", strlen("t.c.work.")) == 0);
}
ASSERT_EQ(pyn_calls(r, "scale", "t.c.work"), 3);
ASSERT_TRUE(pyn_defs_unique(r));
cbm_free_result(r);
PASS();
}
/* Records other than calls take their enclosing function from the same
* unified-walk scope frame as the calls. The nested def's env read carries its
* QN; the outer def's reads, before and after the nested def, keep the outer
* QN once its frame is popped. */
TEST(extract_python_nested_def_env_access_attributed_to_it_issue1548) {
CBMFileResult *r = extract("import os\n"
"\n"
"def outer():\n"
" token = os.environ[\"OUTER_TOKEN\"]\n"
"\n"
" def inner():\n"
" return os.environ[\"INNER_TOKEN\"]\n"
"\n"
" return inner, token, os.environ[\"OUTER_AGAIN\"]\n",
CBM_LANG_PYTHON, "t", "e.py");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
int seen = 0;
for (int i = 0; i < r->env_accesses.count; i++) {
const CBMEnvAccess *ea = &r->env_accesses.items[i];
ASSERT_NOT_NULL(ea->enclosing_func_qn);
if (strcmp(ea->env_key, "INNER_TOKEN") == 0) {
ASSERT_STR_EQ(ea->enclosing_func_qn, "t.e.outer.inner");
seen++;
} else {
ASSERT_STR_EQ(ea->enclosing_func_qn, "t.e.outer");
seen++;
}
}
ASSERT_EQ(seen, 3);
cbm_free_result(r);
PASS();
}
TEST(js_index_module_qn_not_collide_with_folder) {
/* Same bug for JS/TS index.ts files */
CBMFileResult *r = extract("export function App() { return null; }\n", CBM_LANG_TYPESCRIPT,
@@ -12609,6 +12901,13 @@ SUITE(extraction) {
/* __init__.py / index.ts Module QN collision regression */
RUN_TEST(python_init_module_qn_not_collide_with_folder);
RUN_TEST(python_init_nested_module_qn);
RUN_TEST(extract_python_nested_def_qualified_by_enclosing_def_issue1548);
RUN_TEST(extract_python_nested_def_calls_attributed_to_it_issue1548);
RUN_TEST(extract_python_decorated_nested_route_handler_issue1548);
RUN_TEST(extract_python_nested_class_issue1548);
RUN_TEST(extract_python_nested_def_named_like_its_method_issue1548);
RUN_TEST(extract_python_lambda_and_comprehension_stay_in_enclosing_def_issue1548);
RUN_TEST(extract_python_nested_def_env_access_attributed_to_it_issue1548);
RUN_TEST(js_index_module_qn_not_collide_with_folder);
RUN_TEST(python_regular_module_qn_unchanged);
RUN_TEST(extract_java_method_annotations_issue382);
+359
View File
@@ -0,0 +1,359 @@
/*
* test_kotlin_typed_calls.c — Kotlin member calls through a class PROPERTY
* (primary-constructor `val`, body property, `this.prop`) bind to the
* property's declared type, on both resolver venues (#1555).
*
* The cross-file Kotlin walk resolved a typed method PARAMETER by type, but
* a property-typed receiver fell to the name guess: the cross registry held
* the class from extraction defs (properties untyped, constructor vals
* absent), and the walk's typed AST registration of the same class was
* shadowed by that earlier entry. Each fixture call has a same-named decoy
* method on another type, so a name guess is visible as a wrong target:
* - Service.log: `audit.inserir()` on `audit: Audit`, next to
* `repository: Repo` whose interface also declares inserir (the
* sequential venue's guess picks Repo.inserir);
* - two bounded contexts whose use cases both inject `repository`
* (the parallel venue's receiver-name guess bound the feriado use case
* to CompromissoRepository.inserir, the #1555 report).
* One fixture, padded past MIN_FILES_FOR_PARALLEL, is indexed through the
* sequential resolver (CBM_INDEX_SINGLE_THREAD=1) and the parallel one.
*/
#include "../src/foundation/compat.h"
#include "test_framework.h"
#include "test_helpers.h"
#include "pipeline/pipeline.h"
#include "sqlite3.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static const char *const KTC_FILES[][2] = {
{"src/main/kotlin/com/acme/a/Repo.kt",
"package com.acme.a\n\ninterface Repo {\n fun inserir(x: String): Long\n}\n"},
{"src/main/kotlin/com/acme/b/Audit.kt",
"package com.acme.b\n\nclass Audit {\n fun inserir(x: String): Long = 0L\n}\n"},
{"src/main/kotlin/com/acme/app/Service.kt",
"package com.acme.app\n\nimport com.acme.a.Repo\nimport com.acme.b.Audit\n\n"
"class Service(private val repository: Repo, private val audit: Audit) {\n"
" fun save(x: String): Long = repository.inserir(x)\n"
" fun log(x: String): Long = audit.inserir(x)\n}\n\n"
"class Holder {\n private val audit: Audit = Audit()\n"
" fun hold(x: String): Long = this.audit.inserir(x)\n}\n"},
{"src/main/kotlin/com/acme/agenda/domain/CompromissoRepository.kt",
"package com.acme.agenda.domain\n\ninterface CompromissoRepository {\n"
" fun inserir(titulo: String): Long\n}\n"},
{"src/main/kotlin/com/acme/feriado/domain/FeriadoRepository.kt",
"package com.acme.feriado.domain\n\ninterface FeriadoRepository {\n"
" fun inserir(nome: String): Long\n}\n"},
{"src/main/kotlin/com/acme/agenda/app/CriarCompromissoUseCase.kt",
"package com.acme.agenda.app\n\nimport com.acme.agenda.domain.CompromissoRepository\n\n"
"class CriarCompromissoUseCase(private val repository: CompromissoRepository) {\n"
" fun executar(titulo: String): Long = repository.inserir(titulo)\n}\n"},
{"src/main/kotlin/com/acme/feriado/app/CriarFeriadoUseCase.kt",
"package com.acme.feriado.app\n\nimport com.acme.feriado.domain.FeriadoRepository\n\n"
"class CriarFeriadoUseCase(private val repository: FeriadoRepository) {\n"
" fun executar(nome: String): Long = repository.inserir(nome)\n}\n"},
/* Guard: inside an extension function, a bare call names the receiver's
* MEMBER before a same-named extension of the same file (Kotlin
* resolution); main gets this right and the own-file overlay must too. */
{"src/main/kotlin/com/acme/b/Builders.kt",
"package com.acme.b\n\nclass Builder {\n fun add(x: Int): Boolean = true\n"
" fun put(k: String, v: Int): Int = v\n}\n\n"
"fun Builder.add(s: String): Boolean = add(s.length)\n\n"
"fun Builder.putAll(k: String) { put(k, 1) }\n\n"
"fun Builder.put(k: String): Int = put(k, 0)\n"},
/* kotlin.Any members on a receiver typed by ANOTHER file. The per-file
* pass cannot type `type`, so it emits the kotlin.Any placeholder; the
* cross pass names the declared override. Both rows at one occurrence
* were a tombstone in the call join and the call fell to a name guess
* (AllTypes is the decoy). A class with no override keeps kotlin.Any, and
* so does a stdlib receiver type (`count: Int`), whose seeded
* kotlin.Int.toString names no node and left the call to the guess. */
{"src/main/kotlin/com/acme/c/Proto.kt",
"package com.acme.c\n\nclass ProtoType {\n private val s: String = \"\"\n"
" override fun hashCode() = s.length\n\n override fun toString() = s\n}\n\n"
"class Plain(val n: Int)\n"},
{"src/main/kotlin/com/acme/d/Member.kt",
"package com.acme.d\n\nimport com.acme.c.Plain\nimport com.acme.c.ProtoType\n\n"
"class Member(val type: ProtoType, val plain: Plain, val count: Int) {\n"
" override fun hashCode() = type.hashCode() * 37\n\n"
" fun label(): String = type.toString()\n\n"
" fun plainHash(): Int = plain.hashCode()\n\n"
" fun countText(): String = count.toString()\n}\n"},
{"src/main/kotlin/com/acme/e/AllTypes.kt",
"package com.acme.e\n\nclass AllTypes {\n override fun hashCode(): Int = 1\n\n"
" override fun toString(): String = \"all\"\n}\n"},
/* A property READ on a now-typed receiver (a nested class's property, whose
* QN the graph spells differently): it must keep the property's own node
* (USAGE) and never become a CALL_REFERENCE to a same-named function in
* another file (okhttp `server.webSocket` -> a test fun `webSocket`). */
{"src/main/kotlin/com/acme/w/SocketTest.kt",
"package com.acme.w\n\nclass SocketTest {\n"
" private val server: Streams = Streams()\n\n"
" fun loop(): Boolean {\n return server.webSocket != null\n }\n\n"
" class Streams {\n var webSocket: String? = null\n }\n}\n"},
{"src/main/kotlin/com/acme/w2/ModernTest.kt",
"package com.acme.w2\n\nclass ModernTest {\n fun webSocket(): Int = 1\n}\n"},
/* A constructor val next to a same-named member function: a bare
* `handshake()` calls the function (the String? value has no invoke), as
* main resolves it; typing the val must not shadow the member (okhttp
* RealConnection). Other.handshake is the decoy a guess would hit. */
{"src/main/kotlin/com/acme/h/Conn.kt",
"package com.acme.h\n\ninterface Connection {\n fun handshake(): String?\n}\n\n"
"class RealConnection(private val handshake: String?) : Connection {\n"
" override fun handshake(): String? = handshake\n\n"
" fun isEligible(): Boolean = check(handshake()!!.length)\n\n"
" private fun check(n: Int): Boolean = n > 0\n}\n"},
{"src/main/kotlin/com/acme/h2/Other.kt",
"package com.acme.h2\n\nclass Other {\n fun handshake(): String? = null\n}\n"},
/* Inside `repo.let { .. }` / `repo.run { .. }` the now-typed receiver Repo
* becomes the lambda's implicit receiver; the enclosing class's member
* stays callable bare (Kotlin's implicit-receiver chain). */
{"src/main/kotlin/com/acme/l/Svc.kt",
"package com.acme.l\n\nimport com.acme.a.Repo\n\n"
"class Svc(private val repo: Repo) {\n fun ping(): Int = 2\n\n"
" fun viaLet(): Int = repo.let { ping() }\n\n"
" fun viaRun(): Int = repo.run { ping() }\n}\n"},
{"src/main/kotlin/com/acme/l2/Pinger.kt",
"package com.acme.l2\n\nclass Pinger {\n fun ping(): Int = 3\n}\n"},
/* Guard for the receiver chain: inside a companion object, `Ledger(x)` is
* a constructor call (here the secondary constructor), never a member
* method of the enclosing class. */
{"src/main/kotlin/com/acme/k/Ledger.kt",
"package com.acme.k\n\nclass Ledger private constructor(val a: String, val b: String) {\n"
" constructor(a: String) : this(a, \"gen\")\n\n"
" companion object {\n fun of(x: String) = Ledger(x)\n }\n}\n"},
};
static int ktc_write_fixture(const char *dir) {
for (size_t i = 0; i < sizeof(KTC_FILES) / sizeof(KTC_FILES[0]); i++) {
if (th_write_file(TH_PATH(dir, KTC_FILES[i][0]), KTC_FILES[i][1]) != 0) {
return -1;
}
}
/* Past MIN_FILES_FOR_PARALLEL (50): the parallel venue is selected. */
for (int i = 0; i < 55; i++) {
char rel[96];
char body[128];
snprintf(rel, sizeof(rel), "src/main/kotlin/com/acme/pad/Pad%d.kt", i);
snprintf(body, sizeof(body), "package com.acme.pad\n\nclass Pad%d {\n fun v%d(): Int = %d\n}\n",
i, i, i);
if (th_write_file(TH_PATH(dir, rel), body) != 0) {
return -1;
}
}
return 0;
}
/* Target QNs of the CALLS edges whose source QN ends in `.<src_tail>` and whose
* callee text ends in `.<leaf>`, one per line into out; returns the count. */
static int ktc_targets(sqlite3 *db, const char *src_tail, const char *leaf, char *out,
size_t cap) {
out[0] = '\0';
static const char sql[] =
"SELECT t.qualified_name FROM edges e JOIN nodes s ON s.id = e.source_id "
"JOIN nodes t ON t.id = e.target_id WHERE e.type = 'CALLS' "
"AND s.qualified_name LIKE '%.' || ?1 "
"AND (json_extract(e.properties, '$.callee') = ?2 "
"OR json_extract(e.properties, '$.callee') LIKE '%.' || ?2) ORDER BY 1";
sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
return -1;
}
sqlite3_bind_text(st, 1, src_tail, -1, SQLITE_STATIC);
sqlite3_bind_text(st, 2, leaf, -1, SQLITE_STATIC);
int n = 0;
while (sqlite3_step(st) == SQLITE_ROW) {
const char *qn = (const char *)sqlite3_column_text(st, 0);
size_t used = strlen(out);
snprintf(out + used, cap - used, "%s\n", qn ? qn : "");
n++;
}
sqlite3_finalize(st);
return n;
}
/* Exactly one such edge, and its target QN ends in `.<want_tail>`. */
static int ktc_expect(sqlite3 *db, const char *venue, const char *src_tail, const char *leaf,
const char *want_tail) {
char got[2048];
int n = ktc_targets(db, src_tail, leaf, got, sizeof(got));
size_t gl = strlen(got);
size_t wl = strlen(want_tail);
if (n == 1 && gl > wl + 1 && got[gl - wl - 2] == '.' &&
strncmp(got + gl - wl - 1, want_tail, wl) == 0) {
return 1;
}
printf("\n [%s] %s -> .%s: want one edge to ...%s, have %d:\n%s", venue, src_tail, leaf,
want_tail, n, got);
return 0;
}
/* ktc_expect, and the edge comes from the resolver (an lsp_* strategy), not a
* name guess that happens to land on the same target. */
static int ktc_expect_lsp(sqlite3 *db, const char *venue, const char *src_tail, const char *leaf,
const char *want_tail) {
if (!ktc_expect(db, venue, src_tail, leaf, want_tail)) {
return 0;
}
static const char sql[] =
"SELECT coalesce(json_extract(e.properties, '$.strategy'), '') FROM edges e "
"JOIN nodes s ON s.id = e.source_id WHERE e.type = 'CALLS' "
"AND s.qualified_name LIKE '%.' || ?1 "
"AND (json_extract(e.properties, '$.callee') = ?2 "
"OR json_extract(e.properties, '$.callee') LIKE '%.' || ?2)";
sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
return 0;
}
sqlite3_bind_text(st, 1, src_tail, -1, SQLITE_STATIC);
sqlite3_bind_text(st, 2, leaf, -1, SQLITE_STATIC);
char strategy[64] = "";
if (sqlite3_step(st) == SQLITE_ROW) {
const char *s = (const char *)sqlite3_column_text(st, 0);
snprintf(strategy, sizeof(strategy), "%s", s ? s : "");
}
sqlite3_finalize(st);
if (strncmp(strategy, "lsp_", 4) == 0) {
return 1;
}
printf("\n [%s] %s -> .%s: right target, but strategy '%s' (want lsp_*)\n", venue,
src_tail, leaf, strategy);
return 0;
}
/* The reference edges (USAGE / CALL_REFERENCE) from the method whose QN ends in
* `.<src_tail>` to nodes named `name`: exactly one, a USAGE onto a non-callable. */
static int ktc_expect_property_usage(sqlite3 *db, const char *venue, const char *src_tail,
const char *name) {
static const char sql[] =
"SELECT e.type || ' ' || t.label || ' ' || t.qualified_name FROM edges e "
"JOIN nodes s ON s.id = e.source_id JOIN nodes t ON t.id = e.target_id "
"WHERE e.type IN ('USAGE', 'CALL_REFERENCE', 'CALLS') "
"AND s.qualified_name LIKE '%.' || ?1 AND t.name = ?2 ORDER BY 1";
sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
return 0;
}
sqlite3_bind_text(st, 1, src_tail, -1, SQLITE_STATIC);
sqlite3_bind_text(st, 2, name, -1, SQLITE_STATIC);
char got[1024] = "";
int n = 0;
bool ok = false;
while (sqlite3_step(st) == SQLITE_ROW) {
const char *row = (const char *)sqlite3_column_text(st, 0);
size_t used = strlen(got);
snprintf(got + used, sizeof(got) - used, "%s\n", row ? row : "");
ok = row && strncmp(row, "USAGE ", 6) == 0 && strstr(row, " Function ") == NULL &&
strstr(row, " Method ") == NULL;
n++;
}
sqlite3_finalize(st);
if (n == 1 && ok) {
return 1;
}
printf("\n [%s] %s -> %s: want one USAGE onto the property, have %d:\n%s", venue, src_tail,
name, n, got);
return 0;
}
static int ktc_index(const char *dir, const char *db_name, bool single_thread, char *db_out,
size_t cap) {
snprintf(db_out, cap, "%s/%s", dir, db_name);
char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
char *saved_single = old_single ? strdup(old_single) : NULL;
char *old_workers = getenv("CBM_WORKERS");
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
if (single_thread) {
cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1);
} else {
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
cbm_setenv("CBM_WORKERS", "4", 1);
}
int rc = -1;
cbm_pipeline_t *p = cbm_pipeline_new(dir, db_out, CBM_MODE_FULL);
if (p) {
rc = cbm_pipeline_run(p);
cbm_pipeline_free(p);
}
if (saved_single) {
cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1);
free(saved_single);
} else {
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
}
if (saved_workers) {
cbm_setenv("CBM_WORKERS", saved_workers, 1);
free(saved_workers);
} else {
cbm_unsetenv("CBM_WORKERS");
}
return rc;
}
static int ktc_check_venue(const char *dir, const char *venue, bool single_thread) {
char db_path[512];
char name[64];
snprintf(name, sizeof(name), "ktc-%s.db", venue);
if (ktc_index(dir, name, single_thread, db_path, sizeof(db_path)) != 0) {
printf("\n [%s] index failed\n", venue);
return 0;
}
sqlite3 *db = NULL;
if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) {
if (db) {
sqlite3_close(db);
}
return 0;
}
int ok = 1;
/* Constructor vals, each next to a same-named decoy on the other type. */
ok &= ktc_expect(db, venue, "Service.save", "inserir", "Repo.inserir");
ok &= ktc_expect(db, venue, "Service.log", "inserir", "Audit.inserir");
/* Body property read through `this`. */
ok &= ktc_expect(db, venue, "Holder.hold", "inserir", "Audit.inserir");
/* #1555: two ports sharing `inserir`, both injected as `repository`. */
ok &= ktc_expect(db, venue, "CriarFeriadoUseCase.executar", "inserir",
"FeriadoRepository.inserir");
ok &= ktc_expect(db, venue, "CriarCompromissoUseCase.executar", "inserir",
"CompromissoRepository.inserir");
/* Guard: a member wins over a same-named extension of the same file. */
ok &= ktc_expect(db, venue, "Builders.putAll", "put", "Builder.put");
ok &= ktc_expect(db, venue, "Builders.add", "add", "Builder.add");
/* kotlin.Any members: the declared override, else kotlin.Any (external). */
ok &= ktc_expect_lsp(db, venue, "Member.hashCode", "hashCode", "ProtoType.hashCode");
ok &= ktc_expect_lsp(db, venue, "Member.label", "toString", "ProtoType.toString");
ok &= ktc_expect_lsp(db, venue, "Member.plainHash", "hashCode", "Any.hashCode");
ok &= ktc_expect_lsp(db, venue, "Member.countText", "toString", "Any.toString");
/* A typed property read keeps the property node, never a same-named function. */
ok &= ktc_expect_property_usage(db, venue, "SocketTest.loop", "webSocket");
/* A typed constructor val never shadows the same-named member function. */
ok &= ktc_expect_lsp(db, venue, "RealConnection.isEligible", "handshake",
"RealConnection.handshake");
/* The enclosing class's member inside a lambda with a typed receiver. */
ok &= ktc_expect_lsp(db, venue, "Svc.viaLet", "ping", "Svc.ping");
ok &= ktc_expect_lsp(db, venue, "Svc.viaRun", "ping", "Svc.ping");
ok &= ktc_expect_lsp(db, venue, "Ledger.kt.__file__", "Ledger", "Ledger.Ledger.Ledger");
sqlite3_close(db);
return ok;
}
TEST(kotlin_property_receiver_binds_declared_type_both_venues) {
char *dir = th_mktempdir("cbm_kt_typed_calls");
ASSERT_NOT_NULL(dir);
if (ktc_write_fixture(dir) != 0) {
th_cleanup(dir);
FAIL("fixture");
}
int seq = ktc_check_venue(dir, "seq", true);
int par = ktc_check_venue(dir, "par", false);
th_cleanup(dir);
ASSERT_TRUE(seq);
ASSERT_TRUE(par);
PASS();
}
SUITE(kotlin_typed_calls) {
RUN_TEST(kotlin_property_receiver_binds_declared_type_both_venues);
}
+2
View File
@@ -1812,6 +1812,7 @@ extern void suite_ts_lsp(void);
extern void suite_java_lsp(void);
extern void suite_java_lsp_coverage(void);
extern void suite_kotlin_lsp(void);
extern void suite_kotlin_typed_calls(void);
extern void suite_rust_lsp(void);
extern void suite_store_arch(void);
extern void suite_architecture_projection(void);
@@ -2394,6 +2395,7 @@ int main(int argc, char **argv) {
RUN_SELECTED_SUITE(perl_lsp);
RUN_SELECTED_SUITE(py_lsp);
RUN_SELECTED_SUITE(kotlin_lsp);
RUN_SELECTED_SUITE(kotlin_typed_calls);
RUN_SELECTED_SUITE(rust_lsp);
RUN_SELECTED_SUITE_PERF(py_lsp_bench);
RUN_SELECTED_SUITE(py_lsp_stress);
File diff suppressed because it is too large. Load diff
+8 -4
View File
@@ -3629,12 +3629,16 @@ TEST(pylsp_nested_and_local_classbody_calls_join_extracted_caller) {
{"field(35)", "test.main.Controller.build", "test.main.field"},
};
CBMFileResult *r = extract_py(source);
bool correct = pylsp_class_sites(r, source, sites, 7) &&
!pylsp_module_has_def(r, "test.main.build.Local") &&
!pylsp_module_has_def(r, "test.main.Controller.build.Local");
/* #1548: a class declared in a function body is extracted, as a local of
* that function; its class-body calls still join the enclosing function
* (the sites above), never the local class. */
bool sites_ok = pylsp_class_sites(r, source, sites, 7);
bool locals_extracted = r && pylsp_module_has_def(r, "test.main.build.Local") &&
pylsp_module_has_def(r, "test.main.Controller.build.Local");
if (r)
cbm_free_result(r);
ASSERT_TRUE(correct);
ASSERT_TRUE(sites_ok);
ASSERT_TRUE(locals_extracted);
PASS();
}
+170
View File
@@ -1965,6 +1965,172 @@ TEST(registry_python_scope_qualified_terminal_bucket_uncapped) {
PASS();
}
/* #1548: a Python def or class declared in a function body is a local of it.
* It is registered for exact lookups but kept out of the short-name index, so
* no weak strategy offers it to a caller elsewhere. A class nested in a class
* (proj.m.Outer.Inner) is an attribute, not a local, and stays nameable; so
* does the same QN shape from another language. */
TEST(registry_python_function_local_is_exact_only_issue1548) {
cbm_registry_t *r = cbm_registry_new();
const size_t web = strlen("proj.web"); /* symbol prefix of web.py */
static const char *const defs[][3] = {
{"create_app", "proj.web.create_app", "Function"},
{"index", "proj.web.create_app.index", "Function"},
{"Settings", "proj.web.create_app.Settings", "Class"},
{"value", "proj.web.create_app.Settings.value", "Method"},
{"Outer", "proj.web.Outer", "Class"},
{"Inner", "proj.web.Outer.Inner", "Class"},
};
for (size_t i = 0; i < sizeof(defs) / sizeof(defs[0]); i++) {
cbm_registry_add_lang_in_module(r, defs[i][0], defs[i][1], defs[i][2], CBM_LANG_PYTHON,
web);
}
cbm_registry_add_lang_in_module(r, "f", "proj.ts.f", "Function", CBM_LANG_TYPESCRIPT,
strlen("proj.ts"));
cbm_registry_add_lang_in_module(r, "g", "proj.ts.f.g", "Function", CBM_LANG_TYPESCRIPT,
strlen("proj.ts"));
static const char *const local[] = {"proj.web.create_app.index", "proj.web.create_app.Settings",
"proj.web.create_app.Settings.value"};
for (int i = 0; i < 3; i++) {
ASSERT_TRUE(cbm_registry_exists(r, local[i]));
ASSERT_TRUE(cbm_registry_qn_is_function_local(r, local[i], web));
const char *simple = strrchr(local[i], '.') + 1;
const char **out = NULL;
int count = -1;
cbm_registry_find_by_name(r, simple, &out, &count);
ASSERT_EQ(count, 0);
}
static const char *const nameable[][2] = {{"create_app", "proj.web.create_app"},
{"Inner", "proj.web.Outer.Inner"},
{"g", "proj.ts.f.g"}};
for (int i = 0; i < 3; i++) {
const char **out = NULL;
int count = 0;
cbm_registry_find_by_name(r, nameable[i][0], &out, &count);
ASSERT_EQ(count, 1);
ASSERT_STR_EQ(out[0], nameable[i][1]);
}
ASSERT_FALSE(cbm_registry_qn_is_function_local(r, "proj.web.Outer.Inner", web));
/* No module prefix: no local detection. */
ASSERT_FALSE(cbm_registry_qn_is_function_local(r, "proj.web.create_app.index", 0));
/* A weak strategy cannot reach the local; before, index() anywhere in
* the project resolved to it by unique_name. */
cbm_resolution_t res = cbm_registry_resolve(r, "index", "proj.other", NULL, NULL, 0);
ASSERT_NULL(res.qualified_name);
cbm_registry_free(r);
PASS();
}
/* Python LEGB for bare names: the caller's own def first, then each enclosing
* def outward; a class body is not a scope its methods see; the module is left
* to the other strategies. A dotted callee is not a bare name. */
TEST(registry_resolve_lexical_walks_enclosing_defs_issue1548) {
cbm_registry_t *r = cbm_registry_new();
static const char *const defs[][3] = {
{"Tree", "proj.m.Tree", "Class"},
{"visit", "proj.m.Tree.visit", "Method"},
{"visit", "proj.m.Tree.visit.visit", "Function"},
{"helper", "proj.m.Tree.helper", "Method"},
{"factory", "proj.m.factory", "Function"},
{"helper", "proj.m.factory.helper", "Function"},
{"Local", "proj.m.factory.Local", "Class"},
{"run", "proj.m.factory.Local.run", "Method"},
{"top", "proj.m.top", "Function"},
};
for (size_t i = 0; i < sizeof(defs) / sizeof(defs[0]); i++) {
cbm_registry_add_lang_in_module(r, defs[i][0], defs[i][1], defs[i][2], CBM_LANG_PYTHON,
strlen("proj.m"));
}
/* In method Tree.visit, `visit` is its nested def; so it is inside that
* nested def (its own recursion). */
cbm_resolution_t res = cbm_registry_resolve_lexical(r, "visit", "proj.m.Tree.visit", "proj.m");
ASSERT_STR_EQ(res.qualified_name, "proj.m.Tree.visit.visit");
ASSERT_STR_EQ(res.strategy, "lexical_scope");
res = cbm_registry_resolve_lexical(r, "visit", "proj.m.Tree.visit.visit", "proj.m");
ASSERT_STR_EQ(res.qualified_name, "proj.m.Tree.visit.visit");
/* The class body is skipped: Tree.helper is not a bare name in a method. */
res = cbm_registry_resolve_lexical(r, "helper", "proj.m.Tree.visit.visit", "proj.m");
ASSERT_NULL(res.qualified_name);
/* A method of a local class sees the def enclosing its class. */
res = cbm_registry_resolve_lexical(r, "helper", "proj.m.factory.Local.run", "proj.m");
ASSERT_STR_EQ(res.qualified_name, "proj.m.factory.helper");
res = cbm_registry_resolve_lexical(r, "Local", "proj.m.factory", "proj.m");
ASSERT_STR_EQ(res.qualified_name, "proj.m.factory.Local");
/* Module-level names and dotted callees are not this strategy's. */
res = cbm_registry_resolve_lexical(r, "top", "proj.m.factory", "proj.m");
ASSERT_NULL(res.qualified_name);
res = cbm_registry_resolve_lexical(r, "self.helper", "proj.m.factory", "proj.m");
ASSERT_NULL(res.qualified_name);
res = cbm_registry_resolve_lexical(r, "helper", "proj.m", "proj.m");
ASSERT_NULL(res.qualified_name);
cbm_registry_free(r);
PASS();
}
/* A package __init__.py function can share a submodule's QN: poetry's
* layouts/__init__.py `def layout` is proj.pkg.layout, the QN prefix of every
* symbol in layouts/layout.py. Bounded at its own file, class Layout and its
* methods are no locals of that function, in either registration order (full
* index: discovery order; incremental: node-id order), and the lexical walk of
* a caller in layout.py never enters the __init__ function. */
TEST(registry_python_local_bound_at_own_file_issue1548) {
const size_t init_prefix = strlen("proj.pkg"); /* pkg/__init__.py */
const size_t mod_prefix = strlen("proj.pkg.layout"); /* pkg/layout.py */
for (int order = 0; order < 2; order++) {
cbm_registry_t *r = cbm_registry_new();
if (order == 0) {
cbm_registry_add_lang_in_module(r, "layout", "proj.pkg.layout", "Function",
CBM_LANG_PYTHON, init_prefix);
}
cbm_registry_add_lang_in_module(r, "Layout", "proj.pkg.layout.Layout", "Class",
CBM_LANG_PYTHON, mod_prefix);
cbm_registry_add_lang_in_module(r, "create", "proj.pkg.layout.Layout.create", "Method",
CBM_LANG_PYTHON, mod_prefix);
cbm_registry_add_lang_in_module(r, "make", "proj.pkg.layout.make", "Function",
CBM_LANG_PYTHON, mod_prefix);
if (order == 1) {
cbm_registry_add_lang_in_module(r, "layout", "proj.pkg.layout", "Function",
CBM_LANG_PYTHON, init_prefix);
}
static const char *const nameable[][2] = {{"Layout", "proj.pkg.layout.Layout"},
{"create", "proj.pkg.layout.Layout.create"},
{"make", "proj.pkg.layout.make"}};
for (size_t i = 0; i < sizeof(nameable) / sizeof(nameable[0]); i++) {
const char **out = NULL;
int count = 0;
cbm_registry_find_by_name(r, nameable[i][0], &out, &count);
ASSERT_EQ(count, 1);
ASSERT_STR_EQ(out[0], nameable[i][1]);
ASSERT_FALSE(cbm_registry_qn_is_function_local(r, nameable[i][1], mod_prefix));
}
/* `Layout()` in layout.py's make(): no lexical local; module strategies own it. */
cbm_resolution_t res =
cbm_registry_resolve_lexical(r, "Layout", "proj.pkg.layout.make", "proj.pkg.layout");
ASSERT_NULL(res.qualified_name);
cbm_registry_free(r);
}
PASS();
}
/* The prefix every def of a file carries, without allocating; the same as
* cbm_pipeline_fqn_compute(project, path, name) minus ".name". */
TEST(fqn_symbol_prefix_len_matches_fqn_compute) {
static const char *const paths[] = {"pkg/layout.py", "pkg/__init__.py", "a.py",
"deep/x/y/mod.py", "web/index.py", "noext"};
for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) {
char *qn = cbm_pipeline_fqn_compute("proj", paths[i], "sym");
ASSERT_NOT_NULL(qn);
size_t want = strlen(qn) - strlen(".sym");
free(qn);
ASSERT_EQ(cbm_pipeline_fqn_symbol_prefix_len("proj", paths[i]), want);
}
ASSERT_EQ(cbm_pipeline_fqn_symbol_prefix_len("proj", "pkg/__init__.py"), strlen("proj.pkg"));
ASSERT_EQ(cbm_pipeline_fqn_symbol_prefix_len(NULL, "a.py"), 0);
PASS();
}
SUITE(registry) {
RUN_TEST(python_symbol_scope_len_preserves_file_identity);
RUN_TEST(registry_python_package_scope_is_explicit);
@@ -2069,4 +2235,8 @@ SUITE(registry) {
RUN_TEST(python_import_binding_contradicts_only_foreign_chains);
RUN_TEST(python_external_base_contradicts_django_shapes);
RUN_TEST(weak_call_guards_share_one_drop_list);
RUN_TEST(registry_python_function_local_is_exact_only_issue1548);
RUN_TEST(registry_resolve_lexical_walks_enclosing_defs_issue1548);
RUN_TEST(registry_python_local_bound_at_own_file_issue1548);
RUN_TEST(fqn_symbol_prefix_len_matches_fqn_compute);
}
+4
View File
@@ -591,6 +591,9 @@ TEST(rustlsp_shared_registry_resolves_like_per_file) {
defs[1].receiver_type = "test.demo.Database";
defs[1].def_module_qn = "test.demo";
defs[1].return_types = "alloc.string.String";
/* The shared build registers Rust defs only, as the pipeline tags them. */
defs[0].lang = CBM_LANG_RUST;
defs[1].lang = CBM_LANG_RUST;
const char *imp_names[] = {"demo"};
const char *imp_qns[] = {"test::demo"};
@@ -695,6 +698,7 @@ TEST(rustlsp_shared_registry_macro_hidden_call_has_carrier) {
defs[0].label = "Function";
defs[0].def_module_qn = "test.lib";
defs[0].return_types = "str";
defs[0].lang = CBM_LANG_RUST;
const char *imp_names[] = {"lib"};
const char *imp_qns[] = {"test::lib"};