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
2 changes: 1 addition & 1 deletion internal/cbm/cbm.c
Original file line number Diff line number Diff line change
Expand Up @@ -3924,7 +3924,7 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
uint64_t lsp_start = now_ns();
if (!result->lsp_skipped) {
if (language == CBM_LANG_GO) {
cbm_run_go_lsp(a, result, source, source_len, root);
cbm_run_go_lsp(a, result, source, source_len, root, rel_path);
}
if (language == CBM_LANG_C || language == CBM_LANG_CPP || language == CBM_LANG_CUDA) {
if (!cbm_run_c_lsp_with_test_owners(a, result, source, source_len, root,
Expand Down
18 changes: 18 additions & 0 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -7060,6 +7060,24 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
}
}

/* Go allows any number of init() functions per package — even several in
* one file — and they all run at start-up. On the flat QN they all
* collided and the graph upsert kept ONE node per package, silently
* dropping the rest (#1910). Disambiguate with the #495 cfg-twin pattern:
* fold the file basename and the init's per-file ordinal into the QN.
* The ordinal (not the line) keeps the QN stable when code is edited
* above the function — the QN is node identity, and a line suffix would
* churn nodes on every unrelated insertion. Nothing ever joins on the
* plain QN — calling init explicitly is illegal in Go — and the
* call-scope side (compute_func_qn in extract_unified.c) and the Go LSP
* (process_function in lsp/go_lsp.c) name it through the same
* cbm_go_init_qn, so init-body calls keep their attribution. */
if (ctx->language == CBM_LANG_GO && strcmp(def.label, "Function") == 0 &&
strcmp(name, "init") == 0) {
def.qualified_name =
cbm_go_init_qn(a, def.qualified_name, ctx->rel_path, node, ctx->source);
}

// C++/CUDA: out-of-line method definition (`Foo::bar` in a .cc/.cpp). The
// class body in the header is declaration-only, so without this the
// definition is recorded as a free Function. Promote it to a Method whose QN
Expand Down
17 changes: 17 additions & 0 deletions internal/cbm/extract_unified.c
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,23 @@ static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLan
}
}

/* Go init(): the def extractor folds the file basename and the init's
* per-file ordinal into the QN so every init in a package survives the
* upsert (#1910, the #495 cfg-twin pattern; ordinal, not line, so the QN
* is stable under edits above the function). Name it through the same
* cbm_go_init_qn, or init-body calls fall back to File-node attribution
* (calls_find_source). */
if (ctx->language == CBM_LANG_GO && strcmp(name, "init") == 0 &&
strcmp(ts_node_type(node), "function_declaration") == 0) {
const char *base_qn = cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path,
name, ctx->language);
/* NULL only on allocation failure: fall through to the flat QN, as
* the receiver branch above does. */
if (base_qn) {
return cbm_go_init_qn(ctx->arena, base_qn, ctx->rel_path, node, ctx->source);
}
}

/* 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
Expand Down
59 changes: 59 additions & 0 deletions internal/cbm/helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -2330,6 +2330,65 @@ int cbm_classify_string(const char *str, int len) {
return NOT_FOUND;
}

/* 1-based source-order ordinal of a top-level Go `init` function_declaration
* among the file's `init`s. Unlike a line number, the ordinal is stable under
* edits above the function — QNs are node identity, and a line-based suffix
* would churn nodes on every unrelated insertion (the #495 cfg suffix and the
* GoogleTest derivation are both edit-stable for the same reason).
* One cursor pass over the top-level declarations: ts_node_named_child(root, i)
* is O(i), so an indexed loop is O(n^2) per init on a wide file, and generated
* Go (.pb.go, tables) carries thousands of top-level declarations next to its
* init(). */
static int go_init_ordinal(TSNode fn_node, const char *source) {
TSNode root = fn_node;
for (TSNode p = ts_node_parent(root); !ts_node_is_null(p); p = ts_node_parent(p)) {
root = p;
}
int ordinal = 0;
TSTreeCursor cur = ts_tree_cursor_new(root);
if (ts_tree_cursor_goto_first_child(&cur)) {
do {
TSNode child = ts_tree_cursor_current_node(&cur);
if (strcmp(ts_node_type(child), "function_declaration") != 0) {
continue;
}
TSNode name = ts_node_child_by_field_name(child, TS_FIELD("name"));
if (ts_node_is_null(name)) {
continue;
}
uint32_t start = ts_node_start_byte(name);
if (ts_node_end_byte(name) - start != 4 || memcmp(source + start, "init", 4) != 0) {
continue;
}
ordinal++;
if (ts_node_eq(child, fn_node)) {
break;
}
} while (ts_tree_cursor_goto_next_sibling(&cur));
}
ts_tree_cursor_delete(&cur);
return ordinal;
}

