Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions internal/cbm/cbm.h
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@ typedef struct {
* HTTP_CALLS edge to base + path. Tail fields: zero-init stays valid. */
const char *http_client;
const char *http_base_url;
/* C# only: declared namespace of a TOP-LEVEL type (NULL for nested types,
* the global namespace and every other language). Per type, because the
* file-level namespace_name records only a file's first namespace. */
const char *decl_namespace;
} CBMDefinition;

/* Argument captured from a call expression */
Expand Down
55 changes: 55 additions & 0 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -4584,6 +4584,58 @@ static bool extract_sql_ddl_class_def(CBMExtractCtx *ctx, TSNode node, const cha
return true;
}

/* Name of a C# namespace declaration node (block or file-scoped), or NULL. */
static const char *cs_namespace_decl_name(CBMArena *a, TSNode ns, const char *source) {
TSNode nm = ts_node_child_by_field_name(ns, TS_FIELD("name"));
if (ts_node_is_null(nm)) {
return NULL;
}
const char *text = cbm_node_text(a, nm, source);
return text && text[0] ? text : NULL;
}

/* Declared namespace of a C# type declaration: every enclosing namespace
* block (outer to inner) under an optional file-scoped namespace, which the
* grammar may attach either as an ancestor or as a preceding sibling of the
* compilation unit's members. NULL = global namespace. The file-level
* namespace_name keeps only the FIRST namespace of a file, which mislabels
* every type of a multi-namespace file (#2120). */
static const char *cs_type_decl_namespace(CBMArena *a, TSNode node, const char *source) {
const char *acc = NULL;
bool file_scoped = false;
TSNode top = node;
for (TSNode cur = ts_node_parent(node); !ts_node_is_null(cur); cur = ts_node_parent(cur)) {
const char *k = ts_node_type(cur);
bool is_file_scoped = strcmp(k, "file_scoped_namespace_declaration") == 0;
if (is_file_scoped || strcmp(k, "namespace_declaration") == 0) {
const char *name = cs_namespace_decl_name(a, cur, source);
if (name) {
acc = acc ? cbm_arena_sprintf(a, "%s.%s", name, acc) : name;
}
file_scoped = file_scoped || is_file_scoped;
}
top = cur;
}
if (file_scoped) {
return acc;
}
uint32_t start = ts_node_start_byte(node);
uint32_t n = ts_node_named_child_count(top);
for (uint32_t i = 0; i < n; i++) {
TSNode c = ts_node_named_child(top, i);
if (ts_node_start_byte(c) >= start) {
break;
}
if (strcmp(ts_node_type(c), "file_scoped_namespace_declaration") == 0) {
const char *name = cs_namespace_decl_name(a, c, source);
if (name) {
return acc ? cbm_arena_sprintf(a, "%s.%s", name, acc) : name;
}
}
}
return acc;
}

static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) {
CBMArena *a = ctx->arena;
const char *kind = ts_node_type(node);
Expand Down Expand Up @@ -4951,6 +5003,9 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
def.end_line = ts_node_end_point(node).row + TS_LINE_OFFSET;
def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET);
def.is_exported = cbm_is_exported(name, ctx->language);
if (ctx->language == CBM_LANG_CSHARP && !ctx->enclosing_class_qn) {
def.decl_namespace = cs_type_decl_namespace(a, node, ctx->source);
}
def.base_classes = extract_base_classes(a, node, ctx->source, ctx->language);
def.decorators = extract_decorators(a, node, ctx->source, ctx->language, spec);
def.docstring = extract_docstring(a, node, ctx->source, ctx->language);
Expand Down
172 changes: 167 additions & 5 deletions internal/cbm/lsp/cs_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ static char *cs_node_text_cached(CSLSPContext *ctx, TSNode node);
static const CBMType *cs_unwrap_task(CSLSPContext *ctx, const CBMType *t);
static const CBMType *cs_unwrap_nullable(const CBMType *t);
static const char *cs_resolve_callable_value(CSLSPContext *ctx, TSNode node, TSNode *callable_leaf);
static const char *cs_top_level_namespace(const char *type_qn, const char *short_name,
const char *module_qn, const char *namespace_name);

/* ── small helpers ──────────────────────────────────────────────── */

Expand Down Expand Up @@ -356,6 +358,91 @@ static const char *cs_try_type_qn(CSLSPContext *ctx, const char *qn) {
return NULL;
}

