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
26 changes: 26 additions & 0 deletions internal/cbm/cbm.h
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,24 @@ typedef struct {
int count;
} CBMReturnTypeTable;

// --- Scala companion cache ---
// The class/trait/enum names of every scope an object_definition has asked
// about, keyed by (scope node, name bytes). A scope is scanned once, on its
// first query; names are byte ranges into the source, never copied. One extra
// key per scanned scope (len == 0) records the scan itself, so a scope without
// any class is not rescanned either. Open addressing, power-of-two capacity.
typedef struct {
const void *scope; // ts_node id of the scope (parent) node; NULL = empty slot
uint32_t off;
uint32_t len;
} CBMScalaScopeName;

typedef struct {
CBMScalaScopeName *slots;
uint32_t cap; // 0 until the first query
uint32_t count;
} CBMScalaCompanionCache;

typedef struct {
CBMArena *arena;
/* Scratch for AST traversal, owned by the cbm_extract_file_ex call that
Expand Down Expand Up @@ -738,6 +756,9 @@ typedef struct {
* POD section index. */
void *doc_memo;
void *doc_pod_index;
/* Scala only; lives in `scratch` (or `arena` without one). Nothing in a
* CBMFileResult points at it. */
CBMScalaCompanionCache scala_companions;
} CBMExtractCtx;

// --- Public API ---
Expand Down Expand Up @@ -945,6 +966,11 @@ void cbm_impltrait_push(CBMImplTraitArray *arr, CBMArena *a, CBMImplTrait it);
void cbm_resolvedcall_push(CBMResolvedCallArray *arr, CBMArena *a, CBMResolvedCall rc);
void cbm_channels_push(CBMChannelArray *arr, CBMArena *a, CBMChannel ch);

/* True for a Scala object_definition with a same-named class/trait/enum
* sibling. O(1) per query after the first one in a scope, which scans the
* scope's named children once into ctx->scala_companions. */
bool cbm_scala_is_companion_object(CBMExtractCtx *ctx, TSNode node);

// --- Sub-extractor entry points ---

void cbm_extract_definitions(CBMExtractCtx *ctx);
Expand Down
18 changes: 15 additions & 3 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -6395,6 +6395,12 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
} else {
class_qn = cbm_fqn_compute_source_lang(a, ctx->project, ctx->rel_path, name, ctx->language);
}
/* A Scala class and its companion object are distinct owners despite sharing
* the source-level name. Use Scala's conventional singleton-class suffix so
* neither container nor their methods overwrite each other in the graph. */
if (ctx->language == CBM_LANG_SCALA && cbm_scala_is_companion_object(ctx, node)) {
class_qn = cbm_arena_sprintf(a, "%s$", class_qn);
}
const char *label = class_label_for_kind(kind);

// Sway/WGSL: label struct defs as "Struct" and Sway `abi` blocks as
Expand Down Expand Up @@ -9250,12 +9256,18 @@ static const char *compute_class_qn(CBMExtractCtx *ctx, TSNode node, const char
char *cname = cbm_node_text(ctx->arena, name_node, ctx->source);
if (cname && cname[0]) {
if (saved_enclosing) {
return cbm_arena_sprintf(ctx->arena, "%s.%s", saved_enclosing, cname);
const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", saved_enclosing, cname);
return ctx->language == CBM_LANG_SCALA && cbm_scala_is_companion_object(ctx, node)
? cbm_arena_sprintf(ctx->arena, "%s$", qn)
: qn;
}
/* Top-level: language-aware module so Java/Go don't double the
* filename stem (matches extract_class_def above). */
return cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path, cname,
ctx->language);
const char *qn = cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path,
cname, ctx->language);
return ctx->language == CBM_LANG_SCALA && cbm_scala_is_companion_object(ctx, node)
? cbm_arena_sprintf(ctx->arena, "%s$", qn)
: qn;
}
}
return saved_enclosing;
Expand Down
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
12 changes: 9 additions & 3 deletions internal/cbm/extract_unified.c
Original file line number Diff line number Diff line change
Expand Up @@ -1156,12 +1156,18 @@ static const char *compute_class_qn(CBMExtractCtx *ctx, TSNode node, const WalkS
* scope QN matches the def-side class QN (extract_defs.c compute_class_qn /
* extract_class_def), which the lsp_resolve join requires for nested types. */
if (state && state->enclosing_class_qn) {
return cbm_arena_sprintf(ctx->arena, "%s.%s", state->enclosing_class_qn, name);
const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", state->enclosing_class_qn, name);
return ctx->language == CBM_LANG_SCALA && cbm_scala_is_companion_object(ctx, node)
? cbm_arena_sprintf(ctx->arena, "%s$", qn)
: qn;
}

/* Java/Go: directory-based module (see compute_func_qn). */
return cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path, name,
ctx->language);
const char *qn =
cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path, name, ctx->language);
return ctx->language == CBM_LANG_SCALA && cbm_scala_is_companion_object(ctx, node)
? cbm_arena_sprintf(ctx->arena, "%s$", qn)
: qn;
}

/* Forward declaration */
Expand Down
Loading
Loading