/* Qualified name of a Go `init` function (#1910). Go allows any number of
* init() per package — even several in one file — so the flat `pkg.init`
* collides and the graph upsert keeps one node. The QN folds in the file
* basename and the init's per-file ordinal: `pkg.init#file.go:N`. Every side
* that names an init calls this one function — the definition extractor, the
* call-scope side (compute_func_qn) and the Go LSP's enclosing-function QN —
* so the three cannot drift. A NULL base QN or path keeps base_qn as is. */
const char *cbm_go_init_qn(CBMArena *a, const char *base_qn, const char *rel_path, TSNode fn_node,
const char *source) {
if (!base_qn || !rel_path) {
return base_qn;
}
const char *base = strrchr(rel_path, '/');
base = base ? base + 1 : rel_path;
const char *qn =
cbm_arena_sprintf(a, "%s#%s:%d", base_qn, base, go_init_ordinal(fn_node, source));
return qn ? qn : base_qn;
}

/* Flatten a JS/TS `template_string` node into plain text (issue #1006).
* String fragments are kept verbatim; each ${...} substitution becomes the
* "{}" placeholder so client URLs built from template literals share the
Expand Down
7 changes: 7 additions & 0 deletions internal/cbm/helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,13 @@ bool cbm_is_module_level(TSNode node, CBMLanguage lang);
// node being classified.
bool cbm_is_module_level_p(TSNode parent, CBMLanguage lang);

/* QN of a Go `init` function_declaration (#1910): base_qn + "#<file basename>:<N>",
* N = its 1-based ordinal among the file's top-level `init`s. The ONE formula
* for the def extractor, compute_func_qn and the Go LSP's enclosing-function
* QN. A NULL base_qn or rel_path returns base_qn unchanged. */
const char *cbm_go_init_qn(CBMArena *a, const char *base_qn, const char *rel_path, TSNode fn_node,
const char *source);

// --- FQN computation ---

// Compute qualified name: project.rel_path_parts.name
Expand Down
28 changes: 18 additions & 10 deletions internal/cbm/lsp/go_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -1994,6 +1994,8 @@ static void process_function(GoLSPContext *ctx, TSNode func_node) {
// resolver. Since #1909 receiver-qualified Go method QNs, that shared
// form is package_qn.ReceiverType.method for methods (the ONE-formula
// contract: cbm_go_receiver_type_name) and package_qn.func for functions.
// A package may hold any number of init() functions, so each one is
// package_qn.init#<file>:<N> (#1910, cbm_go_init_qn — the same formula).
ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->package_qn, func_name);
if (strcmp(ts_node_type(func_node), "method_declaration") == 0) {
TSNode recv = ts_node_child_by_field_name(func_node, "receiver", 8);
Expand All @@ -2004,6 +2006,9 @@ static void process_function(GoLSPContext *ctx, TSNode func_node) {
recv_type, func_name);
}
}
} else if (strcmp(func_name, "init") == 0) {
ctx->enclosing_func_qn = cbm_go_init_qn(ctx->arena, ctx->enclosing_func_qn, ctx->rel_path,
func_node, ctx->source);
}

