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
193 changes: 178 additions & 15 deletions internal/cbm/extract_defs.c

Large diffs are not rendered by default.

35 changes: 32 additions & 3 deletions internal/cbm/extract_unified.c
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,7 @@ static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLan
return NULL;
}

char *name = cbm_func_name_node_text(ctx->arena, name_node, ctx->source);
char *name = cbm_func_name_node_text(ctx->arena, name_node, ctx->source, ctx->language);
if (!name || !name[0]) {
return NULL;
}
Expand All @@ -644,12 +644,24 @@ static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLan
}
}

/* Nix: a binding's own attrpath contributes scope (`a.b.fn = …`), and the def
* extractor bakes it into the def QN. Compose it identically here — otherwise
* an in-body call sources to a QN one or more segments short of the def, and
* the edge is dropped at write. */
const char *qn_name = name;
if (ctx->language == CBM_LANG_NIX) {
qn_name = cbm_nix_qn_name(ctx->arena, node, ctx->source, name);
if (!qn_name || !qn_name[0]) {
return NULL;
}
}

if (state->enclosing_class_qn) {
return cbm_arena_sprintf(ctx->arena, "%s.%s", state->enclosing_class_qn, name);
return cbm_arena_sprintf(ctx->arena, "%s.%s", state->enclosing_class_qn, qn_name);
}
/* Java/Go: directory-based module so this enclosing-func QN matches the def
* QN and the LSP caller_qn (the lsp_resolve join keys on exact equality). */
return cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path, name,
return cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path, qn_name,
ctx->language);
}

