mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Merge remote-tracking branch 'origin/main' into fix/php-global-use-known-absent
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
commit
7cb58d3f15
21 files changed
+2315
-133
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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). */
|
||||
@@ -1162,9 +1179,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). */
|
||||
|
||||
+140
-17
@@ -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
@@ -4128,6 +4128,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. */
|
||||
@@ -4514,6 +4540,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.
|
||||
@@ -4524,7 +4553,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));
|
||||
@@ -4568,7 +4597,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". */
|
||||
@@ -6323,7 +6352,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;
|
||||
@@ -6331,29 +6363,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;
|
||||
}
|
||||
|
||||
@@ -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("");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -309,8 +309,8 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def)
|
||||
}
|
||||
size_t pos = (size_t)n;
|
||||
append_json_string(buf, bufsize, &pos, "docstring", def->docstring);
|
||||
/* Right after the docstring: the buffer is sized for exactly these three
|
||||
* uncapped fields (pd_props_buf), so none can be squeezed out. */
|
||||
/* The buffer is sized for every field below (pd_props_buf), so none can be
|
||||
* squeezed out. */
|
||||
append_json_raw(buf, bufsize, &pos, "variants", def->variants);
|
||||
append_json_string(buf, bufsize, &pos, "variant_key", def->variant_key);
|
||||
append_json_string(buf, bufsize, &pos, "signature", def->signature);
|
||||
@@ -353,24 +353,71 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def)
|
||||
}
|
||||
}
|
||||
|
||||
/* A def's properties buffer: CBM_SZ_2K for every other field plus the whole
|
||||
* serialized docstring, variants and variant_key fields, which have no length
|
||||
* cap (a field that does not fit is dropped whole). Returns `stack` for a def
|
||||
* with none of them, or when the larger buffer cannot be allocated. Twin of
|
||||
* pass_parallel.c -- keep both in sync. */
|
||||
static char *pd_props_buf(const CBMDefinition *def, char *stack, size_t *size) {
|
||||
size_t need = *size;
|
||||
if (def->docstring && def->docstring[0]) {
|
||||
need += strlen("docstring") + def_json_escaped_len(def->docstring) + PD_JSON_FIELD_OVERHEAD;
|
||||
/* Upper bound of build_def_props' fixed prefix: the Function/Method metrics
|
||||
* literal (about 330 B) with nine ints and seven true/false/test_role strings at
|
||||
* their widest stays under 600 B. */
|
||||
enum { PD_PROPS_PREFIX_MAX = CBM_SZ_1K };
|
||||
|
||||
/* Serialized size of one optional field as append_json_string emits it
|
||||
* (0 when the field is absent). */
|
||||
static size_t pd_str_field_len(const char *key, const char *val) {
|
||||
return val && val[0] ? strlen(key) + def_json_escaped_len(val) + PD_JSON_FIELD_OVERHEAD : 0;
|
||||
}
|
||||
|
||||
/* Serialized size of a string array as append_json_str_array emits it. */
|
||||
static size_t pd_arr_field_len(const char *key, const char **arr) {
|
||||
if (!arr || !arr[0]) {
|
||||
return 0;
|
||||
}
|
||||
size_t n = strlen(key) + PD_JSON_FIELD_OVERHEAD;
|
||||
for (int i = 0; arr[i]; i++) {
|
||||
n += def_json_escaped_len(arr[i]) + PD_ESC_SPACE + (i > 0 ? SKIP_ONE : 0);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* A def's properties buffer, sized so EVERY field build_def_props appends fits:
|
||||
* the fixed metrics prefix (PD_PROPS_PREFIX_MAX) plus each optional field's
|
||||
* whole serialized form. A field that does not fit is dropped whole, so a buffer
|
||||
* sized for only some of them let a long signature, decorator list or parameter
|
||||
* list squeeze out the MinHash fingerprint ("fp"), structural profile ("sp") and
|
||||
* body tokens ("bt") behind them: no SIMILAR_TO edge and fewer
|
||||
* SEMANTICALLY_RELATED edges for exactly the widest definitions (jellyfin's
|
||||
* ImageController actions, 2026-10-18). fp/sp/bt are appended behind their own
|
||||
* room checks, so both the check's reserve and the field are counted. Returns
|
||||
* `stack` when everything fits it, or when the larger buffer cannot be
|
||||
* allocated. Twin of pass_parallel.c -- keep both in sync. */
|
||||
static char *pd_props_buf(const CBMDefinition *def, char *stack, size_t *size) {
|
||||
size_t need = PD_PROPS_PREFIX_MAX + pd_str_field_len("docstring", def->docstring);
|
||||
if (def->variants && def->variants[0]) {
|
||||
need += strlen("variants") + strlen(def->variants) + PD_JSON_FIELD_OVERHEAD;
|
||||
}
|
||||
if (def->variant_key && def->variant_key[0]) {
|
||||
need +=
|
||||
strlen("variant_key") + def_json_escaped_len(def->variant_key) + PD_JSON_FIELD_OVERHEAD;
|
||||
need += pd_str_field_len("variant_key", def->variant_key) +
|
||||
pd_str_field_len("signature", def->signature) +
|
||||
pd_str_field_len("return_type", def->return_type) +
|
||||
pd_str_field_len("parent_class", def->parent_class) +
|
||||
pd_arr_field_len("decorators", def->decorators) +
|
||||
pd_arr_field_len("base_classes", def->base_classes) +
|
||||
pd_arr_field_len("param_names", def->param_names) +
|
||||
pd_arr_field_len("param_types", def->param_types) +
|
||||
pd_str_field_len("route_path", def->route_path) +
|
||||
pd_str_field_len("route_method", def->route_method) +
|
||||
pd_str_field_len("http_client", def->http_client) +
|
||||
pd_str_field_len("http_base_url", def->http_base_url);
|
||||
for (const char **kv = def->extra_props; kv && kv[0] && kv[SKIP_ONE]; kv += PAIR_LEN) {
|
||||
need += pd_str_field_len(kv[0], kv[SKIP_ONE]);
|
||||
}
|
||||
if (need == *size) {
|
||||
if (def->fingerprint && def->fingerprint_k > 0) {
|
||||
need += CBM_MINHASH_HEX_LEN + CBM_MINHASH_JSON_OVERHEAD + SKIP_ONE;
|
||||
}
|
||||
if (def->structural_profile) {
|
||||
need += CBM_AST_PROFILE_BUF + pd_str_field_len("sp", def->structural_profile) + SKIP_ONE;
|
||||
}
|
||||
if (def->body_tokens) {
|
||||
need += CBM_SZ_512 + pd_str_field_len("bt", def->body_tokens) + SKIP_ONE;
|
||||
}
|
||||
need += PD_ESC_SPACE; /* the closing '}' + NUL */
|
||||
if (need <= *size) {
|
||||
return stack;
|
||||
}
|
||||
char *buf = cbm_alloc(CBM_MEM_CLASS_GBUF_STRING, need);
|
||||
@@ -432,7 +479,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 +506,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 +1110,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++;
|
||||
}
|
||||
|
||||
@@ -1598,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;
|
||||
}
|
||||
|
||||
@@ -1709,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;
|
||||
}
|
||||
|
||||
@@ -539,8 +539,8 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def)
|
||||
}
|
||||
size_t pos = (size_t)n;
|
||||
append_json_string(buf, bufsize, &pos, "docstring", def->docstring);
|
||||
/* Right after the docstring: the buffer is sized for exactly these three
|
||||
* uncapped fields (pp_props_buf), so none can be squeezed out. */
|
||||
/* The buffer is sized for every field below (pp_props_buf), so none can be
|
||||
* squeezed out. */
|
||||
append_json_raw(buf, bufsize, &pos, "variants", def->variants);
|
||||
append_json_string(buf, bufsize, &pos, "variant_key", def->variant_key);
|
||||
append_json_string(buf, bufsize, &pos, "signature", def->signature);
|
||||
@@ -731,24 +731,71 @@ typedef struct {
|
||||
* to the response/logfile — this only throttles the stderr noise). */
|
||||
enum { PP_OVERSIZED_WARN_MAX = 32 };
|
||||
|
||||
/* A def's properties buffer: CBM_SZ_2K for every other field plus the whole
|
||||
* serialized docstring, variants and variant_key fields, which have no length
|
||||
* cap (a field that does not fit is dropped whole). Returns `stack` for a def
|
||||
* with none of them, or when the larger buffer cannot be allocated. Twin of
|
||||
* pass_definitions.c -- keep both in sync. */
|
||||
static char *pp_props_buf(const CBMDefinition *def, char *stack, size_t *size) {
|
||||
size_t need = *size;
|
||||
if (def->docstring && def->docstring[0]) {
|
||||
need += strlen("docstring") + pp_json_escaped_len(def->docstring) + PP_JSON_FIELD_OVERHEAD;
|
||||
/* Upper bound of build_def_props' fixed prefix: the Function/Method metrics
|
||||
* literal (about 330 B) with nine ints and seven true/false/test_role strings at
|
||||
* their widest stays under 600 B. */
|
||||
enum { PP_PROPS_PREFIX_MAX = CBM_SZ_1K };
|
||||
|
||||
/* Serialized size of one optional field as append_json_string emits it
|
||||
* (0 when the field is absent). */
|
||||
static size_t pp_str_field_len(const char *key, const char *val) {
|
||||
return val && val[0] ? strlen(key) + pp_json_escaped_len(val) + PP_JSON_FIELD_OVERHEAD : 0;
|
||||
}
|
||||
|
||||
/* Serialized size of a string array as append_json_str_array emits it. */
|
||||
static size_t pp_arr_field_len(const char *key, const char **arr) {
|
||||
if (!arr || !arr[0]) {
|
||||
return 0;
|
||||
}
|
||||
size_t n = strlen(key) + PP_JSON_FIELD_OVERHEAD;
|
||||
for (int i = 0; arr[i]; i++) {
|
||||
n += pp_json_escaped_len(arr[i]) + PP_ESC_SPACE + (i > 0 ? SKIP_ONE : 0);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* A def's properties buffer, sized so EVERY field build_def_props appends fits:
|
||||
* the fixed metrics prefix (PP_PROPS_PREFIX_MAX) plus each optional field's
|
||||
* whole serialized form. A field that does not fit is dropped whole, so a buffer
|
||||
* sized for only some of them let a long signature, decorator list or parameter
|
||||
* list squeeze out the MinHash fingerprint ("fp"), structural profile ("sp") and
|
||||
* body tokens ("bt") behind them: no SIMILAR_TO edge and fewer
|
||||
* SEMANTICALLY_RELATED edges for exactly the widest definitions (jellyfin's
|
||||
* ImageController actions, 2026-10-18). fp/sp/bt are appended behind their own
|
||||
* room checks, so both the check's reserve and the field are counted. Returns
|
||||
* `stack` when everything fits it, or when the larger buffer cannot be
|
||||
* allocated. Twin of pass_definitions.c -- keep both in sync. */
|
||||
static char *pp_props_buf(const CBMDefinition *def, char *stack, size_t *size) {
|
||||
size_t need = PP_PROPS_PREFIX_MAX + pp_str_field_len("docstring", def->docstring);
|
||||
if (def->variants && def->variants[0]) {
|
||||
need += strlen("variants") + strlen(def->variants) + PP_JSON_FIELD_OVERHEAD;
|
||||
}
|
||||
if (def->variant_key && def->variant_key[0]) {
|
||||
need +=
|
||||
strlen("variant_key") + pp_json_escaped_len(def->variant_key) + PP_JSON_FIELD_OVERHEAD;
|
||||
need += pp_str_field_len("variant_key", def->variant_key) +
|
||||
pp_str_field_len("signature", def->signature) +
|
||||
pp_str_field_len("return_type", def->return_type) +
|
||||
pp_str_field_len("parent_class", def->parent_class) +
|
||||
pp_arr_field_len("decorators", def->decorators) +
|
||||
pp_arr_field_len("base_classes", def->base_classes) +
|
||||
pp_arr_field_len("param_names", def->param_names) +
|
||||
pp_arr_field_len("param_types", def->param_types) +
|
||||
pp_str_field_len("route_path", def->route_path) +
|
||||
pp_str_field_len("route_method", def->route_method) +
|
||||
pp_str_field_len("http_client", def->http_client) +
|
||||
pp_str_field_len("http_base_url", def->http_base_url);
|
||||
for (const char **kv = def->extra_props; kv && kv[0] && kv[SKIP_ONE]; kv += PAIR_LEN) {
|
||||
need += pp_str_field_len(kv[0], kv[SKIP_ONE]);
|
||||
}
|
||||
if (need == *size) {
|
||||
if (def->fingerprint && def->fingerprint_k > 0) {
|
||||
need += CBM_MINHASH_HEX_LEN + CBM_MINHASH_JSON_OVERHEAD + SKIP_ONE;
|
||||
}
|
||||
if (def->structural_profile) {
|
||||
need += CBM_AST_PROFILE_BUF + pp_str_field_len("sp", def->structural_profile) + SKIP_ONE;
|
||||
}
|
||||
if (def->body_tokens) {
|
||||
need += CBM_SZ_512 + pp_str_field_len("bt", def->body_tokens) + SKIP_ONE;
|
||||
}
|
||||
need += PP_ESC_SPACE; /* the closing '}' + NUL */
|
||||
if (need <= *size) {
|
||||
return stack;
|
||||
}
|
||||
char *buf = cbm_alloc(CBM_MEM_CLASS_GBUF_STRING, need);
|
||||
@@ -1773,7 +1820,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 +1829,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 +2037,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, ®_entries);
|
||||
defines_edges +=
|
||||
register_and_link_def(ctx, &result->defs.items[d], file_node_id,
|
||||
files[i].language, py_prefix_len, ®_entries);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2410,8 +2464,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 +3331,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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
|
||||
+35
-1
@@ -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.
|
||||
|
||||
@@ -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)));
|
||||
|
||||
|
||||
@@ -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
@@ -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. */
|
||||
|
||||
@@ -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,
|
||||
@@ -12551,6 +12843,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);
|
||||
|
||||
@@ -847,6 +847,91 @@ TEST(parallel_args_json_no_overflow) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* F14: a definition's properties buffer was sized for its docstring and variants
|
||||
* only. A wide signature (50 annotated parameters) filled the rest of the 2 KB, and
|
||||
* the MinHash fingerprint "fp", structural profile "sp" and body tokens "bt"
|
||||
* appended behind it were dropped whole: no SIMILAR_TO edge and fewer
|
||||
* SEMANTICALLY_RELATED edges for exactly the widest definitions (jellyfin's
|
||||
* ImageController actions, 2026-10-18). `small` has the same body and is the
|
||||
* control that the fixture produces all three at all. Both resolvers. */
|
||||
static const char f14_body[] =
|
||||
" total = 0\n"
|
||||
" for item in range(10):\n"
|
||||
" if item % 3 == 0:\n"
|
||||
" total += item * 2\n"
|
||||
" elif item % 5 == 1:\n"
|
||||
" total -= len(str(item))\n"
|
||||
" else:\n"
|
||||
" total ^= hash((item, total))\n"
|
||||
" values = [x * x for x in range(total % 7 + 3)]\n"
|
||||
" mapping = {str(k): v for k, v in enumerate(values)}\n"
|
||||
" while total > 100:\n"
|
||||
" total //= 3\n"
|
||||
" try:\n"
|
||||
" result = sorted(mapping.items(), key=lambda kv: kv[1])\n"
|
||||
" except ValueError as err:\n"
|
||||
" result = [(\"error\", str(err))]\n"
|
||||
" return result, total, mapping\n";
|
||||
|
||||
static bool f14_props_have(const cbm_gbuf_t *gb, const char *project, const char *name,
|
||||
const char *venue) {
|
||||
char *qn = cbm_pipeline_fqn_compute(project, "pkg/wide.py", name);
|
||||
const cbm_gbuf_node_t *n = qn ? cbm_gbuf_find_by_qn(gb, qn) : NULL;
|
||||
free(qn);
|
||||
const char *p = n && n->properties_json ? n->properties_json : "";
|
||||
bool fp = strstr(p, "\"fp\":\"") != NULL;
|
||||
bool sp = strstr(p, "\"sp\":\"") != NULL;
|
||||
bool bt = strstr(p, "\"bt\":\"") != NULL;
|
||||
bool sig = strstr(p, "\"signature\":\"") != NULL;
|
||||
printf(" f14 %s %s: found=%d len=%zu fp=%d sp=%d bt=%d signature=%d\n", venue, name, n != NULL,
|
||||
strlen(p), fp, sp, bt, sig);
|
||||
return n && fp && sp && bt && sig;
|
||||
}
|
||||
|
||||
TEST(parallel_wide_definition_keeps_fp_sp_bt_f14) {
|
||||
char params[CBM_SZ_4K];
|
||||
size_t pos = 0;
|
||||
for (int i = 0; i < 50; i++) {
|
||||
pos += (size_t)snprintf(params + pos, sizeof(params) - pos, "%sp%02d: Dict[str, int] = None",
|
||||
i ? ", " : "", i);
|
||||
}
|
||||
char src[CBM_SZ_8K];
|
||||
snprintf(src, sizeof(src), "def small(a, b):\n%s\n\ndef wide(%s):\n%s", f14_body, params,
|
||||
f14_body);
|
||||
const char *project = "cbm_f14";
|
||||
char tmpdir[256];
|
||||
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_f14_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmpdir)) {
|
||||
FAIL("mkdtemp failed");
|
||||
}
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s/pkg/wide.py", tmpdir);
|
||||
if (th_write_file(path, src) != 0) {
|
||||
th_rmtree(tmpdir);
|
||||
FAIL("failed to write the F14 fixture");
|
||||
}
|
||||
cbm_file_info_t files[1] = {0};
|
||||
files[0].path = path;
|
||||
files[0].rel_path = (char *)"pkg/wide.py";
|
||||
files[0].language = CBM_LANG_PYTHON;
|
||||
cbm_gbuf_t *graphs[2] = {run_sequential(project, tmpdir, files, 1),
|
||||
run_parallel(project, tmpdir, files, 1, 2)};
|
||||
ASSERT_NOT_NULL(graphs[0]);
|
||||
ASSERT_NOT_NULL(graphs[1]);
|
||||
bool all = true;
|
||||
for (int g = 0; g < 2; g++) {
|
||||
const char *venue = g == 0 ? "sequential" : "parallel";
|
||||
bool control = f14_props_have(graphs[g], project, "small", venue);
|
||||
bool wide = f14_props_have(graphs[g], project, "wide", venue);
|
||||
all = all && control && wide;
|
||||
}
|
||||
cbm_gbuf_free(graphs[0]);
|
||||
cbm_gbuf_free(graphs[1]);
|
||||
th_rmtree(tmpdir);
|
||||
ASSERT_TRUE(all);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Graph buffer merge tests ─────────────────────────────────────── */
|
||||
|
||||
TEST(gbuf_shared_ids_unique) {
|
||||
@@ -5515,6 +5600,7 @@ SUITE(parallel) {
|
||||
RUN_TEST(parallel_post_extract_projection_spills_before_resolve);
|
||||
RUN_TEST(parallel_empty_files);
|
||||
RUN_TEST(parallel_args_json_no_overflow);
|
||||
RUN_TEST(parallel_wide_definition_keeps_fp_sp_bt_f14);
|
||||
|
||||
/* Cleanup shared state */
|
||||
parity_teardown();
|
||||
|
||||
@@ -8868,6 +8868,180 @@ TEST(pipeline_go_rw_usage_never_cross_into_c) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The Rust registries were built from the mixed-language def set: a Perl
|
||||
* `sub new` became a Rust free function, and the project-wide
|
||||
* lsp_short_name_unique fallback bound `Store::new()` to it at 0.90 (found by
|
||||
* the generic-engine lane on its 12-language fixture). A Rust call never
|
||||
* targets another language's definition. pad_files > 0 forces the parallel
|
||||
* resolver, whose Rust registry is the shared all-defs build. */
|
||||
/* Pads a Rust fixture with pad_files filler modules (pad_files > 0 forces the
|
||||
* parallel resolver: CBM_WORKERS=4 for the run), runs the full pipeline into
|
||||
* <tmp>/<db_name> and opens the store. NULL store = the run failed. */
|
||||
static cbm_store_t *rust_fixture_run(const char *tmp, int pad_files, const char *db_name,
|
||||
cbm_pipeline_t **out_p) {
|
||||
for (int i = 0; i < pad_files; i++) {
|
||||
char name[64];
|
||||
char body[128];
|
||||
snprintf(name, sizeof(name), "src/filler%d.rs", i);
|
||||
snprintf(body, sizeof(body), "pub fn filler%d() -> u32 {\n %d\n}\n", i, i);
|
||||
write_temp_file(tmp, name, body);
|
||||
}
|
||||
char *saved = NULL;
|
||||
if (pad_files > 0) {
|
||||
char *old_workers = getenv("CBM_WORKERS");
|
||||
saved = old_workers ? strdup(old_workers) : NULL;
|
||||
cbm_setenv("CBM_WORKERS", "4", 1);
|
||||
}
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/%s", tmp, db_name);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
int rc = p ? cbm_pipeline_run(p) : -1;
|
||||
if (pad_files > 0) {
|
||||
if (saved) {
|
||||
cbm_setenv("CBM_WORKERS", saved, 1);
|
||||
free(saved);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
}
|
||||
*out_p = p;
|
||||
return rc == 0 ? cbm_store_open_path(db_path) : NULL;
|
||||
}
|
||||
|
||||
static int rust_registry_rust_only_case(int pad_files) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_rust_only_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_temp_file(tmp, "Cargo.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n");
|
||||
write_temp_file(tmp, "src/store.rs",
|
||||
"pub struct Store {\n"
|
||||
" n: u32,\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"impl Store {\n"
|
||||
" pub fn new() -> Store {\n"
|
||||
" Store { n: 0 }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" pub fn get(&self, k: u32) -> u32 {\n"
|
||||
" self.n + k\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "src/main.rs",
|
||||
"mod store;\n"
|
||||
"\n"
|
||||
"use crate::store::Store;\n"
|
||||
"\n"
|
||||
"fn run() -> u32 {\n"
|
||||
" let s = Store::new();\n"
|
||||
" s.get(1)\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"fn main() {\n"
|
||||
" run();\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "perlsrc/Lib.pm",
|
||||
"package Lib;\n"
|
||||
"\n"
|
||||
"sub new {\n"
|
||||
" my ($class) = @_;\n"
|
||||
" return bless {}, $class;\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"1;\n");
|
||||
cbm_pipeline_t *p = NULL;
|
||||
cbm_store_t *s = rust_fixture_run(tmp, pad_files, "rust_only.db", &p);
|
||||
ASSERT_NOT_NULL(s);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
|
||||
/* Never onto the Perl constructor ... */
|
||||
ASSERT_EQ(named_edge_to_file_count(s, project, "CALLS", "run", "new", "perlsrc/Lib.pm"), 0);
|
||||
/* ... and the Rust call still binds the Rust associated function. */
|
||||
ASSERT_GTE(named_edge_to_file_count(s, project, "CALLS", "run", "new", "src/store.rs"), 1);
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pipeline_rust_call_never_binds_another_language) {
|
||||
return rust_registry_rust_only_case(0);
|
||||
}
|
||||
|
||||
TEST(pipeline_rust_call_never_binds_another_language_parallel) {
|
||||
return rust_registry_rust_only_case(52);
|
||||
}
|
||||
|
||||
/* `Opts::write(true)` is an associated function. The LSP leaves it to the
|
||||
* registry (import_map -> fsx.Opts.write), but its free-function short-name
|
||||
* fallback bound it to the project's only free `write` (cache.write) first, and
|
||||
* that LSP row pre-empted the right answer. With the cross-language namesakes
|
||||
* gone (above), this hijack replaced 600+ correct qualified_suffix/import_map
|
||||
* edges on the Rust corpus. A type-qualified call never takes that fallback. */
|
||||
static int rust_assoc_call_free_namesake_case(int pad_files) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_rust_assoc_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_temp_file(tmp, "Cargo.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n");
|
||||
write_temp_file(tmp, "src/fsx.rs",
|
||||
"pub struct Opts {\n"
|
||||
" pub w: bool,\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"impl Opts {\n"
|
||||
" pub fn write(w: bool) -> Opts {\n"
|
||||
" Opts { w }\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "src/cache.rs",
|
||||
"pub fn write(p: &str) -> usize {\n"
|
||||
" p.len()\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "src/main.rs",
|
||||
"mod cache;\n"
|
||||
"mod fsx;\n"
|
||||
"\n"
|
||||
"use crate::fsx::Opts;\n"
|
||||
"\n"
|
||||
"fn run() -> bool {\n"
|
||||
" let o = Opts::write(true);\n"
|
||||
" o.w\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"fn main() {\n"
|
||||
" run();\n"
|
||||
" cache::write(\"x\");\n"
|
||||
"}\n");
|
||||
cbm_pipeline_t *p = NULL;
|
||||
cbm_store_t *s = rust_fixture_run(tmp, pad_files, "rust_assoc.db", &p);
|
||||
ASSERT_NOT_NULL(s);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
|
||||
/* The associated function, never the free namesake ... */
|
||||
ASSERT_GTE(named_edge_to_file_count(s, project, "CALLS", "run", "write", "src/fsx.rs"), 1);
|
||||
ASSERT_EQ(named_edge_to_file_count(s, project, "CALLS", "run", "write", "src/cache.rs"), 0);
|
||||
/* ... while the module-qualified free call keeps its target. */
|
||||
ASSERT_GTE(named_edge_to_file_count(s, project, "CALLS", "main", "write", "src/cache.rs"), 1);
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pipeline_rust_assoc_call_never_binds_free_namesake) {
|
||||
return rust_assoc_call_free_namesake_case(0);
|
||||
}
|
||||
|
||||
TEST(pipeline_rust_assoc_call_never_binds_free_namesake_parallel) {
|
||||
return rust_assoc_call_free_namesake_case(52);
|
||||
}
|
||||
|
||||
TEST(pipeline_go_rw_usage_never_cross_into_c_parallel) {
|
||||
/* Parallel twin of the test above: USAGE and WRITES/READS each have a
|
||||
* second, independent resolver in pass_parallel.c (resolve_file_usages /
|
||||
@@ -14416,6 +14590,731 @@ static void teardown_lang_repo(void) {
|
||||
g_lang_tmpdir[0] = '\0';
|
||||
}
|
||||
|
||||
/* ── #1548: Python defs nested in a function body ─────────────────── */
|
||||
|
||||
/* The QN of `rel` in `project` ("web.create_app.health"). Route QNs
|
||||
* ("__route__GET__/health") carry no project and are taken as they are. */
|
||||
static void pyn_qn(char *buf, size_t cap, const char *project, const char *rel) {
|
||||
if (strncmp(rel, "__route__", strlen("__route__")) == 0) {
|
||||
snprintf(buf, cap, "%s", rel);
|
||||
} else {
|
||||
snprintf(buf, cap, "%s.%s", project, rel);
|
||||
}
|
||||
}
|
||||
|
||||
/* True iff an edge of `type` runs from node `src_rel` to node `tgt_rel`. */
|
||||
static bool pyn_edge(cbm_store_t *s, const char *project, const char *src_rel, const char *tgt_rel,
|
||||
const char *type) {
|
||||
char sq[512];
|
||||
char tq[512];
|
||||
pyn_qn(sq, sizeof(sq), project, src_rel);
|
||||
pyn_qn(tq, sizeof(tq), project, tgt_rel);
|
||||
cbm_node_t src = {0};
|
||||
cbm_node_t tgt = {0};
|
||||
bool found = false;
|
||||
if (cbm_store_find_node_by_qn(s, project, sq, &src) == CBM_STORE_OK &&
|
||||
cbm_store_find_node_by_qn(s, project, tq, &tgt) == CBM_STORE_OK) {
|
||||
cbm_edge_t *edges = NULL;
|
||||
int ec = 0;
|
||||
cbm_store_find_edges_by_source_type(s, src.id, type, &edges, &ec);
|
||||
for (int i = 0; i < ec && !found; i++) {
|
||||
found = edges[i].target_id == tgt.id;
|
||||
}
|
||||
if (edges) {
|
||||
cbm_store_free_edges(edges, ec);
|
||||
}
|
||||
}
|
||||
cbm_node_free_fields(&src);
|
||||
cbm_node_free_fields(&tgt);
|
||||
return found;
|
||||
}
|
||||
|
||||
/* The fixtures of the #1548 pipeline tests. */
|
||||
static const char pyn_web_py[] = "from flask import Flask\n"
|
||||
"\n"
|
||||
"def load_user(uid):\n"
|
||||
" return uid\n"
|
||||
"\n"
|
||||
"def render(name):\n"
|
||||
" return name\n"
|
||||
"\n"
|
||||
"def create_app():\n"
|
||||
" app = Flask(__name__)\n"
|
||||
"\n"
|
||||
" @app.route(\"/users/<uid>\")\n"
|
||||
" def show_user(uid):\n"
|
||||
" return render(load_user(uid))\n"
|
||||
"\n"
|
||||
" @app.get(\"/health\")\n"
|
||||
" def health():\n"
|
||||
" return render(\"ok\")\n"
|
||||
"\n"
|
||||
" def ping():\n"
|
||||
" return render(\"pong\")\n"
|
||||
"\n"
|
||||
" app.add_url_rule(\"/ping\", \"ping\", ping)\n"
|
||||
" return app\n";
|
||||
/* A nested def named like its method, and a local helper whose name exists
|
||||
* nowhere else -- b.py calls that name without binding it. */
|
||||
static const char pyn_a_py[] = "class Tree:\n"
|
||||
" def visit(self, node):\n"
|
||||
" def visit(n):\n"
|
||||
" if n:\n"
|
||||
" visit(n.child)\n"
|
||||
"\n"
|
||||
" visit(node)\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def outer():\n"
|
||||
" def helper_only_here():\n"
|
||||
" return 1\n"
|
||||
"\n"
|
||||
" return helper_only_here()\n";
|
||||
static const char pyn_b_py[] = "def elsewhere():\n"
|
||||
" return helper_only_here()\n";
|
||||
/* Two defs, each with its own local class of the same name. */
|
||||
static const char pyn_c_py[] = "def make_forms():\n"
|
||||
" class LocalForm:\n"
|
||||
" def clean(self):\n"
|
||||
" return 1\n"
|
||||
"\n"
|
||||
" form = LocalForm()\n"
|
||||
" return form.clean()\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def make_other_forms():\n"
|
||||
" class LocalForm:\n"
|
||||
" def clean(self):\n"
|
||||
" return 2\n"
|
||||
"\n"
|
||||
" form = LocalForm()\n"
|
||||
" return form.clean()\n";
|
||||
/* A local def named like a stdlib function another module imports. */
|
||||
static const char pyn_x_py[] = "def outer():\n"
|
||||
" def getpass(prompt):\n"
|
||||
" return prompt\n"
|
||||
"\n"
|
||||
" return getpass(\"?\")\n";
|
||||
static const char pyn_y_py[] = "from getpass import getpass\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def ask():\n"
|
||||
" return getpass(\"password: \")\n";
|
||||
|
||||
/* The issue's headline: an app factory's route handlers are defs inside
|
||||
* create_app. They were never extracted, so the HTTP layer vanished -- no
|
||||
* HANDLES, and load_user()'s only caller was pinned on the file node. */
|
||||
TEST(pipeline_python_app_factory_routes_issue1548) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_factory_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_temp_file(tmp, "web.py", pyn_web_py);
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/factory.db", tmp);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(cbm_pipeline_run(p), 0);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
|
||||
ASSERT_TRUE(
|
||||
pyn_edge(s, project, "web.create_app.show_user", "__route__ANY__/users/{}", "HANDLES"));
|
||||
ASSERT_TRUE(pyn_edge(s, project, "web.create_app.health", "__route__GET__/health", "HANDLES"));
|
||||
ASSERT_TRUE(pyn_edge(s, project, "web.create_app.show_user", "web.load_user", "CALLS"));
|
||||
ASSERT_TRUE(pyn_edge(s, project, "web.create_app.health", "web.render", "CALLS"));
|
||||
ASSERT_FALSE(pyn_edge(s, project, "web.py.__file__", "web.load_user", "CALLS"));
|
||||
/* A call-registered handler: add_url_rule names create_app's local ping. */
|
||||
ASSERT_TRUE(pyn_edge(s, project, "web.create_app.ping", "__route__ANY__/ping", "HANDLES"));
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A def nested in a function is a local of it (Python LEGB). A bare call
|
||||
* resolves to the local bound in the nearest enclosing function: visit(node)
|
||||
* in Tree.visit reaches the nested visit, whose own recursion stays a
|
||||
* self-call instead of landing on the method. And no caller outside the
|
||||
* function can reach the local by name -- b.py's unbound helper_only_here()
|
||||
* must not be matched to it by a short-name strategy. */
|
||||
TEST(pipeline_python_nested_def_is_function_local_issue1548) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_local_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_temp_file(tmp, "a.py", pyn_a_py);
|
||||
write_temp_file(tmp, "b.py", pyn_b_py);
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/local.db", tmp);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(cbm_pipeline_run(p), 0);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
|
||||
ASSERT_TRUE(pyn_edge(s, project, "a.Tree.visit", "a.Tree.visit.visit", "CALLS"));
|
||||
ASSERT_FALSE(pyn_edge(s, project, "a.Tree.visit.visit", "a.Tree.visit", "CALLS"));
|
||||
ASSERT_TRUE(pyn_edge(s, project, "a.outer", "a.outer.helper_only_here", "CALLS"));
|
||||
ASSERT_FALSE(pyn_edge(s, project, "b.elsewhere", "a.outer.helper_only_here", "CALLS"));
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Two defs each declare `class LocalForm`. Each constructor call reaches its
|
||||
* own def's class, and a method call on the instance never reaches the other
|
||||
* def's class: the Python LSP must not bind a local class module-wide by its
|
||||
* short name (every same-named local class collapsed onto one). */
|
||||
TEST(pipeline_python_local_classes_stay_in_their_def_issue1548) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_localcls_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_temp_file(tmp, "c.py", pyn_c_py);
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/localcls.db", tmp);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(cbm_pipeline_run(p), 0);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
|
||||
ASSERT_TRUE(pyn_edge(s, project, "c.make_forms", "c.make_forms.LocalForm", "CALLS"));
|
||||
ASSERT_TRUE(
|
||||
pyn_edge(s, project, "c.make_other_forms", "c.make_other_forms.LocalForm", "CALLS"));
|
||||
ASSERT_FALSE(pyn_edge(s, project, "c.make_forms", "c.make_other_forms.LocalForm", "CALLS"));
|
||||
ASSERT_FALSE(pyn_edge(s, project, "c.make_other_forms", "c.make_forms.LocalForm", "CALLS"));
|
||||
ASSERT_FALSE(
|
||||
pyn_edge(s, project, "c.make_forms", "c.make_other_forms.LocalForm.clean", "CALLS"));
|
||||
ASSERT_FALSE(
|
||||
pyn_edge(s, project, "c.make_other_forms", "c.make_forms.LocalForm.clean", "CALLS"));
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* No import can name a function's local. The symbol-name import fallback
|
||||
* bound `from getpass import getpass` (the stdlib module, not in the project)
|
||||
* to x.outer's local `def getpass`, and ask()'s call followed the import. */
|
||||
TEST(pipeline_python_import_never_binds_a_local_def_issue1548) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_localimp_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_temp_file(tmp, "x.py", pyn_x_py);
|
||||
write_temp_file(tmp, "y.py", pyn_y_py);
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/localimp.db", tmp);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(cbm_pipeline_run(p), 0);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
|
||||
ASSERT_TRUE(pyn_edge(s, project, "x.outer", "x.outer.getpass", "CALLS"));
|
||||
ASSERT_FALSE(pyn_edge(s, project, "y.py.__file__", "x.outer.getpass", "IMPORTS"));
|
||||
ASSERT_FALSE(pyn_edge(s, project, "y.ask", "x.outer.getpass", "CALLS"));
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The same four fixtures through the parallel path: >= 50 files and
|
||||
* CBM_WORKERS=4 force pass_parallel.c and the cross-file Python LSP, which
|
||||
* register, resolve and bind through their own code. */
|
||||
TEST(pipeline_python_nested_defs_parallel_issue1548) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_nested_par_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_temp_file(tmp, "web.py", pyn_web_py);
|
||||
write_temp_file(tmp, "a.py", pyn_a_py);
|
||||
write_temp_file(tmp, "b.py", pyn_b_py);
|
||||
write_temp_file(tmp, "c.py", pyn_c_py);
|
||||
write_temp_file(tmp, "x.py", pyn_x_py);
|
||||
write_temp_file(tmp, "y.py", pyn_y_py);
|
||||
for (int i = 0; i < 52; i++) {
|
||||
char name[64];
|
||||
char body[128];
|
||||
snprintf(name, sizeof(name), "filler%d.py", i);
|
||||
snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i);
|
||||
write_temp_file(tmp, name, body);
|
||||
}
|
||||
|
||||
char *old_workers = getenv("CBM_WORKERS");
|
||||
char *saved = old_workers ? strdup(old_workers) : NULL;
|
||||
cbm_setenv("CBM_WORKERS", "4", 1);
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/nested_par.db", tmp);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(cbm_pipeline_run(p), 0);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
|
||||
ASSERT_TRUE(
|
||||
pyn_edge(s, project, "web.create_app.show_user", "__route__ANY__/users/{}", "HANDLES"));
|
||||
ASSERT_TRUE(pyn_edge(s, project, "web.create_app.health", "__route__GET__/health", "HANDLES"));
|
||||
ASSERT_TRUE(pyn_edge(s, project, "web.create_app.show_user", "web.load_user", "CALLS"));
|
||||
ASSERT_TRUE(pyn_edge(s, project, "web.create_app.ping", "__route__ANY__/ping", "HANDLES"));
|
||||
ASSERT_TRUE(pyn_edge(s, project, "a.Tree.visit", "a.Tree.visit.visit", "CALLS"));
|
||||
ASSERT_FALSE(pyn_edge(s, project, "a.Tree.visit.visit", "a.Tree.visit", "CALLS"));
|
||||
ASSERT_TRUE(pyn_edge(s, project, "a.outer", "a.outer.helper_only_here", "CALLS"));
|
||||
ASSERT_FALSE(pyn_edge(s, project, "b.elsewhere", "a.outer.helper_only_here", "CALLS"));
|
||||
ASSERT_TRUE(pyn_edge(s, project, "c.make_forms", "c.make_forms.LocalForm", "CALLS"));
|
||||
ASSERT_FALSE(pyn_edge(s, project, "c.make_forms", "c.make_other_forms.LocalForm", "CALLS"));
|
||||
ASSERT_FALSE(
|
||||
pyn_edge(s, project, "c.make_forms", "c.make_other_forms.LocalForm.clean", "CALLS"));
|
||||
ASSERT_FALSE(
|
||||
pyn_edge(s, project, "c.make_other_forms", "c.make_forms.LocalForm.clean", "CALLS"));
|
||||
ASSERT_TRUE(pyn_edge(s, project, "x.outer", "x.outer.getpass", "CALLS"));
|
||||
ASSERT_FALSE(pyn_edge(s, project, "y.py.__file__", "x.outer.getpass", "IMPORTS"));
|
||||
ASSERT_FALSE(pyn_edge(s, project, "y.ask", "x.outer.getpass", "CALLS"));
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
if (saved) {
|
||||
cbm_setenv("CBM_WORKERS", saved, 1);
|
||||
free(saved);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* An incremental re-index seeds its registry from the stored graph, not from
|
||||
* extraction. It must keep a function's locals out of the short-name index
|
||||
* exactly as the full index does: re-resolving the touched b.py must not bind
|
||||
* its unbound helper_only_here() to a.outer's local. */
|
||||
TEST(pipeline_python_nested_def_local_after_incremental_issue1548) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_local_incr_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_temp_file(tmp, "a.py", pyn_a_py);
|
||||
write_temp_file(tmp, "b.py", pyn_b_py);
|
||||
/* An extensionless script is Python by its shebang: its locals are locals
|
||||
* in the incremental seed too, not only in the full index. */
|
||||
write_temp_file(tmp, "tool",
|
||||
"#!/usr/bin/env python3\n"
|
||||
"def run():\n"
|
||||
" def helper_in_tool():\n"
|
||||
" return 1\n"
|
||||
"\n"
|
||||
" return helper_in_tool()\n");
|
||||
write_temp_file(tmp, "d.py",
|
||||
"def elsewhere_too():\n"
|
||||
" return helper_in_tool()\n");
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/local_incr.db", tmp);
|
||||
cbm_pipeline_t *p1 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p1);
|
||||
ASSERT_EQ(cbm_pipeline_run(p1), 0);
|
||||
cbm_pipeline_free(p1);
|
||||
|
||||
char b_path[512];
|
||||
snprintf(b_path, sizeof(b_path), "%s/b.py", tmp);
|
||||
ASSERT_EQ(th_append_file(b_path, "\n# touched: re-resolve b.py incrementally\n"), 0);
|
||||
snprintf(b_path, sizeof(b_path), "%s/d.py", tmp);
|
||||
ASSERT_EQ(th_append_file(b_path, "\n# touched: re-resolve d.py incrementally\n"), 0);
|
||||
|
||||
cbm_pipeline_t *p2 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p2);
|
||||
ASSERT_EQ(cbm_pipeline_run(p2), 0);
|
||||
const char *project = cbm_pipeline_project_name(p2);
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
|
||||
ASSERT_TRUE(pyn_edge(s, project, "a.outer", "a.outer.helper_only_here", "CALLS"));
|
||||
ASSERT_FALSE(pyn_edge(s, project, "b.elsewhere", "a.outer.helper_only_here", "CALLS"));
|
||||
ASSERT_TRUE(pyn_edge(s, project, "tool.run", "tool.run.helper_in_tool", "CALLS"));
|
||||
ASSERT_FALSE(pyn_edge(s, project, "d.elsewhere_too", "tool.run.helper_in_tool", "CALLS"));
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p2);
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* As pyn_edge, and the edge's properties also contain `fragment`. */
|
||||
static bool pyn_edge_with(cbm_store_t *s, const char *project, const char *src_rel,
|
||||
const char *tgt_rel, const char *type, const char *fragment) {
|
||||
char sq[512];
|
||||
char tq[512];
|
||||
pyn_qn(sq, sizeof(sq), project, src_rel);
|
||||
pyn_qn(tq, sizeof(tq), project, tgt_rel);
|
||||
cbm_node_t src = {0};
|
||||
cbm_node_t tgt = {0};
|
||||
bool found = false;
|
||||
if (cbm_store_find_node_by_qn(s, project, sq, &src) == CBM_STORE_OK &&
|
||||
cbm_store_find_node_by_qn(s, project, tq, &tgt) == CBM_STORE_OK) {
|
||||
cbm_edge_t *edges = NULL;
|
||||
int ec = 0;
|
||||
cbm_store_find_edges_by_source_type(s, src.id, type, &edges, &ec);
|
||||
for (int i = 0; i < ec && !found; i++) {
|
||||
found = edges[i].target_id == tgt.id && edges[i].properties_json &&
|
||||
strstr(edges[i].properties_json, fragment);
|
||||
}
|
||||
if (edges) {
|
||||
cbm_store_free_edges(edges, ec);
|
||||
}
|
||||
}
|
||||
cbm_node_free_fields(&src);
|
||||
cbm_node_free_fields(&tgt);
|
||||
return found;
|
||||
}
|
||||
|
||||
/* A local def shadows a module def of the same name (Python LEGB), in both
|
||||
* orders of the two callers within one file: the registry's per-file resolve
|
||||
* cache is keyed by the callee name only, so a caller-dependent answer must
|
||||
* never come from it. The undecorated local is bound by the Python LSP itself
|
||||
* (an lsp_* edge); a decorated one is no exact callable value, so the LSP
|
||||
* shadows it and the registry's lexical_scope step binds it. */
|
||||
static const char pyn_shadow_local_first_py[] = "def helper():\n"
|
||||
" return 0\n"
|
||||
"\n"
|
||||
"def uses_local():\n"
|
||||
" def helper():\n"
|
||||
" return 1\n"
|
||||
" return helper()\n"
|
||||
"\n"
|
||||
"def uses_module():\n"
|
||||
" return helper()\n"
|
||||
"\n"
|
||||
"def uses_decorated():\n"
|
||||
" @staticmethod\n"
|
||||
" def helper():\n"
|
||||
" return 2\n"
|
||||
" return helper()\n";
|
||||
static const char pyn_shadow_module_first_py[] = "def helper():\n"
|
||||
" return 0\n"
|
||||
"\n"
|
||||
"def uses_module():\n"
|
||||
" return helper()\n"
|
||||
"\n"
|
||||
"def uses_local():\n"
|
||||
" if True:\n"
|
||||
" def helper():\n"
|
||||
" return 1\n"
|
||||
" return helper()\n";
|
||||
|
||||
/* A class declared in a def is walked by the LSP under the def's QN: super()
|
||||
* and self calls in its methods resolve through the class's own bases. */
|
||||
static const char pyn_local_class_methods_py[] = "class Base:\n"
|
||||
" def greet(self):\n"
|
||||
" return 1\n"
|
||||
"\n"
|
||||
"def factory():\n"
|
||||
" class Local(Base):\n"
|
||||
" def greet(self):\n"
|
||||
" return super().greet()\n"
|
||||
"\n"
|
||||
" def other(self):\n"
|
||||
" return self.greet()\n"
|
||||
" return Local\n";
|
||||
|
||||
static int pyn_assert_shadowing(cbm_store_t *s, const char *project) {
|
||||
ASSERT_TRUE(pyn_edge_with(s, project, "sc.factory.Local.greet", "sc.Base.greet", "CALLS",
|
||||
"\"strategy\":\"lsp_super\""));
|
||||
ASSERT_TRUE(pyn_edge_with(s, project, "sc.factory.Local.other", "sc.factory.Local.greet",
|
||||
"CALLS", "\"strategy\":\"lsp_"));
|
||||
/* sa's local def is a plain statement of the body: the LSP binds it as an
|
||||
* exact callable. sb's sits under `if True:`; at that compound statement's
|
||||
* join the LSP invalidates what the branch bound (the def may not exist on
|
||||
* every path), so it emits nothing and the registry's lexical step binds
|
||||
* the local -- still never the module def. H1 (the LSP walks compound
|
||||
* statements) may legitimately move sb to an lsp_* strategy; that PR
|
||||
* must update this expectation deliberately. */
|
||||
const char *files[] = {"sa", "sb"};
|
||||
const char *local_strategy[] = {"\"strategy\":\"lsp_", "\"strategy\":\"lexical_scope\""};
|
||||
for (int f = 0; f < 2; f++) {
|
||||
char local_caller[64], local_def[64], module_caller[64], module_def[64];
|
||||
snprintf(local_caller, sizeof(local_caller), "%s.uses_local", files[f]);
|
||||
snprintf(local_def, sizeof(local_def), "%s.uses_local.helper", files[f]);
|
||||
snprintf(module_caller, sizeof(module_caller), "%s.uses_module", files[f]);
|
||||
snprintf(module_def, sizeof(module_def), "%s.helper", files[f]);
|
||||
ASSERT_TRUE(
|
||||
pyn_edge_with(s, project, local_caller, local_def, "CALLS", local_strategy[f]));
|
||||
ASSERT_FALSE(pyn_edge(s, project, local_caller, module_def, "CALLS"));
|
||||
ASSERT_TRUE(pyn_edge(s, project, module_caller, module_def, "CALLS"));
|
||||
ASSERT_FALSE(pyn_edge(s, project, module_caller, local_def, "CALLS"));
|
||||
}
|
||||
ASSERT_TRUE(pyn_edge_with(s, project, "sa.uses_decorated", "sa.uses_decorated.helper",
|
||||
"CALLS", "\"strategy\":\"lexical_scope\""));
|
||||
ASSERT_FALSE(pyn_edge(s, project, "sa.uses_decorated", "sa.helper", "CALLS"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int pyn_run_shadowing(int fillers) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_shadow_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_temp_file(tmp, "sa.py", pyn_shadow_local_first_py);
|
||||
write_temp_file(tmp, "sb.py", pyn_shadow_module_first_py);
|
||||
write_temp_file(tmp, "sc.py", pyn_local_class_methods_py);
|
||||
for (int i = 0; i < fillers; i++) {
|
||||
char name[64];
|
||||
char body[128];
|
||||
snprintf(name, sizeof(name), "filler%d.py", i);
|
||||
snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i);
|
||||
write_temp_file(tmp, name, body);
|
||||
}
|
||||
char *old_workers = getenv("CBM_WORKERS");
|
||||
char *saved = old_workers ? strdup(old_workers) : NULL;
|
||||
if (fillers > 0) {
|
||||
cbm_setenv("CBM_WORKERS", "4", 1);
|
||||
}
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/shadow.db", tmp);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
int rc = p ? cbm_pipeline_run(p) : -1;
|
||||
cbm_store_t *s = rc == 0 ? cbm_store_open_path(db_path) : NULL;
|
||||
int verdict = s ? pyn_assert_shadowing(s, cbm_pipeline_project_name(p)) : 1;
|
||||
if (s) {
|
||||
cbm_store_close(s);
|
||||
}
|
||||
if (p) {
|
||||
cbm_pipeline_free(p);
|
||||
}
|
||||
if (saved) {
|
||||
cbm_setenv("CBM_WORKERS", saved, 1);
|
||||
free(saved);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
th_rmtree(tmp);
|
||||
ASSERT_EQ(rc, 0);
|
||||
return verdict;
|
||||
}
|
||||
|
||||
TEST(pipeline_python_local_def_shadows_module_def_issue1548) {
|
||||
return pyn_run_shadowing(0);
|
||||
}
|
||||
|
||||
TEST(pipeline_python_local_def_shadows_module_def_parallel_issue1548) {
|
||||
return pyn_run_shadowing(52);
|
||||
}
|
||||
|
||||
/* L7 review of #1548: a package __init__.py function can share a submodule's QN
|
||||
* (poetry: layouts/__init__.py `def layout` is the QN prefix of every symbol in
|
||||
* layouts/layout.py). The function-local detection is bounded at the symbol's
|
||||
* own file, so class Layout stays a module-level, importable class. src/ layout
|
||||
* without a manifest: the module resolver misses `rowpkg.layout`, so the import
|
||||
* relies on the symbol-name fallback, which skips function locals -- unbounded,
|
||||
* Layout read as a local of layout() and its import and calls vanished. Both
|
||||
* resolvers; the second run re-indexes incrementally after an edit of
|
||||
* layout.py, so the incremental registry seed is held to the same bound. */
|
||||
static const char pyn_layout_init_py[] = "def layout(name):\n"
|
||||
" return name\n";
|
||||
static const char pyn_layout_mod_py[] = "class Layout:\n"
|
||||
" def __init__(self, name):\n"
|
||||
" self.name = name\n"
|
||||
"\n"
|
||||
" def create(self):\n"
|
||||
" return self.name\n";
|
||||
static const char pyn_layout_app_py[] = "from rowpkg.layout import Layout\n"
|
||||
"\n"
|
||||
"def test_build():\n"
|
||||
" return Layout(\"demo\").create()\n";
|
||||
|
||||
static int pyn_run_init_collision(int fillers) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_initqn_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_temp_file(tmp, "src/rowpkg/__init__.py", pyn_layout_init_py);
|
||||
write_temp_file(tmp, "src/rowpkg/layout.py", pyn_layout_mod_py);
|
||||
write_temp_file(tmp, "tests/test_app.py", pyn_layout_app_py);
|
||||
for (int i = 0; i < fillers; i++) {
|
||||
char name[64];
|
||||
char body[128];
|
||||
snprintf(name, sizeof(name), "filler%d.py", i);
|
||||
snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i);
|
||||
write_temp_file(tmp, name, body);
|
||||
}
|
||||
char *old_workers = getenv("CBM_WORKERS");
|
||||
char *saved = old_workers ? strdup(old_workers) : NULL;
|
||||
if (fillers > 0) {
|
||||
cbm_setenv("CBM_WORKERS", "4", 1);
|
||||
}
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/initqn.db", tmp);
|
||||
int rc[2] = {-1, -1};
|
||||
bool imports[2] = {false, false};
|
||||
bool calls[2] = {false, false};
|
||||
bool method[2] = {false, false};
|
||||
for (int run = 0; run < 2; run++) {
|
||||
if (run == 1) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s/src/rowpkg/layout.py", tmp);
|
||||
th_append_file(path, "\n# edited: the second run re-indexes incrementally\n");
|
||||
}
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
rc[run] = p ? cbm_pipeline_run(p) : -1;
|
||||
cbm_store_t *s = rc[run] == 0 ? cbm_store_open_path(db_path) : NULL;
|
||||
if (s) {
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
imports[run] = pyn_edge(s, project, "tests.test_app.py.__file__",
|
||||
"src.rowpkg.layout.Layout", "IMPORTS");
|
||||
calls[run] =
|
||||
pyn_edge(s, project, "tests.test_app.test_build", "src.rowpkg.layout.Layout",
|
||||
"CALLS");
|
||||
method[run] = pyn_edge(s, project, "tests.test_app.test_build",
|
||||
"src.rowpkg.layout.Layout.create", "CALLS");
|
||||
cbm_store_close(s);
|
||||
}
|
||||
if (p) {
|
||||
cbm_pipeline_free(p);
|
||||
}
|
||||
}
|
||||
if (saved) {
|
||||
cbm_setenv("CBM_WORKERS", saved, 1);
|
||||
free(saved);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
th_rmtree(tmp);
|
||||
for (int run = 0; run < 2; run++) {
|
||||
ASSERT_EQ(rc[run], 0);
|
||||
ASSERT_TRUE(imports[run]); /* the class stays importable */
|
||||
ASSERT_TRUE(calls[run]); /* Layout("demo") */
|
||||
ASSERT_TRUE(method[run]); /* .create() through the constructed type */
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pipeline_python_init_function_shares_submodule_qn_issue1548) {
|
||||
return pyn_run_init_collision(0);
|
||||
}
|
||||
|
||||
TEST(pipeline_python_init_function_shares_submodule_qn_parallel_issue1548) {
|
||||
return pyn_run_init_collision(52);
|
||||
}
|
||||
|
||||
/* #1548 keeps TESTS for calls made through a test's own nested helper: the
|
||||
* call now starts at the extracted helper def (not at the test FILE node, as
|
||||
* before #1548), so TESTS is attributed to the enclosing test function. A
|
||||
* helper nested in a NON-test function stays without TESTS, like a module-level
|
||||
* helper. Both resolvers. */
|
||||
static const char pyn_tests_prod_py[] = "def target(value):\n"
|
||||
" return value\n"
|
||||
"\n"
|
||||
"def other_target(value):\n"
|
||||
" return value\n";
|
||||
static const char pyn_tests_test_py[] = "from pkg.prod import target, other_target\n"
|
||||
"\n"
|
||||
"def test_through_nested_helper():\n"
|
||||
" def check(v):\n"
|
||||
" return target(v)\n"
|
||||
" assert check(1) == 1\n"
|
||||
"\n"
|
||||
"class TestGroup:\n"
|
||||
" def test_method_helper(self):\n"
|
||||
" def probe():\n"
|
||||
" return target(2)\n"
|
||||
" assert probe() == 2\n"
|
||||
"\n"
|
||||
"def make_fixture():\n"
|
||||
" def build():\n"
|
||||
" return other_target(3)\n"
|
||||
" return build\n";
|
||||
|
||||
static int pyn_run_nested_helper_tests(int fillers) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_ntests_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_temp_file(tmp, "pkg/__init__.py", "");
|
||||
write_temp_file(tmp, "pkg/prod.py", pyn_tests_prod_py);
|
||||
write_temp_file(tmp, "tests/test_rows.py", pyn_tests_test_py);
|
||||
for (int i = 0; i < fillers; i++) {
|
||||
char name[64];
|
||||
char body[128];
|
||||
snprintf(name, sizeof(name), "filler%d.py", i);
|
||||
snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i);
|
||||
write_temp_file(tmp, name, body);
|
||||
}
|
||||
char *old_workers = getenv("CBM_WORKERS");
|
||||
char *saved = old_workers ? strdup(old_workers) : NULL;
|
||||
if (fillers > 0) {
|
||||
cbm_setenv("CBM_WORKERS", "4", 1);
|
||||
}
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/ntests.db", tmp);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
int rc = p ? cbm_pipeline_run(p) : -1;
|
||||
cbm_store_t *s = rc == 0 ? cbm_store_open_path(db_path) : NULL;
|
||||
bool fn_tests = false, method_tests = false, helper_calls = false, fixture_tests = true;
|
||||
if (s) {
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
helper_calls = pyn_edge(s, project, "tests.test_rows.test_through_nested_helper.check",
|
||||
"pkg.prod.target", "CALLS");
|
||||
fn_tests = pyn_edge(s, project, "tests.test_rows.test_through_nested_helper",
|
||||
"pkg.prod.target", "TESTS");
|
||||
method_tests = pyn_edge(s, project, "tests.test_rows.TestGroup.test_method_helper",
|
||||
"pkg.prod.target", "TESTS");
|
||||
fixture_tests = pyn_edge(s, project, "tests.test_rows.make_fixture",
|
||||
"pkg.prod.other_target", "TESTS") ||
|
||||
pyn_edge(s, project, "tests.test_rows.make_fixture.build",
|
||||
"pkg.prod.other_target", "TESTS");
|
||||
cbm_store_close(s);
|
||||
}
|
||||
if (p) {
|
||||
cbm_pipeline_free(p);
|
||||
}
|
||||
if (saved) {
|
||||
cbm_setenv("CBM_WORKERS", saved, 1);
|
||||
free(saved);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
th_rmtree(tmp);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_TRUE(helper_calls); /* the call starts at the nested helper (#1548) */
|
||||
ASSERT_TRUE(fn_tests); /* ... and the enclosing test function still TESTS the target */
|
||||
ASSERT_TRUE(method_tests); /* the same inside a test method */
|
||||
ASSERT_FALSE(fixture_tests); /* a helper nested in a non-test function: no TESTS */
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pipeline_python_nested_helper_keeps_tests_issue1548) {
|
||||
return pyn_run_nested_helper_tests(0);
|
||||
}
|
||||
|
||||
TEST(pipeline_python_nested_helper_keeps_tests_parallel_issue1548) {
|
||||
return pyn_run_nested_helper_tests(52);
|
||||
}
|
||||
|
||||
TEST(pipeline_python_project) {
|
||||
/* Port of TestPipelinePythonProject */
|
||||
const char *files[] = {"main.py", "utils.py"};
|
||||
@@ -23845,6 +24744,10 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_html_embedded_member_call_stays_unbound);
|
||||
RUN_TEST(pipeline_go_rw_usage_never_cross_into_c);
|
||||
RUN_TEST(pipeline_go_rw_usage_never_cross_into_c_parallel);
|
||||
RUN_TEST(pipeline_rust_call_never_binds_another_language);
|
||||
RUN_TEST(pipeline_rust_call_never_binds_another_language_parallel);
|
||||
RUN_TEST(pipeline_rust_assoc_call_never_binds_free_namesake);
|
||||
RUN_TEST(pipeline_rust_assoc_call_never_binds_free_namesake_parallel);
|
||||
RUN_TEST(pipeline_rust_std_receiver_never_binds_project_method);
|
||||
RUN_TEST(pipeline_rust_std_receiver_never_binds_project_method_parallel);
|
||||
RUN_TEST(pipeline_go_bare_ref_never_binds_field);
|
||||
@@ -23913,6 +24816,18 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_c_variants_on_sequential_and_parallel_paths_c1);
|
||||
RUN_TEST(pipeline_cpp_overloads_are_listed_as_variants_c1);
|
||||
RUN_TEST(pipeline_semantic_version_3_index_is_rebuilt_in_full_c1);
|
||||
RUN_TEST(pipeline_python_app_factory_routes_issue1548);
|
||||
RUN_TEST(pipeline_python_nested_def_is_function_local_issue1548);
|
||||
RUN_TEST(pipeline_python_local_classes_stay_in_their_def_issue1548);
|
||||
RUN_TEST(pipeline_python_import_never_binds_a_local_def_issue1548);
|
||||
RUN_TEST(pipeline_python_nested_defs_parallel_issue1548);
|
||||
RUN_TEST(pipeline_python_nested_def_local_after_incremental_issue1548);
|
||||
RUN_TEST(pipeline_python_local_def_shadows_module_def_issue1548);
|
||||
RUN_TEST(pipeline_python_local_def_shadows_module_def_parallel_issue1548);
|
||||
RUN_TEST(pipeline_python_init_function_shares_submodule_qn_issue1548);
|
||||
RUN_TEST(pipeline_python_init_function_shares_submodule_qn_parallel_issue1548);
|
||||
RUN_TEST(pipeline_python_nested_helper_keeps_tests_issue1548);
|
||||
RUN_TEST(pipeline_python_nested_helper_keeps_tests_parallel_issue1548);
|
||||
RUN_TEST(pipeline_header_include_target_is_independent_of_registration_order);
|
||||
RUN_TEST(pipeline_imports_multi_symbol_edges);
|
||||
RUN_TEST(pipeline_go_cross_package_call);
|
||||
|
||||
+8
-4
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"};
|
||||
|
||||
Reference in new issue
Block a user