/* Namespaces through which `qualifier` (the part of a type reference before
* its simple name, NULL for a simple name) can reach a top-level type, in C#
* lookup order: each enclosing namespace innermost outward, then the global
* namespace (qualified references only) or the file's `using` namespaces
* (simple names only -- a using-namespace directive does not import nested
* namespaces). Returns the count written to `out` (at most `cap`). */
static int cs_visible_namespaces(CSLSPContext *ctx, const char *qualifier, const char **out,
int cap) {
int n = 0;
const char *ns = ctx->namespace_count > 0 ? cs_namespace_qn(ctx) : NULL;
while (ns && *ns && n < cap) {
out[n++] = qualifier ? cbm_arena_sprintf(ctx->arena, "%s.%s", ns, qualifier) : ns;
const char *dot = strrchr(ns, '.');
ns = dot ? cbm_arena_strndup(ctx->arena, ns, (size_t)(dot - ns)) : NULL;
}
if (qualifier) {
if (n < cap)
out[n++] = qualifier;
return n;
}
for (int i = 0; i < ctx->using_count && n < cap; i++) {
const CBMCSUsing *u = &ctx->usings[i];
if (u->kind == CBM_CS_USING_NAMESPACE && u->target_qn && u->target_qn[0])
out[n++] = u->target_qn;
}
/* The global namespace is checked last: an unqualified name binds to a
* top-level type declared without a namespace whatever the file imports
* (#2120 residual — such types used to be invisible to step 8b). */
if (n < cap)
out[n++] = "";
return n;
}

/* Bind a (possibly namespace-qualified) type reference to the project type
* whose DECLARED namespace the current file can see, preferring the earliest
* namespace in C# lookup order; equal ranks keep registration order. NULL when
* no top-level type with that simple name is visible (#2120). */
static const char *cs_resolve_by_declared_namespace(CSLSPContext *ctx, const char *name) {
if (!ctx->registry || !name || !*name)
return NULL;
const char *simple = cs_short_name(name);
if (!simple || !*simple)
return NULL;
const char *qualifier =
simple > name ? cbm_arena_strndup(ctx->arena, name, (size_t)(simple - name - 1)) : NULL;
enum { CS_VISIBLE_NS_MAX = 128 };
const char *visible[CS_VISIBLE_NS_MAX];
int visible_count = cs_visible_namespaces(ctx, qualifier, visible, CS_VISIBLE_NS_MAX);
if (visible_count == 0)
return NULL;

const CBMRegisteredType *best = NULL;
int best_rank = visible_count;
CBMTypeShortIter it;
cbm_registry_types_by_short_name_chain(ctx->registry, simple, &it);
int i;
while ((i = cbm_type_short_iter_next(&it)) >= 0) {
const CBMRegisteredType *cand = &it.reg->types[i];
if (!cand->namespace_qn || !cand->short_name || strcmp(cand->short_name, simple) != 0)
continue;
for (int r = 0; r < best_rank; r++) {
if (strcmp(cand->namespace_qn, visible[r]) == 0) {
best = cand;
best_rank = r;
break;
}
}
}
/* No namespace-qualified hit: a qualified `A.B` may name a NESTED type —
* A a type in scope (resolved recursively, one segment shorter), B a
* member type of it. Nested QNs are the parent QN plus the simple name,
* which step 8b's namespace scan can never produce because nested types
* carry no namespace_qn (#2120 residual: `new Command.Result(...)`,
* `new Grain.Handler(dep).Handle(...)`). */
if (!best && qualifier) {
const char *qtype = cs_resolve_type_name(ctx, qualifier);
if (qtype) {
const char *nested = cbm_arena_sprintf(ctx->arena, "%s.%s", qtype, simple);
if (cs_lookup_type_qn(ctx, nested))
return nested;
}
}
return best ? best->qualified_name : NULL;
}

