mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Merge remote-tracking branch 'origin/main' into fix/php-global-use-known-absent
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
commit
4ae1909223
19 files changed
+1721
-51
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
@@ -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(®, &idx_arena);
|
||||
|
||||
/* Phase C: run the resolver. */
|
||||
PHPLSPContext ctx;
|
||||
php_lsp_init(&ctx, arena, source, source_len, ®, 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(®, &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
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user