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:
Martin Vogel committed 2026-10-19 05:47:30 +02:00
commit 4ae1909223
19 files changed
+1721 -51

No files matched your search

+25
View File
@@ -402,6 +402,31 @@ void cbm_imports_push(CBMImportArray *arr, CBMArena *a, CBMImport imp) {
arr->items[arr->count++] = imp;
}
bool cbm_imports_local_span_binds_runtime_name(const CBMImportArray *imports, const char *name,
size_t len) {
if (!imports || !name) {
return true;
}
bool seen = false;
for (int i = 0; i < imports->count; i++) {
const CBMImport *imp = &imports->items[i];
if (!imp->local_name || strncmp(imp->local_name, name, len) != 0 ||
imp->local_name[len] != '\0') {
continue;
}
if (cbm_import_binds_runtime_name(imp)) {
return true;
}
seen = true;
}
return !seen;
}
bool cbm_imports_local_binds_runtime_name(const CBMImportArray *imports, const char *local_name) {
return !local_name ||
cbm_imports_local_span_binds_runtime_name(imports, local_name, strlen(local_name));
}
void cbm_usages_push(CBMUsageArray *arr, CBMArena *a, CBMUsage usage) {
GROW_ARRAY(arr, a);
arr->items[arr->count++] = usage;
+19
View File
@@ -398,9 +398,20 @@ typedef struct {
const char *member; // ES named import: the exported name the local binds (`get` in
// `import { get as navigate } from "axios"`); NULL otherwise
bool is_default; // ES default import (`import X from "Y"`), JS/TS only (#1916)
bool type_checking_only; // Python: inside a module-level `if TYPE_CHECKING:` block --
// binds the name for annotations, never at runtime (#1277)
CBMImportKind kind; // CBM_IMPORT_KIND_DEFAULT unless the syntax names a member kind
} CBMImport;
/* True when an import binds its local name at runtime. A Python import under
* `if TYPE_CHECKING:` exists for annotations only: a call through that name
* cannot happen, so no CALLS import map may bind it (an import_map edge
* through it would be fabricated by construction). THE one rule every CALLS
* import-map builder applies. */
static inline bool cbm_import_binds_runtime_name(const CBMImport *imp) {
return imp && !imp->type_checking_only;
}
/* Content-only ordered Python namespace facts; no resolved target identities. */
#define CBM_PY_NAMESPACE_FACTS_VERSION 1u
@@ -1306,6 +1317,14 @@ void cbm_py_namespace_test_copy_fail_after(int successful_allocations);
void cbm_defs_push(CBMDefArray *arr, CBMArena *a, CBMDefinition def);
void cbm_calls_push(CBMCallArray *arr, CBMArena *a, CBMCall call);
void cbm_imports_push(CBMImportArray *arr, CBMArena *a, CBMImport imp);
/* True unless every import of `imports` that binds `local_name` is
* annotation-only (cbm_import_binds_runtime_name), so also true when no import
* names it: the rule for the builders that key IMPORTS edges by local name. */
bool cbm_imports_local_binds_runtime_name(const CBMImportArray *imports, const char *local_name);
/* The same for the `len` bytes at `name` (not NUL-terminated): lets a caller
* decide before it copies the name. */
bool cbm_imports_local_span_binds_runtime_name(const CBMImportArray *imports, const char *name,
size_t len);
void cbm_usages_push(CBMUsageArray *arr, CBMArena *a, CBMUsage usage);
void cbm_throws_push(CBMThrowArray *arr, CBMArena *a, CBMThrow thr);
void cbm_rw_push(CBMRWArray *arr, CBMArena *a, CBMReadWrite rw);
+4 -1
View File
@@ -11493,9 +11493,12 @@ static void push_nested_class_nodes(TSNode body, const CBMLangSpec *spec, wd_sta
wd_push(s, child, enclosing_qn);
} else {
const char *ck = ts_node_type(child);
/* A nested class inside #if/#else is still nested here. */
/* A nested class inside #if/#else is still nested here, and so is
* a decorated one (Python `@dataclass class Meta:` in a class
* body): the decorated_definition wraps the class_definition. */
if (strcmp(ck, "field_declaration") == 0 ||
strcmp(ck, "template_declaration") == 0 || strcmp(ck, "declaration") == 0 ||
(ctx->language == CBM_LANG_PYTHON && strcmp(ck, "decorated_definition") == 0) ||
cbm_kind_in_set(child, cbm_conditional_block_types(spec->language))) {
ts_nstack_push(&nc_stack, child);
}
+152 -11
View File
@@ -123,6 +123,19 @@ static const char *python_import_root(CBMArena *a, const char *path) {
return dot ? cbm_arena_strndup(a, path, (size_t)(dot - path)) : path;
}
/* Spell `from <module> import <name>` as one dotted path. A package-relative
* module (`from . import x`, `from .. import x`) is nothing but dots, which
* already end in the separator: joining with another '.' would add a level and
* name the PARENT package ("..x"), so the import resolved one package too high
* and fell to a same-named definition anywhere in the project. */
static const char *python_join_from_import(CBMArena *a, const char *module, const char *name) {
size_t len = strlen(module);
if (len > 0 && module[len - SKIP_ONE] == '.') {
return cbm_arena_sprintf(a, "%s%s", module, name);
}
return cbm_arena_sprintf(a, "%s.%s", module, name);
}
// --- Go imports ---
// import_declaration -> import_spec_list -> import_spec -> (name, path)
@@ -209,7 +222,7 @@ static void emit_py_aliased_import(CBMExtractCtx *ctx, TSNode child, const char
}
const char *local = !ts_node_is_null(alias_node) ? cbm_node_text(a, alias_node, ctx->source)
: path_last(a, name);
const char *full = mod_prefix ? cbm_arena_sprintf(a, "%s.%s", mod_prefix, name) : name;
const char *full = mod_prefix ? python_join_from_import(a, mod_prefix, name) : name;
CBMImport imp = {.local_name = local, .module_path = full};
cbm_imports_push(&ctx->result->imports, a, imp);
}
@@ -267,7 +280,7 @@ static void emit_py_import_from_name(CBMExtractCtx *ctx, TSNode child, const cha
CBMArena *a = ctx->arena;
char *name = cbm_node_text(a, child, ctx->source);
if (name && name[0]) {
const char *full = mod_path ? cbm_arena_sprintf(a, "%s.%s", mod_path, name) : name;
const char *full = mod_path ? python_join_from_import(a, mod_path, name) : name;
CBMImport imp = {.local_name = name, .module_path = full};
cbm_imports_push(&ctx->result->imports, a, imp);
}
@@ -321,6 +334,139 @@ static void process_py_import_from(CBMExtractCtx *ctx, TSNode node) {
}
}
/* `TYPE_CHECKING` or `<module>.TYPE_CHECKING` (typing.TYPE_CHECKING, t.TYPE_CHECKING). */
static bool py_is_type_checking_condition(CBMExtractCtx *ctx, TSNode cond) {
if (ts_node_is_null(cond)) {
return false;
}
TSNode name = cond;
if (strcmp(ts_node_type(cond), "attribute") == 0) {
name = ts_node_child_by_field_name(cond, TS_FIELD("attribute"));
}
if (ts_node_is_null(name) || strcmp(ts_node_type(name), "identifier") != 0) {
return false;
}
const char *text = cbm_node_text(ctx->arena, name, ctx->source);
return text && strcmp(text, "TYPE_CHECKING") == 0;
}
/* One import statement; `type_only` flags every import it records. */
static void process_py_import_node(CBMExtractCtx *ctx, TSNode node, bool type_only) {
const char *kind = ts_node_type(node);
int before = ctx->result->imports.count;
if (strcmp(kind, "import_statement") == 0) {
process_py_import_stmt(ctx, node);
} else if (strcmp(kind, "import_from_statement") == 0 ||
strcmp(kind, "future_import_statement") == 0) {
// `from __future__ import annotations` is a distinct node type in
// tree-sitter-python but has the same shape (module + name list).
process_py_import_from(ctx, node);
}
for (int i = before; type_only && i < ctx->result->imports.count; i++) {
ctx->result->imports.items[i].type_checking_only = true;
}
}
static bool py_is_import_node(TSNode node) {
const char *kind = ts_node_type(node);
return strcmp(kind, "import_statement") == 0 || strcmp(kind, "import_from_statement") == 0 ||
strcmp(kind, "future_import_statement") == 0;
}
static bool py_import_is_wildcard(TSNode import_node) {
uint32_t n = ts_node_child_count(import_node);
for (uint32_t i = 0; i < n; i++) {
if (strcmp(ts_node_type(ts_node_child(import_node, i)), "wildcard_import") == 0) {
return true;
}
}
return false;
}
/* Records, unflagged, every import statement anywhere under `root` (the
* runtime alternative of a TYPE_CHECKING block usually nests them in
* try/except ImportError). An iterative pre-order walk: adversarially deep
* nesting must not exhaust the stack. Returns true when one of them is a star
* import. */
static bool py_record_subtree_imports(CBMExtractCtx *ctx, TSNode root) {
bool star = false;
TSTreeCursor cursor = ts_tree_cursor_new(root);
for (;;) {
TSNode node = ts_tree_cursor_current_node(&cursor);
bool is_import = py_is_import_node(node);
if (is_import) {
process_py_import_node(ctx, node, false);
star = star || py_import_is_wildcard(node);
}
if (!is_import && ts_tree_cursor_goto_first_child(&cursor)) {
continue;
}
while (!ts_tree_cursor_goto_next_sibling(&cursor)) {
if (!ts_tree_cursor_goto_parent(&cursor)) {
ts_tree_cursor_delete(&cursor);
return star;
}
}
}
}
/* Drops the imports [first, mark) whose local name an import at [mark, count)
* binds as well (every one when `star`), then the imports [mark, count). */
static void py_drop_rebound_imports(CBMImportArray *imports, int first, int mark, bool star) {
int keep = first;
for (int i = first; i < mark; i++) {
const CBMImport *imp = &imports->items[i];
bool rebound = star;
for (int j = mark; !rebound && j < imports->count; j++) {
rebound = imp->local_name && imports->items[j].local_name &&
strcmp(imp->local_name, imports->items[j].local_name) == 0;
}
if (!rebound) {
imports->items[keep++] = *imp;
}
}
imports->count = keep;
}
/* A module-level `if TYPE_CHECKING:` holds imports a type checker sees and
* the interpreter never runs (#1277: an annotated field's type is usually
* imported exactly there, to avoid an import cycle). Record the imports of
* its consequence block, flagged type_checking_only. Its elif/else branches
* are runtime alternatives and stay unrecorded, as every other compound
* statement's imports do; a name they bind again (the compat shape: the same
* name imported in `else:` under try/except ImportError) is bound at runtime,
* so its TYPE_CHECKING import is no annotation-only import and is dropped as
* well, and a star import there may bind any name. */
static void process_py_type_checking_block(CBMExtractCtx *ctx, TSNode if_node) {
if (!py_is_type_checking_condition(
ctx, ts_node_child_by_field_name(if_node, TS_FIELD("condition")))) {
return;
}
TSNode body = ts_node_child_by_field_name(if_node, TS_FIELD("consequence"));
if (ts_node_is_null(body)) {
return;
}
int first = ctx->result->imports.count;
TSTreeCursor cursor = ts_tree_cursor_new(body);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
process_py_import_node(ctx, ts_tree_cursor_current_node(&cursor), true);
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
int mark = ctx->result->imports.count;
bool star = false;
uint32_t n = ts_node_child_count(if_node);
for (uint32_t i = 0; i < n; i++) {
TSNode alt = ts_node_child(if_node, i);
const char *kind = ts_node_type(alt);
if (strcmp(kind, "elif_clause") == 0 || strcmp(kind, "else_clause") == 0) {
star = py_record_subtree_imports(ctx, alt) || star;
}
}
py_drop_rebound_imports(&ctx->result->imports, first, mark, star);
}
static void parse_python_imports(CBMExtractCtx *ctx) {
TSTreeCursor cursor = ts_tree_cursor_new(ctx->root);
if (!ts_tree_cursor_goto_first_child(&cursor)) {
@@ -329,15 +475,10 @@ static void parse_python_imports(CBMExtractCtx *ctx) {
}
do {
TSNode node = ts_tree_cursor_current_node(&cursor);
const char *kind = ts_node_type(node);
if (strcmp(kind, "import_statement") == 0) {
process_py_import_stmt(ctx, node);
} else if (strcmp(kind, "import_from_statement") == 0 ||
strcmp(kind, "future_import_statement") == 0) {
// `from __future__ import annotations` is a distinct node type in
// tree-sitter-python but has the same shape (module + name list).
process_py_import_from(ctx, node);
if (strcmp(ts_node_type(node), "if_statement") == 0) {
process_py_type_checking_block(ctx, node);
} else {
process_py_import_node(ctx, node, false);
}
} while (ts_tree_cursor_goto_next_sibling(&cursor));
ts_tree_cursor_delete(&cursor);
+8
View File
@@ -17,8 +17,16 @@ extern _Thread_local uint64_t cbm_lsp_work_steps;
#define CBM_LSP_WORK(n) (cbm_lsp_work_steps += (uint64_t)(n))
/* Read and reset the calling thread's counter. */
uint64_t cbm_lsp_work_take(void);
/* Test-only work ceiling (0 = none). A regression test that reverts a bound
* sets it so the reverted, unbounded loop stops deterministically and the test
* reports RED instead of hanging the runner. Seam builds only: a product build
* has no ceiling, so it can never decide graph content. */
extern _Thread_local uint64_t cbm_lsp_work_ceiling;
#define CBM_LSP_WORK_CEILING_HIT() \
(cbm_lsp_work_ceiling != 0 && cbm_lsp_work_steps > cbm_lsp_work_ceiling)
#else
#define CBM_LSP_WORK(n) ((void)0)
#define CBM_LSP_WORK_CEILING_HIT() (0)
#endif
#endif /* CBM_LSP_WORK_H */
+17
View File
@@ -4372,6 +4372,17 @@ void cbm_run_php_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
}
}
/* Index the registry (F12). Unindexed, every lookup below was a linear
* scan over the stdlib plus every definition of the file: O(lookups x
* definitions), quadratic in the size of one file. First-registered
* answers keep the result of that scan (the first registration of a
* duplicate QN wins), so the output does not change. Index memory goes to
* a scratch arena that dies with this call, as in cbm_run_php_lsp_cross. */
reg.index_first_registered = true;
CBMArena idx_arena;
cbm_arena_init(&idx_arena);
cbm_registry_finalize_into(&reg, &idx_arena);
/* Phase C: run the resolver. */
PHPLSPContext ctx;
php_lsp_init(&ctx, arena, source, source_len, &reg, module_qn, &result->resolved_calls);
@@ -4401,6 +4412,11 @@ void cbm_run_php_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
ctx.use_count = 0;
}
/* Trait flattening registered methods after the index was built; lookups
* would tail-scan them. Re-index once, first-registered as above, so the
* answers equal those of the indexed part followed by the tail scan. */
cbm_registry_finalize_into(&reg, &idx_arena);
php_lsp_process_file(&ctx, root);
if (ctx.debug) {
@@ -4414,6 +4430,7 @@ void cbm_run_php_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
rc->confidence);
}
}
cbm_arena_destroy(&idx_arena);
}
/* ── Cross-file + batch ───────────────────────────────────────── */
+288 -1
View File
@@ -1224,9 +1224,231 @@ static const CBMRegisteredFunc *py_lookup_attribute_depth(PyLSPContext *ctx, con
return NULL;
}
/* ── The C3 method resolution order ──────────────────────────────── */
/* Longest linearization followed. A longer one (or more bases than this) keeps
* the depth-first walk above. */
#define PY_C3_MAX 64
enum { PY_MRO_CACHE_INITIAL_CAP = 64 }; /* power of two (index masking) */
enum { PY_MRO_CACHE_MAX_CAP = 1 << 20 }; /* hard stop for adversarial files */
static uint32_t py_mro_hash(const char *qn) {
uint32_t h = 2166136261u; /* FNV-1a */
for (const unsigned char *p = (const unsigned char *)qn; *p; p++) {
h = (h ^ *p) * 16777619u;
}
return h;
}
static const CBMPyMroCacheEntry *py_mro_cache_find(const PyLSPContext *ctx, const char *qn,
uint32_t h) {
if (!ctx->mro_cache || ctx->mro_cache_cap == 0)
return NULL;
uint32_t mask = (uint32_t)ctx->mro_cache_cap - 1;
uint32_t idx = h & mask;
for (int probed = 0; probed < ctx->mro_cache_cap; probed++) {
const CBMPyMroCacheEntry *e = &ctx->mro_cache[idx];
if (!e->qn)
return NULL;
if (e->hash == h && strcmp(e->qn, qn) == 0)
return e;
idx = (idx + 1) & mask;
}
return NULL;
}
/* Doubles the table (arena allocation: the old table is freed with the
* per-file arena). */
static bool py_mro_cache_grow(PyLSPContext *ctx) {
if (ctx->mro_cache_cap >= PY_MRO_CACHE_MAX_CAP)
return false;
int new_cap = ctx->mro_cache_cap ? ctx->mro_cache_cap * 2 : PY_MRO_CACHE_INITIAL_CAP;
CBMPyMroCacheEntry *entries = (CBMPyMroCacheEntry *)cbm_arena_alloc(
ctx->arena, (size_t)new_cap * sizeof(CBMPyMroCacheEntry));
if (!entries)
return false;
memset(entries, 0, (size_t)new_cap * sizeof(CBMPyMroCacheEntry));
uint32_t mask = (uint32_t)new_cap - 1;
for (int i = 0; i < ctx->mro_cache_cap; i++) {
const CBMPyMroCacheEntry *e = &ctx->mro_cache[i];
if (!e->qn)
continue;
uint32_t idx = e->hash & mask;
while (entries[idx].qn)
idx = (idx + 1) & mask;
entries[idx] = *e;
}
ctx->mro_cache = entries;
ctx->mro_cache_cap = new_cap;
return true;
}
/* Records the order of `qn` (len -1: none). A table that cannot grow only
* costs a recomputation later. */
static void py_mro_cache_put(PyLSPContext *ctx, const char *qn, uint32_t h, const char **mro,
int len) {
if ((ctx->mro_cache_count + 1) * 4 > ctx->mro_cache_cap * 3 && !py_mro_cache_grow(ctx))
return;
const char *key = cbm_arena_strdup(ctx->arena, qn);
if (!key)
return;
uint32_t mask = (uint32_t)ctx->mro_cache_cap - 1;
uint32_t idx = h & mask;
while (ctx->mro_cache[idx].qn)
idx = (idx + 1) & mask;
ctx->mro_cache[idx].qn = key;
ctx->mro_cache[idx].hash = h;
ctx->mro_cache[idx].len = len;
ctx->mro_cache[idx].mro = mro;
ctx->mro_cache_count++;
}
static bool py_c3_in_tail(const char *qn, const char *const *seq, int head, int len) {
for (int k = head + 1; k < len; k++) {
if (strcmp(seq[k], qn) == 0)
return true;
}
return false;
}
/* The C3 head to take next: the first sequence head that is in no sequence's
* tail. NULL when every head is blocked (Python rejects such a class). */
static const char *py_c3_pick(const char **const *seqs, const int *lens, const int *heads,
int nseq) {
for (int s = 0; s < nseq; s++) {
if (heads[s] >= lens[s])
continue;
const char *cand = seqs[s][heads[s]];
bool blocked = false;
for (int t = 0; t < nseq && !blocked; t++) {
blocked = heads[t] < lens[t] && py_c3_in_tail(cand, seqs[t], heads[t], lens[t]);
}
if (!blocked)
return cand;
}
return NULL;
}
/* The C3 merge of `nseq` sequences into mro[1..]: returns the order's length,
* or -1 when there is none or it is longer than PY_C3_MAX. */
static int py_c3_merge(const char **const *seqs, const int *lens, int *heads, int nseq,
const char **mro) {
int n = 1;
for (;;) {
bool pending = false;
for (int s = 0; s < nseq && !pending; s++) {
pending = heads[s] < lens[s];
}
if (!pending)
return n;
const char *pick = py_c3_pick(seqs, lens, heads, nseq);
if (!pick || n >= PY_C3_MAX)
return -1;
mro[n++] = pick;
for (int s = 0; s < nseq; s++) {
if (heads[s] < lens[s] && strcmp(seqs[s][heads[s]], pick) == 0)
heads[s]++;
}
}
}
/* Python's method resolution order of `type_qn`: the class, then the C3 merge
* of its bases' linearizations and of the base list. A common base therefore
* follows every class deriving from it. Computed once per class and file
* (ctx->mro_cache). Sets *out to an arena array and returns its length, or -1
* when there is no C3 order, the hierarchy is deeper than
* CBM_LSP_MAX_LOOKUP_DEPTH, or the order is longer than PY_C3_MAX. A base the
* registry does not know (external, unresolved) linearizes to itself. Only an
* order computed from the top (depth 0) is cached as missing: below it, the
* depth cap may have cut a hierarchy that is fine on its own. */
static int py_c3_linearize(PyLSPContext *ctx, const char *type_qn, const char ***out, int depth) {
uint32_t h = py_mro_hash(type_qn);
const CBMPyMroCacheEntry *hit = py_mro_cache_find(ctx, type_qn, h);
if (hit) {
*out = hit->mro;
return hit->len;
}
int n = -1;
const char **mro = NULL;
const CBMRegisteredType *rt =
depth <= CBM_LSP_MAX_LOOKUP_DEPTH ? cbm_registry_lookup_type(ctx->registry, type_qn) : NULL;
const char **bases = rt ? rt->embedded_types : NULL;
int nb = 0;
while (bases && bases[nb])
nb++;
if (depth > CBM_LSP_MAX_LOOKUP_DEPTH || nb >= PY_C3_MAX) {
n = -1;
} else if (rt && rt->alias_of) {
n = py_c3_linearize(ctx, rt->alias_of, &mro, depth + 1);
} else {
const char ***seqs =
(const char ***)cbm_arena_alloc(ctx->arena, (size_t)(nb + 1) * sizeof(const char **));
int *lens = (int *)cbm_arena_alloc(ctx->arena, (size_t)(nb + 1) * 2 * sizeof(int));
mro = (const char **)cbm_arena_alloc(ctx->arena, PY_C3_MAX * sizeof(const char *));
bool ok = seqs && lens && mro;
int *heads = lens ? lens + nb + 1 : NULL;
for (int b = 0; ok && b < nb; b++) {
lens[b] = py_c3_linearize(ctx, bases[b], &seqs[b], depth + 1);
heads[b] = 0;
ok = lens[b] >= 0;
}
if (ok) {
seqs[nb] = bases;
lens[nb] = nb;
heads[nb] = 0;
mro[0] = cbm_arena_strdup(ctx->arena, type_qn);
n = mro[0] ? py_c3_merge((const char **const *)seqs, lens, heads, nb + 1, mro) : -1;
}
}
if (n >= 0 || depth == 0)
py_mro_cache_put(ctx, type_qn, h, n >= 0 ? mro : NULL, n);
*out = n >= 0 ? mro : NULL;
return n;
}
/* Attribute lookup along the class's C3 order: the class itself, then each
* class of its order that declares the member. A hierarchy without a C3 order
* keeps the depth-first walk. */
static const CBMRegisteredFunc *py_lookup_attribute(PyLSPContext *ctx, const char *type_qn,
const char *member_name) {
return py_lookup_attribute_depth(ctx, type_qn, member_name, 0);
if (!ctx || !type_qn || !member_name)
return NULL;
const CBMRegisteredFunc *f = cbm_registry_lookup_method(ctx->registry, type_qn, member_name);
if (f)
return f;
const char **mro = NULL;
int n = py_c3_linearize(ctx, type_qn, &mro, 0);
if (n < 0)
return py_lookup_attribute_depth(ctx, type_qn, member_name, 0);
/* mro[0] is the class itself, already asked, or an alias's target. */
for (int i = strcmp(mro[0], type_qn) == 0 ? 1 : 0; i < n; i++) {
f = cbm_registry_lookup_method(ctx->registry, mro[i], member_name);
if (f)
return f;
}
return NULL;
}
/* The method super() reaches from inside `class_qn`: the first class AFTER it in
* its C3 order that declares `member` itself. *have_mro is false when the
* hierarchy has no C3 order; the caller then keeps its base-by-base walk. */
static const CBMRegisteredFunc *py_lookup_super_attribute(PyLSPContext *ctx, const char *class_qn,
const char *member, bool *have_mro) {
*have_mro = false;
if (!ctx || !class_qn || !member)
return NULL;
const char **mro = NULL;
int n = py_c3_linearize(ctx, class_qn, &mro, 0);
if (n < 0)
return NULL;
*have_mro = true;
for (int i = 1; i < n; i++) {
const CBMRegisteredFunc *f = cbm_registry_lookup_method(ctx->registry, mro[i], member);
if (f)
return f;
}
return NULL;
}
/* Per-file field overlay: look up a (class_qn, field_name) recorded during resolve
@@ -2964,6 +3186,22 @@ static void py_emit_call_for(PyLSPContext *ctx, TSNode call_node) {
const CBMRegisteredType *enclosing =
cbm_registry_lookup_type(ctx->registry, ctx->enclosing_class_qn);
if (enclosing && enclosing->embedded_types) {
// The proxy walks the enclosing class's C3 order, in which a
// diamond's shared base follows every class deriving from it:
// RadarrCalendarEntity(RadarrEntity, CalendarEntity) reaches
// CalendarEntity before Entity, which the first base reaches
// through its own ancestry. The base-by-base walk below stays for
// a hierarchy without a C3 order and for an undeclared __init__.
bool have_mro = false;
const CBMRegisteredFunc *sf = py_lookup_super_attribute(
ctx, ctx->enclosing_class_qn, attr_name, &have_mro);
if (sf) {
bool is_init = strcmp(attr_name, "__init__") == 0;
py_emit_resolved_call(ctx, sf->qualified_name,
is_init ? "lsp_super_init" : "lsp_super",
is_init ? 0.90f : 0.88f, call_node);
return;
}
for (int i = 0; enclosing->embedded_types[i]; i++) {
// super().__init__() is a constructor delegation:
// lsp_super_init is the MORE SPECIFIC, more accurate
@@ -4278,6 +4516,11 @@ static const CBMType *py_resolve_annotation(PyLSPContext *ctx, const char *ann)
const CBMType *t = cbm_scope_lookup(ctx->current_scope, ann);
if (!cbm_type_is_unknown(t))
return t;
if (ctx->annotation_scope) {
t = cbm_scope_lookup(ctx->annotation_scope, ann);
if (!cbm_type_is_unknown(t))
return t;
}
if (ctx->module_qn) {
const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, ann);
const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, qn);
@@ -5360,6 +5603,27 @@ static void py_replay_import_statement(PyLSPContext *ctx, TSNode stmt, unsigned
}
}
/* The consequence block of a module-level `if TYPE_CHECKING:` /
* `if <module>.TYPE_CHECKING:` statement, else a null node. Mirrors the
* extraction's py_is_type_checking_condition (extract_imports.c). */
static TSNode py_type_checking_body(PyLSPContext *ctx, TSNode stmt) {
TSNode none = {0};
if (strcmp(ts_node_type(stmt), "if_statement") != 0)
return none;
TSNode cond = ts_node_child_by_field_name(stmt, "condition", 9);
if (ts_node_is_null(cond))
return none;
TSNode name = cond;
if (strcmp(ts_node_type(cond), "attribute") == 0)
name = ts_node_child_by_field_name(cond, "attribute", 9);
if (ts_node_is_null(name) || strcmp(ts_node_type(name), "identifier") != 0)
return none;
char *text = py_node_text(ctx, name);
if (!text || strcmp(text, "TYPE_CHECKING") != 0)
return none;
return ts_node_child_by_field_name(stmt, "consequence", 11);
}
static bool py_expression_is_annotation_only_assignment(TSNode statement) {
if (ts_node_named_child_count(statement) != 1)
return false;
@@ -5444,6 +5708,29 @@ void py_lsp_process_file(PyLSPContext *ctx, TSNode root) {
* Invalidate only syntactic binding targets at the control-flow
* join; a later unconditional statement may restore exact proof. */
py_invalidate_possible_bindings(ctx, c, 0);
/* `if TYPE_CHECKING:` holds the imports annotations are written
* against (#1277). Bind them into the annotation-only frame: an
* annotation resolves through them, a runtime call never does (the
* interpreter never runs the block). Extraction recorded them
* flagged type_checking_only, so they reach this walk through the
* LSP import map only, never a CALLS import map. */
TSNode tc_body = py_type_checking_body(ctx, c);
if (!ts_node_is_null(tc_body)) {
if (!ctx->annotation_scope)
ctx->annotation_scope = cbm_scope_push(ctx->arena, NULL);
CBMScope *runtime_scope = ctx->current_scope;
PyKids tk = py_kids(ctx->arena, tc_body);
for (uint32_t t = 0; ctx->annotation_scope && t < tk.n && !ctx->eval_failure; t++) {
TSNode stmt = py_kid(&tk, t);
const char *sk = ts_node_type(stmt);
if (strcmp(sk, "import_statement") == 0 ||
strcmp(sk, "import_from_statement") == 0) {
ctx->current_scope = ctx->annotation_scope;
py_replay_import_statement(ctx, stmt, consumed_imports);
ctx->current_scope = runtime_scope;
}
}
}
}
}
// Pass 2: top-level calls (rare) and nested definitions.
+19
View File
@@ -38,6 +38,16 @@ typedef struct {
const CBMType *result; // full-fidelity result, never NULL in a live entry
} CBMPyTypeCacheEntry;
// A class's C3 method resolution order (py_c3_linearize), computed once per
// class and file. The registry is complete before the walk starts, so an entry
// never goes stale.
typedef struct {
const char *qn; // class QN; NULL marks an empty slot
uint32_t hash; // hash of qn
int len; // entries in mro; -1 = the class has no C3 order
const char **mro; // arena-allocated, mro[0] is the class (or its alias target)
} CBMPyMroCacheEntry;
// PyLSPContext holds state for one Python file's type evaluation.
typedef struct {
CBMArena *arena;
@@ -49,6 +59,10 @@ typedef struct {
* cbm_registry_*_for_update, never into anything behind ->fallback. */
CBMTypeRegistry *registry_head;
CBMScope *current_scope;
// Names a module-level `if TYPE_CHECKING:` imports (#1277): consulted by
// annotation resolution only, after the ordinary scope chain misses, so a
// runtime call through such a name never resolves. NULL when none.
CBMScope *annotation_scope;
// Import map: local_name -> module_qn (arena-allocated, NULL-terminated).
// Built from CBMFileResult.imports.
@@ -109,6 +123,11 @@ typedef struct {
int eval_depth; // evaluator recursion depth (PY_LSP_MAX_EVAL_DEPTH)
uint32_t eval_truncations; // depth-cap cutoff count — gates memo inserts
CBMLSPStatus eval_failure; // sticky: explicit failure makes this walk incomplete
// C3 orders of the classes whose attributes this file looks up: one
// linearization per class, open addressing, arena-allocated.
CBMPyMroCacheEntry *mro_cache;
int mro_cache_count; // occupied slots (kept < 75% of cap)
int mro_cache_cap; // power-of-two capacity
// Pending links of the left-associative chains being evaluated
// iteratively (operator, receiver, subscript, conditional and
// parenthesis chains): one stack shared by nested evaluations.
+1
View File
@@ -5,6 +5,7 @@
#ifdef CBM_ENABLE_TEST_SEAMS
_Thread_local uint64_t cbm_lsp_work_steps = 0;
_Thread_local uint64_t cbm_lsp_work_ceiling = 0;
uint64_t cbm_lsp_work_take(void) {
uint64_t v = cbm_lsp_work_steps;
cbm_lsp_work_steps = 0;
+21 -8
View File
@@ -95,21 +95,24 @@ static const char *itoa_log(int val) {
/* Build per-file import map from cached extraction result or graph buffer edges.
* Returns parallel arrays of (local_name, module_qn) pairs. Caller frees. */
/* Parse "local_name":"value" from JSON properties string. Returns strdup'd key or NULL. */
static char *extract_local_name_from_json(const char *props_json) {
/* The "local_name":"value" of a JSON properties string, as a span of it;
* false when absent or empty. */
static bool local_name_span_from_json(const char *props_json, const char **out, size_t *len) {
if (!props_json) {
return NULL;
return false;
}
const char *start = strstr(props_json, "\"local_name\":\"");
if (!start) {
return NULL;
return false;
}
start += strlen("\"local_name\":\"");
const char *end = strchr(start, '"');
if (!end || end <= start) {
return NULL;
return false;
}
return cbm_strndup(start, end - start);
*out = start;
*len = (size_t)(end - start);
return true;
}
/* The graph node whose QN is cbm_pipeline_fqn_compute(project, rel_path, name),
@@ -141,7 +144,8 @@ static int build_import_map(cbm_pipeline_ctx_t *ctx, const char *rel_path,
for (int i = 0; i < result->imports.count; i++) {
const CBMImport *imp = &result->imports.items[i];
if (!imp->local_name || !imp->local_name[0] || !imp->module_path) {
if (!imp->local_name || !imp->local_name[0] || !imp->module_path ||
!cbm_import_binds_runtime_name(imp)) {
continue;
}
const cbm_gbuf_node_t *target =
@@ -192,7 +196,16 @@ static int build_import_map(cbm_pipeline_ctx_t *ctx, const char *rel_path,
if (!target) {
continue;
}
char *key = extract_local_name_from_json(e->properties_json);
const char *name = NULL;
size_t name_len = 0;
/* An annotation-only import (#1277) binds no CALLS name: skipped before
* the key is copied. */
if (!local_name_span_from_json(e->properties_json, &name, &name_len) ||
!cbm_imports_local_span_binds_runtime_name(result ? &result->imports : NULL, name,
name_len)) {
continue;
}
char *key = cbm_strndup(name, name_len);
if (key) {
keys[count] = key;
vals[count] = target->qualified_name;
+46 -14
View File
@@ -305,10 +305,16 @@ static const char *pxc_python_dotted_base_qn(const pxc_base_scope_t *bs, const c
* Returns NULL when the base is not a confidently-known project type;
* stdlib and third-party bases land here and keep their raw spelling. */
static const char *pxc_resolve_base_qn(const pxc_base_scope_t *bs, const char *raw,
const char *module_qn) {
const char *module_qn, const char *class_qn) {
if (!bs || !bs->reg || !raw || !raw[0]) {
return NULL;
}
/* A base of a class nested in a class body: an earlier sibling first. */
const char *sibling =
cbm_registry_resolve_python_class_body_base(bs->reg, bs->gbuf, class_qn, raw, bs->lang);
if (sibling) {
return sibling;
}
const char *dotted = pxc_python_dotted_base_qn(bs, raw);
if (dotted) {
return dotted;
@@ -338,7 +344,8 @@ static const char *pxc_resolve_base_qn(const pxc_base_scope_t *bs, const char *r
* source spelling. Unresolved entries pass through verbatim so a base the
* registry does not know keeps working exactly as before. */
static const char *pxc_join_base_qns(CBMArena *arena, const char *const *bases,
const pxc_base_scope_t *bs, const char *module_qn) {
const pxc_base_scope_t *bs, const char *module_qn,
const char *class_qn) {
if (!bases || !bases[0]) {
return NULL;
}
@@ -352,7 +359,7 @@ static const char *pxc_join_base_qns(CBMArena *arena, const char *const *bases,
return pxc_join_pipe(arena, bases);
}
for (int i = 0; i < count; i++) {
const char *qn = pxc_resolve_base_qn(bs, bases[i], module_qn);
const char *qn = pxc_resolve_base_qn(bs, bases[i], module_qn, class_qn);
resolved[i] = qn ? qn : bases[i];
}
resolved[count] = NULL;
@@ -520,9 +527,10 @@ static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const ch
/* Languages whose cross registrars read embedded_types as QNs get their
* bases resolved against the project registry; everyone else keeps the
* raw source spelling their own registrar already knows how to handle. */
dst->embedded_types = (bs && bs->reg && pxc_lang_resolves_base_qns(lang))
? pxc_join_base_qns(arena, src->base_classes, bs, module_qn)
: pxc_join_pipe(arena, src->base_classes);
dst->embedded_types =
(bs && bs->reg && pxc_lang_resolves_base_qns(lang))
? pxc_join_base_qns(arena, src->base_classes, bs, module_qn, src->qualified_name)
: pxc_join_pipe(arena, src->base_classes);
dst->signature_param_types = NULL;
dst->signature_param_count = 0;
if (src->signature_param_types && src->signature_param_count > 0) {
@@ -727,7 +735,7 @@ static void pxc_fold_py_field_types(CBMArena *arena, const CBMFileResult *result
fields[f].type_qn = NULL;
CBMLSPDef *owner = ft->class_qn ? (CBMLSPDef *)cbm_ht_get(by_qn, ft->class_qn) : NULL;
const char *name = owner ? pxc_py_annotation_type_name(arena, ft->type_text) : NULL;
const char *qn = name ? pxc_resolve_base_qn(bs, name, module_qn) : NULL;
const char *qn = name ? pxc_resolve_base_qn(bs, name, module_qn, NULL) : NULL;
if (!qn || !ft->field_name || !ft->field_name[0]) {
continue;
}
@@ -876,9 +884,11 @@ CBMLSPDef *cbm_pxc_collect_all_defs(const cbm_pipeline_ctx_t *ctx, CBMArena *are
if (ctx && ctx->registry && pxc_lang_resolves_base_qns(files[fi].language)) {
bs.reg = ctx->registry;
bs.gbuf = ctx->gbuf;
cbm_pxc_build_import_map(ctx->gbuf, project_name, files[fi].rel_path,
files[fi].language, fr, &bs.imp_keys, &bs.imp_vals,
&bs.imp_count);
/* Bases and field annotations resolve through annotation-only
* imports too: `if TYPE_CHECKING:` exists for them (#1277). */
cbm_pxc_build_import_map_ex(ctx->gbuf, project_name, files[fi].rel_path,
files[fi].language, fr, true, &bs.imp_keys, &bs.imp_vals,
&bs.imp_count);
}
for (int di = 0; di < fr->defs.count; di++) {
if (pxc_build_lsp_def(arena, &fr->defs.items[di], def_modules[fi], namespace_name,
@@ -1068,10 +1078,26 @@ static char *pxc_python_import_from_metadata(const cbm_gbuf_t *gbuf, const char
/* Build per-file import map (local_name -> semantic import QN) from gbuf
* IMPORTS edges. Both pipeline drivers call this implementation. Returns 0
* with *out_count = 0 when the file has no IMPORTS edges. */
* with *out_count = 0 when the file has no IMPORTS edges. The CALLS map. */
int cbm_pxc_build_import_map(const cbm_gbuf_t *gbuf, const char *project_name, const char *rel_path,
CBMLanguage lang, const CBMFileResult *result, const char ***out_keys,
const char ***out_vals, int *out_count) {
return cbm_pxc_build_import_map_ex(gbuf, project_name, rel_path, lang, result, false, out_keys,
out_vals, out_count);
}
bool cbm_pxc_result_has_type_only_import(const CBMFileResult *result) {
for (int i = 0; result && i < result->imports.count; i++) {
if (!cbm_import_binds_runtime_name(&result->imports.items[i]))
return true;
}
return false;
}
int cbm_pxc_build_import_map_ex(const cbm_gbuf_t *gbuf, const char *project_name,
const char *rel_path, CBMLanguage lang, const CBMFileResult *result,
bool include_type_only, const char ***out_keys,
const char ***out_vals, int *out_count) {
*out_keys = NULL;
*out_vals = NULL;
*out_count = 0;
@@ -1129,7 +1155,10 @@ int cbm_pxc_build_import_map(const cbm_gbuf_t *gbuf, const char *project_name, c
continue;
memcpy(local, start, n);
local[n] = '\0';
char *value = pxc_import_value_qn(lang, result, local, target);
/* An annotation-only import (#1277) binds no CALLS name: it gets no value. */
bool binds = include_type_only ||
cbm_imports_local_binds_runtime_name(result ? &result->imports : NULL, local);
char *value = binds ? pxc_import_value_qn(lang, result, local, target) : NULL;
if (!value) {
free(local);
continue;
@@ -1146,6 +1175,7 @@ int cbm_pxc_build_import_map(const cbm_gbuf_t *gbuf, const char *project_name, c
for (int i = 0; i < metadata_count; i++) {
const CBMImport *imp = &result->imports.items[i];
if (!imp->local_name || !imp->local_name[0] || !imp->module_path ||
(!include_type_only && !cbm_import_binds_runtime_name(imp)) ||
pxc_import_map_has_local(keys, count, imp->local_name)) {
continue;
}
@@ -2212,8 +2242,10 @@ int cbm_pipeline_pass_lsp_cross(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *
const char **imp_keys = NULL;
const char **imp_vals = NULL;
int imp_count = 0;
cbm_pxc_build_import_map(ctx->gbuf, ctx->project_name, files[i].rel_path, lang, cache[i],
&imp_keys, &imp_vals, &imp_count);
/* The LSP map: annotation-only imports bind for annotations (#1277).
* MUST match the LSP map of pass_parallel.c. */
cbm_pxc_build_import_map_ex(ctx->gbuf, ctx->project_name, files[i].rel_path, lang, cache[i],
true, &imp_keys, &imp_vals, &imp_count);
/* Journal around the resolve: a hang here must be attributed to THIS
* file, not to a stale extraction marker (the innocent-quarantine
+11
View File
@@ -83,6 +83,17 @@ void cbm_pxc_ts_modes(CBMLanguage lang, const char *rel_path, bool *out_js, bool
int cbm_pxc_build_import_map(const cbm_gbuf_t *gbuf, const char *project_name, const char *rel_path,
CBMLanguage lang, const CBMFileResult *result, const char ***out_keys,
const char ***out_vals, int *out_count);
/* As cbm_pxc_build_import_map (which is include_type_only = false, the CALLS
* map), but with include_type_only also binding annotation-only imports
* (Python `if TYPE_CHECKING:`), for the consumers that resolve annotations:
* the Python LSP and the cross-file base/field-type resolution (#1277). */
int cbm_pxc_build_import_map_ex(const cbm_gbuf_t *gbuf, const char *project_name,
const char *rel_path, CBMLanguage lang, const CBMFileResult *result,
bool include_type_only, const char ***out_keys,
const char ***out_vals, int *out_count);
/* True when the file records an annotation-only import (the LSP map then
* differs from the CALLS map and must be built separately). */
bool cbm_pxc_result_has_type_only_import(const CBMFileResult *result);
void cbm_pxc_free_import_map(const char **keys, const char **vals, int count);
+30 -9
View File
@@ -3910,13 +3910,17 @@ static void resolve_def_inherits(resolve_ctx_t *rc, resolve_worker_state_t *ws,
if (cbm_pipeline_php_vendor_bound(php_vendor, def->base_classes[b])) {
continue;
}
const char *bqn =
resolve_as_class(rc->registry, def->base_classes[b], mq, ik, iv, ic, fs->lang);
/* Same external-base gate as the sequential semantic pass. */
if (!bqn ||
cbm_python_external_base_contradicts(fs->lang, fs->imports, def->base_classes[b], bqn,
rc->main_gbuf, rc->project_name, fs->rel)) {
continue;
/* Same order as the sequential semantic pass: an earlier sibling of
* the class body first (no external-base gate), then the registry. */
const char *bqn = cbm_registry_resolve_python_class_body_base(
rc->registry, rc->main_gbuf, def->qualified_name, def->base_classes[b], fs->lang);
if (!bqn) {
bqn = resolve_as_class(rc->registry, def->base_classes[b], mq, ik, iv, ic, fs->lang);
if (!bqn || cbm_python_external_base_contradicts(
fs->lang, fs->imports, def->base_classes[b], bqn, rc->main_gbuf,
rc->project_name, fs->rel)) {
continue;
}
}
const cbm_gbuf_node_t *bn = cbm_gbuf_find_by_qn(rc->main_gbuf, bqn);
if (bn && node->id != bn->id) {
@@ -4370,10 +4374,27 @@ static void resolve_worker(int worker_id, void *ctx_ptr) {
* this file, not to a stale extraction marker. */
cbm_index_mark_start(rel);
bool had_error = result->has_error;
/* The LSP map also binds annotation-only imports (#1277);
* only a file that records one needs its own map, every
* other file passes the CALLS map unchanged. MUST match the
* sequential dispatch in pass_lsp_cross.c. */
const char **lsp_keys = NULL;
const char **lsp_vals = NULL;
int lsp_count = 0;
bool own_lsp_map = cbm_pxc_result_has_type_only_import(result);
if (own_lsp_map) {
cbm_pxc_build_import_map_ex(rc->main_gbuf, rc->project_name, rel, lang, result,
true, &lsp_keys, &lsp_vals, &lsp_count);
}
cbm_pxc_dispatch_file_with_php_bindings(
lang, result, lsp_source, lsp_source_len, rel, def_module, rc->cross_registries,
rc->module_def_index, rc->all_defs, rc->def_count, imp_keys, imp_vals,
imp_count, pp_rust_shared_registry_get, rc, &php_vendor);
rc->module_def_index, rc->all_defs, rc->def_count,
own_lsp_map ? lsp_keys : imp_keys, own_lsp_map ? lsp_vals : imp_vals,
own_lsp_map ? lsp_count : imp_count, pp_rust_shared_registry_get, rc,
&php_vendor);
if (own_lsp_map) {
cbm_pxc_free_import_map(lsp_keys, lsp_vals, lsp_count);
}
cbm_index_mark_done(rel);
if (!had_error && result->has_error && result->error_msg &&
(strcmp(result->error_msg, CBM_C_LSP_MEMO_ERROR) == 0 ||
+12 -6
View File
@@ -643,12 +643,18 @@ static void sem_process_def_edges(cbm_pipeline_ctx_t *ctx, const CBMDefinition *
if (cbm_pipeline_php_vendor_bound(php_vendor, def->base_classes[b])) {
continue;
}
const char *base_qn = resolve_as_class(ctx->registry, def->base_classes[b], module_qn,
imp_keys, imp_vals, imp_count, lang);
if (!base_qn || cbm_python_external_base_contradicts(
fi->language, &result->imports, def->base_classes[b], base_qn,
ctx->gbuf, ctx->project_name, fi->rel_path)) {
continue; /* unresolved, or an external base (`unittest.TestCase`) */
/* An earlier sibling of the class body shadows every module or
* imported name, so it needs no external-base gate. */
const char *base_qn = cbm_registry_resolve_python_class_body_base(
ctx->registry, ctx->gbuf, def->qualified_name, def->base_classes[b], lang);
if (!base_qn) {
base_qn = resolve_as_class(ctx->registry, def->base_classes[b], module_qn, imp_keys,
imp_vals, imp_count, lang);
if (!base_qn || cbm_python_external_base_contradicts(
fi->language, &result->imports, def->base_classes[b], base_qn,
ctx->gbuf, ctx->project_name, fi->rel_path)) {
continue; /* unresolved, or an external base (`unittest.TestCase`) */
}
}
const cbm_gbuf_node_t *base_node = cbm_gbuf_find_by_qn(ctx->gbuf, base_qn);
if (base_node && node->id != base_node->id) {
+8
View File
@@ -270,6 +270,14 @@ cbm_resolution_t cbm_registry_resolve_call_scoped(const cbm_registry_t *r, const
const char **import_map_vals,
int import_map_count, CBMLanguage lang,
const char *scope_qn);
/* Python: the bare base `base` of the class `class_qn` nested in a class body
* binds the sibling class of that body (`<enclosing class>.<base>`) when the
* sibling is defined earlier in the same file (Python evaluates the bases in
* the class body). Exact lookups only; the registry-owned sibling QN, or NULL
* to resolve the base as before. Every base-class resolver asks this first. */
const char *cbm_registry_resolve_python_class_body_base(const cbm_registry_t *r,
const cbm_gbuf_t *gb, const char *class_qn,
const char *base, CBMLanguage lang);
#ifdef CBM_ENABLE_TEST_SEAMS
void cbm_pxc_test_import_probe_fail_after(int successful_allocations);
#endif
+41
View File
@@ -1383,6 +1383,47 @@ const char *cbm_registry_value_target(const cbm_registry_t *r, const char *qn) {
return owned ? owned : qn;
}
/* Python evaluates a class statement's bases in the namespace of the body
* that encloses it. For a class nested in a class, a bare base naming a class
* defined EARLIER in that same class body is that sibling
* (`class IntegrityError(DatabaseError)` in sqlalchemy's asyncpg adapter),
* whatever same-named classes the project has elsewhere. A sibling defined
* later is not bound yet when the base is evaluated. Exact lookups only. */
const char *cbm_registry_resolve_python_class_body_base(const cbm_registry_t *r,
const cbm_gbuf_t *gb, const char *class_qn,
const char *base, CBMLanguage lang) {
if (lang != CBM_LANG_PYTHON || !r || !gb || !class_qn || !base || !base[0] ||
strchr(base, '.')) {
return NULL;
}
const char *dot = strrchr(class_qn, '.');
if (!dot || dot == class_qn) {
return NULL;
}
char sibling[CBM_SZ_1K];
int enclosing_len = (int)(dot - class_qn);
int n = snprintf(sibling, sizeof(sibling), "%.*s", enclosing_len, class_qn);
if (n < 0 || (size_t)n >= sizeof(sibling) ||
!cbm_label_is_type_like(cbm_registry_label_of(r, sibling))) {
return NULL; /* not nested in a class body (module level, or a def) */
}
n = snprintf(sibling, sizeof(sibling), "%.*s.%s", enclosing_len, class_qn, base);
if (n < 0 || (size_t)n >= sizeof(sibling) || strcmp(sibling, class_qn) == 0) {
return NULL;
}
const char *owned = cbm_ht_get_key(r->exact, sibling);
if (!owned || !cbm_label_is_type_like(cbm_registry_label_of(r, owned))) {
return NULL;
}
const cbm_gbuf_node_t *cls = cbm_gbuf_find_by_qn(gb, class_qn);
const cbm_gbuf_node_t *sib = cbm_gbuf_find_by_qn(gb, owned);
if (!cls || !sib || !cls->file_path || !sib->file_path ||
strcmp(cls->file_path, sib->file_path) != 0 || sib->start_line >= cls->start_line) {
return NULL;
}
return owned;
}
int cbm_registry_find_by_name(const cbm_registry_t *r, const char *name, const char ***out,
int *count) {
if (!r || !out || !count) {
+121
View File
@@ -1506,6 +1506,82 @@ TEST(python_annotated_instance_fields_exported_issue1277) {
PASS();
}
/* #1277: the import an annotated field's type comes from usually sits under
* `if TYPE_CHECKING:`. It is recorded, flagged annotation-only; a runtime
* import is not flagged; an else branch (a runtime alternative) and an
* ordinary `if` stay unrecorded as before. */
static const CBMImport *py_find_import(const CBMFileResult *r, const char *local) {
for (int i = 0; i < r->imports.count; i++) {
if (r->imports.items[i].local_name && strcmp(r->imports.items[i].local_name, local) == 0)
return &r->imports.items[i];
}
return NULL;
}
TEST(python_type_checking_imports_flagged_issue1277) {
const char *src = "from typing import TYPE_CHECKING\n"
"import typing\n"
"if TYPE_CHECKING:\n"
" from trainer import Trainer\n"
" import contracts\n"
"else:\n"
" from fallback import Runtime\n"
"if typing.TYPE_CHECKING:\n"
" from engine import Engine as Eng\n"
"if FLAG:\n"
" from other import Conditional\n";
CBMFileResult *r = extract(src, CBM_LANG_PYTHON, "t", "loop.py");
ASSERT_NOT_NULL(r);
const CBMImport *tc = py_find_import(r, "TYPE_CHECKING");
const CBMImport *trainer = py_find_import(r, "Trainer");
const CBMImport *contracts = py_find_import(r, "contracts");
const CBMImport *eng = py_find_import(r, "Eng");
ASSERT_NOT_NULL(tc);
ASSERT_FALSE(tc->type_checking_only);
ASSERT_NOT_NULL(trainer);
ASSERT_TRUE(trainer->type_checking_only);
ASSERT_NOT_NULL(contracts);
ASSERT_TRUE(contracts->type_checking_only);
ASSERT_NOT_NULL(eng);
ASSERT_TRUE(eng->type_checking_only);
ASSERT_NULL(py_find_import(r, "Runtime"));
ASSERT_NULL(py_find_import(r, "Conditional"));
cbm_free_result(r);
PASS();
}
/* A name the alternative of an `if TYPE_CHECKING:` binds again (the airflow
* compat shape, usually inside try/except ImportError) is bound at runtime, so
* its TYPE_CHECKING import is not annotation-only: it stays unrecorded, as the
* runtime import itself does. A star import in an alternative may bind any
* name, so it covers every name of the block. */
TEST(python_type_checking_rebound_in_else_unrecorded_issue1277) {
const char *src = "from typing import TYPE_CHECKING\n"
"if TYPE_CHECKING:\n"
" from airflow.sdk.execution_time.secrets_masker import mask_secret\n"
" from airflow.sdk.types import OnlyTyped\n"
"else:\n"
" try:\n"
" from airflow.sdk.log import mask_secret\n"
" except ImportError:\n"
" mask_secret = None\n"
"if TYPE_CHECKING:\n"
" from compat.types import Starred\n"
"elif SOMETHING:\n"
" from compat import *\n";
CBMFileResult *r = extract(src, CBM_LANG_PYTHON, "t", "hook.py");
ASSERT_NOT_NULL(r);
const CBMImport *typed = py_find_import(r, "OnlyTyped");
ASSERT_NOT_NULL(typed);
ASSERT_TRUE(typed->type_checking_only); /* only the type checker binds it */
ASSERT_NULL(py_find_import(r, "mask_secret")); /* the else branch binds it at runtime */
ASSERT_NULL(py_find_import(r, "Starred")); /* the elif star import may bind it */
ASSERT_NULL(py_find_import(r, "compat")); /* alternatives stay unrecorded */
ASSERT_NOT_NULL(py_find_import(r, "TYPE_CHECKING"));
cbm_free_result(r);
PASS();
}
TEST(python_function) {
CBMFileResult *r = extract(
"def greet(name):\n return f\"Hello {name}\"\n\ndef main():\n greet(\"World\")\n",
@@ -3790,6 +3866,48 @@ TEST(python_imports) {
PASS();
}
/* The module_path recorded for a local name, or NULL. */
static const char *import_path_of(CBMFileResult *r, const char *local) {
for (int i = 0; i < r->imports.count; i++) {
if (r->imports.items[i].local_name && strcmp(r->imports.items[i].local_name, local) == 0)
return r->imports.items[i].module_path;
}
return NULL;
}
/* `from . import x` names the importing file's OWN package: the path is ".x",
* as `from .m import f` is ".m.f". Joining the all-dots module with another '.'
* recorded "..x" (the parent package), so the import resolved one package too
* high and fell to an arbitrary same-named definition (home-assistant: 7,175
* calls bound to another integration's function). */
TEST(python_package_relative_import_paths) {
CBMFileResult *r = extract("from . import async_register\n"
"from .. import shared\n"
"from . import local_fn as lf\n"
"from .helpers import helper_fn\n"
"from pkg.db import get_row\n",
CBM_LANG_PYTHON, "t", "comp/web/sub/mod.py");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
const char *own = import_path_of(r, "async_register");
const char *parent = import_path_of(r, "shared");
const char *aliased = import_path_of(r, "lf");
const char *member = import_path_of(r, "helper_fn");
const char *absolute = import_path_of(r, "get_row");
ASSERT_NOT_NULL(own);
ASSERT_NOT_NULL(parent);
ASSERT_NOT_NULL(aliased);
ASSERT_NOT_NULL(member);
ASSERT_NOT_NULL(absolute);
ASSERT_STR_EQ(own, ".async_register");
ASSERT_STR_EQ(parent, "..shared");
ASSERT_STR_EQ(aliased, ".local_fn");
ASSERT_STR_EQ(member, ".helpers.helper_fn"); /* control: unchanged */
ASSERT_STR_EQ(absolute, "pkg.db.get_row"); /* control: unchanged */
cbm_free_result(r);
PASS();
}
TEST(js_imports) {
CBMFileResult *r = extract("import React from 'react';\nimport { useState } from "
"'react';\nconst fs = require('fs');\n",
@@ -12683,6 +12801,8 @@ SUITE(extraction) {
RUN_TEST(python_function);
RUN_TEST(python_class);
RUN_TEST(python_annotated_instance_fields_exported_issue1277);
RUN_TEST(python_type_checking_imports_flagged_issue1277);
RUN_TEST(python_type_checking_rebound_in_else_unrecorded_issue1277);
RUN_TEST(js_function);
RUN_TEST(js_class);
RUN_TEST(ts_function);
@@ -12847,6 +12967,7 @@ SUITE(extraction) {
RUN_TEST(python_iris_invokeClassMethod);
RUN_TEST(go_calls);
RUN_TEST(python_imports);
RUN_TEST(python_package_relative_import_paths);
RUN_TEST(js_imports);
RUN_TEST(go_imports);
RUN_TEST(go_cgo_pseudo_import_dropped);
+77
View File
@@ -19,6 +19,9 @@
#include "cbm.h"
#include "../src/pipeline/lsp_resolve.h"
#include "lsp/php_lsp.h"
#include "lsp/lsp_work.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ── Helpers ───────────────────────────────────────────────────── */
@@ -625,6 +628,79 @@ TEST(phplsp_class_aliased_trait_name_collision_terminates) {
PASS();
}
/* ── 23e. #765 / #1265: trait flattening work is counted, bounded, linear ──
*
* The guards above prove only that extraction returns; a regression would hang
* the runner instead of failing it. These tests count the LSP work
* (CBM_LSP_WORK, which flatten_trait_into_class adds to once per registry
* function it visits) under a test-only ceiling (lsp_work.h): if a flatten
* bound is reverted, the runaway loop stops at the ceiling and the test is RED
* deterministically. No clock is read (O9). */
enum { PHPLSP_FLATTEN_CEILING = 50000000 };
static uint64_t phplsp_counted_work(const char *src, int *ceiling_hit, CBMFileResult **out) {
(void)cbm_lsp_work_take();
cbm_lsp_work_ceiling = PHPLSP_FLATTEN_CEILING;
CBMFileResult *r = extract_php(src);
uint64_t work = cbm_lsp_work_take();
cbm_lsp_work_ceiling = 0;
*ceiling_hit = work > PHPLSP_FLATTEN_CEILING;
if (out) {
*out = r;
} else if (r) {
cbm_free_result(r);
}
return work;
}
/* F12: the single-file PHP LSP (cbm_run_php_lsp) never indexed its registry,
* so every type and method lookup was a linear scan over the stdlib plus every
* definition of the file. Each class here looks up the previous class and its
* method once: twice the classes must cost about twice the counted work; the
* linear scans drift toward four. */
static char *phplsp_chain_classes_source(int n) {
size_t sz = (size_t)n * 96 + 256;
char *src = malloc(sz);
if (!src) {
return NULL;
}
size_t pos = (size_t)snprintf(src, sz,
"<?php\nnamespace App\\Chain;\n\n"
"class C0 { public function m(): int { return 0; } }\n");
for (int i = 1; i < n; i++) {
pos += (size_t)snprintf(src + pos, sz - pos,
"class C%d { public function m(): int { return (new C%d())->m(); } }\n",
i, i - 1);
}
return src;
}
TEST(phplsp_single_file_lookup_work_is_linear_f12) {
enum { N1 = 1000, N2 = 2000 };
char *s1 = phplsp_chain_classes_source(N1);
char *s2 = phplsp_chain_classes_source(N2);
ASSERT_NOT_NULL(s1);
ASSERT_NOT_NULL(s2);
int hit1 = 0;
int hit2 = 0;
CBMFileResult *r2 = NULL;
uint64_t w1 = phplsp_counted_work(s1, &hit1, NULL);
uint64_t w2 = phplsp_counted_work(s2, &hit2, &r2);
free(s1);
free(s2);
printf(" chained classes %d -> %d: work %llu -> %llu (ratio %.2f)\n", N1, N2,
(unsigned long long)w1, (unsigned long long)w2, w1 ? (double)w2 / (double)w1 : 0.0);
ASSERT_NOT_NULL(r2);
ASSERT_FALSE(hit1);
ASSERT_FALSE(hit2);
ASSERT(w1 > 0);
/* The last class's call still resolves to the previous class's method. */
ASSERT(find_resolved(r2, "C1999.m", "C1998.m") >= 0);
cbm_free_result(r2);
ASSERT(w2 * 10 <= w1 * 25); /* ratio <= 2.5 */
PASS();
}
/* ── 24. Match expression result type ──────────────────────────── */
TEST(phplsp_match_result_type) {
@@ -5671,6 +5747,7 @@ SUITE(php_lsp) {
RUN_TEST(phplsp_trait_self_use_terminates);
RUN_TEST(phplsp_trait_aliased_import_name_collision_terminates);
RUN_TEST(phplsp_class_aliased_trait_name_collision_terminates);
RUN_TEST(phplsp_single_file_lookup_work_is_linear_f12);
RUN_TEST(phplsp_match_result_type);
RUN_TEST(phplsp_ternary_result_type);
RUN_TEST(phplsp_method_chain_depth_three);
+821 -1
View File
@@ -10618,6 +10618,258 @@ TEST(pipeline_python_bare_local_binding_parallel_suppresses_weak_edge) {
PASS();
}
/* CALLS edges from a caller to a callee, both matched by QN suffix (start each
* suffix with '.' to anchor it at a segment boundary), any strategy. *guessed
* is set when a matching edge came from a name guess rather than an import
* binding or the LSP. */
static int py_calls_between(cbm_store_t *s, const char *project, const char *caller_suffix,
const char *callee_suffix, bool *guessed) {
cbm_edge_t *edges = NULL;
int edge_count = 0;
cbm_store_find_edges_by_type(s, project, "CALLS", &edges, &edge_count);
int hits = 0;
size_t cs = strlen(caller_suffix);
size_t ts = strlen(callee_suffix);
for (int i = 0; i < edge_count; i++) {
cbm_node_t src = {0};
cbm_node_t tgt = {0};
if (cbm_store_find_node_by_id(s, edges[i].source_id, &src) == CBM_STORE_OK &&
cbm_store_find_node_by_id(s, edges[i].target_id, &tgt) == CBM_STORE_OK) {
size_t sl = strlen(src.qualified_name);
size_t tl = strlen(tgt.qualified_name);
if (sl >= cs && tl >= ts && strcmp(src.qualified_name + sl - cs, caller_suffix) == 0 &&
strcmp(tgt.qualified_name + tl - ts, callee_suffix) == 0) {
hits++;
const char *pj = edges[i].properties_json ? edges[i].properties_json : "";
if (guessed && !strstr(pj, "\"import_map\"") && !strstr(pj, "\"lsp")) {
*guessed = true;
}
}
}
cbm_node_free_fields(&src);
cbm_node_free_fields(&tgt);
}
if (edges)
cbm_store_free_edges(edges, edge_count);
return hits;
}
/* Pin CBM_WORKERS=4 for a padded (> 50 files) parallel run; restore after. */
typedef struct {
char *saved;
bool had;
int rc;
} py_workers_pin_t;
static py_workers_pin_t py_pin_workers(bool parallel) {
py_workers_pin_t pin = {0};
if (!parallel) {
return pin;
}
const char *old = getenv("CBM_WORKERS");
pin.had = old != NULL;
pin.saved = old ? strdup(old) : NULL;
pin.rc = (old && !pin.saved) ? -1 : cbm_setenv("CBM_WORKERS", "4", 1);
return pin;
}
static int py_unpin_workers(bool parallel, py_workers_pin_t *pin) {
if (!parallel) {
return 0;
}
int rc = pin->had ? cbm_setenv("CBM_WORKERS", pin->saved, 1) : cbm_unsetenv("CBM_WORKERS");
free(pin->saved);
pin->saved = NULL;
return rc;
}
static void py_write_padding(const char *tmp, const char *stem, bool parallel) {
for (int i = 0; parallel && i < 52; i++) {
char name[64];
char body[96];
snprintf(name, sizeof(name), "%s%d.py", stem, i);
snprintf(body, sizeof(body), "def %s%d():\n return %d\n", stem, i, i);
write_temp_file(tmp, name, body);
}
}
/* `from . import x`, `from .. import x` and `from . import x as y` name the
* importing file's own (or parent) package. The extractor recorded them one
* package too high ("..x"), so the import fell to pass_pkgmap's symbol-name
* guess -- the lexicographically first same-named project definition -- and
* every call became a confident edge into comp.aaa, through the import map and
* the Python LSP's alias binding alike (home-assistant: 7,175 call pairs).
* Both resolvers: <= 50 files sequential, padded > 50 parallel. */
static int check_python_package_relative_imports(bool parallel) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_relpkg_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "comp/__init__.py", "");
write_temp_file(tmp, "comp/aaa/__init__.py",
"def async_register(hass):\n"
" return 1\n\n"
"def local_fn():\n"
" return 2\n");
write_temp_file(tmp, "comp/web/__init__.py",
"def async_register(hass):\n"
" return 3\n");
write_temp_file(tmp, "comp/web/trigger.py",
"from . import async_register\n\n"
"def attach(hass):\n"
" return async_register(hass)\n");
write_temp_file(tmp, "comp/web/sub/__init__.py",
"def local_fn():\n"
" return 4\n");
write_temp_file(tmp, "comp/web/sub/mod.py",
"from .. import async_register\n"
"from . import local_fn as lf\n\n"
"def deep(hass):\n"
" async_register(hass)\n"
" return lf()\n");
py_write_padding(tmp, "relpad", parallel);
py_workers_pin_t pin = py_pin_workers(parallel);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/relpkg.db", tmp);
cbm_pipeline_t *p = pin.rc == 0 ? cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL) : NULL;
int run_rc = p ? cbm_pipeline_run(p) : -1;
cbm_store_t *s = run_rc == 0 ? cbm_store_open_path(db_path) : NULL;
bool opened = s != NULL;
bool guessed = false;
int own = -1, own_decoy = -1, parent = -1, parent_decoy = -1, alias = -1, alias_decoy = -1;
if (s) {
const char *project = cbm_pipeline_project_name(p);
own = py_calls_between(s, project, ".comp.web.trigger.attach", ".comp.web.async_register",
&guessed);
own_decoy = py_calls_between(s, project, ".comp.web.trigger.attach",
".comp.aaa.async_register", NULL);
parent = py_calls_between(s, project, ".comp.web.sub.mod.deep", ".comp.web.async_register",
&guessed);
parent_decoy = py_calls_between(s, project, ".comp.web.sub.mod.deep",
".comp.aaa.async_register", NULL);
alias = py_calls_between(s, project, ".comp.web.sub.mod.deep", ".comp.web.sub.local_fn",
&guessed);
alias_decoy =
py_calls_between(s, project, ".comp.web.sub.mod.deep", ".comp.aaa.local_fn", NULL);
cbm_store_close(s);
}
cbm_pipeline_free(p);
int unpin_rc = py_unpin_workers(parallel, &pin);
th_rmtree(tmp);
ASSERT_EQ(pin.rc, 0);
ASSERT_EQ(unpin_rc, 0);
ASSERT_EQ(run_rc, 0);
ASSERT_TRUE(opened);
ASSERT_EQ(own, 1); /* from . import x: the file's own package */
ASSERT_EQ(own_decoy, 0); /* never the lexicographically first namesake */
ASSERT_EQ(parent, 1); /* from .. import x: the parent package */
ASSERT_EQ(parent_decoy, 0);
ASSERT_EQ(alias, 1); /* from . import x as y */
ASSERT_EQ(alias_decoy, 0);
ASSERT_FALSE(guessed); /* bound by the import, not by a name guess */
PASS();
}
TEST(pipeline_python_package_relative_imports) {
return check_python_package_relative_imports(false);
}
TEST(pipeline_python_package_relative_imports_parallel) {
return check_python_package_relative_imports(true);
}
/* #1237: a plain `from pkg.db import get_row_or_none` and bare calls resolve to
* the imported module-level def (function and method callers, plus the
* same-file caller), never to a same-named def elsewhere that a name guess
* would rank first (legacy.compat), and a re-index that only ADDS an unrelated
* file (the reporter's watcher re-index after writing benchmark scripts into the
* indexed tree) keeps exactly the same CALLS. Main already resolved this shape
* when the guard landed: it pins the shape and has no RED on revert. */
static int check_python_from_import_bare_call_issue1237(bool parallel) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_1237_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "pkg/__init__.py", "");
write_temp_file(tmp, "pkg/db.py",
"def get_row_or_none(model, **filters):\n"
" return None\n\n"
"def other_helper():\n"
" return get_row_or_none(1, id=2)\n");
write_temp_file(tmp, "pkg/views.py",
"from pkg.db import get_row_or_none\n\n"
"def show(pk):\n"
" return get_row_or_none(object, id=pk)\n\n"
"class Detail:\n"
" def get(self, pk):\n"
" return get_row_or_none(object, pk=pk)\n");
write_temp_file(tmp, "legacy/__init__.py", "");
write_temp_file(tmp, "legacy/compat.py",
"def get_row_or_none(model):\n"
" return model\n");
py_write_padding(tmp, "rowpad", parallel);
py_workers_pin_t pin = py_pin_workers(parallel);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/rows.db", tmp);
int run_rc[2] = {-1, -1};
int show[2] = {-1, -1};
int method[2] = {-1, -1};
int same[2] = {-1, -1};
int decoy[2] = {-1, -1};
int calls[2] = {-1, -1};
bool guessed = false;
for (int run = 0; run < 2 && pin.rc == 0; run++) {
if (run == 1) {
/* Second run on the same db: routes to the incremental re-index. */
write_temp_file(tmp, "bench_rows.py", "def bench():\n return 0\n");
}
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
run_rc[run] = p ? cbm_pipeline_run(p) : -1;
cbm_store_t *s = run_rc[run] == 0 ? cbm_store_open_path(db_path) : NULL;
if (s) {
const char *project = cbm_pipeline_project_name(p);
const char *target = ".pkg.db.get_row_or_none";
const char *namesake = ".legacy.compat.get_row_or_none";
show[run] = py_calls_between(s, project, ".pkg.views.show", target, &guessed);
method[run] = py_calls_between(s, project, ".pkg.views.Detail.get", target, &guessed);
same[run] = py_calls_between(s, project, ".pkg.db.other_helper", target, &guessed);
decoy[run] = py_calls_between(s, project, ".pkg.views.show", namesake, NULL) +
py_calls_between(s, project, ".pkg.views.Detail.get", namesake, NULL);
calls[run] = cbm_store_count_edges_by_type(s, project, "CALLS");
cbm_store_close(s);
}
cbm_pipeline_free(p);
}
int unpin_rc = py_unpin_workers(parallel, &pin);
th_rmtree(tmp);
ASSERT_EQ(pin.rc, 0);
ASSERT_EQ(unpin_rc, 0);
for (int run = 0; run < 2; run++) {
ASSERT_EQ(run_rc[run], 0);
ASSERT_EQ(show[run], 1); /* cross-file bare call through a plain from-import */
ASSERT_EQ(method[run], 1); /* the same from a method */
ASSERT_EQ(same[run], 1); /* same-file bare call */
ASSERT_EQ(decoy[run], 0); /* never the namesake a name guess would pick */
}
ASSERT_EQ(calls[1], calls[0]); /* adding a call-free file moves no CALLS edge */
ASSERT_FALSE(guessed);
PASS();
}
TEST(pipeline_python_from_import_bare_call_issue1237) {
return check_python_from_import_bare_call_issue1237(false);
}
TEST(pipeline_python_from_import_bare_call_issue1237_parallel) {
return check_python_from_import_bare_call_issue1237(true);
}
/* Reproduce-first: pass_parallel's fused cross-LSP eligibility currently counts
* only parser-backed calls/call references. A Python binary operator has no
* parser CBMCall; its __add__ semantic record and carrier are created together
@@ -13754,8 +14006,17 @@ TEST(pipeline_python_init_import_probe_failure_falls_back) {
/* #1277: typed instance fields of a Python class imported from another file.
* Counts CALLS edges caller -> callee (QN suffixes) whose properties name an
* LSP strategy. */
static int count_calls_with(cbm_store_t *s, const char *project, const char *caller_suffix,
const char *callee_suffix, const char *fragment);
static int count_lsp_calls(cbm_store_t *s, const char *project, const char *caller_suffix,
const char *callee_suffix) {
return count_calls_with(s, project, caller_suffix, callee_suffix, "lsp");
}
/* As count_lsp_calls, with any properties fragment (NULL = every CALLS edge). */
static int count_calls_with(cbm_store_t *s, const char *project, const char *caller_suffix,
const char *callee_suffix, const char *fragment) {
cbm_edge_t *edges = NULL;
int edge_count = 0;
cbm_store_find_edges_by_type(s, project, "CALLS", &edges, &edge_count);
@@ -13771,7 +14032,8 @@ static int count_lsp_calls(cbm_store_t *s, const char *project, const char *call
size_t ts = strlen(callee_suffix);
if (sl >= cs && tl >= ts && strcmp(src.qualified_name + sl - cs, caller_suffix) == 0 &&
strcmp(tgt.qualified_name + tl - ts, callee_suffix) == 0 &&
edges[i].properties_json && strstr(edges[i].properties_json, "lsp")) {
(!fragment ||
(edges[i].properties_json && strstr(edges[i].properties_json, fragment)))) {
hits++;
}
}
@@ -13890,6 +14152,110 @@ TEST(python_crossfile_typed_field_calls_issue1277_parallel) {
return check_python_crossfile_typed_field_calls_issue1277(true);
}
/* #1277, second half: the reporter's chain with both annotation imports
* under `if TYPE_CHECKING:` (the usual shape when the import would cycle).
* trainer.strategies is typed through a TYPE_CHECKING import of Contract, and
* run()'s parameter through a typing.TYPE_CHECKING import of Trainer; the call
* resolves by the LSP exactly as with runtime imports. A runtime call through
* a TYPE_CHECKING-only name cannot happen: it gets no CALLS edge at all. */
static int check_python_type_checking_typed_field_issue1277(bool parallel) {
const char *contracts = "class Contract:\n"
" def process_batch(self) -> None:\n"
" ...\n";
const char *trainer = "from __future__ import annotations\n"
"from typing import TYPE_CHECKING\n\n"
"if TYPE_CHECKING:\n"
" from contracts import Contract\n\n"
"class Trainer:\n"
" def __init__(self, strategies: Contract) -> None:\n"
" self.strategies = strategies\n";
const char *loop = "from __future__ import annotations\n"
"import typing\n\n"
"if typing.TYPE_CHECKING:\n"
" from trainer import Trainer\n\n"
"def run(trainer: Trainer) -> None:\n"
" trainer.strategies.process_batch()\n\n"
"def bad():\n"
" return Trainer()\n";
if (setup_usages_repo("contracts.py", contracts, "trainer.py", trainer) != 0) {
FAIL("failed to create temp dir");
}
char path[512];
snprintf(path, sizeof(path), "%s/loop.py", g_usages_tmpdir);
if (th_write_file(path, loop) != 0) {
teardown_usages_repo();
FAIL("failed to write loop.py");
}
if (parallel) {
for (int i = 0; i < 50; i++) {
snprintf(path, sizeof(path), "%s/tc_pad_%02d.py", g_usages_tmpdir, i);
if (th_write_file(path, "tc_padding = 0\n") != 0) {
teardown_usages_repo();
FAIL("failed to write parallel-selection fixture");
}
}
}
const char *old_workers = getenv("CBM_WORKERS");
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
char *saved_single = old_single ? strdup(old_single) : NULL;
if ((old_workers && !saved_workers) || (old_single && !saved_single)) {
free(saved_workers);
free(saved_single);
teardown_usages_repo();
FAIL("failed to save pipeline environment");
}
int pin_workers_rc = parallel ? cbm_setenv("CBM_WORKERS", "4", 1) : 0;
int pin_single_rc = parallel ? cbm_unsetenv("CBM_INDEX_SINGLE_THREAD") : 0;
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/test_1277_tc.db", g_usages_tmpdir);
cbm_pipeline_t *p = pin_workers_rc == 0 && pin_single_rc == 0
? cbm_pipeline_new(g_usages_tmpdir, db_path, CBM_MODE_FULL)
: NULL;
int run_rc = p ? cbm_pipeline_run(p) : -1;
cbm_store_t *s = run_rc == 0 ? cbm_store_open_path(db_path) : NULL;
bool store_opened = s != NULL;
int via_field = -1, runtime_import = -1, runtime_lsp = -1;
if (s) {
const char *project = cbm_pipeline_project_name(p);
via_field = count_lsp_calls(s, project, "loop.run", "contracts.Contract.process_batch");
runtime_import = count_calls_with(s, project, "loop.bad", "trainer.Trainer",
"\"strategy\":\"import_map");
runtime_lsp = count_lsp_calls(s, project, "loop.bad", "trainer.Trainer");
cbm_store_close(s);
}
cbm_pipeline_free(p);
int restore_workers_rc =
saved_workers ? cbm_setenv("CBM_WORKERS", saved_workers, 1) : cbm_unsetenv("CBM_WORKERS");
int restore_single_rc = saved_single ? cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1)
: cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
free(saved_workers);
free(saved_single);
teardown_usages_repo();
ASSERT_EQ(pin_workers_rc, 0);
ASSERT_EQ(pin_single_rc, 0);
ASSERT_EQ(restore_workers_rc, 0);
ASSERT_EQ(restore_single_rc, 0);
ASSERT_EQ(run_rc, 0);
ASSERT_TRUE(store_opened);
ASSERT_EQ(via_field, 1); /* annotation through TYPE_CHECKING imports */
ASSERT_EQ(runtime_import, 0); /* control: no CALLS import map binds the name */
ASSERT_EQ(runtime_lsp, 0); /* control: the LSP binds it for annotations only */
PASS();
}
TEST(python_type_checking_typed_field_issue1277) {
return check_python_type_checking_typed_field_issue1277(false);
}
TEST(python_type_checking_typed_field_issue1277_parallel) {
return check_python_type_checking_typed_field_issue1277(true);
}
/* A base class spelled through a root-binding import (`import pkg.mod.nodes`
* binds `pkg`; `class X(pkg.mod.nodes.Context)`) is the same class as the
* from-imported spelling. The Python LSP's inheritance view never resolved the
@@ -15211,6 +15577,200 @@ TEST(pipeline_python_local_def_shadows_module_def_parallel_issue1548) {
return pyn_run_shadowing(52);
}
/* ── super() follows the C3 method resolution order ──────────────── */
/* The home-assistant radarr shape: RadarrCalendarEntity(RadarrEntity,
* CalendarEntity), where RadarrEntity reaches the common base Entity through
* CoordinatorEntity and CalendarEntity overrides Entity's method. Python's C3
* order is [RadarrCalendarEntity, RadarrEntity, CoordinatorEntity,
* CalendarEntity, Entity]: a common base comes after every class that
* derives from it, so super() reaches CalendarEntity, not Entity through the
* first base's ancestry. */
static const char pym_base_py[] = "class Entity:\n"
" def __init__(self):\n"
" self.ready = False\n"
"\n"
" def write_state(self):\n"
" return 0\n"
"\n"
"class CoordinatorEntity(Entity):\n"
" pass\n"
"\n"
"class CalendarEntity(Entity):\n"
" def __init__(self):\n"
" self.events = []\n"
"\n"
" def write_state(self):\n"
" return 1\n";
static const char pym_radarr_py[] = "from base import CoordinatorEntity, CalendarEntity\n"
"\n"
"class RadarrEntity(CoordinatorEntity):\n"
" pass\n"
"\n"
"class RadarrCalendarEntity(RadarrEntity, CalendarEntity):\n"
" def __init__(self):\n"
" super().__init__()\n"
"\n"
" def write_state(self):\n"
" return super().write_state()\n"
"\n"
"class PlainCalendar(CalendarEntity):\n"
" def write_state(self):\n"
" return super().write_state()\n";
static int pym_run_super_c3(int fillers) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_superc3_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "base.py", pym_base_py);
write_temp_file(tmp, "radarr.py", pym_radarr_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/superc3.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
int rc = p ? cbm_pipeline_run(p) : -1;
cbm_store_t *s = rc == 0 ? cbm_store_open_path(db_path) : NULL;
const char *pr = p ? cbm_pipeline_project_name(p) : NULL;
const char *rce = "radarr.RadarrCalendarEntity";
char src[256];
snprintf(src, sizeof(src), "%s.write_state", rce);
bool m_c3 = s && pyn_edge(s, pr, src, "base.CalendarEntity.write_state", "CALLS");
bool m_dfs = s && pyn_edge(s, pr, src, "base.Entity.write_state", "CALLS");
snprintf(src, sizeof(src), "%s.__init__", rce);
bool i_c3 = s && pyn_edge(s, pr, src, "base.CalendarEntity.__init__", "CALLS");
bool i_dfs = s && pyn_edge(s, pr, src, "base.Entity.__init__", "CALLS");
bool plain = s && pyn_edge(s, pr, "radarr.PlainCalendar.write_state",
"base.CalendarEntity.write_state", "CALLS");
if (s) {
cbm_store_close(s);
}
if (p) {
cbm_pipeline_free(p);
}
if (saved) {
cbm_setenv("CBM_WORKERS", saved, 1);
free(saved);
} else {
cbm_unsetenv("CBM_WORKERS");
}
th_rmtree(tmp);
ASSERT_EQ(rc, 0);
ASSERT_TRUE(m_c3); /* super().write_state(): CalendarEntity comes before Entity */
ASSERT_FALSE(m_dfs); /* ... never Entity through RadarrEntity's ancestry */
ASSERT_TRUE(i_c3); /* super().__init__(): the same order */
ASSERT_FALSE(i_dfs);
ASSERT_TRUE(plain); /* control: single inheritance */
PASS();
}
TEST(pipeline_python_super_follows_c3_mro) {
return pym_run_super_c3(0);
}
TEST(pipeline_python_super_follows_c3_mro_parallel) {
return pym_run_super_c3(52);
}
/* ── attribute lookup follows the C3 method resolution order ─────── */
/* The same diamond as the super() test, seen from an ordinary attribute:
* `self.write_state()` in RadarrCalendarEntity and `e.write_state()` on a
* RadarrCalendarEntity both look the method up along the class's C3 order,
* which reaches CalendarEntity before the shared base Entity. */
static const char pya_radarr_py[] = "from base import CoordinatorEntity, CalendarEntity\n"
"\n"
"class RadarrEntity(CoordinatorEntity):\n"
" pass\n"
"\n"
"class RadarrCalendarEntity(RadarrEntity, CalendarEntity):\n"
" def refresh(self):\n"
" return self.write_state()\n"
"\n"
"class PlainCalendar(CalendarEntity):\n"
" def refresh(self):\n"
" return self.write_state()\n"
"\n"
"def publish(e: RadarrCalendarEntity):\n"
" return e.write_state()\n";
static int pya_run_attr_c3(int fillers) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_attrc3_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "base.py", pym_base_py);
write_temp_file(tmp, "radarr.py", pya_radarr_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/attrc3.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
int rc = p ? cbm_pipeline_run(p) : -1;
cbm_store_t *s = rc == 0 ? cbm_store_open_path(db_path) : NULL;
const char *pr = p ? cbm_pipeline_project_name(p) : NULL;
const char *c3 = "base.CalendarEntity.write_state";
const char *dfs = "base.Entity.write_state";
const char *self_src = "radarr.RadarrCalendarEntity.refresh";
bool self_c3 = s && pyn_edge(s, pr, self_src, c3, "CALLS");
bool self_dfs = s && pyn_edge(s, pr, self_src, dfs, "CALLS");
bool typed_c3 = s && pyn_edge(s, pr, "radarr.publish", c3, "CALLS");
bool typed_dfs = s && pyn_edge(s, pr, "radarr.publish", dfs, "CALLS");
bool plain = s && pyn_edge(s, pr, "radarr.PlainCalendar.refresh", c3, "CALLS");
if (s) {
cbm_store_close(s);
}
if (p) {
cbm_pipeline_free(p);
}
if (saved) {
cbm_setenv("CBM_WORKERS", saved, 1);
free(saved);
} else {
cbm_unsetenv("CBM_WORKERS");
}
th_rmtree(tmp);
ASSERT_EQ(rc, 0);
ASSERT_TRUE(self_c3); /* self.write_state(): CalendarEntity before Entity */
ASSERT_FALSE(self_dfs); /* ... never Entity through RadarrEntity's ancestry */
ASSERT_TRUE(typed_c3); /* e.write_state() on a RadarrCalendarEntity */
ASSERT_FALSE(typed_dfs);
ASSERT_TRUE(plain); /* control: single inheritance */
PASS();
}
TEST(pipeline_python_attribute_follows_c3_mro) {
return pya_run_attr_c3(0);
}
TEST(pipeline_python_attribute_follows_c3_mro_parallel) {
return pya_run_attr_c3(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
@@ -15308,6 +15868,164 @@ TEST(pipeline_python_init_function_shares_submodule_qn_parallel_issue1548) {
return pyn_run_init_collision(52);
}
/* ── F15: a nested class's bare base resolves in its class body first ── */
/* The sqlalchemy asyncpg shape: an adapter class re-declares the DBAPI
* exception hierarchy as nested classes, each based on an EARLIER sibling
* spelled bare (`class IntegrityError(DatabaseError)`). Python evaluates a
* class statement's bases in the enclosing class body, so the sibling wins
* over the same-named module classes of exc.py, which the adapter imports
* as a module (so a name guess prefers them, as on sqlalchemy once its
* relative `from ... import exc` resolves). A sibling defined LATER in the
* body is not bound yet: `Early(Later)` names the module-level Later. */
static const char pyb_exc_py[] = "class DBAPIError(Exception):\n"
" pass\n"
"\n"
"class DatabaseError(DBAPIError):\n"
" pass\n"
"\n"
"class IntegrityError(DatabaseError):\n"
" pass\n";
static const char pyb_adapter_py[] = "import exc\n"
"from exc import DBAPIError\n"
"\n"
"class Later(Exception):\n"
" pass\n"
"\n"
"class AsyncAdapt_dbapi:\n"
" class Error(DBAPIError):\n"
" pass\n"
"\n"
" class DatabaseError(Error):\n"
" pass\n"
"\n"
" class IntegrityError(DatabaseError):\n"
" pass\n"
"\n"
" class NotNullViolationError(IntegrityError):\n"
" pass\n"
"\n"
" class Early(Later):\n"
" pass\n"
"\n"
" class Later(Exception):\n"
" pass\n";
/* A second adapter with the same nested names, as sqlalchemy has one per
* driver: a name-only guess has several same-named candidates to pick. */
static const char pyb_adapter2_py[] = "class AsyncAdapt_other_dbapi:\n"
" class Error(Exception):\n"
" pass\n"
"\n"
" class DatabaseError(Error):\n"
" pass\n"
"\n"
" class IntegrityError(DatabaseError):\n"
" pass\n";
static int pyb_run_class_body_base(int fillers) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_cbbase_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "exc.py", pyb_exc_py);
write_temp_file(tmp, "adapter.py", pyb_adapter_py);
write_temp_file(tmp, "adapter2.py", pyb_adapter2_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/cbbase.db", tmp);
int rc[2] = {-1, -1};
bool db_sib[2] = {false, false};
bool db_mod[2] = {true, true};
bool ie_sib[2] = {false, false};
bool ie_mod[2] = {true, true};
bool nn_sib[2] = {false, false};
bool err_imp[2] = {false, false};
bool early_mod[2] = {false, false};
bool early_later_sib[2] = {true, true};
bool other_sib[2] = {false, false};
for (int run = 0; run < 2; run++) {
if (run == 1) {
char path[512];
snprintf(path, sizeof(path), "%s/adapter.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 *pr = cbm_pipeline_project_name(p);
const char *cls = "adapter.AsyncAdapt_dbapi";
char a[256];
char b[256];
snprintf(a, sizeof(a), "%s.DatabaseError", cls);
snprintf(b, sizeof(b), "%s.Error", cls);
db_sib[run] = pyn_edge(s, pr, a, b, "INHERITS");
db_mod[run] = pyn_edge(s, pr, a, "adapter2.AsyncAdapt_other_dbapi.Error", "INHERITS");
snprintf(a, sizeof(a), "%s.IntegrityError", cls);
snprintf(b, sizeof(b), "%s.DatabaseError", cls);
ie_sib[run] = pyn_edge(s, pr, a, b, "INHERITS");
ie_mod[run] = pyn_edge(s, pr, a, "exc.DatabaseError", "INHERITS");
snprintf(a, sizeof(a), "%s.NotNullViolationError", cls);
snprintf(b, sizeof(b), "%s.IntegrityError", cls);
nn_sib[run] = pyn_edge(s, pr, a, b, "INHERITS");
snprintf(a, sizeof(a), "%s.Error", cls);
err_imp[run] = pyn_edge(s, pr, a, "exc.DBAPIError", "INHERITS");
snprintf(a, sizeof(a), "%s.Early", cls);
snprintf(b, sizeof(b), "%s.Later", cls);
early_mod[run] = pyn_edge(s, pr, a, "adapter.Later", "INHERITS");
early_later_sib[run] = pyn_edge(s, pr, a, b, "INHERITS");
other_sib[run] = pyn_edge(s, pr, "adapter2.AsyncAdapt_other_dbapi.IntegrityError",
"adapter2.AsyncAdapt_other_dbapi.DatabaseError", "INHERITS");
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(db_sib[run]); /* DatabaseError(Error): the earlier sibling */
ASSERT_FALSE(db_mod[run]); /* ... not the other adapter's Error */
ASSERT_TRUE(ie_sib[run]); /* IntegrityError(DatabaseError): the sibling */
ASSERT_FALSE(ie_mod[run]); /* ... not exc.DatabaseError */
ASSERT_TRUE(nn_sib[run]); /* a chain of siblings */
ASSERT_TRUE(err_imp[run]); /* control: a non-sibling base keeps the import */
ASSERT_TRUE(early_mod[run]); /* a LATER sibling is not bound yet: module Later */
ASSERT_FALSE(early_later_sib[run]); /* ... never the later sibling */
ASSERT_TRUE(other_sib[run]); /* the second adapter binds its own sibling */
}
PASS();
}
TEST(pipeline_python_class_body_sibling_base) {
return pyb_run_class_body_base(0);
}
TEST(pipeline_python_class_body_sibling_base_parallel) {
return pyb_run_class_body_base(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
@@ -15403,6 +16121,94 @@ TEST(pipeline_python_nested_helper_keeps_tests_parallel_issue1548) {
return pyn_run_nested_helper_tests(52);
}
/* A decorated class nested in a class body (`@dataclass class Meta:` inside a
* class) is a nested class like an undecorated one: it gets its Class node and
* its methods their Method nodes. push_nested_class_nodes looked through
* field/template/declaration and conditional wrappers but not through Python's
* decorated_definition, so the decorated class and everything in it was never
* extracted. Both resolvers. */
static const char pyn_decorated_nested_py[] = "def deco(cls):\n"
" return cls\n"
"\n"
"class Outer:\n"
" @deco\n"
" class Decorated:\n"
" def run(self):\n"
" return 1\n"
"\n"
" class Plain:\n"
" def go(self):\n"
" return 2\n";
static bool pyn_node_exists(cbm_store_t *s, const char *project, const char *rel_qn,
const char *label) {
char qn[512];
pyn_qn(qn, sizeof(qn), project, rel_qn);
cbm_node_t n = {0};
bool found = cbm_store_find_node_by_qn(s, project, qn, &n) == CBM_STORE_OK && n.label &&
strcmp(n.label, label) == 0;
cbm_node_free_fields(&n);
return found;
}
static int pyn_run_decorated_nested(int fillers) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_decnest_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "sd.py", pyn_decorated_nested_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/decnest.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
int rc = p ? cbm_pipeline_run(p) : -1;
cbm_store_t *s = rc == 0 ? cbm_store_open_path(db_path) : NULL;
const char *project = p ? cbm_pipeline_project_name(p) : NULL;
bool dec_class = s && pyn_node_exists(s, project, "sd.Outer.Decorated", "Class");
bool dec_method = s && pyn_node_exists(s, project, "sd.Outer.Decorated.run", "Method");
bool plain_class = s && pyn_node_exists(s, project, "sd.Outer.Plain", "Class");
bool plain_method = s && pyn_node_exists(s, project, "sd.Outer.Plain.go", "Method");
if (s) {
cbm_store_close(s);
}
if (p) {
cbm_pipeline_free(p);
}
if (saved) {
cbm_setenv("CBM_WORKERS", saved, 1);
free(saved);
} else {
cbm_unsetenv("CBM_WORKERS");
}
th_rmtree(tmp);
ASSERT_EQ(rc, 0);
ASSERT_TRUE(dec_class); /* the decorated nested class is extracted */
ASSERT_TRUE(dec_method); /* ... and its method */
ASSERT_TRUE(plain_class); /* control: undecorated nested class */
ASSERT_TRUE(plain_method); /* control */
PASS();
}
TEST(pipeline_python_decorated_nested_class_extracted) {
return pyn_run_decorated_nested(0);
}
TEST(pipeline_python_decorated_nested_class_extracted_parallel) {
return pyn_run_decorated_nested(52);
}
TEST(pipeline_python_project) {
/* Port of TestPipelinePythonProject */
const char *files[] = {"main.py", "utils.py"};
@@ -24853,6 +25659,10 @@ SUITE(pipeline) {
RUN_TEST(pipeline_python_receiver_parallel_suppresses_weak_method_edges);
RUN_TEST(pipeline_python_bare_local_binding_suppresses_weak_edge);
RUN_TEST(pipeline_python_bare_local_binding_parallel_suppresses_weak_edge);
RUN_TEST(pipeline_python_package_relative_imports);
RUN_TEST(pipeline_python_package_relative_imports_parallel);
RUN_TEST(pipeline_python_from_import_bare_call_issue1237);
RUN_TEST(pipeline_python_from_import_bare_call_issue1237_parallel);
RUN_TEST(pipeline_parallel_python_cross_only_dunder_gets_synthetic_carrier);
RUN_TEST(pipeline_parallel_rust_cross_only_macro_hidden_gets_synthetic_carrier);
RUN_TEST(pipeline_arg_url_rejects_non_http_slash_arguments);
@@ -24888,6 +25698,8 @@ SUITE(pipeline) {
RUN_TEST(override_python_through_intermediate_class);
RUN_TEST(python_crossfile_typed_field_calls_issue1277);
RUN_TEST(python_crossfile_typed_field_calls_issue1277_parallel);
RUN_TEST(python_type_checking_typed_field_issue1277);
RUN_TEST(python_type_checking_typed_field_issue1277_parallel);
RUN_TEST(python_dotted_base_inheritance);
RUN_TEST(python_dotted_base_inheritance_parallel);
RUN_TEST(python_typed_field_external_import_stays_unresolved);
@@ -24914,10 +25726,18 @@ SUITE(pipeline) {
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_super_follows_c3_mro);
RUN_TEST(pipeline_python_super_follows_c3_mro_parallel);
RUN_TEST(pipeline_python_attribute_follows_c3_mro);
RUN_TEST(pipeline_python_attribute_follows_c3_mro_parallel);
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_class_body_sibling_base);
RUN_TEST(pipeline_python_class_body_sibling_base_parallel);
RUN_TEST(pipeline_python_nested_helper_keeps_tests_issue1548);
RUN_TEST(pipeline_python_nested_helper_keeps_tests_parallel_issue1548);
RUN_TEST(pipeline_python_decorated_nested_class_extracted);
RUN_TEST(pipeline_python_decorated_nested_class_extracted_parallel);
RUN_TEST(pipeline_header_include_target_is_independent_of_registration_order);
RUN_TEST(pipeline_imports_multi_symbol_edges);
RUN_TEST(pipeline_go_cross_package_call);