Skip to content
Merged
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 @@ -283,6 +283,10 @@ typedef struct {
* qualified_name (base QN = the first qn_sig_off bytes); 0 = no suffix.
* Always 0 until a language enables its callable_identity mode. */
uint32_t qn_sig_off;
/* 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;
CBMTestDefinitionRole test_role; /* NONE preserves legacy helper/test-file semantics */
/* Configured raw definitions only; zero for all legacy rows. Exact spans
* are bound to the owning result's source identity before cross-file use. */
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 @@ -7097,6 +7097,58 @@ static void extract_py_field_types(CBMExtractCtx *ctx, TSNode class_node, const
}
}

/* 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 @@ -7497,6 +7549,9 @@ static void emit_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *s
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(ctx, node, name);
Expand Down
150 changes: 145 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,72 @@ 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;
}
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;
}
}
}
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 +433,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 +527,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 +545,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 +609,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 +641,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 +3590,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 +3743,21 @@ 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 || !namespace_name || !namespace_name[0])
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;
return 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 +3774,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 @@ -67,6 +67,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 @@ -276,6 +276,7 @@ static void cr_walk_def(cr_ctx_t *c, CBMDefinition *d) {
cr_str(c, &d->http_client);
cr_str(c, &d->http_base_url);
cr_str(c, &d->variants);
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 @@ -441,6 +441,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 @@ -1850,7 +1850,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