Expand All @@ -658,6 +670,13 @@ static const char *compute_class_qn(CBMExtractCtx *ctx, TSNode node, const WalkS
if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL) {
return objectscript_get_class_name(ctx, node);
}
/* Nix: an attrset-valued `binding` is a named scope. Same shared helper the def
* extractor's compute_class_qn uses — these are two separate functions, and a
* one-segment disagreement between them drops every CALLS edge sourced from a
* nested binding. */
if (ctx->language == CBM_LANG_NIX) {
return cbm_nix_binding_scope_qn(ctx, node, state ? state->enclosing_class_qn : NULL);
}
TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name"));
/* Newer tree-sitter-kotlin: class/object name is a type_identifier child. */
if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_KOTLIN) {
Expand Down Expand Up @@ -1418,6 +1437,16 @@ static void push_boundary_scopes(CBMExtractCtx *ctx, TSNode node, const CBMLangS
state->os_type_map.class_base_count = 0;
}
}
} else if (ctx->language == CBM_LANG_NIX && strcmp(ts_node_type(node), "binding") == 0 &&
cbm_nix_binding_is_attrset_scope(node)) {
/* Nix: a binding whose value is an attribute set is a named scope, exactly
* as is_namespace_scope_kind treats it on the def side. Pushing it here is
* what makes an in-body call source to `proj.file.setA.fn` rather than the
* bare `proj.file.fn` that no node carries once defs are attrpath-qualified. */
const char *cqn = compute_class_qn(ctx, node, state);
if (cqn) {
push_scope(state, SCOPE_CLASS, depth, cqn);
}
} else if (ctx->language == CBM_LANG_RUST && strcmp(ts_node_type(node), "impl_item") == 0) {
TSNode type_node = ts_node_child_by_field_name(node, TS_FIELD("type"));
if (!ts_node_is_null(type_node)) {
Expand Down
172 changes: 170 additions & 2 deletions internal/cbm/helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -832,7 +832,7 @@ TSNode cbm_resolve_c_declarator_name_node(TSNode func_node) {
// space. Without this the conversion operator is indexed as "operator bool()
// const", and a member lookup for "operator bool" (the implicit call in
// `if (obj)`) misses.
char *cbm_func_name_node_text(CBMArena *a, TSNode name_node, const char *source) {
char *cbm_func_name_node_text(CBMArena *a, TSNode name_node, const char *source, CBMLanguage lang) {
char *text = cbm_node_text(a, name_node, source);
if (text && strcmp(ts_node_type(name_node), "operator_cast") == 0) {
char *paren = strchr(text, '(');
Expand All @@ -843,9 +843,177 @@ char *cbm_func_name_node_text(CBMArena *a, TSNode name_node, const char *source)
*paren = '\0';
}
}
/* Nix quoted attrpath segment: `"kebab-case" = a: a;` and `services."my.svc" = …`
* are ordinary names that merely need quoting in source. The node text carries
* the delimiters, so without this the def is named `"kebab-case"` — quotes and
* all — and every consumer keying on the name (search, CALLS resolution, the
* LSP join) would have to know to re-quote. Stripped here rather than in the
* resolver so the def name and the call-scope QN, which both route through this
* function, cannot disagree. */
if (text && lang == CBM_LANG_NIX) {
cbm_nix_strip_attr_quotes(text);
}
return text;
}

/* ── Nix attrpath helpers ───────────────────────────────────
* A Nix binding's name is a PATH (`a.b.c = …`), whose segments may be quoted or
* interpolated. These render it the way the rest of the extractor expects: leaf
* segment as the name, leading segments as scope. Shared by the defs and unified
* (call-scope) extractors so both compute the same QN — if they disagree, a CALLS
* edge names a source node that does not exist and is dropped at write, which is
* precisely how the function-header bug manifested. */

/* Bound on the interpolation scan below. An attrpath segment is an identifier or a
* string, so its subtree is shallow; this only has to stop a pathological input. */
enum { NIX_ATTR_SCAN_MAX = 32 };

/* Strip one matching pair of surrounding double quotes, in place. */
void cbm_nix_strip_attr_quotes(char *text) {
if (!text) {
return;
}
size_t len = strlen(text);
if (len >= CBM_QUOTE_PAIR && text[0] == '"' && text[len - SKIP_ONE] == '"') {
memmove(text, text + SKIP_ONE, len - PAIR_LEN);
text[len - PAIR_LEN] = '\0';
}
}

/* True when a segment contains a `${...}` interpolation, and therefore has no
* statically knowable name. Iterative and bounded — the lint forbids unlisted
* recursion, and an attrpath segment is shallow by construction. */
bool cbm_nix_attr_is_interpolated(TSNode attr) {
if (ts_node_is_null(attr)) {
return false;
}
TSNode stack[NIX_ATTR_SCAN_MAX];
int top = 0;
stack[top++] = attr;
while (top > 0) {
TSNode cur = stack[--top];
if (strcmp(ts_node_type(cur), "interpolation") == 0) {
return true;
}
uint32_t n = ts_node_named_child_count(cur);
for (uint32_t i = 0; i < n && top < NIX_ATTR_SCAN_MAX; i++) {
stack[top++] = ts_node_named_child(cur, i);
}
}
return false;
}

/* The leaf segment of an attrpath — the name. `attr` is a FIELD in this grammar,
* not a node type (segments are identifier / string_expression / interpolation),
* so iterate named children rather than matching on a type string. */
TSNode cbm_nix_attrpath_last_attr(TSNode attrpath) {
TSNode last = {0};
if (ts_node_is_null(attrpath)) {
return last;
}
uint32_t n = ts_node_named_child_count(attrpath);
if (n == 0) {
return last;
}
return ts_node_named_child(attrpath, n - SKIP_ONE);
}

/* The scope prefix of an attrpath: every segment except the leaf, quote-stripped
* and dot-joined. `a.b.fn = …` yields "a.b" so it qualifies identically to the
* nested spelling `a = { b = { fn = …; }; }`. Returns NULL for a single-segment
* path (no scope) or when a leading segment is interpolated (not nameable). */
const char *cbm_nix_attrpath_scope(CBMArena *a, TSNode attrpath, const char *source) {
if (ts_node_is_null(attrpath)) {
return NULL;
}
uint32_t n = ts_node_named_child_count(attrpath);
if (n <= SKIP_ONE) {
return NULL;
}
const char *scope = NULL;
for (uint32_t i = 0; i + SKIP_ONE < n; i++) {
TSNode seg = ts_node_named_child(attrpath, i);
if (cbm_nix_attr_is_interpolated(seg)) {
return NULL;
}
char *seg_text = cbm_node_text(a, seg, source);
if (!seg_text || !seg_text[0]) {
return NULL;
}
cbm_nix_strip_attr_quotes(seg_text);
scope = scope ? cbm_arena_sprintf(a, "%s.%s", scope, seg_text) : seg_text;
}
return scope;
}

/* True when a Nix `binding`'s value is an attribute set, i.e. the binding names a
* scope rather than defining a value. Deliberately excludes `let` bindings and
* lambda-valued bindings: the former are lexical (C++ does not qualify by block
* scope either), the latter are definitions in their own right. */
bool cbm_nix_binding_is_attrset_scope(TSNode node) {
if (ts_node_is_null(node) || strcmp(ts_node_type(node), "binding") != 0) {
return false;
}
TSNode value = ts_node_child_by_field_name(node, TS_FIELD("expression"));
if (ts_node_is_null(value)) {
return false;
}
const char *vk = ts_node_type(value);
return strcmp(vk, "attrset_expression") == 0 || strcmp(vk, "rec_attrset_expression") == 0;
}

/* The scope QN contributed by a Nix `binding` whose value is an attribute set —
* `setA = { … }` contributes "…file.setA", and a dotted `a.b = { … }` contributes
* "…file.a.b". Returns saved_enclosing unchanged when the binding cannot name a
* scope (empty or interpolated attrpath).
*
* Lives here, called by BOTH extract_defs.c and extract_unified.c, because those
* two files carry separate compute_class_qn implementations. A def QN and a
* call-scope QN that disagree by even one segment make the CALLS edge name a
* source node that was never minted, and it is silently dropped at write. Sharing
* the computation makes that class of drift impossible rather than merely
* unlikely. */
const char *cbm_nix_binding_scope_qn(CBMExtractCtx *ctx, TSNode node, const char *saved_enclosing) {
if (!ctx || ts_node_is_null(node) || strcmp(ts_node_type(node), "binding") != 0) {
return saved_enclosing;
}
TSNode attrpath = ts_node_child_by_field_name(node, TS_FIELD("attrpath"));
TSNode leaf = cbm_nix_attrpath_last_attr(attrpath);
if (ts_node_is_null(leaf) || cbm_nix_attr_is_interpolated(leaf)) {
return saved_enclosing;
}
char *leaf_text = cbm_node_text(ctx->arena, leaf, ctx->source);
if (!leaf_text || !leaf_text[0]) {
return saved_enclosing;
}
cbm_nix_strip_attr_quotes(leaf_text);
const char *scope = cbm_nix_attrpath_scope(ctx->arena, attrpath, ctx->source);
const char *rel = scope ? cbm_arena_sprintf(ctx->arena, "%s.%s", scope, leaf_text) : leaf_text;
if (saved_enclosing) {
return cbm_arena_sprintf(ctx->arena, "%s.%s", saved_enclosing, rel);
}
return cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path, rel, ctx->language);
}

/* The QN-relative name of a Nix binding: its attrpath scope joined to its leaf
* name. `a.b.fn = …` yields "a.b.fn"; a bare `fn = …` yields "fn". Callers prepend
* either the enclosing attrset scope or the module QN, so the two scope sources —
* a dotted attrpath and an enclosing attrset — compose. Takes the already-resolved
* leaf name rather than re-deriving it, so it cannot disagree with the name the
* def was minted under. */
const char *cbm_nix_qn_name(CBMArena *a, TSNode func_node, const char *source, const char *name) {
if (!name) {
return NULL;
}
TSNode parent = ts_node_parent(func_node);
if (ts_node_is_null(parent) || strcmp(ts_node_type(parent), "binding") != 0) {
return name;
}
TSNode attrpath = ts_node_child_by_field_name(parent, TS_FIELD("attrpath"));
const char *scope = cbm_nix_attrpath_scope(a, attrpath, source);
return scope ? cbm_arena_sprintf(a, "%s.%s", scope, name) : name;
}

static const char *func_node_name(CBMArena *a, TSNode func_node, const char *source,
CBMLanguage lang) {
// Wolfram: set_delayed_top/set_top/set_delayed/set — LHS is apply(user_symbol("f"), ...)
Expand Down Expand Up @@ -884,7 +1052,7 @@ static const char *func_node_name(CBMArena *a, TSNode func_node, const char *sou
if (strcmp(ts_node_type(func_node), "function_definition") == 0) {
TSNode dn = cbm_resolve_c_declarator_name_node(func_node);
if (!ts_node_is_null(dn)) {
return cbm_func_name_node_text(a, dn, source);
return cbm_func_name_node_text(a, dn, source, lang);
}
}
return NULL;
Expand Down
41 changes: 40 additions & 1 deletion internal/cbm/helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,46 @@ TSNode cbm_resolve_c_declarator_name_node(TSNode func_node);
// C++ conversion-operator's `operator_cast` node (which spans the full
// "operator bool() const") down to "operator bool". Shared by the defs and
// unified extractors so the def name and call-scope QN agree.
char *cbm_func_name_node_text(CBMArena *a, TSNode name_node, const char *source);
// Also strips the surrounding quotes from a Nix quoted attrpath segment, so
// `"kebab-case" = a: a;` is named kebab-case rather than "kebab-case". Takes the
// language for that reason; every caller must pass ctx->language.
char *cbm_func_name_node_text(CBMArena *a, TSNode name_node, const char *source, CBMLanguage lang);

// ── Nix attrpath helpers ──
// A Nix binding's name is a PATH (`a.b.c = …`) whose segments may be quoted or
// interpolated. Shared by the defs and unified (call-scope) extractors so both
// derive the same name and the same scope prefix — divergence makes a CALLS edge
// name a source node that does not exist, and it is dropped at write.

// Strip one matching pair of surrounding double quotes, in place.
void cbm_nix_strip_attr_quotes(char *text);

// True when an attrpath segment contains a `${...}` interpolation and therefore
// has no statically knowable name.
bool cbm_nix_attr_is_interpolated(TSNode attr);

// The leaf segment of an attrpath — the name. Null node for an empty attrpath.
TSNode cbm_nix_attrpath_last_attr(TSNode attrpath);

// The scope prefix of an attrpath: all segments but the leaf, quote-stripped and
// dot-joined, so `a.b.fn = …` qualifies identically to `a = { b = { fn = …; }; }`.
// NULL for a single-segment path, or when a leading segment is interpolated.
const char *cbm_nix_attrpath_scope(CBMArena *a, TSNode attrpath, const char *source);

// True when a Nix `binding`'s value is an attribute set — the binding names a
// scope rather than defining a value. Excludes let-bindings and lambda values.
bool cbm_nix_binding_is_attrset_scope(TSNode node);

// The scope QN contributed by a Nix `binding` whose value is an attribute set.
// Called by BOTH extract_defs.c and extract_unified.c, which carry separate
// compute_class_qn implementations — sharing this makes a def/call-scope QN
// mismatch (which silently drops the CALLS edge) structurally impossible.
const char *cbm_nix_binding_scope_qn(CBMExtractCtx *ctx, TSNode node, const char *saved_enclosing);

// The QN-relative name of a Nix binding — its attrpath scope joined to `name`.
// Callers prepend the enclosing attrset scope (or the module QN), so a dotted
// attrpath and an enclosing attrset compose into one qualified name.
const char *cbm_nix_qn_name(CBMArena *a, TSNode func_node, const char *source, const char *name);

// Resolve a function/method definition node's NAME node across all ~130 grammars
// (generic `name` field, arrow→declarator, C/C++ declarator chain, plus the many
Expand Down
Loading
Loading