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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 65 additions & 2 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 —
Expand Down Expand Up @@ -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;
}

Expand Down
33 changes: 28 additions & 5 deletions internal/cbm/extract_unified.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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). */
Expand Down Expand Up @@ -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). */
Expand Down
157 changes: 140 additions & 17 deletions internal/cbm/lsp/py_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 ─────────────────────────────────────────────
*
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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)) {
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
24 changes: 24 additions & 0 deletions src/pipeline/fqn.c
Original file line number Diff line number Diff line change
Expand Up @@ -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("");
Expand Down
Loading
Loading