diff --git a/internal/cbm/cbm.c b/internal/cbm/cbm.c index 23b871dbc..1156a6a73 100644 --- a/internal/cbm/cbm.c +++ b/internal/cbm/cbm.c @@ -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 */ } diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index ef5fad51f..98d403975 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -314,6 +314,12 @@ typedef struct { * change of all, store_impact.h follow_variant_keys). NULL on every other * definition. Tail field: zero-init stays valid. */ const char *variant_key; + /* Go only (#2033): the raw aliased type text of an identity alias + * `type A = B` ("B"). Carried to the cross-file registries so a method + * call on A from another file follows the alias to B's method set. NULL + * for a defined type `type A B` (its method set is its own) and in every + * other language. Tail field: zero-init stays valid. */ + const char *alias_target; } CBMDefinition; /* extra_props key of a generated member, one the compiler writes and the diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index f266f8c1e..93f606a50 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -8300,6 +8300,15 @@ static void emit_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *s def.decl_namespace = cs_type_decl_namespace(a, node, ctx->source); } def.base_classes = extract_base_classes(a, node, ctx->source, ctx->language); + // Go identity alias `type A = B` (#2033): keep the aliased type text so the + // cross-file registries can set alias_of. A defined type `type A B` is a + // type_spec and never gets here with this kind. + if (ctx->language == CBM_LANG_GO && strcmp(kind, "type_alias") == 0) { + TSNode target = ts_node_child_by_field_name(node, TS_FIELD("type")); + if (!ts_node_is_null(target)) { + def.alias_target = cbm_node_text(a, target, ctx->source); + } + } def.decorators = extract_decorators(a, node, ctx->source, ctx->language, spec); def.docstring = extract_docstring(ctx, node, name); diff --git a/internal/cbm/lsp/go_lsp.c b/internal/cbm/lsp/go_lsp.c index 34fad4b9e..dd9572747 100644 --- a/internal/cbm/lsp/go_lsp.c +++ b/internal/cbm/lsp/go_lsp.c @@ -2705,6 +2705,27 @@ static const char **split_pipe_strings(CBMArena *a, const char *text) { return idx > 0 ? arr : NULL; } +/* QN of the type an identity alias `type A = B` names, for the def-driven + * cross-file registries (#2033). The per-file AST scan sets alias_of only for + * the file it walks, so an alias declared in another file of the package + * reaches a call only through here. Only a bare identifier is qualified (B + * declared in A's package); a qualified, pointer, composite or generic target + * stays NULL rather than guessed. */ +static const char *go_alias_target_qn(CBMArena *arena, const char *target, + const char *def_module_qn) { + if (!target || !target[0] || !def_module_qn || !def_module_qn[0]) { + return NULL; + } + for (const unsigned char *p = (const unsigned char *)target; *p; p++) { + bool ident = (*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9') || *p == '_' || *p >= 0x80; + if (!ident) { + return NULL; + } + } + return cbm_arena_sprintf(arena, "%s.%s", def_module_qn, target); +} + // Helper: parse "|"-separated "name:type" field definitions and populate a registered type. // Format: "Binder:Binder|Name:string|Count:int" // type_text is resolved relative to def_module_qn. @@ -3273,6 +3294,7 @@ void cbm_run_go_lsp_cross(CBMArena *arena, const char *source, int source_len, if (rt.is_interface && d->method_names_str && d->method_names_str[0]) { rt.method_names = split_pipe_strings(arena, d->method_names_str); } + rt.alias_of = go_alias_target_qn(arena, d->alias_target, def_mod); cbm_registry_add_type(®, rt); @@ -3569,6 +3591,7 @@ CBMTypeRegistry *cbm_go_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, i if (rt.is_interface && d->method_names_str && d->method_names_str[0]) { rt.method_names = split_pipe_strings(arena, d->method_names_str); } + rt.alias_of = go_alias_target_qn(arena, d->alias_target, def_mod); cbm_registry_add_type(reg, rt); GO_NOTE_NEW_TYPES(); if (d->field_defs && d->field_defs[0]) { diff --git a/internal/cbm/lsp/go_lsp.h b/internal/cbm/lsp/go_lsp.h index 5874cbeef..9e742a715 100644 --- a/internal/cbm/lsp/go_lsp.h +++ b/internal/cbm/lsp/go_lsp.h @@ -102,6 +102,10 @@ typedef struct { /* Callable identity (#2061): signature suffix of this def's graph QN; * qualified_name stays the base QN. NULL = none (always, until enabled). */ const char *sig; + /* Go only (#2033): raw aliased type text of `type A = B` ("B"), qualified + * by the registrar against def_module_qn like return_types. NULL = not an + * identity alias. */ + const char *alias_target; } CBMLSPDef; // Parse source, build registry from defs + stdlib, run LSP. diff --git a/internal/cbm/lsp/ts_lsp.c b/internal/cbm/lsp/ts_lsp.c index b7d966c63..25d0b2f12 100644 --- a/internal/cbm/lsp/ts_lsp.c +++ b/internal/cbm/lsp/ts_lsp.c @@ -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 `.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, ®); diff --git a/internal/cbm/lsp/ts_lsp.h b/internal/cbm/lsp/ts_lsp.h index fc67895b3..b61c2ff73 100644 --- a/internal/cbm/lsp/ts_lsp.h +++ b/internal/cbm/lsp/ts_lsp.h @@ -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 diff --git a/internal/cbm/result_compact.c b/internal/cbm/result_compact.c index 76606ac51..46bf6eae1 100644 --- a/internal/cbm/result_compact.c +++ b/internal/cbm/result_compact.c @@ -279,6 +279,7 @@ static void cr_walk_def(cr_ctx_t *c, CBMDefinition *d) { cr_list(c, &d->extra_props); cr_str(c, &d->decl_namespace); cr_str(c, &d->variant_key); + cr_str(c, &d->alias_target); } static void cr_walk_call(cr_ctx_t *c, CBMCall *call) { diff --git a/src/cli/cli.c b/src/cli/cli.c index 95beda3f4..3db818c28 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -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); diff --git a/src/foundation/compat.h b/src/foundation/compat.h index 821d901f0..3080930f8 100644 --- a/src/foundation/compat.h +++ b/src/foundation/compat.h @@ -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; } diff --git a/src/pipeline/lsp_surface.c b/src/pipeline/lsp_surface.c index 308fcf959..f8d43ff69 100644 --- a/src/pipeline/lsp_surface.c +++ b/src/pipeline/lsp_surface.c @@ -34,8 +34,10 @@ #include "yyjson/yyjson.h" /* 3: Go defs carry their parsed result list `|`-joined in "ret" (F-G8 D1); - * a version-2 surface holds the raw result text and must be re-extracted. */ -enum { SURFACE_CODEC_VERSION = 3 }; + * a version-2 surface holds the raw result text and must be re-extracted. + * 4: Go identity aliases carry their target in "al" (#2033); a version-3 + * surface has no alias targets and must be re-extracted. */ +enum { SURFACE_CODEC_VERSION = 4 }; /* Labels the incremental name registry serves that pxc_map_label does NOT * carry into the CBMLSPDef set. Their (name, qn, label) triple must still @@ -394,6 +396,7 @@ static char *surface_file_to_json(const CBMFileResult *result, CBMLanguage langu yyjson_mut_obj_add_bool(doc, o, "ir", d->is_rust_impl_relation); yyjson_mut_obj_add_bool(doc, o, "ab", d->is_abstract); add_str_array_or_null(doc, o, "dec", d->decorators, -1); + add_str_or_null(doc, o, "al", d->alias_target); yyjson_mut_arr_add_val(lsp, o); } yyjson_mut_obj_add_val(doc, root, "lsp", lsp); @@ -670,6 +673,7 @@ int cbm_lsp_surface_defs_from_json(CBMArena *arena, const char *defs_json, CBMLS d->is_abstract = yyjson_get_bool(yyjson_obj_get(o, "ab")); int dec_count = 0; d->decorators = arena_str_array(arena, yyjson_obj_get(o, "dec"), true, &dec_count); + d->alias_target = arena_str_or_null(arena, yyjson_obj_get(o, "al")); if (!d->qualified_name || !d->short_name || !d->label) { /* A def the writer could not have produced: qn/sn/label are * unconditionally present in build_lsp_def output. Corrupt row. */ diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index 5aa955112..a057f9a7b 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -715,6 +715,9 @@ static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const ch } } dst->lang = lang; + dst->alias_target = (lang == CBM_LANG_GO && src->alias_target) + ? cbm_arena_strdup(arena, src->alias_target) + : NULL; dst->decorators = NULL; if (src->decorators) { int n = 0; diff --git a/src/pipeline/pipeline.c b/src/pipeline/pipeline.c index 56e00e202..2727e41d1 100644 --- a/src/pipeline/pipeline.c +++ b/src/pipeline/pipeline.c @@ -2559,7 +2559,7 @@ static void log_result_census(const char *tag, CBMFileResult **cache, int file_c str_def_misc += census_len(def->route_path) + census_len(def->route_method) + census_len(def->impl_trait) + census_len(def->http_base_url) + census_list(def->extra_props) + census_len(def->decl_namespace) + - census_len(def->variant_key); + census_len(def->variant_key) + census_len(def->alias_target); } for (int c = 0; c < r->calls.count; c++) { const CBMCall *call = &r->calls.items[c]; diff --git a/tests/test_extraction.c b/tests/test_extraction.c index cb71c7cce..286892cfb 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -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) */ @@ -13339,7 +13340,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); diff --git a/tests/test_go_lsp.c b/tests/test_go_lsp.c index e852f4aca..b79101c07 100644 --- a/tests/test_go_lsp.c +++ b/tests/test_go_lsp.c @@ -1378,6 +1378,127 @@ TEST(golsp_crossfile_aliased_field_requal) { PASS(); } +/* #2033: an identity alias declared in another file of the same package. + * `type FeatureSet = bitmap` lives in one file, `bitmap.Set` in a second, the + * call `reqs.Set` in a third, so none of it is in the calling file's AST and + * only the defs can carry the alias. A defined type (`type Defined bitmap`) + * must not inherit bitmap's methods. The decoy `other.Set` keeps a name-only + * match from passing. Both cross-file registries are checked: the per-file + * one and the prebuilt one shared by the parallel resolve. */ +static CBMLSPDef go_crossfile_alias_defs[] = { + {.qualified_name = "test.main.bitmap", + .short_name = "bitmap", + .label = "Class", + .def_module_qn = "test.main"}, + {.qualified_name = "test.main.bitmap.Set", + .short_name = "Set", + .label = "Method", + .def_module_qn = "test.main", + .receiver_type = "test.main.bitmap"}, + {.qualified_name = "test.main.FeatureSet", + .short_name = "FeatureSet", + .label = "Type", + .def_module_qn = "test.main", + .alias_target = "bitmap"}, + {.qualified_name = "test.main.Defined", + .short_name = "Defined", + .label = "Class", + .def_module_qn = "test.main"}, + {.qualified_name = "test.main.Framework", + .short_name = "Framework", + .label = "Struct", + .def_module_qn = "test.main"}, + {.qualified_name = "test.main.Framework.NewFeatureSet", + .short_name = "NewFeatureSet", + .label = "Method", + .def_module_qn = "test.main", + .receiver_type = "test.main.Framework", + .return_types = "FeatureSet"}, + {.qualified_name = "test.main.Framework.NewDefined", + .short_name = "NewDefined", + .label = "Method", + .def_module_qn = "test.main", + .receiver_type = "test.main.Framework", + .return_types = "Defined"}, + {.qualified_name = "test.main.other", + .short_name = "other", + .label = "Struct", + .def_module_qn = "test.main"}, + {.qualified_name = "test.main.other.Set", + .short_name = "Set", + .label = "Method", + .def_module_qn = "test.main", + .receiver_type = "test.main.other"}, +}; + +static const char *go_crossfile_alias_source = "package main\n\n" + "func useAlias(f *Framework) {\n" + "\treqs := f.NewFeatureSet()\n" + "\treqs.Set(3)\n}\n\n" + "func useDefined(f *Framework) {\n" + "\td := f.NewDefined()\n" + "\td.Set(3)\n}\n"; + +static int go_crossfile_alias_check(const CBMResolvedCallArray *out) { + int idx = find_resolved_arr_confident(out, "useAlias", ".Set"); + if (idx < 0 || strcmp(out->items[idx].callee_qn, "test.main.bitmap.Set") != 0) { + printf(" resolved calls (%d):\n", out->count); + for (int i = 0; i < out->count; i++) { + const CBMResolvedCall *rc = &out->items[i]; + printf(" %s -> %s [%s %.2f]\n", rc->caller_qn ? rc->caller_qn : "(null)", + rc->callee_qn ? rc->callee_qn : "(null)", rc->strategy ? rc->strategy : "(null)", + rc->confidence); + } + } + ASSERT_GTE(idx, 0); + ASSERT_STR_EQ(out->items[idx].callee_qn, "test.main.bitmap.Set"); + /* Reached through alias_of, so the emitter reports it the way it reports + * a same-file alias today (lsp_embed_dispatch): typed, not a name guess. */ + ASSERT_TRUE(strncmp(out->items[idx].strategy, "lsp_", 4) == 0); + ASSERT_TRUE(strcmp(out->items[idx].strategy, "lsp_unresolved") != 0); + ASSERT_TRUE(out->items[idx].confidence >= 0.9f); + /* A defined type has its own (empty) method set. */ + ASSERT_EQ(find_resolved_arr_confident(out, "useDefined", "bitmap.Set"), -1); + return 0; +} + +TEST(golsp_crossfile_type_alias_method) { + int n = (int)(sizeof(go_crossfile_alias_defs) / sizeof(go_crossfile_alias_defs[0])); + CBMArena arena; + cbm_arena_init(&arena); + CBMResolvedCallArray out = {0}; + + cbm_run_go_lsp_cross(&arena, go_crossfile_alias_source, (int)strlen(go_crossfile_alias_source), + "test.main", go_crossfile_alias_defs, n, NULL, NULL, 0, NULL, &out); + if (go_crossfile_alias_check(&out) != 0) { + cbm_arena_destroy(&arena); + return 1; + } + + cbm_arena_destroy(&arena); + PASS(); +} + +TEST(golsp_crossfile_type_alias_method_prebuilt) { + int n = (int)(sizeof(go_crossfile_alias_defs) / sizeof(go_crossfile_alias_defs[0])); + CBMArena arena; + cbm_arena_init(&arena); + CBMResolvedCallArray out = {0}; + + CBMTypeRegistry *reg = cbm_go_build_cross_registry(&arena, go_crossfile_alias_defs, n); + ASSERT_NOT_NULL(reg); + cbm_run_go_lsp_cross_with_registry(&arena, go_crossfile_alias_source, + (int)strlen(go_crossfile_alias_source), "test.main", reg, + NULL, NULL, 0, NULL, &out); + if (go_crossfile_alias_check(&out) != 0) { + cbm_arena_destroy(&arena); + return 1; + } + + cbm_arena_destroy(&arena); + PASS(); +} + TEST(golsp_crossfile_local_interface_single_impl) { const char *source = "package main\n\n" @@ -1542,6 +1663,8 @@ SUITE(go_lsp) { RUN_TEST(golsp_crossfile_interface_dispatch); RUN_TEST(golsp_crossfile_interface_field_chain); RUN_TEST(golsp_crossfile_aliased_field_requal); + RUN_TEST(golsp_crossfile_type_alias_method); + RUN_TEST(golsp_crossfile_type_alias_method_prebuilt); RUN_TEST(golsp_crossfile_map_index); RUN_TEST(golsp_crossfile_stdlib_interface); RUN_TEST(golsp_crossfile_local_interface_single_impl); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index ddc119e57..4dc97b2cb 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -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) { @@ -10931,6 +11029,101 @@ TEST(pipeline_go_multi_return_receiver_is_typed_parallel) { return go_multi_return_case(52); } +/* #2033: an identity alias declared in one file is followed from another file + * of the same package. `type FeatureSet = bitmap` (featureset.go), the method + * `bitmap.Set` (bitmap.go) and the call `reqs.Set(3)` (framework.go) are three + * files, so the alias reaches the call only through the cross-file defs. The + * same-package decoy `other.Set` means the call cannot bind by name. Before the + * fix the call had no CALLS edge at all. pad_files > 0 forces the parallel + * resolver and its prebuilt registry. */ +static int go_crossfile_type_alias_case(int pad_files) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_alias_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + write_temp_file(tmp, "go.mod", "module example.com/fx\n\ngo 1.22\n"); + write_temp_file(tmp, "fs/bitmap.go", + "package fs\n" + "\n" + "type bitmap []uint64\n" + "\n" + "func (b bitmap) Set(i int) { b[i/64] |= 1 << (i % 64) }\n"); + write_temp_file(tmp, "fs/featureset.go", + "package fs\n" + "\n" + "type FeatureSet = bitmap\n"); + write_temp_file( + tmp, "fs/framework.go", + "package fs\n" + "\n" + "type Framework struct{}\n" + "\n" + "func (f *Framework) NewFeatureSet() FeatureSet { return make(FeatureSet, 1) }\n" + "\n" + "func Use(f *Framework) {\n" + "\treqs := f.NewFeatureSet()\n" + "\treqs.Set(3)\n" + "}\n"); + write_temp_file(tmp, "fs/other.go", + "package fs\n" + "\n" + "type other struct{}\n" + "\n" + "func (o other) Set(i int) {}\n"); + for (int i = 0; i < pad_files; i++) { + char name[64]; + char body[128]; + snprintf(name, sizeof(name), "filler/filler%d.go", i); + snprintf(body, sizeof(body), "package filler\n\nfunc Filler%d() int { return %d }\n", i, i); + write_temp_file(tmp, name, body); + } + char *saved = NULL; + if (pad_files > 0) { + char *old_workers = getenv("CBM_WORKERS"); + saved = old_workers ? strdup(old_workers) : NULL; + cbm_setenv("CBM_WORKERS", "4", 1); + } + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/go_alias.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + int rc = p ? cbm_pipeline_run(p) : -1; + if (pad_files > 0) { + if (saved) { + cbm_setenv("CBM_WORKERS", saved, 1); + free(saved); + } else { + cbm_unsetenv("CBM_WORKERS"); + } + } + ASSERT_NOT_NULL(p); + ASSERT_EQ(rc, 0); + const char *project = cbm_pipeline_project_name(p); + cbm_store_t *s = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(s); + + int right = named_edge_to_file_count(s, project, "CALLS", "Use", "Set", "fs/bitmap.go"); + int decoy = named_edge_to_file_count(s, project, "CALLS", "Use", "Set", "fs/other.go"); + bool typed = cross_file_call_has_strategy(s, project, "Use", "Set", "\"strategy\":\"lsp_"); + if (right != 1 || decoy != 0 || !typed) { + printf(" Use -> Set: right=%d decoy=%d lsp=%d\n", right, decoy, typed); + FAIL("call through a cross-file type alias not bound to the aliased type's method"); + } + + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + PASS(); +} + +TEST(pipeline_go_crossfile_type_alias_method) { + return go_crossfile_type_alias_case(0); +} + +TEST(pipeline_go_crossfile_type_alias_method_parallel) { + return go_crossfile_type_alias_case(52); +} + TEST(pipeline_go_rw_usage_never_cross_into_c) { /* #1928: USAGE and WRITES/READS resolve through the same short-name * registry as CALLS but never consulted the #725 cross-language guard. @@ -26178,7 +26371,7 @@ TEST(pipeline_python_namespace_change_admission) { } int body = -9, signature = -9, alias = -9, order = -9; int empty = -9, literal = -9, unknown = -9, deletion = -9, transition = -9; - const char *non_python = "{\"v\":3,\"lang\":0,\"lsp\":[],\"reg\":[],\"py\":null}"; + const char *non_python = "{\"v\":4,\"lang\":0,\"lsp\":[],\"reg\":[],\"py\":null}"; if (complete) { body = cbm_lsp_surface_python_change(json[0], json[1]); signature = cbm_lsp_surface_python_change(json[0], json[2]); @@ -26309,24 +26502,24 @@ TEST(pipeline_python_namespace_codec_rejects_non_authoritative_rows) { * it re-extracts in full. */ const char *bad[] = { "{\"v\":1,\"lsp\":[]}", - "{\"v\":3,\"lang\":1,\"lsp\":[]}", - "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":null}", - "{\"v\":3,\"lang\":1.0,\"lsp\":[],\"py\":null}", - "{\"v\":3,\"v\":3,\"lang\":0,\"lsp\":[],\"py\":null}", - "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," + "{\"v\":4,\"lang\":1,\"lsp\":[]}", + "{\"v\":4,\"lang\":1,\"lsp\":[],\"py\":null}", + "{\"v\":4,\"lang\":1.0,\"lsp\":[],\"py\":null}", + "{\"v\":4,\"v\":4,\"lang\":0,\"lsp\":[],\"py\":null}", + "{\"v\":4,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\",\"status\":3,\"failure\":2,\"events\":[]}}", - "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":1,\"lang\":1," + "{\"v\":4,\"lang\":1,\"lsp\":[],\"py\":{\"v\":1,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":[]}}", - "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":99,\"lang\":1," + "{\"v\":4,\"lang\":1,\"lsp\":[],\"py\":{\"v\":99,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":[]}}", - "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," + "{\"v\":4,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\\u0000suffix\",\"status\":2," "\"failure\":0,\"events\":[]}}", - "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," + "{\"v\":4,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":[" "{\"k\":999,\"d\":0,\"s\":0,\"r\":0,\"f\":0,\"l\":0," "\"n\":null,\"m\":null,\"i\":null,\"a\":[]}]}}", - "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," + "{\"v\":4,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":[" "{\"k\":8,\"d\":0,\"s\":1,\"r\":0,\"f\":0,\"l\":4294967296," "\"n\":null,\"m\":null,\"i\":null,\"a\":[]}]}}", @@ -26346,7 +26539,7 @@ TEST(pipeline_python_namespace_codec_rejects_non_authoritative_rows) { cbm_arena_destroy(&arena); } const char *unknown = - "{\"v\":3,\"lang\":1,\"lsp\":[],\"reg\":[],\"py\":{\"v\":2,\"lang\":1," + "{\"v\":4,\"lang\":1,\"lsp\":[],\"reg\":[],\"py\":{\"v\":2,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":[" "{\"k\":7,\"d\":0,\"s\":0,\"r\":7,\"f\":0,\"l\":0," "\"n\":null,\"m\":null,\"i\":null,\"a\":[]}]}}"; @@ -26374,7 +26567,7 @@ TEST(pipeline_lsp_surface_large_object_array_decode) { const size_t capacity = (size_t)count * 192 + 64; char *json = malloc(capacity); ASSERT_NOT_NULL(json); - size_t used = (size_t)snprintf(json, capacity, "{\"v\":3,\"lang\":0,\"py\":null,\"lsp\":["); + size_t used = (size_t)snprintf(json, capacity, "{\"v\":4,\"lang\":0,\"py\":null,\"lsp\":["); for (int i = 0; i < count; i++) { int n = snprintf(json + used, capacity - used, "%s{\"qn\":\"pkg.f%d\",\"sn\":\"f%d\",\"lb\":\"Function\"," @@ -26464,10 +26657,10 @@ TEST(pipeline_lsp_surface_persisted_and_body_edit_invariant) { ASSERT_TRUE(row_a->surface_sha && row_a->surface_sha[0]); if (round == 0) { snprintf(sha_baseline, sizeof(sha_baseline), "%s", row_a->surface_sha); - /* The row is the versioned codec envelope (codec version 3) with + /* The row is the versioned codec envelope (codec version 4) with * the def present. */ snprintf(json_probe, sizeof(json_probe), "%.20s", row_a->defs_json); - ASSERT_TRUE(strstr(row_a->defs_json, "\"v\":3") != NULL); + ASSERT_TRUE(strstr(row_a->defs_json, "\"v\":4") != NULL); ASSERT_TRUE(strstr(row_a->defs_json, "surface_probe") != NULL); } else if (round == 1) { ASSERT_STR_EQ(row_a->surface_sha, sha_baseline); @@ -29029,6 +29222,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); @@ -29040,6 +29234,8 @@ SUITE(pipeline) { RUN_TEST(pipeline_go_receiver_parallel_suppresses_weak_method_edge); RUN_TEST(pipeline_go_multi_return_receiver_is_typed); RUN_TEST(pipeline_go_multi_return_receiver_is_typed_parallel); + RUN_TEST(pipeline_go_crossfile_type_alias_method); + RUN_TEST(pipeline_go_crossfile_type_alias_method_parallel); RUN_TEST(pipeline_go_rw_usage_never_cross_into_c); RUN_TEST(pipeline_go_rw_usage_never_cross_into_c_parallel); RUN_TEST(pipeline_rust_call_never_binds_another_language); diff --git a/tests/test_platform.c b/tests/test_platform.c index 8b18a4884..2c383c025 100644 --- a/tests/test_platform.c +++ b/tests/test_platform.c @@ -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(); } diff --git a/tests/test_ts_lsp.c b/tests/test_ts_lsp.c index b21e946d0..01e84a2d2 100644 --- a/tests/test_ts_lsp.c +++ b/tests/test_ts_lsp.c @@ -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 `.`, 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);