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
77 changes: 77 additions & 0 deletions internal/cbm/extract_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -3237,6 +3237,79 @@ static bool python_callee_is_bound_parameter(CBMExtractCtx *ctx, WalkState *stat
return cbm_walk_python_param_is_bound(state, callee_name);
}

/* Scala receiver-aware guard (#2155; same intent as the Python and TS/JS flags).
* Scala has no LSP resolver, so `map.get(k)` or `xs.foreach(_.register())`
* reach the registry and bind an arbitrary project-wide `get` / `register` by
* short name. Flag a call that names a lower-case method on a VALUE receiver —
* `recv.m(...)`, curried `recv.m(a)(b)` and named infix `recv m arg` — so the
* call-resolution pass can suppress weak short-name
* matches for it. Unflagged, because the registry's receiver-chain check already
* judges them: `this`/`super` receivers, and chains rooted at an upper-case name
* (`Utils.helper`, `Bijections.finagle.m`) or applying an upper-case symbol
* through a package path (`http.param.Streaming(x)`). */
static bool scala_call_is_value_member(CBMExtractCtx *ctx, TSNode node) {
const char *kind = ts_node_type(node);
TSNode receiver;
TSNode method;
if (strcmp(kind, "infix_expression") == 0) {
receiver = ts_node_child_by_field_name(node, TS_FIELD("left"));
method = ts_node_child_by_field_name(node, TS_FIELD("operator"));
} else if (strcmp(kind, "call_expression") == 0) {
TSNode fn = ts_node_child_by_field_name(node, TS_FIELD("function"));
while (!ts_node_is_null(fn) && strcmp(ts_node_type(fn), "call_expression") == 0) {
fn = ts_node_child_by_field_name(fn, TS_FIELD("function")); /* curried */
}
if (ts_node_is_null(fn) || strcmp(ts_node_type(fn), "field_expression") != 0) {
return false;
}
receiver = ts_node_child_by_field_name(fn, TS_FIELD("value"));
method = ts_node_child_by_field_name(fn, TS_FIELD("field"));
} else {
return false;
}
if (ts_node_is_null(receiver) || ts_node_is_null(method)) {
return false;
}
/* Only an alphabetic lower-case member is a method name; `a + b` and
* `Foo(x)` applies are not receiver calls in this sense. */
const char *m = cbm_node_text(ctx->arena, method, ctx->source);
if (!m || m[0] < 'a' || m[0] > 'z') {
return false;
}
/* Walk the receiver chain down to its root. */
TSNode root = receiver;
for (;;) {
const char *rk = ts_node_type(root);
if (strcmp(rk, "field_expression") == 0) {
root = ts_node_child_by_field_name(root, TS_FIELD("value"));
} else if (strcmp(rk, "call_expression") == 0 || strcmp(rk, "generic_function") == 0) {
root = ts_node_child_by_field_name(root, TS_FIELD("function"));
} else {
break;
}
if (ts_node_is_null(root)) {
return false;
}
}
const char *rk = ts_node_type(root);
if (strcmp(rk, "this") == 0) {
return false;
}
if (strcmp(rk, "wildcard") == 0) {
return true; /* placeholder receiver `_.m()` — a value of unknown type */
}
if (strcmp(rk, "identifier") == 0 || strcmp(rk, "operator_identifier") == 0) {
const char *name = cbm_node_text(ctx->arena, root, ctx->source);
if (!name || !name[0] || strcmp(name, "this") == 0 || strcmp(name, "super") == 0) {
return false;
}
return !(name[0] >= 'A' && name[0] <= 'Z');
}
/* Literals, parenthesized/tuple/instance expressions, lambdas, strings: a
* value whose type the resolver does not know. */
return true;
}

static bool is_objectscript_language(CBMLanguage language) {
return language == CBM_LANG_OBJECTSCRIPT_UDL || language == CBM_LANG_OBJECTSCRIPT_ROUTINE;
}
Expand Down Expand Up @@ -3913,6 +3986,10 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML
}
}
}
// Scala receiver-aware guard — see scala_call_is_value_member.
if (ctx->language == CBM_LANG_SCALA) {
call.is_method = scala_call_is_value_member(ctx, node);
}

TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
/* tree-sitter-elixir attaches NO field name to a call's arguments
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
Loading
Loading