// Push function scope
Expand Down Expand Up @@ -2261,7 +2266,7 @@ static void go_registry_with_stdlib(CBMTypeRegistry *reg, CBMArena *arena) {
// --- Entry point: build registry from file defs + run LSP ---

void cbm_run_go_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
TSNode root) {
TSNode root, const char *rel_path) {

CBMTypeRegistry reg;
cbm_registry_init(&reg, arena);
Expand Down Expand Up @@ -2619,6 +2624,7 @@ void cbm_run_go_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
// Phase 2: Build LSP context and run
GoLSPContext lsp_ctx;
go_lsp_init(&lsp_ctx, arena, source, source_len, &reg, module_qn, &result->resolved_calls);
lsp_ctx.rel_path = rel_path;

// Add imports
for (int i = 0; i < result->imports.count; i++) {
Expand Down Expand Up @@ -3239,9 +3245,9 @@ static void extract_type_params_from_ast(CBMArena *arena, CBMTypeRegistry *reg,
extern const TSLanguage *tree_sitter_go(void);

void cbm_run_go_lsp_cross(CBMArena *arena, const char *source, int source_len,
const char *module_qn, CBMLSPDef *defs, int def_count,
const char **import_names, const char **import_qns, int import_count,
TSTree *cached_tree, CBMResolvedCallArray *out) {
const char *module_qn, const char *rel_path, CBMLSPDef *defs,
int def_count, const char **import_names, const char **import_qns,
int import_count, TSTree *cached_tree, CBMResolvedCallArray *out) {
if (!source || source_len <= 0)
return;

Expand Down Expand Up @@ -3505,6 +3511,7 @@ void cbm_run_go_lsp_cross(CBMArena *arena, const char *source, int source_len,
// 4. Build LSP context and run
GoLSPContext ctx;
go_lsp_init(&ctx, arena, source, source_len, &reg, module_qn, out);
ctx.rel_path = rel_path;

for (int i = 0; i < import_count; i++) {
if (import_names[i] && import_qns[i]) {
Expand Down Expand Up @@ -3647,10 +3654,10 @@ CBMTypeRegistry *cbm_go_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, i
}

void cbm_run_go_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len,
const char *module_qn, CBMTypeRegistry *reg,
const char **import_names, const char **import_qns,
int import_count, TSTree *cached_tree,
CBMResolvedCallArray *out) {
const char *module_qn, const char *rel_path,
CBMTypeRegistry *reg, const char **import_names,
const char **import_qns, int import_count,
TSTree *cached_tree, CBMResolvedCallArray *out) {
if (!source || source_len <= 0 || !reg)
return;

Expand All @@ -3676,6 +3683,7 @@ void cbm_run_go_lsp_cross_with_registry(CBMArena *arena, const char *source, int

GoLSPContext ctx;
go_lsp_init(&ctx, arena, source, source_len, reg, module_qn, out);
ctx.rel_path = rel_path;
for (int i = 0; i < import_count; i++) {
if (import_names[i] && import_qns[i]) {
go_lsp_add_import(&ctx, import_names[i], import_qns[i]);
Expand Down Expand Up @@ -3825,8 +3833,8 @@ void cbm_batch_go_lsp_cross(CBMArena *arena, CBMBatchGoLSPFile *files, int file_

// Delegate to existing per-file function
cbm_run_go_lsp_cross(&file_arena, file->source, file->source_len, file->module_qn,
file->defs, file->def_count, file->import_names, file->import_qns,
file->import_count, file->cached_tree, &file_out);
file->rel_path, file->defs, file->def_count, file->import_names,
file->import_qns, file->import_count, file->cached_tree, &file_out);

// Copy results to output arena (must outlive per-file arena)
if (file_out.count > 0) {
Expand Down
10 changes: 9 additions & 1 deletion internal/cbm/lsp/go_lsp.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ typedef struct {
// Current function context
const char* enclosing_func_qn;
const char* package_qn; // module QN for this file
const char* rel_path; // repo-relative file path: init() QN suffix (#1910); NULL = flat

// Output: resolved calls accumulate here
CBMResolvedCallArray* resolved_calls;
Expand Down Expand Up @@ -63,8 +64,10 @@ const CBMRegisteredFunc* go_lookup_field_or_method(GoLSPContext* ctx,

// Entry point: build registry from file's own defs + run LSP resolution.
// Called from cbm_extract_file() after definitions and imports are extracted.
// rel_path is the file's repo-relative path: it names the file's init()
// functions exactly as the def extractor does (cbm_go_init_qn, #1910).
void cbm_run_go_lsp(CBMArena* arena, CBMFileResult* result,
const char* source, int source_len, TSNode root);
const char* source, int source_len, TSNode root, const char* rel_path);

// Register Go stdlib types and functions into a registry.
// Auto-generated by scripts/gen-go-stdlib.go.
Expand Down Expand Up @@ -112,11 +115,14 @@ typedef struct {
// Re-parses source with tree-sitter internally.
// defs includes both file-local and cross-file definitions.
// import_names/import_qns are parallel arrays of length import_count.
// rel_path: the file's repo-relative path, for its init() QNs (#1910); NULL
// leaves init callers flat (they then miss the caller join).
// Results written to out (arena-allocated).
void cbm_run_go_lsp_cross(
CBMArena* arena,
const char* source, int source_len,
const char* module_qn,
const char* rel_path,
CBMLSPDef* defs, int def_count,
const char** import_names, const char** import_qns, int import_count,
TSTree* cached_tree, // NULL = parse internally
Expand All @@ -135,6 +141,7 @@ void cbm_run_go_lsp_cross_with_registry(
CBMArena* arena,
const char* source, int source_len,
const char* module_qn,
const char* rel_path, // see cbm_run_go_lsp_cross
CBMTypeRegistry* reg, // pre-built, finalized, READ-ONLY
const char** import_names, const char** import_qns, int import_count,
TSTree* cached_tree, // NULL = parse internally
Expand All @@ -160,6 +167,7 @@ typedef struct {
const char* source;
int source_len;
const char* module_qn;
const char* rel_path; // repo-relative path (init() QNs, #1910); NULL = flat
TSTree* cached_tree; // from TSTree caching (NULL = parse internally)
CBMLSPDef* defs; // combined file-local + cross-file defs
int def_count;
Expand Down
26 changes: 14 additions & 12 deletions src/pipeline/pass_lsp_cross.c
Original file line number Diff line number Diff line change
Expand Up @@ -2082,14 +2082,15 @@ static CBMRustLSPDef *pxc_lspdefs_to_rust(CBMArena *arena, const CBMLSPDef *defs
void cbm_pxc_run_one(CBMLanguage lang, CBMFileResult *r, const char *source, int source_len,
const char *module_qn, CBMLSPDef *defs, int def_count, const char **imp_names,
const char **imp_qns, int imp_count) {
cbm_pxc_run_one_with_php_bindings(lang, r, source, source_len, module_qn, defs, def_count,
cbm_pxc_run_one_with_php_bindings(lang, r, source, source_len, module_qn, NULL, defs, def_count,
imp_names, imp_qns, imp_count, NULL);
}

void cbm_pxc_run_one_with_php_bindings(CBMLanguage lang, CBMFileResult *r, const char *source,
int source_len, const char *module_qn, CBMLSPDef *defs,
int def_count, const char **imp_names, const char **imp_qns,
int imp_count, const CBMPHPImportBindings *bindings) {
int source_len, const char *module_qn, const char *rel_path,
CBMLSPDef *defs, int def_count, const char **imp_names,
const char **imp_qns, int imp_count,
const CBMPHPImportBindings *bindings) {
TSTree *tree = r->cached_tree; /* may be NULL — LSP re-parses then */

CBMArena scratch;
Expand All @@ -2101,8 +2102,8 @@ void cbm_pxc_run_one_with_php_bindings(CBMLanguage lang, CBMFileResult *r, const

switch (lang) {
case CBM_LANG_GO:
cbm_run_go_lsp_cross(&scratch, source, source_len, module_qn, defs, def_count, imp_names,
imp_qns, imp_count, tree, &out);
cbm_run_go_lsp_cross(&scratch, source, source_len, module_qn, rel_path, defs, def_count,
imp_names, imp_qns, imp_count, tree, &out);
break;
case CBM_LANG_C:
case CBM_LANG_CPP:
Expand Down Expand Up @@ -2296,9 +2297,9 @@ void cbm_pxc_dispatch_file_with_php_bindings(
* lsp_unresolved entries -- no parse, no AST walk -- then the
* AST walk against the overlay (chained to the sealed base). */
cbm_go_fast_resolve_qualified_calls(result, prebuilt, imp_keys, imp_vals, imp_count);
cbm_run_go_lsp_cross_with_registry(&scratch, source, source_len, def_module, &overlay,
imp_keys, imp_vals, imp_count, result->cached_tree,
&out);
cbm_run_go_lsp_cross_with_registry(&scratch, source, source_len, def_module, rel,
&overlay, imp_keys, imp_vals, imp_count,
result->cached_tree, &out);
used_prebuilt = true;
break;
case CBM_LANG_PYTHON: {
Expand Down Expand Up @@ -2432,7 +2433,7 @@ void cbm_pxc_dispatch_file_with_php_bindings(
pxc_append_synthetic_calls(&result->arena, &result->calls, &synthetic_calls);
pxc_scratch_give(PXC_SCRATCH_DISPATCH, &scratch);
} else {
cbm_pxc_run_one_with_php_bindings(lang, result, source, source_len, def_module,
cbm_pxc_run_one_with_php_bindings(lang, result, source, source_len, def_module, rel,
file_defs, file_def_count, imp_keys, imp_vals,
imp_count, bindings);
}
Expand All @@ -2444,8 +2445,9 @@ void cbm_pxc_dispatch_file_with_php_bindings(
cbm_pxc_run_one_ts(result, source, source_len, def_module, file_defs, file_def_count,
imp_keys, imp_vals, imp_count, js, jsx, dts);
} else {
cbm_pxc_run_one_with_php_bindings(lang, result, source, source_len, def_module, file_defs,
file_def_count, imp_keys, imp_vals, imp_count, bindings);
cbm_pxc_run_one_with_php_bindings(lang, result, source, source_len, def_module, rel,
file_defs, file_def_count, imp_keys, imp_vals, imp_count,
bindings);
}
free(filtered);
}
Expand Down
Loading
Loading