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
180 changes: 179 additions & 1 deletion internal/cbm/extract_imports.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ static void parse_go_imports(CBMExtractCtx *ctx);
static void parse_python_imports(CBMExtractCtx *ctx);
static void parse_es_imports(CBMExtractCtx *ctx);
static void parse_java_imports(CBMExtractCtx *ctx);
static void parse_scala_imports(CBMExtractCtx *ctx);
static void parse_rust_imports(CBMExtractCtx *ctx);
static void parse_c_imports(CBMExtractCtx *ctx);
static void parse_ruby_imports(CBMExtractCtx *ctx);
Expand Down Expand Up @@ -984,6 +985,146 @@ static void parse_generic_imports(CBMExtractCtx *ctx, const char *node_type) {
ts_tree_cursor_delete(&cursor);
}

// --- Scala imports ---

static char *scala_trim_span(CBMArena *a, const char *start, const char *end) {
while (start < end && isspace((unsigned char)*start)) {
start++;
}
while (end > start && isspace((unsigned char)end[-1])) {
end--;
}
return cbm_arena_strndup(a, start, (size_t)(end - start));
}

static void scala_emit_import(CBMExtractCtx *ctx, const char *path, const char *local) {
if (!path || !path[0] || !local || !local[0]) {
return;
}
CBMImport imp = {.local_name = cbm_arena_strdup(ctx->arena, local),
.module_path = cbm_arena_strdup(ctx->arena, path)};
cbm_imports_push(&ctx->result->imports, ctx->arena, imp);
}

static void scala_parse_selector(CBMExtractCtx *ctx, const char *prefix, const char *start,
const char *end) {
char *selector = scala_trim_span(ctx->arena, start, end);
if (!selector || !selector[0] ||
(strncmp(selector, "given", 5) == 0 &&
(selector[5] == '\0' || isspace((unsigned char)selector[5])))) {
return;
}
if (strcmp(selector, "*") == 0 || strcmp(selector, "_") == 0) {
scala_emit_import(ctx, prefix, "*");
return;
}

char *rename = strstr(selector, "=>");
size_t op_len = 2;
if (!rename) {
rename = strstr(selector, " as ");
op_len = 4;
}
char *name = selector;
char *local = selector;
if (rename) {
local = scala_trim_span(ctx->arena, rename + op_len, selector + strlen(selector));
name = scala_trim_span(ctx->arena, selector, rename);
if (strcmp(local, "_") == 0 || strcmp(local, "*") == 0) {
return; // Scala 2 hidden selector: `Name => _`
}
}
if (!name[0] || !local[0]) {
return;
}
const char *path =
prefix && prefix[0] ? cbm_arena_sprintf(ctx->arena, "%s.%s", prefix, name) : name;
scala_emit_import(ctx, path, local);
}

static void scala_parse_import_expr(CBMExtractCtx *ctx, const char *start, const char *end) {
char *expr = scala_trim_span(ctx->arena, start, end);
if (!expr || !expr[0]) {
return;
}
char *open = strchr(expr, '{');
char *close = strrchr(expr, '}');
if (open && close && close > open) {
char *prefix = scala_trim_span(ctx->arena, expr, open);
size_t plen = strlen(prefix);
while (plen > 0 && (prefix[plen - 1] == '.' || isspace((unsigned char)prefix[plen - 1]))) {
prefix[--plen] = '\0';
}
const char *part = open + 1;
for (const char *p = part;; p++) {
if (p == close || *p == ',') {
scala_parse_selector(ctx, prefix, part, p);
if (p == close) {
break;
}
part = p + 1;
}
}
return;
}

char *rename = strstr(expr, " as ");
if (rename) {
char *path = scala_trim_span(ctx->arena, expr, rename);
char *local = scala_trim_span(ctx->arena, rename + 4, expr + strlen(expr));
scala_emit_import(ctx, path, local);
return;
}
size_t len = strlen(expr);
if ((len > 2 && strcmp(expr + len - 2, ".*") == 0) ||
(len > 2 && strcmp(expr + len - 2, "._") == 0)) {
expr[len - 2] = '\0';
scala_emit_import(ctx, expr, "*");
return;
}
if (len > 6 && strcmp(expr + len - 6, ".given") == 0) {
expr[len - 6] = '\0';
scala_emit_import(ctx, expr, "*");
return;
}
scala_emit_import(ctx, expr, path_last(ctx->arena, expr));
}