/* Returns a fully-qualified type name resolved against:
* 1. predefined alias (int -> System.Int32)
* 2. type-parameter substitution map
Expand All @@ -365,7 +452,9 @@ static const char *cs_try_type_qn(CSLSPContext *ctx, const char *qn) {
* 6. module-prefixed (file-local QN that the unified extractor produced)
* 7. each `using namespace X` prefix
* 8. each `using A = X` alias substitution
* 9. fall back to bare name (registry will fail; pipeline drops the call)
* 8b. declared namespace of a top-level project type (enclosing namespaces,
* then `using` namespaces)
* 9. short-name fallback, then the bare name (registry miss drops the call)
*/
const char *cs_resolve_type_name(CSLSPContext *ctx, const char *raw) {
if (!raw || !*raw)
Expand Down Expand Up @@ -457,6 +546,11 @@ const char *cs_resolve_type_name(CSLSPContext *ctx, const char *raw) {
char *target_bare = cs_strip_generic_args(ctx->arena, u->target_qn);
if (target_bare && cs_lookup_type_qn(ctx, target_bare))
return target_bare;
/* A project type is registered under its path-derived QN, so
* `using A = Ns.Type;` binds through the declared namespace. */
const char *declared = cs_resolve_by_declared_namespace(ctx, target_bare);
if (declared)
return declared;
return cbm_arena_strdup(ctx->arena, u->target_qn);
}
if (bare[alias_len] == '.') {
Expand All @@ -470,6 +564,15 @@ const char *cs_resolve_type_name(CSLSPContext *ctx, const char *raw) {
}
}

/* 8b. Declared-namespace binding for project types (#2120): their QNs are
* path-derived, so steps 5 and 7 cannot hit them; bind by the namespace
* each top-level type declares, in C# lookup order. */
{
const char *declared = cs_resolve_by_declared_namespace(ctx, bare);
if (declared)
return declared;
}

/* 9. Short-name fallback: scan the registry for any type whose
* short_name equals `bare` and whose QN shares the longest namespace
* prefix with the file's namespace stack. This recovers cross-file
Expand Down Expand Up @@ -525,6 +628,28 @@ const char *cs_resolve_type_name(CSLSPContext *ctx, const char *raw) {
return bare;
}

/* Other top-level declarations of the type `t` names: same simple name, same
* declared namespace, different entry. Registration order; at most `cap`. */
static int cs_namespace_twins(CSLSPContext *ctx, const CBMRegisteredType *t,
const CBMRegisteredType **out, int cap) {
if (!ctx->registry || !t || !t->namespace_qn || !t->short_name)
return 0;
int n = 0;
CBMTypeShortIter it;
cbm_registry_types_by_short_name_chain(ctx->registry, t->short_name, &it);
int i;
while (n < cap && (i = cbm_type_short_iter_next(&it)) >= 0) {
const CBMRegisteredType *c = &it.reg->types[i];
if (!c->namespace_qn || !c->short_name || !c->qualified_name ||
strcmp(c->short_name, t->short_name) != 0 ||
strcmp(c->namespace_qn, t->namespace_qn) != 0 ||
strcmp(c->qualified_name, t->qualified_name) == 0)
continue;
out[n++] = c;
}
return n;
}

/* Look up a method on a type, walking inheritance chain. */
const CBMRegisteredFunc *cs_lookup_method(CSLSPContext *ctx, const char *type_qn,
const char *method_name) {
Expand All @@ -535,19 +660,34 @@ const CBMRegisteredFunc *cs_lookup_method(CSLSPContext *ctx, const char *type_qn
if (f)
return f;

/* Walk inheritance chain (base + interfaces + transitive bases). */
const CBMRegisteredType *t = cs_lookup_type_qn(ctx, type_qn);
if (!t)
return NULL;

/* The other declarations of this same type -- `partial` pieces in other
* files, reference-assembly copies -- are separate registry entries
* (path-derived QNs) but one C# type: a member declared on any of them is
* a member of this one, and so is a base declared on any of them. */
const CBMRegisteredType *twins[CS_LSP_PARENT_WALK_MAX] = {0};
int twin_count = cs_namespace_twins(ctx, t, twins, CS_LSP_PARENT_WALK_MAX);
for (int k = 0; k < twin_count; k++) {
f = cbm_registry_lookup_method(ctx->registry, twins[k]->qualified_name, method_name);
if (f)
return f;
}

/* Walk inheritance chain (base + interfaces + transitive bases). */
const char *visited[CS_LSP_PARENT_WALK_MAX];
int visited_count = 0;
const char *frontier[CS_LSP_PARENT_WALK_MAX];
int frontier_count = 0;

if (t->embedded_types) {
for (int i = 0; t->embedded_types[i] && frontier_count < CS_LSP_PARENT_WALK_MAX; i++) {
frontier[frontier_count++] = t->embedded_types[i];
for (int k = -1; k < twin_count; k++) {
const CBMRegisteredType *decl = k < 0 ? t : twins[k];
for (int i = 0; decl->embedded_types && decl->embedded_types[i] &&
frontier_count < CS_LSP_PARENT_WALK_MAX;
i++) {
frontier[frontier_count++] = decl->embedded_types[i];
}
}

Expand Down Expand Up @@ -3469,6 +3609,8 @@ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
memset(&rt, 0, sizeof(rt));
rt.qualified_name = d->qualified_name;
rt.short_name = d->name;
rt.namespace_qn =
cs_top_level_namespace(d->qualified_name, d->name, module_qn, d->decl_namespace);
rt.is_interface = (strcmp(d->label, "Interface") == 0);
if (d->base_classes) {
int bc = 0;
Expand Down Expand Up @@ -3620,6 +3762,24 @@ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source,

/* ── cross-file entry ───────────────────────────────────────────── */

/* The type's declared `namespace_name` when `type_qn` is a TOP-LEVEL type of
* the file whose module QN is `module_qn` (top-level C# type QNs are exactly
* module.Name; nested ones are module.Outer.Name and stay NULL, since a nested
* type is not reachable by its simple name through a namespace). */
static const char *cs_top_level_namespace(const char *type_qn, const char *short_name,
const char *module_qn, const char *namespace_name) {
if (!type_qn || !short_name || !module_qn)
return NULL;
size_t mlen = strlen(module_qn);
if (strncmp(type_qn, module_qn, mlen) != 0 || type_qn[mlen] != '.' ||
strcmp(type_qn + mlen + 1, short_name) != 0)
return NULL;
/* Top-level C# type: its declared namespace, "" when it declares none
* (global namespace) so step 8b can bind it — NULL stays reserved for
* nested types, which no namespace can reach by simple name. */
return (namespace_name && namespace_name[0]) ? namespace_name : "";
}

/* Register one batch of CBMLSPDef[] into a registry. Shared by the
* per-file cross-LSP path and the Tier 2 pre-built registry builder.
* Def-driven (no per-file AST mutation) so deterministic per def set. */
Expand All @@ -3636,6 +3796,8 @@ static void cs_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, CBMLSPDe
memset(&rt, 0, sizeof(rt));
rt.qualified_name = d->qualified_name;
rt.short_name = d->short_name;
rt.namespace_qn = cs_top_level_namespace(d->qualified_name, d->short_name,
d->def_module_qn, d->namespace_name);
rt.is_interface = d->is_interface || (strcmp(d->label, "Interface") == 0);
if (d->embedded_types && *d->embedded_types) {
/* Parse "|"-separated list. */
Expand Down
6 changes: 6 additions & 0 deletions internal/cbm/lsp/type_registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ typedef struct {
const CBMType *index_value_type; // V or NULL
// Generic constraints, parallel to type_param_names. NULL or shorter array means "any".
const CBMType **type_param_constraints; // NULL-terminated, parallel to type_param_names

// --- C#-specific (NULL for every other language) ---
// Declared namespace of a TOP-LEVEL project type. Graph QNs are path-derived,
// so this is the only way to bind a short name through `using` directives and
// enclosing namespaces (#2120). NULL for nested types and unknown namespaces.
const char *namespace_qn;
} CBMRegisteredType;

// Hash-table bucket entry. Chains collisions via next-index list for overload sets.
Expand Down
1 change: 1 addition & 0 deletions internal/cbm/result_compact.c
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ static void cr_walk_def(cr_ctx_t *c, CBMDefinition *d) {
cr_str(c, &d->impl_trait);
cr_str(c, &d->http_client);
cr_str(c, &d->http_base_url);
cr_str(c, &d->decl_namespace);
}

static void cr_walk_call(cr_ctx_t *c, CBMCall *call) {
Expand Down
6 changes: 6 additions & 0 deletions src/pipeline/pass_lsp_cross.c
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,12 @@ static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const ch
dst->label = label;
dst->def_module_qn = module_qn;
dst->namespace_name = namespace_name ? cbm_arena_strdup(arena, namespace_name) : NULL;
/* C# types carry their OWN declared namespace: the file-level one is only
* the file's first namespace, wrong for every later namespace (#2120). */
if (lang == CBM_LANG_CSHARP && cbm_label_is_type_like(src->label)) {
dst->namespace_name =
src->decl_namespace ? cbm_arena_strdup(arena, src->decl_namespace) : NULL;
}
dst->is_interface = (strcmp(label, "Interface") == 0 || strcmp(label, "Protocol") == 0);
/* Single return-type string. The per-language registrars split on '|'
* for multi-return languages (Go); single-return languages just see one
Expand Down
3 changes: 2 additions & 1 deletion src/pipeline/pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -1194,7 +1194,8 @@ static void log_result_census(const char *tag, CBMFileResult **cache, int file_c
}
str_def_fp += def->fingerprint ? (size_t)def->fingerprint_k * sizeof(uint32_t) : 0;
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_len(def->impl_trait) + census_len(def->http_base_url) +
census_len(def->decl_namespace);
}
for (int c = 0; c < r->calls.count; c++) {
const CBMCall *call = &r->calls.items[c];
Expand Down
Loading
Loading