mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Merge origin/main into fix/go-init-qn
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
commit
19b57f3e64
15 files changed
+722
-59
No files matched your search
+7
-2
@@ -30,6 +30,7 @@
|
||||
#include "foundation/hash_table.h" // CBMHashTable — crash-supervisor quarantine set
|
||||
#include "foundation/str_util.h" // cbm_json_escape — variants file_path
|
||||
#include "tree_sitter/api.h" // TSParser, TSNode, TSTree, TSInput, TSLanguage, TSPoint, TSParseOptions, TSParseState
|
||||
#include "foundation/platform.h"
|
||||
#include "foundation/constants.h"
|
||||
#include "mimalloc.h" // mi_malloc/mi_calloc/mi_realloc/mi_free/mi_usable_size — bind 3rd-party allocators (#424)
|
||||
#if defined(CBM_BIND_TS_ALLOCATOR) && CBM_BIND_TS_ALLOCATOR
|
||||
@@ -1259,7 +1260,9 @@ static int count_params_from_signature(const char *sig) {
|
||||
* unlucky in-flight file is never quarantined alone. The env var is set
|
||||
* solely by the supervisor during recovery — a no-op on normal runs. */
|
||||
static void cbm_index_mark(const char *rel_path, char event) {
|
||||
const char *mf = getenv("CBM_INDEX_MARKER_FILE");
|
||||
char marker_path[CBM_SZ_4K];
|
||||
const char *mf =
|
||||
cbm_safe_getenv("CBM_INDEX_MARKER_FILE", marker_path, sizeof(marker_path), NULL);
|
||||
if (!mf || !mf[0] || !rel_path || !rel_path[0]) {
|
||||
return;
|
||||
}
|
||||
@@ -1296,7 +1299,9 @@ enum { CBM_QSET_UNINIT = 0, CBM_QSET_INITING = 1, CBM_QSET_INITED = 2 };
|
||||
static atomic_int g_quarantine_state = CBM_QSET_UNINIT;
|
||||
|
||||
static void cbm_quarantine_load(void) {
|
||||
const char *qf = getenv("CBM_INDEX_QUARANTINE_FILE");
|
||||
char quarantine_path[CBM_SZ_4K];
|
||||
const char *qf = cbm_safe_getenv("CBM_INDEX_QUARANTINE_FILE", quarantine_path,
|
||||
sizeof(quarantine_path), NULL);
|
||||
if (!qf || !qf[0]) {
|
||||
return; /* normal path: empty set */
|
||||
}
|
||||
|
||||
@@ -1039,6 +1039,7 @@ static const char *solidity_field_types[] = {"state_variable_declaration", "stru
|
||||
static const char *solidity_call_types[] = {"call_expression", "call", "new_expression", NULL};
|
||||
static const char *solidity_import_types[] = {"import_directive", "import", "using_directive",
|
||||
NULL};
|
||||
static const char *solidity_decorator_types[] = {"modifier_invocation", NULL};
|
||||
static const char *solidity_branch_types[] = {"if_statement", "for_statement",
|
||||
"while_statement", "do_while_statement",
|
||||
"try_statement", NULL};
|
||||
@@ -2116,7 +2117,7 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {
|
||||
solidity_field_types, solidity_module_types, solidity_call_types,
|
||||
solidity_import_types, empty_types, solidity_branch_types,
|
||||
solidity_var_types, solidity_assign_types, solidity_throw_types, NULL,
|
||||
empty_types, NULL, NULL, tree_sitter_solidity, NULL},
|
||||
solidity_decorator_types, NULL, NULL, tree_sitter_solidity, NULL},
|
||||
|
||||
// CBM_LANG_TYPST
|
||||
[CBM_LANG_TYPST] = {CBM_LANG_TYPST, typst_func_types, empty_types, empty_types,
|
||||
|
||||
+134
-17
@@ -269,8 +269,12 @@ static TSNode find_first_kind(TSNode node, const char *kind) {
|
||||
return null_node;
|
||||
}
|
||||
|
||||
static void ts_emit_resolved_call_at(TSLSPContext *ctx, const char *callee_qn, const char *strategy,
|
||||
float confidence, TSNode site) {
|
||||
/* `reason` carries the name written at the site when it differs from the
|
||||
* callee's leaf (`bar()` through `import { foo as bar }`): the pipeline joins an
|
||||
* alias strategy by that name instead of by the callee QN's last segment. */
|
||||
static void ts_emit_resolved_call_reason_at(TSLSPContext *ctx, const char *callee_qn,
|
||||
const char *strategy, float confidence,
|
||||
const char *reason, TSNode site) {
|
||||
if (!ctx || !ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn)
|
||||
return;
|
||||
CBMResolvedCall rc = {0};
|
||||
@@ -278,7 +282,7 @@ static void ts_emit_resolved_call_at(TSLSPContext *ctx, const char *callee_qn, c
|
||||
rc.callee_qn = callee_qn;
|
||||
rc.strategy = strategy ? strategy : "lsp_ts";
|
||||
rc.confidence = confidence;
|
||||
rc.reason = NULL;
|
||||
rc.reason = reason;
|
||||
rc.kind = CBM_RESOLVED_INVOCATION;
|
||||
if (!ts_node_is_null(site)) {
|
||||
rc.site_start_byte = ts_node_start_byte(site);
|
||||
@@ -287,8 +291,14 @@ static void ts_emit_resolved_call_at(TSLSPContext *ctx, const char *callee_qn, c
|
||||
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
|
||||
}
|
||||
|
||||
static void ts_emit_resolved_reference(TSLSPContext *ctx, const char *callee_qn,
|
||||
const char *strategy, float confidence, TSNode site) {
|
||||
static void ts_emit_resolved_call_at(TSLSPContext *ctx, const char *callee_qn, const char *strategy,
|
||||
float confidence, TSNode site) {
|
||||
ts_emit_resolved_call_reason_at(ctx, callee_qn, strategy, confidence, NULL, site);
|
||||
}
|
||||
|
||||
static void ts_emit_resolved_reference_reason(TSLSPContext *ctx, const char *callee_qn,
|
||||
const char *strategy, float confidence,
|
||||
const char *reason, TSNode site) {
|
||||
if (!ctx || !ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn ||
|
||||
ts_node_is_null(site)) {
|
||||
return;
|
||||
@@ -298,13 +308,18 @@ static void ts_emit_resolved_reference(TSLSPContext *ctx, const char *callee_qn,
|
||||
rc.callee_qn = callee_qn;
|
||||
rc.strategy = strategy ? strategy : "lsp_ts_value_reference";
|
||||
rc.confidence = confidence;
|
||||
rc.reason = NULL;
|
||||
rc.reason = reason;
|
||||
rc.kind = CBM_RESOLVED_CALL_REFERENCE;
|
||||
rc.site_start_byte = ts_node_start_byte(site);
|
||||
rc.site_end_byte = ts_node_end_byte(site);
|
||||
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
|
||||
}
|
||||
|
||||
static void ts_emit_resolved_reference(TSLSPContext *ctx, const char *callee_qn,
|
||||
const char *strategy, float confidence, TSNode site) {
|
||||
ts_emit_resolved_reference_reason(ctx, callee_qn, strategy, confidence, NULL, site);
|
||||
}
|
||||
|
||||
static void ts_emit_unresolved_call_at(TSLSPContext *ctx, const char *expr_text, const char *reason,
|
||||
TSNode site) {
|
||||
if (!ctx || !ctx->resolved_calls || !ctx->enclosing_func_qn)
|
||||
@@ -1425,6 +1440,19 @@ static bool ts_scope_has_binding(const CBMScope *scope, const char *name) {
|
||||
return ts_scope_binding_owner(scope, name) != NULL;
|
||||
}
|
||||
|
||||
/* The member an import binding names in its module: the exported name of a
|
||||
* renamed named import (`foo` for `import { foo as bar }`, #2297), else the
|
||||
* binding itself. Callers have already matched `local` to an import binding. */
|
||||
static const char *ts_import_export_name(const TSLSPContext *ctx, const char *local) {
|
||||
if (!ctx || !local)
|
||||
return local;
|
||||
for (int i = 0; i < ctx->import_alias_count; i++) {
|
||||
if (strcmp(ctx->import_alias_locals[i], local) == 0)
|
||||
return ctx->import_alias_exports[i];
|
||||
}
|
||||
return local;
|
||||
}
|
||||
|
||||
/* Full QN of an imported symbol. An import value is normally the module QN
|
||||
* (symbol = module + "." + name), but may already be the symbol QN. When the
|
||||
* module QN ends in ".name" the spelling is ambiguous: `Foo.ts` exporting
|
||||
@@ -1463,8 +1491,9 @@ static const CBMRegisteredFunc *ts_exact_same_module_function(TSLSPContext *ctx,
|
||||
}
|
||||
|
||||
/* Return a function only when the local import binding maps to one unambiguous
|
||||
* module and that module contains the exact local-name QN. Aliases whose export
|
||||
* name is unavailable in CBMImport deliberately fail closed and remain USAGE. */
|
||||
* module and that module contains the exact QN of the member the binding names
|
||||
* (the exported name for `import { foo as bar }`). A renamed import of a name
|
||||
* the module does not export fails closed and remains USAGE. */
|
||||
static const CBMRegisteredFunc *ts_exact_imported_function(TSLSPContext *ctx, const char *name) {
|
||||
if (!ctx || !ctx->registry || !name || ts_scope_has_binding(ctx->current_scope, name)) {
|
||||
return NULL;
|
||||
@@ -1484,7 +1513,8 @@ static const CBMRegisteredFunc *ts_exact_imported_function(TSLSPContext *ctx, co
|
||||
if (!matched_module) {
|
||||
return NULL;
|
||||
}
|
||||
const char *full_qn = ts_import_symbol_qn(ctx, matched_module, name);
|
||||
const char *full_qn =
|
||||
ts_import_symbol_qn(ctx, matched_module, ts_import_export_name(ctx, name));
|
||||
return full_qn ? cbm_registry_lookup_func(ctx->registry, full_qn) : NULL;
|
||||
}
|
||||
|
||||
@@ -1532,7 +1562,7 @@ static const CBMType *type_of_identifier(TSLSPContext *ctx, const char *name) {
|
||||
|
||||
// Construct the full symbol QN using the same convention as exact
|
||||
// imported function-value resolution.
|
||||
const char *full_qn = ts_import_symbol_qn(ctx, mqn, name);
|
||||
const char *full_qn = ts_import_symbol_qn(ctx, mqn, ts_import_export_name(ctx, name));
|
||||
|
||||
// Try the symbol lookup precedence: registered func, then registered type.
|
||||
const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, full_qn);
|
||||
@@ -2087,7 +2117,8 @@ static const CBMType *ts_new_bare_class_type(TSLSPContext *ctx, const char *cnam
|
||||
bool import_matched = false;
|
||||
const char *import_module = ts_import_module_for_local(ctx, cname, &import_matched);
|
||||
if (import_module)
|
||||
return cbm_type_named(ctx->arena, ts_import_symbol_qn(ctx, import_module, cname));
|
||||
return cbm_type_named(
|
||||
ctx->arena, ts_import_symbol_qn(ctx, import_module, ts_import_export_name(ctx, cname)));
|
||||
return cbm_type_named(ctx->arena, local_qn);
|
||||
}
|
||||
|
||||
@@ -2208,7 +2239,8 @@ static const CBMType *ts_signature_for_call(TSLSPContext *ctx, TSNode function,
|
||||
if (!registered) {
|
||||
const char *module_qn = ts_import_module_for_local(ctx, name, NULL);
|
||||
if (module_qn)
|
||||
registered = ts_lookup_symbol_for_call(ctx, module_qn, name, args);
|
||||
registered = ts_lookup_symbol_for_call(ctx, module_qn,
|
||||
ts_import_export_name(ctx, name), args);
|
||||
}
|
||||
if (registered)
|
||||
return registered->signature;
|
||||
@@ -2566,7 +2598,8 @@ static const CBMType *resolve_type_with_imports(TSLSPContext *ctx, const CBMType
|
||||
continue;
|
||||
if (strcmp(lname, bare) != 0)
|
||||
continue;
|
||||
return cbm_type_named(ctx->arena, ts_import_symbol_qn(ctx, mqn, bare));
|
||||
return cbm_type_named(ctx->arena,
|
||||
ts_import_symbol_qn(ctx, mqn, ts_import_export_name(ctx, bare)));
|
||||
}
|
||||
return t;
|
||||
}
|
||||
@@ -2933,12 +2966,19 @@ static void resolve_value_references_at(TSLSPContext *ctx, TSNode call_node) {
|
||||
}
|
||||
const CBMRegisteredFunc *target = ts_exact_same_module_function(ctx, name);
|
||||
const char *strategy = "lsp_ts_same_module_value_reference";
|
||||
const char *reason = NULL;
|
||||
if (!target) {
|
||||
target = ts_exact_imported_function(ctx, name);
|
||||
strategy = "lsp_ts_import_value_reference";
|
||||
/* `import { foo as bar }`: the site names `bar`, the target is `foo`. */
|
||||
if (target && ts_import_export_name(ctx, name) != name) {
|
||||
strategy = "lsp_callable_value_reference";
|
||||
reason = name;
|
||||
}
|
||||
}
|
||||
if (target) {
|
||||
ts_emit_resolved_reference(ctx, target->qualified_name, strategy, 0.95f, arg);
|
||||
ts_emit_resolved_reference_reason(ctx, target->qualified_name, strategy, 0.95f, reason,
|
||||
arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3025,14 +3065,20 @@ static void resolve_call_at(TSLSPContext *ctx, TSNode call_node) {
|
||||
const char *lname = ctx->import_local_names ? ctx->import_local_names[i] : NULL;
|
||||
const char *mqn = ctx->import_module_qns ? ctx->import_module_qns[i] : NULL;
|
||||
if (lname && mqn && strcmp(lname, name) == 0) {
|
||||
const CBMRegisteredFunc *f = ts_lookup_symbol_for_call(ctx, mqn, name, args);
|
||||
const char *member = ts_import_export_name(ctx, name);
|
||||
const CBMRegisteredFunc *f = ts_lookup_symbol_for_call(ctx, mqn, member, args);
|
||||
if (f && member != name) {
|
||||
ts_emit_resolved_call_reason_at(ctx, f->qualified_name, "lsp_import_alias",
|
||||
0.95f, name, call_node);
|
||||
return;
|
||||
}
|
||||
if (f) {
|
||||
ts_emit_resolved_call_at(ctx, f->qualified_name, "lsp_ts_import", 0.95f,
|
||||
call_node);
|
||||
return;
|
||||
}
|
||||
// Fall through to qualified_name fallback.
|
||||
const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", mqn, name);
|
||||
const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", mqn, member);
|
||||
ts_emit_unresolved_call_at(ctx, qn, "import_symbol_not_in_registry", call_node);
|
||||
return;
|
||||
}
|
||||
@@ -3374,7 +3420,13 @@ static void resolve_jsx_element(TSLSPContext *ctx, TSNode element_node) {
|
||||
* per-file row deliberately keeps the file eligible for the
|
||||
* cross-file pass, where resolved import QNs and project defs are
|
||||
* available. */
|
||||
const CBMRegisteredFunc *f = cbm_registry_lookup_symbol(ctx->registry, mqn, tag_name);
|
||||
const char *member = ts_import_export_name(ctx, tag_name);
|
||||
const CBMRegisteredFunc *f = cbm_registry_lookup_symbol(ctx->registry, mqn, member);
|
||||
if (f && member != tag_name) {
|
||||
ts_emit_resolved_call_reason_at(ctx, f->qualified_name, "lsp_import_alias", 0.95f,
|
||||
tag_name, tag);
|
||||
return;
|
||||
}
|
||||
if (f) {
|
||||
ts_emit_resolved_call_at(ctx, f->qualified_name, "lsp_ts_jsx_import", 0.95f, tag);
|
||||
return;
|
||||
@@ -4005,6 +4057,68 @@ void ts_lsp_add_import(TSLSPContext *ctx, const char *local_name, const char *mo
|
||||
ctx->import_count = new_count;
|
||||
}
|
||||
|
||||
static void ts_lsp_add_import_alias(TSLSPContext *ctx, const char *local, const char *exported) {
|
||||
if ((size_t)ctx->import_alias_count == ctx->import_alias_capacity) {
|
||||
/* The arena retains old arrays: grow geometrically so collecting A
|
||||
* aliases retains O(A) pointers instead of O(A squared). */
|
||||
size_t capacity = ctx->import_alias_capacity ? ctx->import_alias_capacity * 2 : 8;
|
||||
const char **locals =
|
||||
(const char **)cbm_arena_alloc(ctx->arena, capacity * sizeof(const char *));
|
||||
const char **exports =
|
||||
(const char **)cbm_arena_alloc(ctx->arena, capacity * sizeof(const char *));
|
||||
if (!locals || !exports)
|
||||
return;
|
||||
for (int i = 0; i < ctx->import_alias_count; i++) {
|
||||
locals[i] = ctx->import_alias_locals[i];
|
||||
exports[i] = ctx->import_alias_exports[i];
|
||||
}
|
||||
ctx->import_alias_locals = locals;
|
||||
ctx->import_alias_exports = exports;
|
||||
ctx->import_alias_capacity = capacity;
|
||||
}
|
||||
ctx->import_alias_locals[ctx->import_alias_count] = local;
|
||||
ctx->import_alias_exports[ctx->import_alias_count] = exported;
|
||||
ctx->import_alias_count++;
|
||||
}
|
||||
|
||||
/* Record each `import { foo as bar }` binding of the file (#2297). The import
|
||||
* map holds only local bindings, so a renamed binding spelled `<module>.bar`,
|
||||
* named nothing, and every call through it lost its CALLS edge. Reading the
|
||||
* file's own import statements serves the per-file and both cross-file entry
|
||||
* points alike. `default as x` keeps today's local-name lookup (a default
|
||||
* import binds no exported member name); string export names and identity
|
||||
* aliases need no entry. Must run before the type passes, which resolve
|
||||
* imported type names. */
|
||||
static void ts_lsp_collect_import_aliases(TSLSPContext *ctx, TSNode root) {
|
||||
if (!ctx || !ctx->arena || ts_node_is_null(root) || ctx->import_count == 0)
|
||||
return;
|
||||
uint32_t kn = 0;
|
||||
TSNode *kids = collect_children(ctx->arena, root, &kn);
|
||||
for (uint32_t i = 0; i < kn; i++) {
|
||||
if (!node_kind_is(kids[i], "import_statement"))
|
||||
continue;
|
||||
TSNode named = find_first_kind(find_first_kind(kids[i], "import_clause"), "named_imports");
|
||||
if (ts_node_is_null(named))
|
||||
continue;
|
||||
uint32_t sn = 0;
|
||||
TSNode *specs = collect_children(ctx->arena, named, &sn);
|
||||
for (uint32_t j = 0; j < sn; j++) {
|
||||
if (!node_kind_is(specs[j], "import_specifier"))
|
||||
continue;
|
||||
TSNode name = ts_node_child_by_field_name(specs[j], "name", TS_LSP_FIELD_LEN("name"));
|
||||
TSNode alias =
|
||||
ts_node_child_by_field_name(specs[j], "alias", TS_LSP_FIELD_LEN("alias"));
|
||||
if (!node_kind_is(name, "identifier") || !node_kind_is(alias, "identifier"))
|
||||
continue;
|
||||
char *exported = node_text(ctx, name);
|
||||
char *local = node_text(ctx, alias);
|
||||
if (exported && local && strcmp(exported, "default") != 0 &&
|
||||
strcmp(exported, local) != 0)
|
||||
ts_lsp_add_import_alias(ctx, local, exported);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stdlib seeds (Phase 5 will replace with generator) ────────────────────────
|
||||
|
||||
static const CBMType *tt_builtin(CBMArena *a, const char *n) {
|
||||
@@ -5948,6 +6062,7 @@ void cbm_run_ts_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
|
||||
ts_lsp_add_import(&ctx, imp->local_name, imp->module_path);
|
||||
}
|
||||
}
|
||||
ts_lsp_collect_import_aliases(&ctx, root);
|
||||
|
||||
ast_sweep_shapes(&ctx, root, ®);
|
||||
refine_ast_signatures(&ctx, root, ®);
|
||||
@@ -6201,6 +6316,7 @@ void cbm_run_ts_lsp_cross_with_registry(CBMArena *arena, const char *source, int
|
||||
ts_lsp_add_import(&ctx, import_names[i], import_qns[i]);
|
||||
}
|
||||
}
|
||||
ts_lsp_collect_import_aliases(&ctx, root);
|
||||
|
||||
ast_sweep_shapes(&ctx, root, &overlay);
|
||||
refine_ast_signatures(&ctx, root, &overlay);
|
||||
@@ -6416,6 +6532,7 @@ void cbm_run_ts_lsp_cross(CBMArena *arena, const char *source, int source_len,
|
||||
ts_lsp_add_import(&ctx, import_names[i], import_qns[i]);
|
||||
}
|
||||
}
|
||||
ts_lsp_collect_import_aliases(&ctx, root);
|
||||
|
||||
ast_sweep_shapes(&ctx, root, ®);
|
||||
refine_ast_signatures(&ctx, root, ®);
|
||||
|
||||
@@ -33,6 +33,14 @@ typedef struct {
|
||||
const char **import_module_qns;
|
||||
int import_count;
|
||||
|
||||
// Renamed named imports (`import { foo as bar }`): local binding -> the name
|
||||
// the module exports. Parallel arrays of length import_alias_count; a
|
||||
// binding absent here is exported under its local name.
|
||||
const char **import_alias_locals;
|
||||
const char **import_alias_exports;
|
||||
int import_alias_count;
|
||||
size_t import_alias_capacity;
|
||||
|
||||
// File / surrounding context.
|
||||
const char *module_qn; // QN of this file's module
|
||||
const char *enclosing_func_qn; // current function being walked, NULL at module scope
|
||||
|
||||
+3
-1
@@ -810,7 +810,9 @@ static bool cli_activation_production_context_init(cli_activation_production_con
|
||||
context->cleanup_ok = true;
|
||||
context->deadline_ms = cli_activation_deadline_after(CLI_ACTIVATION_DRAIN_TIMEOUT_MS);
|
||||
context->control_deadline_ms = cli_activation_deadline_after(CLI_ACTIVATION_CONTROL_TIMEOUT_MS);
|
||||
const char *original_cache_environment = getenv("CBM_CACHE_DIR");
|
||||
char original_cache_buffer[CLI_BUF_4K];
|
||||
const char *original_cache_environment = cbm_safe_getenv("CBM_CACHE_DIR", original_cache_buffer,
|
||||
sizeof(original_cache_buffer), NULL);
|
||||
context->original_cache_environment_present = original_cache_environment != NULL;
|
||||
if (context->original_cache_environment_present) {
|
||||
context->original_cache_environment = strdup(original_cache_environment);
|
||||
|
||||
@@ -166,11 +166,9 @@ static inline int cbm_setenv(const char *name, const char *value, int overwrite)
|
||||
free(wide_value);
|
||||
return EINVAL;
|
||||
}
|
||||
/* Keep the CRT's narrow environment useful for legacy getenv callers,
|
||||
* then repair the process-wide Windows environment with the actual UTF-16
|
||||
* value. _putenv_s alone routes UTF-8 path bytes through the active ANSI
|
||||
* code page, which corrupts non-ASCII cache roots inherited by children. */
|
||||
int status = _putenv_s(name, value);
|
||||
/* _putenv_s can reject UTF-8 bytes outside the active ANSI code page.
|
||||
* Narrow getenv returns bytes in that code page after this wide update. */
|
||||
int status = _wputenv_s(wide_name, wide_value);
|
||||
if (status == 0 && !SetEnvironmentVariableW(wide_name, wide_value)) {
|
||||
status = EINVAL;
|
||||
}
|
||||
|
||||
@@ -3789,10 +3789,9 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
|
||||
if (tgt && cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
|
||||
continue;
|
||||
}
|
||||
/* #1942/#1962: a bare Go reference can never denote a struct
|
||||
* field; the member half of a selector may. */
|
||||
if (tgt && cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, usage->is_member_access,
|
||||
tgt->label)) {
|
||||
/* #1942/#1962/#2666: a bare Go or C reference can never denote
|
||||
* a struct field; the member half of a selector may. */
|
||||
if (tgt && cbm_suppress_bare_field_ref(lang, usage->is_member_access, tgt->label)) {
|
||||
continue;
|
||||
}
|
||||
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
|
||||
@@ -3918,9 +3917,9 @@ static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFi
|
||||
if (cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
|
||||
continue;
|
||||
}
|
||||
/* #1942/#1962: a bare Go reference can never denote a struct field;
|
||||
* a selector-LHS write (`t.err = x`) may bind it. */
|
||||
if (cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, rw->is_member_access, tgt->label)) {
|
||||
/* #1942/#1962/#2666: a bare Go or C reference can never denote a
|
||||
* struct field; a selector-LHS write (`t.err = x`) may bind it. */
|
||||
if (cbm_suppress_bare_field_ref(lang, rw->is_member_access, tgt->label)) {
|
||||
continue;
|
||||
}
|
||||
const char *etype = rw->is_write ? "WRITES" : "READS";
|
||||
|
||||
@@ -283,10 +283,9 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
|
||||
if (tgt && cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
|
||||
continue;
|
||||
}
|
||||
/* #1942/#1962: a bare Go reference can never denote a struct
|
||||
* field; the member half of a selector may. */
|
||||
if (tgt && cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, usage->is_member_access,
|
||||
tgt->label)) {
|
||||
/* #1942/#1962/#2666: a bare Go or C reference can never denote
|
||||
* a struct field; the member half of a selector may. */
|
||||
if (tgt && cbm_suppress_bare_field_ref(lang, usage->is_member_access, tgt->label)) {
|
||||
continue;
|
||||
}
|
||||
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
|
||||
@@ -416,9 +415,9 @@ static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result
|
||||
if (cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
|
||||
continue;
|
||||
}
|
||||
/* #1942/#1962: a bare Go reference can never denote a struct field;
|
||||
* a selector-LHS write (`t.err = x`) may bind it. */
|
||||
if (cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, rw->is_member_access, tgt->label)) {
|
||||
/* #1942/#1962/#2666: a bare Go or C reference can never denote a
|
||||
* struct field; a selector-LHS write (`t.err = x`) may bind it. */
|
||||
if (cbm_suppress_bare_field_ref(lang, rw->is_member_access, tgt->label)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -467,13 +467,14 @@ bool cbm_suppress_cross_language_suffix_match(CBMLanguage caller_lang, const cha
|
||||
* unit-tested in test_registry.c. */
|
||||
bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target_file_path);
|
||||
|
||||
/* #1942: a bare (dot-less) Go reference can never denote a struct field —
|
||||
* field access is always a selector expression, and selector references
|
||||
* resolve on the LSP path. Drops a READS/WRITES/USAGE bind whose target is a
|
||||
* Field when the reference text carries no '.'. Go only: other OO languages
|
||||
* legitimately reference their own members bare inside method bodies. Pure;
|
||||
* unit-tested in test_registry.c. */
|
||||
bool cbm_go_suppress_bare_field_ref(bool is_go, bool is_member_access, const char *target_label);
|
||||
/* #1942/#2666: a bare Go or C reference can never denote a struct field —
|
||||
* field access is always a selector expression (or, in C, a designator), and
|
||||
* the member half of one is recorded as is_member_access. Drops a
|
||||
* READS/WRITES/USAGE bind whose target is a Field when the reference was not
|
||||
* the member half of a selector. Go and C only: other OO languages (and C++,
|
||||
* which every `.h` is parsed as) legitimately reference their own members
|
||||
* bare inside method bodies. Pure; unit-tested in test_registry.c. */
|
||||
bool cbm_suppress_bare_field_ref(CBMLanguage lang, bool is_member_access, const char *target_label);
|
||||
|
||||
/* What to do with a call whose registry resolution landed on a Field node.
|
||||
* Pure; unit-tested in test_registry.c. */
|
||||
|
||||
+15
-4
@@ -1276,7 +1276,8 @@ bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cbm_go_suppress_bare_field_ref(bool is_go, bool is_member_access, const char *target_label) {
|
||||
bool cbm_suppress_bare_field_ref(CBMLanguage lang, bool is_member_access,
|
||||
const char *target_label) {
|
||||
/* #1942/#1962: a bare Go identifier can never denote a struct field —
|
||||
* field access is always a selector expression (x.f). The extractor
|
||||
* strips the receiver before the resolver runs (resolve_lhs_write_name
|
||||
@@ -1284,10 +1285,20 @@ bool cbm_go_suppress_bare_field_ref(bool is_go, bool is_member_access, const cha
|
||||
* field_identifier), so the reference TEXT is always dot-less and cannot
|
||||
* carry the distinction — the recorded is_member_access shape can. Only a
|
||||
* reference that was never the member half of a selector is refused a
|
||||
* Field bind. Go-gated: a C#/Java/C++/Python method body legitimately
|
||||
* Field bind.
|
||||
*
|
||||
* #2666: C has the same rule. A struct member is named only as the member
|
||||
* half of `a.b`, `a->b` or a designator `.b`, all recorded as
|
||||
* field_identifier; a bare C identifier (a macro argument such as
|
||||
* RUN_SELECTED_SUITE(mcp), a stray name in an unexpanded macro body) is
|
||||
* never one. The call path already drops a bare C call onto a Field
|
||||
* (cbm_call_onto_field_policy).
|
||||
*
|
||||
* Go- and C-gated: a C#/Java/C++/Python method body legitimately
|
||||
* references its own members bare (cp_reads_writes_cs_static_field pins
|
||||
* that shape as required), so a global veto would break those languages. */
|
||||
if (!is_go || is_member_access || !target_label) {
|
||||
* that shape as required), so a global veto would break those languages.
|
||||
* A `.h` is parsed as C++ and keeps that behavior. */
|
||||
if ((lang != CBM_LANG_GO && lang != CBM_LANG_C) || is_member_access || !target_label) {
|
||||
return false;
|
||||
}
|
||||
return strcmp(target_label, "Field") == 0;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
#include "test_framework.h"
|
||||
#include "cbm.h"
|
||||
#include "foundation/platform.h"
|
||||
#include "foundation/constants.h" /* CBM_SZ_* */
|
||||
#include "preprocessor.h" /* cbm_export_macro_candidates (#1989) */
|
||||
#include "../src/foundation/compat.h" /* cbm_clock_gettime (wide-flat scaling guard) */
|
||||
@@ -5428,6 +5429,24 @@ TEST(extract_python_bare_decorator_route_rules) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Issue #2090: Solidity modifier_invocation tracked as decorator. */
|
||||
TEST(solidity_modifier_decorator) {
|
||||
CBMFileResult *r = extract("pragma solidity ^0.8.0;\n"
|
||||
"contract Vault {\n"
|
||||
" modifier onlyOwner() { _; }\n"
|
||||
" function withdraw() public onlyOwner {\n"
|
||||
" }\n"
|
||||
"}\n",
|
||||
CBM_LANG_SOLIDITY, "t", "Vault.sol");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
const CBMDefinition *m = find_def_by_name(r, "withdraw");
|
||||
ASSERT_NOT_NULL(m);
|
||||
ASSERT(decorators_contain(m, "onlyOwner"));
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── ArkTS (HarmonyOS .ets) ─────────────────────────────────────── */
|
||||
|
||||
TEST(arkts_component_struct) {
|
||||
@@ -13383,7 +13402,42 @@ TEST(python_namespace_facts_multiple_bindings_and_unsupported_effects) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
/* The supervisor sets this path through cbm_setenv. Exercise the actual
|
||||
* journal writer, which must read it as UTF-8 before calling cbm_fopen. */
|
||||
TEST(extraction_marker_round_trips_non_ascii_environment_path) {
|
||||
char dir[512] = "/tmp/cbm-marker-XXXXXX";
|
||||
ASSERT_NOT_NULL(cbm_mkdtemp(dir));
|
||||
char marker[640];
|
||||
snprintf(marker, sizeof(marker), "%s/marker-\xce\x94-\xe4\xb8\x81.log", dir);
|
||||
char saved[4096];
|
||||
const char *previous = cbm_safe_getenv("CBM_INDEX_MARKER_FILE", saved, sizeof(saved), NULL);
|
||||
ASSERT_EQ(cbm_setenv("CBM_INDEX_MARKER_FILE", marker, 1), 0);
|
||||
cbm_index_mark_start("synthetic.cpp");
|
||||
cbm_index_mark_done("synthetic.cpp");
|
||||
if (previous) {
|
||||
(void)cbm_setenv("CBM_INDEX_MARKER_FILE", saved, 1);
|
||||
} else {
|
||||
(void)cbm_unsetenv("CBM_INDEX_MARKER_FILE");
|
||||
}
|
||||
FILE *file = cbm_fopen(marker, "rb");
|
||||
char contents[128] = {0};
|
||||
if (file) {
|
||||
(void)fread(contents, 1, sizeof(contents) - 1, file);
|
||||
(void)fclose(file);
|
||||
}
|
||||
(void)cbm_unlink(marker);
|
||||
(void)cbm_rmdir(dir);
|
||||
ASSERT_NOT_NULL(file);
|
||||
ASSERT_STR_EQ(contents, "S synthetic.cpp\nD synthetic.cpp\n");
|
||||
PASS();
|
||||
}
|
||||
#endif
|
||||
|
||||
SUITE(extraction) {
|
||||
#ifdef _WIN32
|
||||
RUN_TEST(extraction_marker_round_trips_non_ascii_environment_path);
|
||||
#endif
|
||||
RUN_TEST(extract_compact_keeps_every_field_and_shrinks_the_arena);
|
||||
RUN_TEST(extract_compact_is_idempotent_and_survives_empty_results);
|
||||
RUN_TEST(extract_spill_round_trip_keeps_every_field);
|
||||
@@ -13775,6 +13829,7 @@ SUITE(extraction) {
|
||||
RUN_TEST(extract_java_method_annotations_issue382);
|
||||
RUN_TEST(extract_python_mock_patch_is_not_route);
|
||||
RUN_TEST(extract_python_bare_decorator_route_rules);
|
||||
RUN_TEST(solidity_modifier_decorator);
|
||||
RUN_TEST(arkts_component_struct);
|
||||
RUN_TEST(arkts_exported_struct_decorators);
|
||||
RUN_TEST(arkts_member_decorators);
|
||||
|
||||
@@ -9743,6 +9743,104 @@ TEST(pipeline_ts_crossfile_new_instance_method_call_issue1354) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Issue #2297: a call through `import { foo as fooShared }` produced no CALLS
|
||||
* edge, so trace_path(foo, inbound) missed the caller. End to end through the
|
||||
* real pipeline: the renamed call, an app-local wrapper that shares the
|
||||
* exported name, and a method on a renamed class import all reach the
|
||||
* exported member, while the plain named import control keeps lsp_ts_import. */
|
||||
TEST(pipeline_ts_renamed_import_call_issue2297) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_ts_2297_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
|
||||
write_temp_file(tmp, "src/lib.ts",
|
||||
"export function fooUniqueQrs(x: number): number {\n"
|
||||
" return x + 1;\n"
|
||||
"}\n"
|
||||
"export function buildHealthUniqueQrs(input: { a: number }): number {\n"
|
||||
" return input.a;\n"
|
||||
"}\n"
|
||||
"export class ConnUniqueQrs {\n"
|
||||
" pingUniqueQrs(): number {\n"
|
||||
" return 1;\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "src/plain.ts",
|
||||
"import { fooUniqueQrs } from './lib';\n"
|
||||
"export function callsPlain(): number {\n"
|
||||
" return fooUniqueQrs(1);\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "src/renamed.ts",
|
||||
"import { fooUniqueQrs as fooShared } from './lib';\n"
|
||||
"export function callsRenamed(): number {\n"
|
||||
" return fooShared(1);\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "src/wrapper.ts",
|
||||
"import { buildHealthUniqueQrs as buildShared } from './lib';\n"
|
||||
"export function buildHealthUniqueQrs(input: { a: number }): number {\n"
|
||||
" return buildShared({ ...input });\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "src/klass.ts",
|
||||
"import { ConnUniqueQrs as Connection } from './lib';\n"
|
||||
"export function viaRenamedClass(): number {\n"
|
||||
" return new Connection().pingUniqueQrs();\n"
|
||||
"}\n");
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/ts_2297.db", tmp);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(cbm_pipeline_run(p), 0);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
|
||||
bool plain =
|
||||
cross_file_call_has_strategy(s, project, "callsPlain", "fooUniqueQrs", "lsp_ts_import");
|
||||
bool renamed = cross_file_call_has_strategy(s, project, "callsRenamed", "fooUniqueQrs",
|
||||
"lsp_import_alias");
|
||||
char wrapper_qn[512];
|
||||
char exported_qn[512];
|
||||
snprintf(wrapper_qn, sizeof(wrapper_qn), "%s.src.wrapper.buildHealthUniqueQrs", project);
|
||||
snprintf(exported_qn, sizeof(exported_qn), "%s.src.lib.buildHealthUniqueQrs", project);
|
||||
cbm_node_t wrapper_node = {0};
|
||||
cbm_node_t exported_node = {0};
|
||||
bool wrapper = false;
|
||||
bool recursive = false;
|
||||
if (cbm_store_find_node_by_qn(s, project, wrapper_qn, &wrapper_node) == CBM_STORE_OK &&
|
||||
cbm_store_find_node_by_qn(s, project, exported_qn, &exported_node) == CBM_STORE_OK) {
|
||||
cbm_edge_t *edges = NULL;
|
||||
int edge_count = 0;
|
||||
cbm_store_find_edges_by_source_type(s, wrapper_node.id, "CALLS", &edges, &edge_count);
|
||||
for (int i = 0; i < edge_count; i++) {
|
||||
if (edges[i].target_id == exported_node.id && edges[i].properties_json &&
|
||||
strstr(edges[i].properties_json, "lsp_import_alias"))
|
||||
wrapper = true;
|
||||
if (edges[i].target_id == wrapper_node.id)
|
||||
recursive = true;
|
||||
}
|
||||
cbm_store_free_edges(edges, edge_count);
|
||||
}
|
||||
cbm_node_free_fields(&wrapper_node);
|
||||
cbm_node_free_fields(&exported_node);
|
||||
bool renamed_class = cross_file_call_has_strategy(s, project, "viaRenamedClass",
|
||||
"pingUniqueQrs", "lsp_ts_method");
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
|
||||
ASSERT_TRUE(plain);
|
||||
ASSERT_TRUE(renamed);
|
||||
ASSERT_TRUE(wrapper);
|
||||
ASSERT_FALSE(recursive);
|
||||
ASSERT_TRUE(renamed_class);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A query failure is distinct from a valid zero-edge result. */
|
||||
static int ts514_pipeline_count(cbm_store_t *store, const char *sql, const char **values,
|
||||
int value_count) {
|
||||
@@ -11753,6 +11851,93 @@ TEST(pipeline_go_bare_ref_never_binds_field_parallel) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #2666: tests/test_main.c runs every suite through a macro,
|
||||
* RUN_SELECTED_SUITE(mcp); the argument is a bare C identifier that names no
|
||||
* declaration, and the registry fallback bound it to whichever struct member
|
||||
* was called `mcp`. toggle() writes a bare name the extractor cannot see
|
||||
* declared (a macro-made global, say) that a struct member also carries.
|
||||
* state.c holds the genuine member uses that must keep their edges.
|
||||
* pad_files pushes the run past MIN_FILES_FOR_PARALLEL for the parallel twin. */
|
||||
static void write_c_bare_field_fixture(const char *tmp, int pad_files) {
|
||||
write_temp_file(tmp, "include/suite.h",
|
||||
"typedef struct suite_state {\n"
|
||||
" char mcp[64];\n"
|
||||
" int count;\n"
|
||||
" int enabled;\n"
|
||||
"} suite_state_t;\n");
|
||||
write_temp_file(tmp, "tests/main.c",
|
||||
"#include \"suite.h\"\n"
|
||||
"#define RUN_SELECTED_SUITE(name) run_suite(#name)\n"
|
||||
"int run_suite(const char *name);\n"
|
||||
"int main(void) {\n"
|
||||
" RUN_SELECTED_SUITE(mcp);\n"
|
||||
" RUN_SELECTED_SUITE(count);\n"
|
||||
" return 0;\n"
|
||||
"}\n"
|
||||
"void toggle(void) {\n"
|
||||
" enabled = 1;\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "src/state.c",
|
||||
"#include \"suite.h\"\n"
|
||||
"int state_count(const suite_state_t *s) { return s->count; }\n"
|
||||
"void state_reset(suite_state_t *s) { s->enabled = 0; }\n");
|
||||
for (int i = 0; i < pad_files; i++) {
|
||||
char name[64];
|
||||
char body[128];
|
||||
snprintf(name, sizeof(name), "pad/filler%d.c", i);
|
||||
snprintf(body, sizeof(body), "int filler%d(void) { return %d; }\n", i, i);
|
||||
write_temp_file(tmp, name, body);
|
||||
}
|
||||
}
|
||||
|
||||
static int run_c_bare_field_case(const char *tmpl, int pad_files) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "%s", tmpl);
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_c_bare_field_fixture(tmp, pad_files);
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/c_bare.db", tmp);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(cbm_pipeline_run(p), 0);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
|
||||
/* The fields must exist for the probes to mean anything. */
|
||||
ASSERT_GTE(fixture_node_count(s, project, "include/suite.h", "mcp", "Field"), 1);
|
||||
ASSERT_GTE(fixture_node_count(s, project, "include/suite.h", "count", "Field"), 1);
|
||||
ASSERT_GTE(fixture_node_count(s, project, "include/suite.h", "enabled", "Field"), 1);
|
||||
/* Reproduce-first: RED before the fix — the macro arguments bind the
|
||||
* same-named struct members, and the bare write binds `enabled`. */
|
||||
ASSERT_FALSE(cross_file_edge_exists(s, project, "main", "mcp", "USAGE"));
|
||||
ASSERT_FALSE(cross_file_edge_exists(s, project, "main", "count", "USAGE"));
|
||||
ASSERT_FALSE(cross_file_edge_exists(s, project, "toggle", "enabled", "WRITES"));
|
||||
/* The member half of `s->count` / `s->enabled = 0` still binds the field. */
|
||||
ASSERT_TRUE(cross_file_edge_exists(s, project, "state_count", "count", "USAGE"));
|
||||
ASSERT_TRUE(cross_file_edge_exists(s, project, "state_reset", "enabled", "WRITES"));
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pipeline_c_bare_ref_never_binds_field) {
|
||||
/* Sequential path: resolve_usage_edges / resolve_rw_edges. */
|
||||
return run_c_bare_field_case("/tmp/cbm_c_bare_XXXXXX", 0);
|
||||
}
|
||||
|
||||
TEST(pipeline_c_bare_ref_never_binds_field_parallel) {
|
||||
/* Parallel twin: resolve_file_usages / resolve_file_rw must consult the
|
||||
* same predicate (#1928's lesson). */
|
||||
return run_c_bare_field_case("/tmp/cbm_c_barep_XXXXXX", 52);
|
||||
}
|
||||
|
||||
/* A C caller of a header function that shares its name with a struct member
|
||||
* elsewhere (kernel: include/linux/mm.h put_page beside the arm64
|
||||
* kvm_pgtable_mm_ops.put_page member). pad_files pushes the run past
|
||||
@@ -28948,6 +29133,7 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_tsjs_receiver_suppresses_weak_method_edge);
|
||||
RUN_TEST(pipeline_axios_wrapper_baseurl_composes_http_calls_issue1916);
|
||||
RUN_TEST(pipeline_ts_crossfile_new_instance_method_call_issue1354);
|
||||
RUN_TEST(pipeline_ts_renamed_import_call_issue2297);
|
||||
RUN_TEST(pipeline_ts_param_property_injection_issue514);
|
||||
RUN_TEST(pipeline_python_receiver_suppresses_weak_method_edge);
|
||||
RUN_TEST(pipeline_php_receiver_suppresses_weak_method_edge);
|
||||
@@ -28974,6 +29160,8 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_rust_std_receiver_never_binds_project_method_parallel);
|
||||
RUN_TEST(pipeline_go_bare_ref_never_binds_field);
|
||||
RUN_TEST(pipeline_go_bare_ref_never_binds_field_parallel);
|
||||
RUN_TEST(pipeline_c_bare_ref_never_binds_field);
|
||||
RUN_TEST(pipeline_c_bare_ref_never_binds_field_parallel);
|
||||
RUN_TEST(pipeline_c_call_reaches_header_function_beside_field);
|
||||
RUN_TEST(pipeline_c_call_reaches_header_function_beside_field_parallel);
|
||||
RUN_TEST(pipeline_c_member_access_binds_field_by_object_type);
|
||||
|
||||
+10
-2
@@ -947,8 +947,8 @@ TEST(platform_env_dup_honours_the_settings_resolver) {
|
||||
* setter must update that same wide environment; _putenv_s alone interprets
|
||||
* UTF-8 path bytes through the active ANSI code page. */
|
||||
TEST(platform_setenv_preserves_utf8_in_wide_environment) {
|
||||
static const char utf8[] = "C:/cbm-cache-\xce\x94-\xe6\x97\xa5\xe6\x9c\xac";
|
||||
static const wchar_t wide[] = L"C:/cbm-cache-\u0394-\u65e5\u672c";
|
||||
static const char utf8[] = "C:/cbm-cache-\xce\x94-\xe6\x97\xa5\xe6\x9c\xac-\xe4\xb8\x81";
|
||||
static const wchar_t wide[] = L"C:/cbm-cache-\u0394-\u65e5\u672c-\u4e01";
|
||||
ASSERT_EQ(cbm_setenv("CBM_CACHE_DIR", utf8, 1), 0);
|
||||
wchar_t observed_wide[128];
|
||||
ASSERT_EQ(GetEnvironmentVariableW(L"CBM_CACHE_DIR", observed_wide, 128), (DWORD)(wcslen(wide)));
|
||||
@@ -956,6 +956,14 @@ TEST(platform_setenv_preserves_utf8_in_wide_environment) {
|
||||
char observed_utf8[128];
|
||||
ASSERT_NOT_NULL(cbm_safe_getenv("CBM_CACHE_DIR", observed_utf8, sizeof(observed_utf8), NULL));
|
||||
ASSERT_STR_EQ(observed_utf8, utf8);
|
||||
/* Narrow getenv must agree with the CRT's active ANSI code page. */
|
||||
char expected_narrow[128];
|
||||
ASSERT_GT(WideCharToMultiByte(CP_ACP, 0, wide, -1, expected_narrow,
|
||||
(int)sizeof(expected_narrow), NULL, NULL),
|
||||
0);
|
||||
const char *observed_narrow = getenv("CBM_CACHE_DIR");
|
||||
ASSERT_NOT_NULL(observed_narrow);
|
||||
ASSERT_STR_EQ(observed_narrow, expected_narrow);
|
||||
(void)cbm_unsetenv("CBM_CACHE_DIR");
|
||||
PASS();
|
||||
}
|
||||
|
||||
+26
-6
@@ -1245,17 +1245,36 @@ TEST(go_bare_ref_never_binds_field) {
|
||||
* trailing name; is_reference_node records the inner field_identifier),
|
||||
* so the selector-vs-bare distinction arrives as the recorded
|
||||
* is_member_access signal, never as a dot in the reference text. */
|
||||
ASSERT_TRUE(cbm_go_suppress_bare_field_ref(true, false, "Field"));
|
||||
ASSERT_TRUE(cbm_suppress_bare_field_ref(CBM_LANG_GO, false, "Field"));
|
||||
/* The member half of a selector may bind a field. */
|
||||
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, true, "Field"));
|
||||
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_GO, true, "Field"));
|
||||
/* Bare references to non-fields are untouched. */
|
||||
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, false, "Variable"));
|
||||
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, false, "Function"));
|
||||
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_GO, false, "Variable"));
|
||||
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_GO, false, "Function"));
|
||||
/* Other languages reference their own members bare inside methods —
|
||||
* never suppressed (cp_reads_writes_cs_static_field pins the C# shape). */
|
||||
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(false, false, "Field"));
|
||||
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_CSHARP, false, "Field"));
|
||||
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_JAVA, false, "Field"));
|
||||
/* Degenerate input → nothing to judge. */
|
||||
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, false, NULL));
|
||||
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_GO, false, NULL));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(c_bare_ref_never_binds_field) {
|
||||
/* #2666: a C struct member is named only as the member half of `a.b`,
|
||||
* `a->b` or a designator `.b` (field_identifier → is_member_access). A
|
||||
* bare C identifier — a macro argument like RUN_SELECTED_SUITE(mcp) — is
|
||||
* never one, so it must not bind a same-named Field. */
|
||||
ASSERT_TRUE(cbm_suppress_bare_field_ref(CBM_LANG_C, false, "Field"));
|
||||
/* The member half still binds (through the C owner rows upstream). */
|
||||
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_C, true, "Field"));
|
||||
/* A bare C name keeps binding globals, functions and types. */
|
||||
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_C, false, "Variable"));
|
||||
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_C, false, "Function"));
|
||||
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_C, false, "Type"));
|
||||
/* C++ (and every `.h`, parsed as C++): a method body names its own
|
||||
* members bare, so the bind is kept. */
|
||||
ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_CPP, false, "Field"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
@@ -2571,6 +2590,7 @@ SUITE(registry) {
|
||||
RUN_TEST(registry_tie_prefers_csharp_implementation_over_ref_stub);
|
||||
RUN_TEST(cross_language_ref_drops_go_vs_c);
|
||||
RUN_TEST(go_bare_ref_never_binds_field);
|
||||
RUN_TEST(c_bare_ref_never_binds_field);
|
||||
RUN_TEST(call_onto_field_follows_language_and_shape);
|
||||
RUN_TEST(c_bare_call_does_not_weigh_field_namesakes);
|
||||
RUN_TEST(c_arrow_member_call_is_c_family_and_arrow_only);
|
||||
|
||||
@@ -2787,6 +2787,125 @@ TEST(tslsp_crossfile_chain_through_return) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The call target and its return signature must use the same exported name,
|
||||
* even when the module also exports a function named like the local alias. */
|
||||
static int ts_renamed_import_return_chain(bool shared_registry) {
|
||||
const char *source = "import { make as alias } from './lib';\n"
|
||||
"function go() { alias().read(); }\n";
|
||||
CBMLSPDef defs[] = {
|
||||
{.qualified_name = "test.main.go",
|
||||
.short_name = "go",
|
||||
.label = "Function",
|
||||
.lang = CBM_LANG_TYPESCRIPT,
|
||||
.def_module_qn = "test.main"},
|
||||
{.qualified_name = "test.lib.Good",
|
||||
.short_name = "Good",
|
||||
.label = "Class",
|
||||
.lang = CBM_LANG_TYPESCRIPT,
|
||||
.def_module_qn = "test.lib",
|
||||
.method_names_str = "read"},
|
||||
{.qualified_name = "test.lib.Good.read",
|
||||
.short_name = "read",
|
||||
.label = "Method",
|
||||
.lang = CBM_LANG_TYPESCRIPT,
|
||||
.def_module_qn = "test.lib",
|
||||
.receiver_type = "test.lib.Good"},
|
||||
{.qualified_name = "test.lib.Bad",
|
||||
.short_name = "Bad",
|
||||
.label = "Class",
|
||||
.lang = CBM_LANG_TYPESCRIPT,
|
||||
.def_module_qn = "test.lib",
|
||||
.method_names_str = "read"},
|
||||
{.qualified_name = "test.lib.Bad.read",
|
||||
.short_name = "read",
|
||||
.label = "Method",
|
||||
.lang = CBM_LANG_TYPESCRIPT,
|
||||
.def_module_qn = "test.lib",
|
||||
.receiver_type = "test.lib.Bad"},
|
||||
{.qualified_name = "test.lib.make",
|
||||
.short_name = "make",
|
||||
.label = "Function",
|
||||
.lang = CBM_LANG_TYPESCRIPT,
|
||||
.def_module_qn = "test.lib",
|
||||
.return_types = "test.lib.Good"},
|
||||
{.qualified_name = "test.lib.alias",
|
||||
.short_name = "alias",
|
||||
.label = "Function",
|
||||
.lang = CBM_LANG_TYPESCRIPT,
|
||||
.def_module_qn = "test.lib",
|
||||
.return_types = "test.lib.Bad"},
|
||||
};
|
||||
enum { NDEFS = (int)(sizeof(defs) / sizeof(defs[0])) };
|
||||
const char *imp_names[] = {"alias"};
|
||||
const char *imp_qns[] = {"test.lib"};
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMResolvedCallArray out = {0};
|
||||
if (shared_registry) {
|
||||
CBMTypeRegistry *reg = cbm_ts_build_cross_registry(&arena, defs, NDEFS);
|
||||
ASSERT_NOT_NULL(reg);
|
||||
cbm_run_ts_lsp_cross_with_registry(&arena, source, (int)strlen(source), "test.main", false,
|
||||
false, false, reg, defs, NDEFS, imp_names, imp_qns, 1,
|
||||
NULL, &out);
|
||||
} else {
|
||||
cbm_run_ts_lsp_cross(&arena, source, (int)strlen(source), "test.main", false, false, false,
|
||||
defs, NDEFS, imp_names, imp_qns, 1, NULL, &out);
|
||||
}
|
||||
bool direct = has_exact_resolved(&out, "test.main.go", "test.lib.make", "lsp_import_alias");
|
||||
bool good = has_exact_resolved(&out, "test.main.go", "test.lib.Good.read", "lsp_ts_method");
|
||||
bool bad = has_exact_resolved(&out, "test.main.go", "test.lib.Bad.read", "lsp_ts_method");
|
||||
cbm_arena_destroy(&arena);
|
||||
ASSERT_TRUE(direct);
|
||||
ASSERT_FALSE(bad);
|
||||
ASSERT_TRUE(good);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(tslsp_crossfile_renamed_import_return_chain) {
|
||||
return ts_renamed_import_return_chain(false);
|
||||
}
|
||||
|
||||
TEST(tslsp_crossfile_renamed_import_return_chain_shared) {
|
||||
return ts_renamed_import_return_chain(true);
|
||||
}
|
||||
|
||||
/* Keep the supplied import map at one entry: this measures the AST alias
|
||||
* collector independently of the older import-map insertion path. */
|
||||
static size_t ts_import_alias_arena_bytes(int count) {
|
||||
size_t capacity = (size_t)count * 48 + 64;
|
||||
char *source = malloc(capacity);
|
||||
if (!source)
|
||||
return 0;
|
||||
size_t len = (size_t)snprintf(source, capacity, "import { ");
|
||||
for (int i = 0; i < count; i++) {
|
||||
len += (size_t)snprintf(source + len, capacity - len, "%sf%d as a%d", i ? ", " : "", i, i);
|
||||
}
|
||||
len += (size_t)snprintf(source + len, capacity - len, " } from './lib';\n");
|
||||
const char *imp_names[] = {"a0"};
|
||||
const char *imp_qns[] = {"test.lib"};
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMResolvedCallArray out = {0};
|
||||
cbm_run_ts_lsp_cross(&arena, source, (int)len, "test.main", false, false, false, NULL, 0,
|
||||
imp_names, imp_qns, 1, NULL, &out);
|
||||
size_t bytes = cbm_arena_total(&arena);
|
||||
cbm_arena_destroy(&arena);
|
||||
free(source);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
TEST(tslsp_import_alias_arena_growth) {
|
||||
size_t small = ts_import_alias_arena_bytes(1024);
|
||||
size_t large = ts_import_alias_arena_bytes(2048);
|
||||
ASSERT_GT(small, 0);
|
||||
ASSERT_GT(large, small);
|
||||
printf(" aliases arena bytes: 1024=%zu 2048=%zu ", small, large);
|
||||
/* Linear source/AST work plus geometric storage stays below 3x; retaining
|
||||
* two complete arrays at every insertion grows approximately 4x. */
|
||||
ASSERT(large < small * 3);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(tslsp_crossfile_no_def_match) {
|
||||
/* Function that imports a symbol not in the def list — should resolve safely
|
||||
* (emit unresolved, no crash). */
|
||||
@@ -2815,6 +2934,132 @@ TEST(tslsp_crossfile_no_def_match) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Issue #2297: `import { foo as fooShared }` bound only the local alias, and
|
||||
* every import lookup spelled `<module>.<alias>`, which names nothing, so a call
|
||||
* through a renamed import produced no CALLS edge. The binding resolves to the
|
||||
* name the module exports, exactly like a plain named import. Driven through
|
||||
* the production Tier-2 path (shared sealed registry + per-file overlay). */
|
||||
TEST(tslsp_crossfile_renamed_import_issue2297) {
|
||||
CBMLSPDef defs[] = {
|
||||
{.qualified_name = "test.lib.foo",
|
||||
.short_name = "foo",
|
||||
.label = "Function",
|
||||
.lang = CBM_LANG_TYPESCRIPT,
|
||||
.def_module_qn = "test.lib",
|
||||
.return_types = "number"},
|
||||
{.qualified_name = "test.conn.Conn",
|
||||
.short_name = "Conn",
|
||||
.label = "Class",
|
||||
.lang = CBM_LANG_TYPESCRIPT,
|
||||
.def_module_qn = "test.conn",
|
||||
.method_names_str = "ping"},
|
||||
{.qualified_name = "test.conn.Conn.ping",
|
||||
.short_name = "ping",
|
||||
.label = "Method",
|
||||
.lang = CBM_LANG_TYPESCRIPT,
|
||||
.def_module_qn = "test.conn",
|
||||
.receiver_type = "test.conn.Conn"},
|
||||
{.qualified_name = "test.main.callsRenamed",
|
||||
.short_name = "callsRenamed",
|
||||
.label = "Function",
|
||||
.lang = CBM_LANG_TYPESCRIPT,
|
||||
.def_module_qn = "test.main"},
|
||||
{.qualified_name = "test.main.viaRenamedClass",
|
||||
.short_name = "viaRenamedClass",
|
||||
.label = "Function",
|
||||
.lang = CBM_LANG_TYPESCRIPT,
|
||||
.def_module_qn = "test.main"},
|
||||
};
|
||||
enum { NDEFS = (int)(sizeof(defs) / sizeof(defs[0])) };
|
||||
const char *imp_names[] = {"fooShared", "Connection"};
|
||||
const char *imp_qns[] = {"test.lib", "test.conn"};
|
||||
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMTypeRegistry *reg = cbm_ts_build_cross_registry(&arena, defs, NDEFS);
|
||||
ASSERT_NOT_NULL(reg);
|
||||
|
||||
const char *src = "import { foo as fooShared } from './lib';\n"
|
||||
"import { Conn as Connection } from './conn';\n"
|
||||
"export function callsRenamed() { return fooShared(1); }\n"
|
||||
"export function viaRenamedClass() { const c = new Connection(); c.ping(); }\n";
|
||||
CBMResolvedCallArray out = {0};
|
||||
cbm_run_ts_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.main", false, false,
|
||||
false, reg, defs, NDEFS, imp_names, imp_qns, 2, NULL, &out);
|
||||
/* The call site names the alias, so the join reads it from `reason`. */
|
||||
ASSERT_TRUE(
|
||||
has_exact_resolved(&out, "test.main.callsRenamed", "test.lib.foo", "lsp_import_alias"));
|
||||
ASSERT_TRUE(has_exact_resolved(&out, "test.main.viaRenamedClass", "test.conn.Conn.ping",
|
||||
"lsp_ts_method"));
|
||||
|
||||
cbm_arena_destroy(&arena);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The export name is looked up exactly. A renamed import of a name the module
|
||||
* does not export must not fall back to the alias: before #2297 the alias `foo`
|
||||
* spelled `test.lib.foo` and bound this call to an unrelated export. */
|
||||
TEST(tslsp_crossfile_renamed_import_missing_export_issue2297) {
|
||||
const char *source = "import { missing as foo } from './lib';\n"
|
||||
"function go() { foo(1); }\n";
|
||||
CBMLSPDef defs[] = {
|
||||
{.qualified_name = "test.main.go",
|
||||
.short_name = "go",
|
||||
.label = "Function",
|
||||
.def_module_qn = "test.main"},
|
||||
{.qualified_name = "test.lib.foo",
|
||||
.short_name = "foo",
|
||||
.label = "Function",
|
||||
.def_module_qn = "test.lib"},
|
||||
};
|
||||
const char *imp_names[] = {"foo"};
|
||||
const char *imp_qns[] = {"test.lib"};
|
||||
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMResolvedCallArray out = {0};
|
||||
|
||||
cbm_run_ts_lsp_cross(&arena, source, (int)strlen(source), "test.main", false, false, false,
|
||||
defs, 2, imp_names, imp_qns, 1, NULL, &out);
|
||||
|
||||
ASSERT_EQ(find_resolved_arr(&out, "go", "test.lib.foo"), -1);
|
||||
|
||||
cbm_arena_destroy(&arena);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* `import { default as greet }` is a default import: it keeps resolving by its
|
||||
* local name, as `import greet from` does. */
|
||||
TEST(tslsp_crossfile_default_as_import_issue2297) {
|
||||
const char *source = "import { default as greet } from './greetings';\n"
|
||||
"function go() { greet('hi'); }\n";
|
||||
CBMLSPDef defs[] = {
|
||||
{.qualified_name = "test.main.go",
|
||||
.short_name = "go",
|
||||
.label = "Function",
|
||||
.def_module_qn = "test.main"},
|
||||
{.qualified_name = "test.greetings.greet",
|
||||
.short_name = "greet",
|
||||
.label = "Function",
|
||||
.def_module_qn = "test.greetings",
|
||||
.return_types = "string"},
|
||||
};
|
||||
const char *imp_names[] = {"greet"};
|
||||
const char *imp_qns[] = {"test.greetings"};
|
||||
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
CBMResolvedCallArray out = {0};
|
||||
|
||||
cbm_run_ts_lsp_cross(&arena, source, (int)strlen(source), "test.main", false, false, false,
|
||||
defs, 2, imp_names, imp_qns, 1, NULL, &out);
|
||||
|
||||
ASSERT_GTE(find_resolved_arr(&out, "go", "test.greetings.greet"), 0);
|
||||
|
||||
cbm_arena_destroy(&arena);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── More stdlib (Date, RegExp, Error, Math) ──────────────────────────────── */
|
||||
|
||||
TEST(tslsp_stdlib_date_toISOString) {
|
||||
@@ -5243,7 +5488,13 @@ SUITE(ts_lsp) {
|
||||
RUN_TEST(tslsp_scale_many_defs_no_crash_issue344);
|
||||
RUN_TEST(tslsp_crossfile_function_call);
|
||||
RUN_TEST(tslsp_crossfile_chain_through_return);
|
||||
RUN_TEST(tslsp_crossfile_renamed_import_return_chain);
|
||||
RUN_TEST(tslsp_crossfile_renamed_import_return_chain_shared);
|
||||
RUN_TEST(tslsp_import_alias_arena_growth);
|
||||
RUN_TEST(tslsp_crossfile_no_def_match);
|
||||
RUN_TEST(tslsp_crossfile_renamed_import_issue2297);
|
||||
RUN_TEST(tslsp_crossfile_renamed_import_missing_export_issue2297);
|
||||
RUN_TEST(tslsp_crossfile_default_as_import_issue2297);
|
||||
|
||||
/* More stdlib */
|
||||
RUN_TEST(tslsp_stdlib_date_toISOString);
|
||||
|
||||
Reference in new issue
Block a user