diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 47b249498..37365d1ed 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -2899,6 +2899,18 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec * proj.myapp.db.Func, not proj.myapp.db.conn.Func). Other langs unchanged. */ def.qualified_name = cbm_fqn_compute_source_lang(a, ctx->project, ctx->rel_path, name, ctx->language); + /* A free function declared inside a namespace (C++/C#/PHP) is qualified by + * the namespace scope the def walk carries (enclosing_class_qn was extended + * by is_namespace_scope_kind), so `ns::serialize` is `proj.file.ns.serialize` + * — without this it collapses to the file scope and namespace-aware + * resolution (ADL, namespace-function lookup) can never see it. Class methods + * never reach here (they go through extract_class_methods), so a set + * enclosing scope here is always a namespace. The out-of-line method path + * below overrides this for `Ns::Cls::method` definitions. */ + if (ctx->enclosing_class_qn && + (ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA)) { + def.qualified_name = cbm_arena_sprintf(a, "%s.%s", ctx->enclosing_class_qn, name); + } def.label = "Function"; def.file_path = ctx->rel_path; def.start_line = ts_node_start_point(node).row + TS_LINE_OFFSET; @@ -5453,6 +5465,20 @@ static bool is_template_class_node(TSNode node, CBMLanguage lang) { } // Compute the enclosing class QN for a class node (for nested class context). +/* A namespace contributes a QN segment so a symbol declared in `namespace ns` + * is `proj.file.ns.sym`, not a top-level `proj.file.sym`. Without the namespace + * in the QN, namespace-aware resolution (C++ ADL) is starved: a bare call + * collapses to the file scope and resolves directly instead. Unlike a class, a + * namespace emits no def of its own — it only extends the enclosing scope for + * its members. C#/PHP need the same treatment paired with their LSP resolvers + * (a def-only change breaks their existing namespace handling), done separately. */ +static bool is_namespace_scope_kind(CBMLanguage lang, const char *kind) { + if (lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA) { + return strcmp(kind, "namespace_definition") == 0; + } + return false; +} + static const char *compute_class_qn(CBMExtractCtx *ctx, TSNode node, const char *saved_enclosing) { TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name")); if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_OBJC) { @@ -6002,6 +6028,21 @@ static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec, continue; } + /* A namespace extends the enclosing scope (so members are QN-qualified by + * it) without being a def itself. Push its children (its declaration_list + * body and any nested namespaces) under the extended scope so each member + * is walked normally — functions AND classes, unlike a class body which + * routes methods through extract_class_methods. Do NOT emit a def or run + * the class/func paths on the namespace node itself. */ + if (is_namespace_scope_kind(ctx->language, kind)) { + const char *new_enclosing = compute_class_qn(ctx, node, frame.enclosing_class_qn); + uint32_t nsc = ts_node_child_count(node); + for (int i = (int)nsc - SKIP_CHAR; i >= 0 && top < CBM_WALK_DEFS_STACK_CAP; i--) { + stack[top++] = (walk_defs_frame_t){ts_node_child(node, (uint32_t)i), new_enclosing}; + } + continue; + } + if (cbm_kind_in_set(node, spec->class_node_types)) { extract_class_def(ctx, node, spec); const char *new_enclosing = compute_class_qn(ctx, node, frame.enclosing_class_qn); diff --git a/internal/cbm/lsp/c_lsp.c b/internal/cbm/lsp/c_lsp.c index 3ab5cf951..be130973c 100644 --- a/internal/cbm/lsp/c_lsp.c +++ b/internal/cbm/lsp/c_lsp.c @@ -744,9 +744,17 @@ static const char *c_adl_resolve(CLSPContext *ctx, const char *name, TSNode call namespaces[ns_count++] = ns; } - // Try each namespace + // Try each namespace, then the module-prefixed form of it. An argument type + // written as `ns::Data` evaluates to the namespace QN `ns`, but the function + // is registered under the module-qualified `.ns.serialize`; without + // the module-prefixed retry the namespace-scoped overload is never found. for (int i = 0; i < ns_count; i++) { const CBMRegisteredFunc *f = cbm_registry_lookup_symbol(ctx->registry, namespaces[i], name); + if (!f && ctx->module_qn) { + const char *prefixed = + cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, namespaces[i]); + f = cbm_registry_lookup_symbol(ctx->registry, prefixed, name); + } if (f) return f->qualified_name; } @@ -2559,6 +2567,45 @@ static const CBMRegisteredFunc *c_lookup_member_depth(CLSPContext *ctx, const ch } } + /* Namespaced-type short-name fallback: a type name that resolves nowhere may + * be a type declared inside a namespace whose registered QN carries the + * namespace ("..Logger"), while the use site only knew the + * file-scoped ".Logger" or the bare "Logger" (e.g. the return type of + * a namespace-scoped factory used outside that namespace). Resolve by the + * SHORT name (last segment) against the registry and retry with the full QN. + * Reached only after the direct/module/alias/base lookups all miss; prefers + * an in-module match. Mirrors the C# short-name type fallback. */ + if (depth == 0 && ctx->registry) { + const char *dot = strrchr(type_qn, '.'); + const char *shortn = dot ? dot + 1 : type_qn; + size_t slen = strlen(shortn); + const char *best_qn = NULL; + for (int i = 0; i < ctx->registry->type_count; i++) { + const char *q = ctx->registry->types[i].qualified_name; + if (!q) { + continue; + } + size_t qlen = strlen(q); + if (qlen <= slen + 1 || q[qlen - slen - 1] != '.' || + strcmp(q + qlen - slen, shortn) != 0) { + continue; + } + if (strcmp(q, type_qn) == 0) { + continue; // already tried as-is above + } + best_qn = q; + if (ctx->module_qn && strncmp(q, ctx->module_qn, strlen(ctx->module_qn)) == 0) { + break; // prefer a match in the current module + } + } + if (best_qn) { + f = c_lookup_member_depth(ctx, best_qn, member_name, depth + 1); + if (f) { + return f; + } + } + } + return NULL; } @@ -4204,8 +4251,15 @@ static void c_process_function(CLSPContext *ctx, TSNode func_node) { if (ctx->enclosing_class_qn && saved_class_qn == ctx->enclosing_class_qn && !strchr(func_qn, '.')) { func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->enclosing_class_qn, func_qn); - } else if (ctx->module_qn && !strchr(func_qn, '.')) { - func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, func_qn); + } else if (!strchr(func_qn, '.')) { + /* A free function in a namespace is qualified by the namespace scope + * (current_namespace is module_qn.ns), matching the def QN the extractor + * now produces; outside any namespace this falls back to the file module + * so non-namespaced free functions are unchanged. */ + const char *scope = ctx->current_namespace ? ctx->current_namespace : ctx->module_qn; + if (scope) { + func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", scope, func_qn); + } } ctx->enclosing_func_qn = func_qn;