static void parse_scala_imports(CBMExtractCtx *ctx) {
TSTreeCursor cursor = ts_tree_cursor_new(ctx->root);
if (!ts_tree_cursor_goto_first_child(&cursor)) {
ts_tree_cursor_delete(&cursor);
return;
}
do {
TSNode node = ts_tree_cursor_current_node(&cursor);
if (strcmp(ts_node_type(node), "import_declaration") != 0) {
continue;
}
char *text = cbm_node_text(ctx->arena, node, ctx->source);
if (!text || strncmp(text, "import", 6) != 0) {
continue;
}
const char *part = text + 6;
int brace_depth = 0;
for (const char *p = part;; p++) {
if (*p == '{') {
brace_depth++;
} else if (*p == '}') {
brace_depth--;
}
if (*p == '\0' || (*p == ',' && brace_depth == 0)) {
scala_parse_import_expr(ctx, part, p);
if (*p == '\0') {
break;
}
part = p + 1;
}
}
} while (ts_tree_cursor_goto_next_sibling(&cursor));
ts_tree_cursor_delete(&cursor);
}

// --- Kotlin imports ---
// tree-sitter-kotlin nests imports: source_file -> import_list -> import_header*.
// parse_generic_imports only scans the DIRECT children of root, and "import" is
Expand Down Expand Up @@ -1659,6 +1800,37 @@ static void parse_embedded_imports(CBMExtractCtx *ctx) {
}
}

// Scala chains braceless clauses: `package a` followed by `package b` puts the
// file's symbols in a.b. Append every directly following braceless
// package_clause to the namespace already recorded from the first one. A
// braced clause (`package b { ... }`) scopes only its body and is left alone.
static void join_chained_scala_packages(CBMExtractCtx *ctx, TSTreeCursor *cursor) {
while (ts_tree_cursor_goto_next_sibling(cursor)) {
TSNode next = ts_tree_cursor_current_node(cursor);
const char *next_kind = ts_node_type(next);
if (strcmp(next_kind, "comment") == 0 || strcmp(next_kind, "block_comment") == 0) {
continue;
}
if (strcmp(next_kind, "package_clause") != 0) {
return;
}
TSNode body = next;
if (find_first_descendant_of(next, "template_body", &body)) {
return;
}
TSNode name_node = next;
if (!find_first_descendant_of(next, "package_identifier", &name_node)) {
return;
}
char *segment = cbm_node_text(ctx->arena, name_node, ctx->source);
if (!segment || !segment[0]) {
return;
}
ctx->result->namespace_name =
cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->result->namespace_name, segment);
}
}

// --- Namespace / package declaration capture ---
// Java/Kotlin/C#/PHP put the file's symbols inside a namespace/package whose
// name is NOT reflected in the path-based QN scheme. Capturing it lets the
Expand All @@ -1670,6 +1842,7 @@ static void capture_namespace_decl(CBMExtractCtx *ctx) {
"file_scoped_namespace_declaration", // C# 10
"package_declaration", // Java / Kotlin
"package_header", // Kotlin
"package_clause", // Scala
"namespace_definition", // PHP
NULL};
TSTreeCursor cursor = ts_tree_cursor_new(ctx->root);
Expand All @@ -1686,6 +1859,7 @@ static void capture_namespace_decl(CBMExtractCtx *ctx) {
// The namespace name is the first qualified_name / scoped_identifier /
// namespace_name / identifier descendant.
static const char *name_kinds[] = {"qualified_name",
"package_identifier",
"scoped_identifier",
"namespace_name",
"identifier",
Expand All @@ -1703,6 +1877,9 @@ static void capture_namespace_decl(CBMExtractCtx *ctx) {
}
}
if (ctx->result->namespace_name) {
if (strcmp(kind, "package_clause") == 0) {
join_chained_scala_packages(ctx, &cursor);
}
break;
}
} while (ts_tree_cursor_goto_next_sibling(&cursor));
Expand Down Expand Up @@ -2969,6 +3146,7 @@ void cbm_extract_imports(CBMExtractCtx *ctx) {
switch (ctx->language) {
case CBM_LANG_JAVA:
case CBM_LANG_KOTLIN:
case CBM_LANG_SCALA:
case CBM_LANG_CSHARP:
case CBM_LANG_PHP:
capture_namespace_decl(ctx);
Expand Down Expand Up @@ -2996,7 +3174,7 @@ void cbm_extract_imports(CBMExtractCtx *ctx) {
parse_kotlin_imports(ctx);
break;
case CBM_LANG_SCALA:
parse_generic_imports(ctx, "import_declaration");
parse_scala_imports(ctx);
break;
case CBM_LANG_CSHARP:
parse_csharp_imports(ctx);
Expand Down
8 changes: 4 additions & 4 deletions scripts/memory-core-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ src/pipeline/pass_compile_commands.c 17
src/pipeline/pass_complexity.c 15
src/pipeline/pass_configlink.c 3
src/pipeline/pass_cross_repo.c 8
src/pipeline/pass_definitions.c 14
src/pipeline/pass_definitions.c 12
src/pipeline/pass_enrichment.c 15
src/pipeline/pass_ensemble_routing.c 10
src/pipeline/pass_envscan.c 2
Expand All @@ -71,15 +71,15 @@ src/pipeline/pass_importance.c 4
src/pipeline/pass_infrascan.c 2
src/pipeline/pass_k8s.c 6
src/pipeline/pass_lsp_cross.c 50
src/pipeline/pass_parallel.c 31
src/pipeline/pass_pkgmap.c 82
src/pipeline/pass_parallel.c 29
src/pipeline/pass_pkgmap.c 80
src/pipeline/pass_semantic.c 3
src/pipeline/pass_tests.c 3
src/pipeline/pass_usages.c 4
src/pipeline/path_alias.c 42
src/pipeline/pipeline.c 134
src/pipeline/pipeline_delta.c 19
src/pipeline/pipeline_incremental.c 144
src/pipeline/pipeline_incremental.c 142
src/pipeline/registry.c 17
src/pipeline/worker_pool.c 2
src/store/store.c 358
Expand Down
9 changes: 5 additions & 4 deletions src/pipeline/pass_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ static const cbm_gbuf_node_t *pc_find_by_computed_qn(const cbm_gbuf_t *gbuf, con
return n;
}

static int build_import_map(cbm_pipeline_ctx_t *ctx, const char *rel_path,
static int build_import_map(cbm_pipeline_ctx_t *ctx, const char *rel_path, CBMLanguage language,
const CBMFileResult *result, const char ***out_keys,
const char ***out_vals, int *out_count) {
*out_keys = NULL;
Expand All @@ -141,7 +141,7 @@ static int build_import_map(cbm_pipeline_ctx_t *ctx, const char *rel_path,
continue;
}
const cbm_gbuf_node_t *target =
cbm_pipeline_resolve_import_node(ctx, rel_path, file_qn, imp, NULL);
cbm_pipeline_resolve_import_node(ctx, rel_path, file_qn, language, imp, NULL, NULL);
if (!target) {
continue;
}
Expand Down Expand Up @@ -1053,7 +1053,7 @@ int cbm_pipeline_pass_calls(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file
const char **imp_keys = NULL;
const char **imp_vals = NULL;
int imp_count = 0;
build_import_map(ctx, rel, result, &imp_keys, &imp_vals, &imp_count);
build_import_map(ctx, rel, files[i].language, result, &imp_keys, &imp_vals, &imp_count);

/* Compute module QN for same-module resolution (directory-based for
* Java/Go so it matches their def-node QNs in the registry). */
Expand Down Expand Up @@ -1216,7 +1216,8 @@ void cbm_pipeline_pass_fastapi_depends(cbm_pipeline_ctx_t *ctx, const cbm_file_i
const char **imp_keys = NULL;
const char **imp_vals = NULL;
int imp_count = 0;
build_import_map(ctx, files[i].rel_path, result, &imp_keys, &imp_vals, &imp_count);
build_import_map(ctx, files[i].rel_path, files[i].language, result, &imp_keys, &imp_vals,
&imp_count);

for (int d = 0; d < result->defs.count; d++) {
CBMDefinition *def = &result->defs.items[d];
Expand Down
34 changes: 20 additions & 14 deletions src/pipeline/pass_definitions.c
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,9 @@ int cbm_pipeline_create_env_configures_for_file(cbm_pipeline_ctx_t *ctx,
/* Create IMPORTS edges for one file's imports. Mirrors the resolution
* logic in pass_parallel.c register_and_link_def — keep the two in sync. */
static int create_import_edges_for_file(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result,
const char *rel, CBMHashTable *namespace_map) {
const char *rel, CBMLanguage language,
CBMHashTable *namespace_map,
const cbm_scala_index_t *scala_index) {
int count = 0;
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__");
const cbm_gbuf_node_t *source_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn);
Expand All @@ -525,8 +527,8 @@ static int create_import_edges_for_file(cbm_pipeline_ctx_t *ctx, const CBMFileRe
if (!imp->module_path) {
continue;
}
const cbm_gbuf_node_t *target =
cbm_pipeline_resolve_import_node(ctx, rel, file_qn, imp, namespace_map);
const cbm_gbuf_node_t *target = cbm_pipeline_resolve_import_node(
ctx, rel, file_qn, language, imp, namespace_map, scala_index);
if (target && target->id != source_node->id) {
char esc_ln[CBM_SZ_128];
cbm_json_escape(esc_ln, sizeof(esc_ln), imp->local_name ? imp->local_name : "");
Expand Down Expand Up @@ -938,7 +940,8 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
* resolve to defs already in the graph, but the file's
* own defs are now persisted before the lookup. No namespace
* map is available without the cache (single-file scope). */
total_imports += create_import_edges_for_file(ctx, result, rel, NULL);
total_imports +=
create_import_edges_for_file(ctx, result, rel, files[i].language, NULL, NULL);
create_channel_edges_for_file(ctx, result, rel);
cbm_pipeline_create_env_configures_for_file(ctx, result, rel);
cbm_free_result(result);
Expand All @@ -952,16 +955,19 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
if (local_cache) {
/* Build a namespace/package → File-QN map so that namespace imports
* (C# `using`, Java/Kotlin `import`, PHP `use`) resolve to the file
* that declares the namespace. */
const char **rels = (const char **)calloc((size_t)file_count, sizeof(char *));
if (rels) {
* that declares the namespace, and the Scala top-level index. */
CBMHashTable *namespace_map = NULL;
cbm_scala_index_t *scala_index = NULL;
const char **namespaces =
cbm_calloc(CBM_MEM_CLASS_OTHER, (size_t)file_count * sizeof(char *));
if (namespaces) {
for (int i = 0; i < file_count; i++) {
rels[i] = files[i].rel_path;
namespaces[i] = local_cache[i] ? local_cache[i]->namespace_name : NULL;
}
cbm_pipeline_import_maps_build(ctx, files, file_count, namespaces, &namespace_map,
&scala_index);
cbm_free(CBM_MEM_CLASS_OTHER, namespaces);
}
CBMHashTable *namespace_map =
cbm_pipeline_namespace_map_build(ctx->project_name, local_cache, rels, file_count);
free(rels);
for (int i = 0; i < file_count; i++) {
if (cbm_pipeline_check_cancel(ctx)) {
break;
Expand All @@ -970,12 +976,12 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
if (!result) {
continue;
}
total_imports +=
create_import_edges_for_file(ctx, result, files[i].rel_path, namespace_map);
total_imports += create_import_edges_for_file(
ctx, result, files[i].rel_path, files[i].language, namespace_map, scala_index);
create_channel_edges_for_file(ctx, result, files[i].rel_path);
cbm_pipeline_create_env_configures_for_file(ctx, result, files[i].rel_path);
}
cbm_pipeline_namespace_map_free(namespace_map);
cbm_pipeline_import_maps_free(namespace_map, scala_index);
if (owns_local_cache) {
for (int i = 0; i < file_count; i++) {
if (local_cache[i]) {
Expand Down
Loading
Loading