mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
commit
dcb6d49c8f
12 files changed
+1081
-53
No files matched your search
+1
-1
@@ -846,7 +846,7 @@ TEST_PY_LSP_SCALE_SRCS = tests/test_py_lsp_scale.c
|
||||
|
||||
TEST_TS_LSP_SRCS = tests/test_ts_lsp.c
|
||||
TEST_JAVA_LSP_SRCS = tests/test_java_lsp.c tests/test_java_lsp_coverage.c
|
||||
TEST_KOTLIN_LSP_SRCS = tests/test_kotlin_lsp.c
|
||||
TEST_KOTLIN_LSP_SRCS = tests/test_kotlin_lsp.c tests/test_kotlin_typed_calls.c
|
||||
TEST_RUST_LSP_SRCS = tests/test_rust_lsp.c
|
||||
|
||||
TEST_INTEGRATION_SRCS = tests/test_integration.c tests/test_incremental.c tests/test_lang_contract.c tests/test_edge_imports.c tests/test_edge_structural.c tests/test_lsp_resolution_probe.c tests/test_node_creation_probe.c tests/test_edge_types_probe.c tests/test_convergence_probe.c tests/test_matrix_known_classes.c tests/test_matrix_new_constructs.c tests/test_grammar_probe_a.c tests/test_grammar_probe_b.c tests/test_grammar_probe_c.c tests/test_grammar_probe_d.c tests/test_grammar_probe_e.c tests/test_grammar_probe_f.c tests/test_grammar_probe_g.c tests/test_conditional_variants.c tests/test_spawns.c
|
||||
|
||||
+156
-14
@@ -395,9 +395,47 @@ static const CBMType *kt_unwrap_nullable(const CBMType *t) {
|
||||
return t;
|
||||
}
|
||||
|
||||
/* A kotlin.Any member (toString / equals / hashCode) resolved on a stdlib or
|
||||
* JDK type -- `count.toString()` on `val count: Int` binds kotlin.Int.toString
|
||||
* from the stdlib seed. That target has no graph node, so the call join fell
|
||||
* back to a name guess and bound the call to whichever project class declares
|
||||
* a same-named override (#1555: detekt `defaultValue.toString()` ->
|
||||
* Entity.toString). The member is the universal kotlin.Any one, an external
|
||||
* builtin node: emit that, no project edge. `kotlin.` and `java.` are reserved
|
||||
* packages (the compilers reject project declarations in them), so the prefix
|
||||
* proves the owner is not a project type; project QNs in this resolver are
|
||||
* package-spelled and keep their declared override. */
|
||||
static bool kt_is_any_member(const char *name) {
|
||||
return strcmp(name, "toString") == 0 || strcmp(name, "equals") == 0 ||
|
||||
strcmp(name, "hashCode") == 0;
|
||||
}
|
||||
|
||||
static const char *kt_external_any_member(KotlinLSPContext *ctx, const char *callee_qn) {
|
||||
const char *dot = strrchr(callee_qn, '.');
|
||||
if (!dot || !kt_is_any_member(dot + 1)) {
|
||||
return NULL;
|
||||
}
|
||||
if (strncmp(callee_qn, "kotlin.Any.", strlen("kotlin.Any.")) == 0) {
|
||||
return NULL; /* already the builtin */
|
||||
}
|
||||
if (strncmp(callee_qn, "kotlin.", strlen("kotlin.")) != 0 &&
|
||||
strncmp(callee_qn, "java.", strlen("java.")) != 0) {
|
||||
return NULL; /* a project override: a real node */
|
||||
}
|
||||
return kt_join_dot(ctx->arena, "kotlin.Any", dot + 1);
|
||||
}
|
||||
|
||||
/* Append a typed resolution result to the output array. */
|
||||
static void kt_emit_resolved_kind(KotlinLSPContext *ctx, const char *callee_qn,
|
||||
const char *strategy, float confidence, CBMResolvedKind kind) {
|
||||
if (kind == CBM_RESOLVED_INVOCATION && callee_qn) {
|
||||
const char *any_qn = kt_external_any_member(ctx, callee_qn);
|
||||
if (any_qn) {
|
||||
callee_qn = any_qn;
|
||||
strategy = "lsp_kt_any";
|
||||
confidence = confidence < KT_CONF_PARTIAL ? confidence : KT_CONF_PARTIAL;
|
||||
}
|
||||
}
|
||||
if (ctx->debug) {
|
||||
fprintf(stderr, "[kotlin_lsp] EMIT %s -> %s [%s %.2f] (resolved_calls=%p enc=%s)\n",
|
||||
ctx->enclosing_func_qn ? ctx->enclosing_func_qn : "(null)",
|
||||
@@ -1127,20 +1165,26 @@ const char *kotlin_resolve_function_name(KotlinLSPContext *ctx, const char *name
|
||||
* package/import/bare/default-import lookups miss, so it never overrides a
|
||||
* more specific match; in the per-file pass the registry holds just this
|
||||
* file's defs, so the candidate is the file's own sole top-level fun. */
|
||||
if (ctx->registry && ctx->registry->funcs) {
|
||||
/* The cross-file walk runs on an own-file overlay chained to the cross
|
||||
* registry (#1555), so the candidates span the whole chain; a function
|
||||
* registered in both layers under one QN is one candidate. */
|
||||
{
|
||||
const char *only_qn = NULL;
|
||||
int matches = 0;
|
||||
for (int i = 0; i < ctx->registry->func_count && matches < 2; i++) {
|
||||
const CBMRegisteredFunc *f = &ctx->registry->funcs[i];
|
||||
if (!f->qualified_name || !f->short_name) {
|
||||
continue;
|
||||
}
|
||||
if (f->receiver_type) { /* method / extension — not a bare top-level fun */
|
||||
continue;
|
||||
}
|
||||
if (strcmp(f->short_name, name) == 0) {
|
||||
only_qn = f->qualified_name;
|
||||
matches++;
|
||||
for (const CBMTypeRegistry *r = ctx->registry; r && matches < 2; r = r->fallback) {
|
||||
for (int i = 0; r->funcs && i < r->func_count && matches < 2; i++) {
|
||||
const CBMRegisteredFunc *f = &r->funcs[i];
|
||||
if (!f->qualified_name || !f->short_name) {
|
||||
continue;
|
||||
}
|
||||
if (f->receiver_type) { /* method / extension — not a bare top-level fun */
|
||||
continue;
|
||||
}
|
||||
if (strcmp(f->short_name, name) == 0 &&
|
||||
!(only_qn && strcmp(only_qn, f->qualified_name) == 0)) {
|
||||
only_qn = f->qualified_name;
|
||||
matches++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (matches == 1 && only_qn) {
|
||||
@@ -1335,6 +1379,22 @@ const CBMRegisteredFunc *kotlin_lookup_method(KotlinLSPContext *ctx, const char
|
||||
const CBMRegisteredFunc *rf =
|
||||
cbm_registry_lookup_method_aliased(ctx->registry, cur_qn, method_name);
|
||||
if (rf) {
|
||||
/* A member wins over an extension of the same name (Kotlin
|
||||
* resolution). The file's own extension functions carry the
|
||||
* extended class as receiver_type, so the receiver lookup can
|
||||
* return `fun Builder.add(..)` for a call meant for the member
|
||||
* `Builder.add`; the member's QN nests under the class. */
|
||||
size_t cur_len = strlen(cur_qn);
|
||||
bool is_member = rf->qualified_name &&
|
||||
strncmp(rf->qualified_name, cur_qn, cur_len) == 0 &&
|
||||
rf->qualified_name[cur_len] == '.';
|
||||
if (!is_member) {
|
||||
const CBMRegisteredFunc *member = cbm_registry_lookup_func(
|
||||
ctx->registry, kt_join_dot(ctx->arena, cur_qn, method_name));
|
||||
if (member) {
|
||||
return member;
|
||||
}
|
||||
}
|
||||
return rf;
|
||||
}
|
||||
|
||||
@@ -2907,6 +2967,37 @@ out:
|
||||
return result ? result : cbm_type_unknown();
|
||||
}
|
||||
|
||||
/* `handshake()` inside a class whose constructor declares `private val
|
||||
* handshake: Handshake?` next to `override fun handshake()` (okhttp
|
||||
* RealConnection): the property is in the method scope (class fields are bound
|
||||
* there), but a value of a known type without an `invoke` operator is no call
|
||||
* candidate, so Kotlin resolves the call to the member function. Once #1555
|
||||
* typed the class's constructor vals, the lexical-shadow rule below failed
|
||||
* such calls closed and they fell to a name guess. An unknown or callable
|
||||
* binding still shadows, as before. */
|
||||
static bool kt_value_binding_yields_to_member(KotlinLSPContext *ctx, const CBMType *bound,
|
||||
const char *name) {
|
||||
if (!bound || cbm_type_is_unknown(bound) || bound->kind == CBM_TYPE_CALLABLE ||
|
||||
bound->kind == CBM_TYPE_FUNC || !ctx->this_type || cbm_type_is_unknown(ctx->this_type)) {
|
||||
return false;
|
||||
}
|
||||
const CBMType *value = kt_unwrap_nullable(bound);
|
||||
const char *value_qn = kt_type_qn_of(value);
|
||||
if (value_qn && kotlin_lookup_method(ctx, value_qn, "invoke")) {
|
||||
return false; /* an invokable value: the call may be `value.invoke()` */
|
||||
}
|
||||
const char *this_qn = kt_type_qn_of(ctx->this_type);
|
||||
if (!this_qn) {
|
||||
return false;
|
||||
}
|
||||
const CBMRegisteredFunc *rf = kotlin_lookup_method(ctx, this_qn, name);
|
||||
if (!rf || !rf->qualified_name) {
|
||||
return false;
|
||||
}
|
||||
size_t len = strlen(this_qn);
|
||||
return strncmp(rf->qualified_name, this_qn, len) == 0 && rf->qualified_name[len] == '.';
|
||||
}
|
||||
|
||||
static const CBMType *kt_eval_constructor_or_func_call(KotlinLSPContext *ctx, TSNode call_node,
|
||||
const char *callee_text) {
|
||||
/* callee_text is a bare identifier — it may be:
|
||||
@@ -2944,7 +3035,10 @@ static const CBMType *kt_eval_constructor_or_func_call(KotlinLSPContext *ctx, TS
|
||||
bound->data.func.return_types[0]) {
|
||||
return bound->data.func.return_types[0];
|
||||
}
|
||||
return cbm_type_unknown();
|
||||
if (!kt_value_binding_yields_to_member(ctx, bound, callee_text)) {
|
||||
return cbm_type_unknown();
|
||||
}
|
||||
/* A known non-invokable value yields to the member function: step 1. */
|
||||
}
|
||||
|
||||
/* 1. `this`-method dispatch: if we're inside a class method or DSL
|
||||
@@ -2967,6 +3061,37 @@ static const CBMType *kt_eval_constructor_or_func_call(KotlinLSPContext *ctx, TS
|
||||
}
|
||||
}
|
||||
|
||||
/* 1b. Implicit receivers form a chain, innermost first: inside a lambda
|
||||
* whose receiver (or `it` type, which kt_process_lambda also installs as
|
||||
* `this`) is another type, and inside a member extension whose extension
|
||||
* receiver is another type, the enclosing class's members stay callable
|
||||
* bare. Once #1555 typed more receivers, `repo.let { ping() }` resolved
|
||||
* against Repo only and fell to a name guess. A call naming a class is a
|
||||
* constructor call, left to step 2 (a secondary constructor is registered
|
||||
* as a same-named member and would otherwise be taken for a method). */
|
||||
const char *ctor_cls = kotlin_resolve_class_name(ctx, callee_text);
|
||||
bool is_ctor_call =
|
||||
ctor_cls && ctx->registry && cbm_registry_lookup_type(ctx->registry, ctor_cls) != NULL;
|
||||
if (ctx->enclosing_class_qn && !is_ctor_call) {
|
||||
const char *this_qn = (ctx->this_type && !cbm_type_is_unknown(ctx->this_type))
|
||||
? kt_type_qn_of(ctx->this_type)
|
||||
: NULL;
|
||||
if (!this_qn || strcmp(this_qn, ctx->enclosing_class_qn) != 0) {
|
||||
const CBMRegisteredFunc *rf =
|
||||
kotlin_lookup_method(ctx, ctx->enclosing_class_qn, callee_text);
|
||||
if (rf && rf->qualified_name) {
|
||||
kt_emit_resolved_at(ctx, rf->qualified_name, "lsp_kt_this", KT_CONF_THIS,
|
||||
call_node);
|
||||
if (rf->signature && rf->signature->kind == CBM_TYPE_FUNC &&
|
||||
rf->signature->data.func.return_types &&
|
||||
rf->signature->data.func.return_types[0]) {
|
||||
return rf->signature->data.func.return_types[0];
|
||||
}
|
||||
return cbm_type_unknown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 2. Class? */
|
||||
const char *cls_qn = kotlin_resolve_class_name(ctx, callee_text);
|
||||
if (cls_qn && ctx->registry) {
|
||||
@@ -4533,6 +4658,11 @@ void kotlin_lsp_process_file(KotlinLSPContext *ctx, TSNode root) {
|
||||
|
||||
/* 2. Collect top-level definitions for intra-file resolution. */
|
||||
kt_collect_top_level_decls(ctx, root);
|
||||
/* Cross-file: index the own-file overlay now that its declarations are
|
||||
* in; later additions stay visible through the post-finalize tail. */
|
||||
if (ctx->overlay_index_arena && ctx->registry) {
|
||||
cbm_registry_finalize_into((CBMTypeRegistry *)ctx->registry, ctx->overlay_index_arena);
|
||||
}
|
||||
|
||||
/* 3. Walk for call resolution. */
|
||||
kt_walk_top_level_for_resolution(ctx, root, NULL);
|
||||
@@ -5436,12 +5566,24 @@ void cbm_run_kotlin_lsp_cross(CBMArena *arena, const char *source, int source_le
|
||||
project_name = module_qn;
|
||||
}
|
||||
|
||||
/* Own-file overlay (#1555). The cross registry above already holds this
|
||||
* file's classes, built from extraction defs whose properties carry no
|
||||
* type (primary-constructor vals none at all). The walk re-registers each
|
||||
* class from the AST with typed fields; registered into `reg` under the
|
||||
* same QN that entry was shadowed by the earlier one, so every
|
||||
* `repository.inserir()` on an injected property fell to the name guess.
|
||||
* In an overlay checked first, the AST entry wins. */
|
||||
CBMTypeRegistry overlay;
|
||||
cbm_registry_init(&overlay, arena);
|
||||
overlay.fallback = ®
|
||||
|
||||
KotlinLSPContext ctx;
|
||||
/* Match the per-file pass: until a source package declaration overrides
|
||||
* it, the path-derived module is the callable-QN prefix. This preserves
|
||||
* exact caller identity for default-package functions across files. */
|
||||
kotlin_lsp_init(&ctx, arena, source, source_len, ®, module_qn ? module_qn : "",
|
||||
kotlin_lsp_init(&ctx, arena, source, source_len, &overlay, module_qn ? module_qn : "",
|
||||
module_qn ? module_qn : "", project_name, /*rel_path=*/NULL, out);
|
||||
ctx.overlay_index_arena = &idx_arena;
|
||||
|
||||
/* Unique short type name -> registered QN, for receiver annotations that
|
||||
* name a type defined in another file without an import. Two types
|
||||
|
||||
@@ -147,6 +147,13 @@ typedef struct KotlinLSPContext {
|
||||
* each field's REAL def QN here (kotlin class properties are minted with
|
||||
* module-level QNs, so composing <class>.<member> would miss the node). */
|
||||
const void *cross_field_map;
|
||||
/* Cross-file only (NULL per-file): when set, `registry` is a per-file
|
||||
* overlay chained to the cross registry, and kotlin_lsp_process_file
|
||||
* indexes the overlay into this scratch arena once the file's own
|
||||
* declarations are collected. The overlay holds the AST registrations
|
||||
* (typed primary-constructor and body properties), so they are found
|
||||
* before the extraction-built entries of the same QN (#1555). */
|
||||
CBMArena *overlay_index_arena;
|
||||
} KotlinLSPContext;
|
||||
|
||||
/* Initialize a KotlinLSPContext for processing one file. */
|
||||
|
||||
+56
-19
@@ -4128,6 +4128,32 @@ static bool rust_qn_has_suffix(const char *qualified_name, const char *suffix) {
|
||||
(qn_len == suffix_len || qualified_name[qn_len - suffix_len - 1U] == '.');
|
||||
}
|
||||
|
||||
/* True when the segment that qualifies a call path's tail names a TYPE:
|
||||
* `Store::new`, `Self::helper`, `T::default`, `<T as Tr>::f` (turbofish is
|
||||
* already stripped, so a `>` before the last `::` closes a qualified path).
|
||||
* Such a call is an associated function, never a free function, so the
|
||||
* free-function short-name fallbacks must not bind it: with a project-wide
|
||||
* unique free `new`/`write`, `Store::new()` landed on that function and the
|
||||
* LSP row pre-empted the registry's correct `Store.new`. Module segments are
|
||||
* snake_case, so an ASCII-uppercase initial is a type. `tail` must point just
|
||||
* past the path's last `::`, or equal `path` when there is none. */
|
||||
static bool rust_path_qualifier_is_type(const char *path, const char *tail) {
|
||||
if (!path || !tail || tail - path < 3) {
|
||||
return false;
|
||||
}
|
||||
const char *seg_end = tail - 2; /* the last "::" */
|
||||
if (seg_end[-1] == '>') {
|
||||
return true;
|
||||
}
|
||||
const char *seg = path;
|
||||
for (const char *p = path; p + 1 < seg_end; p++) {
|
||||
if (p[0] == ':' && p[1] == ':') {
|
||||
seg = p + 2;
|
||||
}
|
||||
}
|
||||
return seg < seg_end && seg[0] >= 'A' && seg[0] <= 'Z';
|
||||
}
|
||||
|
||||
/* Resolve only an exact identifier/scoped-path value. No branch, closure, or
|
||||
* other composite expression enters this helper. A suffix fallback is admitted
|
||||
* only for a qualified import path and only when it identifies one free func. */
|
||||
@@ -4514,6 +4540,9 @@ static void rust_resolve_call_expression_inner(RustLSPContext *ctx, TSNode node)
|
||||
} else {
|
||||
tail = path;
|
||||
}
|
||||
/* `Type::f` is an associated function: the two free-function
|
||||
* fallbacks below never bind it (see rust_path_qualifier_is_type). */
|
||||
const bool assoc_call = rust_path_qualifier_is_type(path, tail);
|
||||
|
||||
/* Cross-crate workspace-member resolution (#56): when the call
|
||||
* path's head is a declared Cargo workspace member (e.g.
|
||||
@@ -4524,7 +4553,7 @@ static void rust_resolve_call_expression_inner(RustLSPContext *ctx, TSNode node)
|
||||
* MEMBER crate by matching the registered QN's `.<member>.`
|
||||
* path segment plus the call tail. Requires a parsed manifest
|
||||
* (threaded through pass_lsp_cross.c); NULL manifest skips this. */
|
||||
if (ctx->cargo_manifest && tail && *tail) {
|
||||
if (!assoc_call && ctx->cargo_manifest && tail && *tail) {
|
||||
const char *head_sep = strstr(path, "::");
|
||||
if (head_sep && head_sep > path) {
|
||||
char *head = cbm_arena_strndup(ctx->arena, path, (size_t)(head_sep - path));
|
||||
@@ -4568,7 +4597,7 @@ static void rust_resolve_call_expression_inner(RustLSPContext *ctx, TSNode node)
|
||||
* foo; use foo::bar; bar()` a chance to resolve when the
|
||||
* intermediate module wasn't tracked through an explicit
|
||||
* use-map entry. */
|
||||
if (tail && *tail && ctx->module_qn) {
|
||||
if (!assoc_call && tail && *tail && ctx->module_qn) {
|
||||
/* Crate prefix is the first dotted segment of module_qn after
|
||||
* the project name, but for simplicity we just match on
|
||||
* "starts with first dot-segment". */
|
||||
@@ -6323,7 +6352,10 @@ static void rust_resolve_against_registry(CBMArena *arena, const char *source, i
|
||||
* read-only, and shared across every Rust file's resolve (mirrors C/py/cs/ts).
|
||||
* Converts the pipeline's CBMLSPDef into CBMRustLSPDef inline (same field copy as
|
||||
* pass_lsp_cross.c's pxc_lspdefs_to_rust, including proven trait provenance).
|
||||
* module_qn=NULL is byte-identical because all_defs always carry def_module_qn. */
|
||||
* module_qn=NULL is byte-identical because all_defs always carry def_module_qn.
|
||||
* all_defs is mixed-language: only Rust defs are registered, exactly like the
|
||||
* per-file conversion, so a Perl `new` or a C function can never become the
|
||||
* target of a Rust call (the Go builder filters the same way). */
|
||||
CBMTypeRegistry *cbm_rust_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, int def_count) {
|
||||
if (!arena)
|
||||
return NULL;
|
||||
@@ -6331,29 +6363,34 @@ CBMTypeRegistry *cbm_rust_build_cross_registry(CBMArena *arena, CBMLSPDef *defs,
|
||||
if (!reg)
|
||||
return NULL;
|
||||
CBMRustLSPDef *rdefs = NULL;
|
||||
int n = 0;
|
||||
if (def_count > 0) {
|
||||
rdefs = (CBMRustLSPDef *)cbm_arena_alloc(arena, (size_t)def_count * sizeof(CBMRustLSPDef));
|
||||
if (!rdefs)
|
||||
return NULL;
|
||||
for (int i = 0; i < def_count; i++) {
|
||||
rdefs[i].qualified_name = defs[i].qualified_name;
|
||||
rdefs[i].short_name = defs[i].short_name;
|
||||
rdefs[i].label = defs[i].label;
|
||||
rdefs[i].receiver_type = defs[i].receiver_type;
|
||||
rdefs[i].def_module_qn = defs[i].def_module_qn;
|
||||
rdefs[i].return_types = defs[i].return_types;
|
||||
rdefs[i].embedded_types = defs[i].embedded_types;
|
||||
rdefs[i].field_defs = defs[i].field_defs;
|
||||
rdefs[i].method_names_str = defs[i].method_names_str;
|
||||
rdefs[i].signature_param_types = defs[i].signature_param_types;
|
||||
rdefs[i].signature_param_count = defs[i].signature_param_count;
|
||||
rdefs[i].trait_qn = defs[i].trait_qn;
|
||||
rdefs[i].is_interface = defs[i].is_interface;
|
||||
rdefs[i].is_rust_impl_relation = defs[i].is_rust_impl_relation;
|
||||
rdefs[i].is_abstract = defs[i].is_abstract;
|
||||
if (defs[i].lang != CBM_LANG_RUST) {
|
||||
continue;
|
||||
}
|
||||
rdefs[n].qualified_name = defs[i].qualified_name;
|
||||
rdefs[n].short_name = defs[i].short_name;
|
||||
rdefs[n].label = defs[i].label;
|
||||
rdefs[n].receiver_type = defs[i].receiver_type;
|
||||
rdefs[n].def_module_qn = defs[i].def_module_qn;
|
||||
rdefs[n].return_types = defs[i].return_types;
|
||||
rdefs[n].embedded_types = defs[i].embedded_types;
|
||||
rdefs[n].field_defs = defs[i].field_defs;
|
||||
rdefs[n].method_names_str = defs[i].method_names_str;
|
||||
rdefs[n].signature_param_types = defs[i].signature_param_types;
|
||||
rdefs[n].signature_param_count = defs[i].signature_param_count;
|
||||
rdefs[n].trait_qn = defs[i].trait_qn;
|
||||
rdefs[n].is_interface = defs[i].is_interface;
|
||||
rdefs[n].is_rust_impl_relation = defs[i].is_rust_impl_relation;
|
||||
rdefs[n].is_abstract = defs[i].is_abstract;
|
||||
n++;
|
||||
}
|
||||
}
|
||||
rust_populate_cross_registry(reg, arena, rdefs, def_count, /*module_qn=*/NULL);
|
||||
rust_populate_cross_registry(reg, arena, n > 0 ? rdefs : NULL, n, /*module_qn=*/NULL);
|
||||
reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */
|
||||
return reg;
|
||||
}
|
||||
|
||||
@@ -223,6 +223,24 @@ static inline bool cbm_pipeline_node_is_callable_target(const cbm_gbuf_node_t *n
|
||||
strcmp(node->label, "Constructor") == 0 || strcmp(node->label, "Class") == 0;
|
||||
}
|
||||
|
||||
/* The Kotlin resolver claims a property READ on a typed receiver with an exact
|
||||
* row (lsp_kt_property_ref) so that the raw-name fallback cannot borrow a
|
||||
* same-named function. That row names a property, never a function value. Its
|
||||
* QN is not always materialized under that spelling (a property of a nested
|
||||
* class); the graph lookup then finds nothing, or its unique-callable step
|
||||
* picks a same-named function elsewhere (#1555: okhttp `server.webSocket` -> a
|
||||
* test function `webSocket`, a fabricated CALL_REFERENCE). Such a row is not
|
||||
* admissible: the caller falls back by name, restricted to non-callable
|
||||
* targets, which binds the property's own node as before the receiver was
|
||||
* typed and can never borrow a function. */
|
||||
static inline bool cbm_pipeline_reference_target_admissible(const CBMResolvedCall *ref,
|
||||
const cbm_gbuf_node_t *tgt) {
|
||||
if (!ref || !ref->strategy || strcmp(ref->strategy, "lsp_kt_property_ref") != 0) {
|
||||
return true;
|
||||
}
|
||||
return tgt && !cbm_pipeline_node_is_callable_target(tgt);
|
||||
}
|
||||
|
||||
static inline int cbm_pipeline_qn_class_method_tail_eq(const char *qn, const char *tail) {
|
||||
const char *qt = cbm_pipeline_qn_class_method_tail(qn);
|
||||
return qt && tail && strcmp(qt, tail) == 0;
|
||||
|
||||
+159
-19
@@ -238,6 +238,67 @@ typedef struct {
|
||||
CBMLanguage lang;
|
||||
} pxc_base_scope_t;
|
||||
|
||||
/* A type-like graph node, or NULL. */
|
||||
static const cbm_gbuf_node_t *pxc_type_node(const cbm_gbuf_node_t *node) {
|
||||
return node && node->qualified_name && cbm_label_is_type_like(node->label) ? node : NULL;
|
||||
}
|
||||
|
||||
/* Python `import a.b.c` binds the ROOT name `a`, so a base spelled through it
|
||||
* (`class X(a.b.c.Base)`) is module a.b.c's Base. The import map holds the
|
||||
* imported MODULE under that root local (the IMPORTS edge target), and the
|
||||
* registry's import_map step appends the whole remaining spelling to it
|
||||
* (a.b.c + .b.c.Base), so a dotted base never resolved and every member
|
||||
* inherited through it was invisible to the LSP. Resolve the spelling against
|
||||
* the module the import names: the import map's module when that local is
|
||||
* unique there, else the dotted module path under the project's QN rule.
|
||||
* Import-backed and exact -- no short-name step -- and only a type-like
|
||||
* project node is accepted. `import a.b as m` and `from a import b` bind the
|
||||
* module itself and keep resolving through the import map. */
|
||||
static const char *pxc_python_dotted_base_qn(const pxc_base_scope_t *bs, const char *raw) {
|
||||
if (bs->lang != CBM_LANG_PYTHON || !bs->imports || !bs->gbuf || !strchr(raw, '.')) {
|
||||
return NULL;
|
||||
}
|
||||
size_t root_len = strcspn(raw, ".");
|
||||
for (int i = 0; i < bs->imports->count; i++) {
|
||||
const CBMImport *imp = &bs->imports->items[i];
|
||||
const char *local = imp->local_name;
|
||||
const char *path = imp->module_path;
|
||||
if (!local || !path || strlen(local) != root_len || strncmp(raw, local, root_len) != 0 ||
|
||||
strncmp(path, local, root_len) != 0 ||
|
||||
(path[root_len] != '\0' && path[root_len] != '.')) {
|
||||
continue; /* not a root binding of this base's first segment */
|
||||
}
|
||||
size_t path_len = strlen(path);
|
||||
if (strncmp(raw, path, path_len) != 0 || raw[path_len] != '.') {
|
||||
continue; /* the base does not extend this import's module path */
|
||||
}
|
||||
const char *module_qn = NULL;
|
||||
int hits = 0;
|
||||
for (int k = 0; k < bs->imp_count; k++) {
|
||||
if (bs->imp_keys[k] && bs->imp_vals[k] && strcmp(bs->imp_keys[k], local) == 0) {
|
||||
module_qn = bs->imp_vals[k];
|
||||
hits++;
|
||||
}
|
||||
}
|
||||
const cbm_gbuf_node_t *node = NULL;
|
||||
if (hits == 1) {
|
||||
char candidate[CBM_SZ_512];
|
||||
int n = snprintf(candidate, sizeof(candidate), "%s%s", module_qn, raw + path_len);
|
||||
if (n > 0 && (size_t)n < sizeof(candidate)) {
|
||||
node = pxc_type_node(cbm_gbuf_find_by_qn(bs->gbuf, candidate));
|
||||
}
|
||||
}
|
||||
if (!node) {
|
||||
node =
|
||||
pxc_type_node(cbm_pipeline_lsp_target_node(bs->gbuf, bs->project_name, raw, false));
|
||||
}
|
||||
if (node) {
|
||||
return node->qualified_name;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Resolve one base-class spelling to a project QN. Mirrors
|
||||
* pass_semantic.c::resolve_as_class — same registry, same type-like veto —
|
||||
* then additionally rejects weak short-name strategies (see above).
|
||||
@@ -248,6 +309,10 @@ static const char *pxc_resolve_base_qn(const pxc_base_scope_t *bs, const char *r
|
||||
if (!bs || !bs->reg || !raw || !raw[0]) {
|
||||
return NULL;
|
||||
}
|
||||
const char *dotted = pxc_python_dotted_base_qn(bs, raw);
|
||||
if (dotted) {
|
||||
return dotted;
|
||||
}
|
||||
cbm_resolution_t res = cbm_registry_resolve_lang(bs->reg, raw, module_qn, bs->imp_keys,
|
||||
bs->imp_vals, bs->imp_count, bs->lang);
|
||||
if (!res.qualified_name || !res.qualified_name[0]) {
|
||||
@@ -1285,6 +1350,68 @@ static void pxc_supersede_occurrence(CBMArena *dst_arena, CBMResolvedCallArray *
|
||||
dst_calls->count = kept;
|
||||
}
|
||||
|
||||
/* The Kotlin universal-member placeholder: `x.hashCode()` / `toString()` /
|
||||
* `equals()` on a receiver the resolver could not type is emitted on kotlin.Any
|
||||
* (strategy lsp_kt_any, reduced confidence) -- the "no narrower receiver known"
|
||||
* answer. */
|
||||
static bool pxc_is_kt_any_placeholder(const CBMResolvedCall *rc) {
|
||||
return rc->strategy && strcmp(rc->strategy, "lsp_kt_any") == 0;
|
||||
}
|
||||
|
||||
static char *pxc_occurrence_key(CBMArena *keys, const CBMResolvedCall *rc) {
|
||||
return cbm_arena_sprintf(keys, "%u\x1f%s\x1f%u:%u\x1f%u", (unsigned)rc->kind, rc->caller_qn,
|
||||
rc->site_start_byte, rc->site_end_byte, (unsigned)rc->source_origin);
|
||||
}
|
||||
|
||||
/* #1555: the per-file pass sees only the file's own declarations, so a receiver
|
||||
* typed by another file (`val type: ProtoType`) is unknown there and
|
||||
* `type.hashCode()` gets the kotlin.Any placeholder. The cross pass types the
|
||||
* receiver and answers with the declared override (`ProtoType.hashCode`), or
|
||||
* with kotlin.Any again when no class on the receiver's chain declares one. Two
|
||||
* distinct targets at one occurrence are a tombstone in the call join
|
||||
* (pass_parallel.c LSP_IDX_AMBIGUOUS), so the occurrence fell to the name-based
|
||||
* registry guess (wire: `type.hashCode` -> AllTypes.hashCode). A placeholder is
|
||||
* therefore dropped when a cross row resolves the same occurrence; the cross row
|
||||
* is then the only evidence. One hash pass per side keeps this linear. */
|
||||
static void pxc_drop_superseded_kt_any(CBMArena *keys, CBMResolvedCallArray *dst_calls,
|
||||
const CBMResolvedCallArray *src_out) {
|
||||
bool has_placeholder = false;
|
||||
for (int i = 0; i < dst_calls->count && !has_placeholder; i++) {
|
||||
has_placeholder = pxc_is_kt_any_placeholder(&dst_calls->items[i]);
|
||||
}
|
||||
if (!has_placeholder) {
|
||||
return;
|
||||
}
|
||||
CBMHashTable *resolved = cbm_ht_create((uint32_t)src_out->count + 1);
|
||||
for (int j = 0; j < src_out->count; j++) {
|
||||
const CBMResolvedCall *src = &src_out->items[j];
|
||||
if (!src->caller_qn || !src->callee_qn || pxc_is_kt_any_placeholder(src) ||
|
||||
src->site_end_byte <= src->site_start_byte) {
|
||||
continue;
|
||||
}
|
||||
char *k = pxc_occurrence_key(keys, src);
|
||||
if (k) {
|
||||
cbm_ht_set(resolved, k, (void *)(uintptr_t)1);
|
||||
}
|
||||
}
|
||||
int kept = 0;
|
||||
for (int i = 0; i < dst_calls->count; i++) {
|
||||
const CBMResolvedCall *cur = &dst_calls->items[i];
|
||||
if (pxc_is_kt_any_placeholder(cur) && cur->caller_qn) {
|
||||
char *k = pxc_occurrence_key(keys, cur);
|
||||
if (k && cbm_ht_get(resolved, k)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (kept != i) {
|
||||
dst_calls->items[kept] = *cur;
|
||||
}
|
||||
kept++;
|
||||
}
|
||||
dst_calls->count = kept;
|
||||
cbm_ht_free(resolved);
|
||||
}
|
||||
|
||||
static void pxc_append_results(CBMArena *dst_arena, CBMResolvedCallArray *dst_calls,
|
||||
const CBMResolvedCallArray *src_out) {
|
||||
if (!dst_calls || !src_out)
|
||||
@@ -1312,6 +1439,7 @@ static void pxc_append_results(CBMArena *dst_arena, CBMResolvedCallArray *dst_ca
|
||||
}
|
||||
pxc_supersede_occurrence(dst_arena, dst_calls, src);
|
||||
}
|
||||
pxc_drop_superseded_kt_any(&keys, dst_calls, src_out);
|
||||
|
||||
for (int i = 0; i < dst_calls->count; i++) {
|
||||
const CBMResolvedCall *rc = &dst_calls->items[i];
|
||||
@@ -1470,31 +1598,42 @@ const struct CBMCargoManifest *cbm_pxc_get_rust_manifest(void) {
|
||||
/* Convert a CBMLSPDef array (the pipeline's lingua franca, go_lsp.h:73)
|
||||
* into a CBMRustLSPDef array (rust_lsp.h) inside `arena`. The two structs
|
||||
* have similar fields but different layouts; CBMLSPDef adds
|
||||
* language/namespace metadata, so a memcpy is unsafe — copy field-by-field. */
|
||||
static CBMRustLSPDef *pxc_lspdefs_to_rust(CBMArena *arena, const CBMLSPDef *defs, int def_count) {
|
||||
* language/namespace metadata, so a memcpy is unsafe — copy field-by-field.
|
||||
* Only Rust defs are kept: the def set is mixed-language, and a Perl or C
|
||||
* function must never become a Rust call target (the Go registrar filters
|
||||
* the same way). *out_count gets the number kept. */
|
||||
static CBMRustLSPDef *pxc_lspdefs_to_rust(CBMArena *arena, const CBMLSPDef *defs, int def_count,
|
||||
int *out_count) {
|
||||
*out_count = 0;
|
||||
if (!defs || def_count <= 0)
|
||||
return NULL;
|
||||
CBMRustLSPDef *out =
|
||||
(CBMRustLSPDef *)cbm_arena_alloc(arena, (size_t)def_count * sizeof(CBMRustLSPDef));
|
||||
if (!out)
|
||||
return NULL;
|
||||
int n = 0;
|
||||
for (int i = 0; i < def_count; i++) {
|
||||
out[i].qualified_name = defs[i].qualified_name;
|
||||
out[i].short_name = defs[i].short_name;
|
||||
out[i].label = defs[i].label;
|
||||
out[i].receiver_type = defs[i].receiver_type;
|
||||
out[i].def_module_qn = defs[i].def_module_qn;
|
||||
out[i].return_types = defs[i].return_types;
|
||||
out[i].embedded_types = defs[i].embedded_types;
|
||||
out[i].field_defs = defs[i].field_defs;
|
||||
out[i].method_names_str = defs[i].method_names_str;
|
||||
out[i].signature_param_types = defs[i].signature_param_types;
|
||||
out[i].signature_param_count = defs[i].signature_param_count;
|
||||
out[i].trait_qn = defs[i].trait_qn;
|
||||
out[i].is_interface = defs[i].is_interface;
|
||||
out[i].is_rust_impl_relation = defs[i].is_rust_impl_relation;
|
||||
out[i].is_abstract = defs[i].is_abstract;
|
||||
if (defs[i].lang != CBM_LANG_RUST) {
|
||||
continue;
|
||||
}
|
||||
out[n].qualified_name = defs[i].qualified_name;
|
||||
out[n].short_name = defs[i].short_name;
|
||||
out[n].label = defs[i].label;
|
||||
out[n].receiver_type = defs[i].receiver_type;
|
||||
out[n].def_module_qn = defs[i].def_module_qn;
|
||||
out[n].return_types = defs[i].return_types;
|
||||
out[n].embedded_types = defs[i].embedded_types;
|
||||
out[n].field_defs = defs[i].field_defs;
|
||||
out[n].method_names_str = defs[i].method_names_str;
|
||||
out[n].signature_param_types = defs[i].signature_param_types;
|
||||
out[n].signature_param_count = defs[i].signature_param_count;
|
||||
out[n].trait_qn = defs[i].trait_qn;
|
||||
out[n].is_interface = defs[i].is_interface;
|
||||
out[n].is_rust_impl_relation = defs[i].is_rust_impl_relation;
|
||||
out[n].is_abstract = defs[i].is_abstract;
|
||||
n++;
|
||||
}
|
||||
*out_count = n;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -1581,9 +1720,10 @@ void cbm_pxc_run_one_with_php_bindings(CBMLanguage lang, CBMFileResult *r, const
|
||||
* but diverge after, so convert into the scratch arena. The
|
||||
* workspace manifest (set once by the sequential driver) lets
|
||||
* `crate_a::foo` route across the crate boundary (#56). */
|
||||
CBMRustLSPDef *rdefs = pxc_lspdefs_to_rust(&scratch, defs, def_count);
|
||||
int rust_def_count = 0;
|
||||
CBMRustLSPDef *rdefs = pxc_lspdefs_to_rust(&scratch, defs, def_count, &rust_def_count);
|
||||
cbm_run_rust_lsp_cross_with_manifest(&scratch, source, source_len, module_qn, rdefs,
|
||||
def_count, imp_names, imp_qns, imp_count, tree,
|
||||
rust_def_count, imp_names, imp_qns, imp_count, tree,
|
||||
g_pxc_rust_manifest, &out, &synthetic_calls);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -3635,6 +3635,7 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
|
||||
const cbm_gbuf_node_t *tgt = NULL;
|
||||
bool precise_call_reference = false;
|
||||
const CBMResolvedCall *semantic_reference = NULL;
|
||||
bool property_read = false;
|
||||
if (cbm_pipeline_usage_semantic_reference_candidate(usage)) {
|
||||
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
|
||||
semantic_reference = cbm_pipeline_find_lsp_reference_indexed_in_graph(
|
||||
@@ -3647,6 +3648,13 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
|
||||
if (semantic_reference) {
|
||||
tgt = cbm_pipeline_lsp_target_node(rc->main_gbuf, rc->project_name,
|
||||
semantic_reference->callee_qn, allow_tail);
|
||||
if (!cbm_pipeline_reference_target_admissible(semantic_reference, tgt)) {
|
||||
/* a property read: the name fallback below may still bind the
|
||||
* property's own node, never a callable */
|
||||
tgt = NULL;
|
||||
semantic_reference = NULL;
|
||||
property_read = true;
|
||||
}
|
||||
precise_call_reference = cbm_pipeline_node_is_callable_target(tgt);
|
||||
}
|
||||
}
|
||||
@@ -3702,6 +3710,9 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
|
||||
cbm_pipeline_node_is_callable_target(tgt))) {
|
||||
continue;
|
||||
}
|
||||
if (property_read && cbm_pipeline_node_is_callable_target(tgt)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!tgt || src->id == tgt->id) {
|
||||
continue;
|
||||
|
||||
@@ -213,6 +213,7 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
|
||||
const cbm_gbuf_node_t *tgt = NULL;
|
||||
bool precise_call_reference = false;
|
||||
const CBMResolvedCall *semantic_reference = NULL;
|
||||
bool property_read = false;
|
||||
if (cbm_pipeline_usage_semantic_reference_candidate(usage)) {
|
||||
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
|
||||
semantic_reference = cbm_pipeline_find_lsp_reference_indexed_in_graph(
|
||||
@@ -225,6 +226,13 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
|
||||
if (semantic_reference) {
|
||||
tgt = cbm_pipeline_lsp_target_node(ctx->gbuf, ctx->project_name,
|
||||
semantic_reference->callee_qn, allow_tail);
|
||||
if (!cbm_pipeline_reference_target_admissible(semantic_reference, tgt)) {
|
||||
/* a property read: the name fallback below may still bind the
|
||||
* property's own node, never a callable */
|
||||
tgt = NULL;
|
||||
semantic_reference = NULL;
|
||||
property_read = true;
|
||||
}
|
||||
precise_call_reference = cbm_pipeline_node_is_callable_target(tgt);
|
||||
}
|
||||
}
|
||||
@@ -285,6 +293,9 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
|
||||
cbm_pipeline_node_is_callable_target(tgt))) {
|
||||
continue;
|
||||
}
|
||||
if (property_read && cbm_pipeline_node_is_callable_target(tgt)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!tgt || src->id == tgt->id) {
|
||||
continue;
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
* test_kotlin_typed_calls.c — Kotlin member calls through a class PROPERTY
|
||||
* (primary-constructor `val`, body property, `this.prop`) bind to the
|
||||
* property's declared type, on both resolver venues (#1555).
|
||||
*
|
||||
* The cross-file Kotlin walk resolved a typed method PARAMETER by type, but
|
||||
* a property-typed receiver fell to the name guess: the cross registry held
|
||||
* the class from extraction defs (properties untyped, constructor vals
|
||||
* absent), and the walk's typed AST registration of the same class was
|
||||
* shadowed by that earlier entry. Each fixture call has a same-named decoy
|
||||
* method on another type, so a name guess is visible as a wrong target:
|
||||
* - Service.log: `audit.inserir()` on `audit: Audit`, next to
|
||||
* `repository: Repo` whose interface also declares inserir (the
|
||||
* sequential venue's guess picks Repo.inserir);
|
||||
* - two bounded contexts whose use cases both inject `repository`
|
||||
* (the parallel venue's receiver-name guess bound the feriado use case
|
||||
* to CompromissoRepository.inserir, the #1555 report).
|
||||
* One fixture, padded past MIN_FILES_FOR_PARALLEL, is indexed through the
|
||||
* sequential resolver (CBM_INDEX_SINGLE_THREAD=1) and the parallel one.
|
||||
*/
|
||||
#include "../src/foundation/compat.h"
|
||||
#include "test_framework.h"
|
||||
#include "test_helpers.h"
|
||||
#include "pipeline/pipeline.h"
|
||||
#include "sqlite3.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char *const KTC_FILES[][2] = {
|
||||
{"src/main/kotlin/com/acme/a/Repo.kt",
|
||||
"package com.acme.a\n\ninterface Repo {\n fun inserir(x: String): Long\n}\n"},
|
||||
{"src/main/kotlin/com/acme/b/Audit.kt",
|
||||
"package com.acme.b\n\nclass Audit {\n fun inserir(x: String): Long = 0L\n}\n"},
|
||||
{"src/main/kotlin/com/acme/app/Service.kt",
|
||||
"package com.acme.app\n\nimport com.acme.a.Repo\nimport com.acme.b.Audit\n\n"
|
||||
"class Service(private val repository: Repo, private val audit: Audit) {\n"
|
||||
" fun save(x: String): Long = repository.inserir(x)\n"
|
||||
" fun log(x: String): Long = audit.inserir(x)\n}\n\n"
|
||||
"class Holder {\n private val audit: Audit = Audit()\n"
|
||||
" fun hold(x: String): Long = this.audit.inserir(x)\n}\n"},
|
||||
{"src/main/kotlin/com/acme/agenda/domain/CompromissoRepository.kt",
|
||||
"package com.acme.agenda.domain\n\ninterface CompromissoRepository {\n"
|
||||
" fun inserir(titulo: String): Long\n}\n"},
|
||||
{"src/main/kotlin/com/acme/feriado/domain/FeriadoRepository.kt",
|
||||
"package com.acme.feriado.domain\n\ninterface FeriadoRepository {\n"
|
||||
" fun inserir(nome: String): Long\n}\n"},
|
||||
{"src/main/kotlin/com/acme/agenda/app/CriarCompromissoUseCase.kt",
|
||||
"package com.acme.agenda.app\n\nimport com.acme.agenda.domain.CompromissoRepository\n\n"
|
||||
"class CriarCompromissoUseCase(private val repository: CompromissoRepository) {\n"
|
||||
" fun executar(titulo: String): Long = repository.inserir(titulo)\n}\n"},
|
||||
{"src/main/kotlin/com/acme/feriado/app/CriarFeriadoUseCase.kt",
|
||||
"package com.acme.feriado.app\n\nimport com.acme.feriado.domain.FeriadoRepository\n\n"
|
||||
"class CriarFeriadoUseCase(private val repository: FeriadoRepository) {\n"
|
||||
" fun executar(nome: String): Long = repository.inserir(nome)\n}\n"},
|
||||
/* Guard: inside an extension function, a bare call names the receiver's
|
||||
* MEMBER before a same-named extension of the same file (Kotlin
|
||||
* resolution); main gets this right and the own-file overlay must too. */
|
||||
{"src/main/kotlin/com/acme/b/Builders.kt",
|
||||
"package com.acme.b\n\nclass Builder {\n fun add(x: Int): Boolean = true\n"
|
||||
" fun put(k: String, v: Int): Int = v\n}\n\n"
|
||||
"fun Builder.add(s: String): Boolean = add(s.length)\n\n"
|
||||
"fun Builder.putAll(k: String) { put(k, 1) }\n\n"
|
||||
"fun Builder.put(k: String): Int = put(k, 0)\n"},
|
||||
/* kotlin.Any members on a receiver typed by ANOTHER file. The per-file
|
||||
* pass cannot type `type`, so it emits the kotlin.Any placeholder; the
|
||||
* cross pass names the declared override. Both rows at one occurrence
|
||||
* were a tombstone in the call join and the call fell to a name guess
|
||||
* (AllTypes is the decoy). A class with no override keeps kotlin.Any, and
|
||||
* so does a stdlib receiver type (`count: Int`), whose seeded
|
||||
* kotlin.Int.toString names no node and left the call to the guess. */
|
||||
{"src/main/kotlin/com/acme/c/Proto.kt",
|
||||
"package com.acme.c\n\nclass ProtoType {\n private val s: String = \"\"\n"
|
||||
" override fun hashCode() = s.length\n\n override fun toString() = s\n}\n\n"
|
||||
"class Plain(val n: Int)\n"},
|
||||
{"src/main/kotlin/com/acme/d/Member.kt",
|
||||
"package com.acme.d\n\nimport com.acme.c.Plain\nimport com.acme.c.ProtoType\n\n"
|
||||
"class Member(val type: ProtoType, val plain: Plain, val count: Int) {\n"
|
||||
" override fun hashCode() = type.hashCode() * 37\n\n"
|
||||
" fun label(): String = type.toString()\n\n"
|
||||
" fun plainHash(): Int = plain.hashCode()\n\n"
|
||||
" fun countText(): String = count.toString()\n}\n"},
|
||||
{"src/main/kotlin/com/acme/e/AllTypes.kt",
|
||||
"package com.acme.e\n\nclass AllTypes {\n override fun hashCode(): Int = 1\n\n"
|
||||
" override fun toString(): String = \"all\"\n}\n"},
|
||||
/* A property READ on a now-typed receiver (a nested class's property, whose
|
||||
* QN the graph spells differently): it must keep the property's own node
|
||||
* (USAGE) and never become a CALL_REFERENCE to a same-named function in
|
||||
* another file (okhttp `server.webSocket` -> a test fun `webSocket`). */
|
||||
{"src/main/kotlin/com/acme/w/SocketTest.kt",
|
||||
"package com.acme.w\n\nclass SocketTest {\n"
|
||||
" private val server: Streams = Streams()\n\n"
|
||||
" fun loop(): Boolean {\n return server.webSocket != null\n }\n\n"
|
||||
" class Streams {\n var webSocket: String? = null\n }\n}\n"},
|
||||
{"src/main/kotlin/com/acme/w2/ModernTest.kt",
|
||||
"package com.acme.w2\n\nclass ModernTest {\n fun webSocket(): Int = 1\n}\n"},
|
||||
/* A constructor val next to a same-named member function: a bare
|
||||
* `handshake()` calls the function (the String? value has no invoke), as
|
||||
* main resolves it; typing the val must not shadow the member (okhttp
|
||||
* RealConnection). Other.handshake is the decoy a guess would hit. */
|
||||
{"src/main/kotlin/com/acme/h/Conn.kt",
|
||||
"package com.acme.h\n\ninterface Connection {\n fun handshake(): String?\n}\n\n"
|
||||
"class RealConnection(private val handshake: String?) : Connection {\n"
|
||||
" override fun handshake(): String? = handshake\n\n"
|
||||
" fun isEligible(): Boolean = check(handshake()!!.length)\n\n"
|
||||
" private fun check(n: Int): Boolean = n > 0\n}\n"},
|
||||
{"src/main/kotlin/com/acme/h2/Other.kt",
|
||||
"package com.acme.h2\n\nclass Other {\n fun handshake(): String? = null\n}\n"},
|
||||
/* Inside `repo.let { .. }` / `repo.run { .. }` the now-typed receiver Repo
|
||||
* becomes the lambda's implicit receiver; the enclosing class's member
|
||||
* stays callable bare (Kotlin's implicit-receiver chain). */
|
||||
{"src/main/kotlin/com/acme/l/Svc.kt",
|
||||
"package com.acme.l\n\nimport com.acme.a.Repo\n\n"
|
||||
"class Svc(private val repo: Repo) {\n fun ping(): Int = 2\n\n"
|
||||
" fun viaLet(): Int = repo.let { ping() }\n\n"
|
||||
" fun viaRun(): Int = repo.run { ping() }\n}\n"},
|
||||
{"src/main/kotlin/com/acme/l2/Pinger.kt",
|
||||
"package com.acme.l2\n\nclass Pinger {\n fun ping(): Int = 3\n}\n"},
|
||||
/* Guard for the receiver chain: inside a companion object, `Ledger(x)` is
|
||||
* a constructor call (here the secondary constructor), never a member
|
||||
* method of the enclosing class. */
|
||||
{"src/main/kotlin/com/acme/k/Ledger.kt",
|
||||
"package com.acme.k\n\nclass Ledger private constructor(val a: String, val b: String) {\n"
|
||||
" constructor(a: String) : this(a, \"gen\")\n\n"
|
||||
" companion object {\n fun of(x: String) = Ledger(x)\n }\n}\n"},
|
||||
};
|
||||
|
||||
static int ktc_write_fixture(const char *dir) {
|
||||
for (size_t i = 0; i < sizeof(KTC_FILES) / sizeof(KTC_FILES[0]); i++) {
|
||||
if (th_write_file(TH_PATH(dir, KTC_FILES[i][0]), KTC_FILES[i][1]) != 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
/* Past MIN_FILES_FOR_PARALLEL (50): the parallel venue is selected. */
|
||||
for (int i = 0; i < 55; i++) {
|
||||
char rel[96];
|
||||
char body[128];
|
||||
snprintf(rel, sizeof(rel), "src/main/kotlin/com/acme/pad/Pad%d.kt", i);
|
||||
snprintf(body, sizeof(body), "package com.acme.pad\n\nclass Pad%d {\n fun v%d(): Int = %d\n}\n",
|
||||
i, i, i);
|
||||
if (th_write_file(TH_PATH(dir, rel), body) != 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Target QNs of the CALLS edges whose source QN ends in `.<src_tail>` and whose
|
||||
* callee text ends in `.<leaf>`, one per line into out; returns the count. */
|
||||
static int ktc_targets(sqlite3 *db, const char *src_tail, const char *leaf, char *out,
|
||||
size_t cap) {
|
||||
out[0] = '\0';
|
||||
static const char sql[] =
|
||||
"SELECT t.qualified_name FROM edges e JOIN nodes s ON s.id = e.source_id "
|
||||
"JOIN nodes t ON t.id = e.target_id WHERE e.type = 'CALLS' "
|
||||
"AND s.qualified_name LIKE '%.' || ?1 "
|
||||
"AND (json_extract(e.properties, '$.callee') = ?2 "
|
||||
"OR json_extract(e.properties, '$.callee') LIKE '%.' || ?2) ORDER BY 1";
|
||||
sqlite3_stmt *st = NULL;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
|
||||
return -1;
|
||||
}
|
||||
sqlite3_bind_text(st, 1, src_tail, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(st, 2, leaf, -1, SQLITE_STATIC);
|
||||
int n = 0;
|
||||
while (sqlite3_step(st) == SQLITE_ROW) {
|
||||
const char *qn = (const char *)sqlite3_column_text(st, 0);
|
||||
size_t used = strlen(out);
|
||||
snprintf(out + used, cap - used, "%s\n", qn ? qn : "");
|
||||
n++;
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Exactly one such edge, and its target QN ends in `.<want_tail>`. */
|
||||
static int ktc_expect(sqlite3 *db, const char *venue, const char *src_tail, const char *leaf,
|
||||
const char *want_tail) {
|
||||
char got[2048];
|
||||
int n = ktc_targets(db, src_tail, leaf, got, sizeof(got));
|
||||
size_t gl = strlen(got);
|
||||
size_t wl = strlen(want_tail);
|
||||
if (n == 1 && gl > wl + 1 && got[gl - wl - 2] == '.' &&
|
||||
strncmp(got + gl - wl - 1, want_tail, wl) == 0) {
|
||||
return 1;
|
||||
}
|
||||
printf("\n [%s] %s -> .%s: want one edge to ...%s, have %d:\n%s", venue, src_tail, leaf,
|
||||
want_tail, n, got);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ktc_expect, and the edge comes from the resolver (an lsp_* strategy), not a
|
||||
* name guess that happens to land on the same target. */
|
||||
static int ktc_expect_lsp(sqlite3 *db, const char *venue, const char *src_tail, const char *leaf,
|
||||
const char *want_tail) {
|
||||
if (!ktc_expect(db, venue, src_tail, leaf, want_tail)) {
|
||||
return 0;
|
||||
}
|
||||
static const char sql[] =
|
||||
"SELECT coalesce(json_extract(e.properties, '$.strategy'), '') FROM edges e "
|
||||
"JOIN nodes s ON s.id = e.source_id WHERE e.type = 'CALLS' "
|
||||
"AND s.qualified_name LIKE '%.' || ?1 "
|
||||
"AND (json_extract(e.properties, '$.callee') = ?2 "
|
||||
"OR json_extract(e.properties, '$.callee') LIKE '%.' || ?2)";
|
||||
sqlite3_stmt *st = NULL;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
|
||||
return 0;
|
||||
}
|
||||
sqlite3_bind_text(st, 1, src_tail, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(st, 2, leaf, -1, SQLITE_STATIC);
|
||||
char strategy[64] = "";
|
||||
if (sqlite3_step(st) == SQLITE_ROW) {
|
||||
const char *s = (const char *)sqlite3_column_text(st, 0);
|
||||
snprintf(strategy, sizeof(strategy), "%s", s ? s : "");
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
if (strncmp(strategy, "lsp_", 4) == 0) {
|
||||
return 1;
|
||||
}
|
||||
printf("\n [%s] %s -> .%s: right target, but strategy '%s' (want lsp_*)\n", venue,
|
||||
src_tail, leaf, strategy);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The reference edges (USAGE / CALL_REFERENCE) from the method whose QN ends in
|
||||
* `.<src_tail>` to nodes named `name`: exactly one, a USAGE onto a non-callable. */
|
||||
static int ktc_expect_property_usage(sqlite3 *db, const char *venue, const char *src_tail,
|
||||
const char *name) {
|
||||
static const char sql[] =
|
||||
"SELECT e.type || ' ' || t.label || ' ' || t.qualified_name FROM edges e "
|
||||
"JOIN nodes s ON s.id = e.source_id JOIN nodes t ON t.id = e.target_id "
|
||||
"WHERE e.type IN ('USAGE', 'CALL_REFERENCE', 'CALLS') "
|
||||
"AND s.qualified_name LIKE '%.' || ?1 AND t.name = ?2 ORDER BY 1";
|
||||
sqlite3_stmt *st = NULL;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
|
||||
return 0;
|
||||
}
|
||||
sqlite3_bind_text(st, 1, src_tail, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(st, 2, name, -1, SQLITE_STATIC);
|
||||
char got[1024] = "";
|
||||
int n = 0;
|
||||
bool ok = false;
|
||||
while (sqlite3_step(st) == SQLITE_ROW) {
|
||||
const char *row = (const char *)sqlite3_column_text(st, 0);
|
||||
size_t used = strlen(got);
|
||||
snprintf(got + used, sizeof(got) - used, "%s\n", row ? row : "");
|
||||
ok = row && strncmp(row, "USAGE ", 6) == 0 && strstr(row, " Function ") == NULL &&
|
||||
strstr(row, " Method ") == NULL;
|
||||
n++;
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
if (n == 1 && ok) {
|
||||
return 1;
|
||||
}
|
||||
printf("\n [%s] %s -> %s: want one USAGE onto the property, have %d:\n%s", venue, src_tail,
|
||||
name, n, got);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int ktc_index(const char *dir, const char *db_name, bool single_thread, char *db_out,
|
||||
size_t cap) {
|
||||
snprintf(db_out, cap, "%s/%s", dir, db_name);
|
||||
char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
|
||||
char *saved_single = old_single ? strdup(old_single) : NULL;
|
||||
char *old_workers = getenv("CBM_WORKERS");
|
||||
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
|
||||
if (single_thread) {
|
||||
cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
|
||||
cbm_setenv("CBM_WORKERS", "4", 1);
|
||||
}
|
||||
int rc = -1;
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(dir, db_out, CBM_MODE_FULL);
|
||||
if (p) {
|
||||
rc = cbm_pipeline_run(p);
|
||||
cbm_pipeline_free(p);
|
||||
}
|
||||
if (saved_single) {
|
||||
cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1);
|
||||
free(saved_single);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
|
||||
}
|
||||
if (saved_workers) {
|
||||
cbm_setenv("CBM_WORKERS", saved_workers, 1);
|
||||
free(saved_workers);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int ktc_check_venue(const char *dir, const char *venue, bool single_thread) {
|
||||
char db_path[512];
|
||||
char name[64];
|
||||
snprintf(name, sizeof(name), "ktc-%s.db", venue);
|
||||
if (ktc_index(dir, name, single_thread, db_path, sizeof(db_path)) != 0) {
|
||||
printf("\n [%s] index failed\n", venue);
|
||||
return 0;
|
||||
}
|
||||
sqlite3 *db = NULL;
|
||||
if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) {
|
||||
if (db) {
|
||||
sqlite3_close(db);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int ok = 1;
|
||||
/* Constructor vals, each next to a same-named decoy on the other type. */
|
||||
ok &= ktc_expect(db, venue, "Service.save", "inserir", "Repo.inserir");
|
||||
ok &= ktc_expect(db, venue, "Service.log", "inserir", "Audit.inserir");
|
||||
/* Body property read through `this`. */
|
||||
ok &= ktc_expect(db, venue, "Holder.hold", "inserir", "Audit.inserir");
|
||||
/* #1555: two ports sharing `inserir`, both injected as `repository`. */
|
||||
ok &= ktc_expect(db, venue, "CriarFeriadoUseCase.executar", "inserir",
|
||||
"FeriadoRepository.inserir");
|
||||
ok &= ktc_expect(db, venue, "CriarCompromissoUseCase.executar", "inserir",
|
||||
"CompromissoRepository.inserir");
|
||||
/* Guard: a member wins over a same-named extension of the same file. */
|
||||
ok &= ktc_expect(db, venue, "Builders.putAll", "put", "Builder.put");
|
||||
ok &= ktc_expect(db, venue, "Builders.add", "add", "Builder.add");
|
||||
/* kotlin.Any members: the declared override, else kotlin.Any (external). */
|
||||
ok &= ktc_expect_lsp(db, venue, "Member.hashCode", "hashCode", "ProtoType.hashCode");
|
||||
ok &= ktc_expect_lsp(db, venue, "Member.label", "toString", "ProtoType.toString");
|
||||
ok &= ktc_expect_lsp(db, venue, "Member.plainHash", "hashCode", "Any.hashCode");
|
||||
ok &= ktc_expect_lsp(db, venue, "Member.countText", "toString", "Any.toString");
|
||||
/* A typed property read keeps the property node, never a same-named function. */
|
||||
ok &= ktc_expect_property_usage(db, venue, "SocketTest.loop", "webSocket");
|
||||
/* A typed constructor val never shadows the same-named member function. */
|
||||
ok &= ktc_expect_lsp(db, venue, "RealConnection.isEligible", "handshake",
|
||||
"RealConnection.handshake");
|
||||
/* The enclosing class's member inside a lambda with a typed receiver. */
|
||||
ok &= ktc_expect_lsp(db, venue, "Svc.viaLet", "ping", "Svc.ping");
|
||||
ok &= ktc_expect_lsp(db, venue, "Svc.viaRun", "ping", "Svc.ping");
|
||||
ok &= ktc_expect_lsp(db, venue, "Ledger.kt.__file__", "Ledger", "Ledger.Ledger.Ledger");
|
||||
sqlite3_close(db);
|
||||
return ok;
|
||||
}
|
||||
|
||||
TEST(kotlin_property_receiver_binds_declared_type_both_venues) {
|
||||
char *dir = th_mktempdir("cbm_kt_typed_calls");
|
||||
ASSERT_NOT_NULL(dir);
|
||||
if (ktc_write_fixture(dir) != 0) {
|
||||
th_cleanup(dir);
|
||||
FAIL("fixture");
|
||||
}
|
||||
int seq = ktc_check_venue(dir, "seq", true);
|
||||
int par = ktc_check_venue(dir, "par", false);
|
||||
th_cleanup(dir);
|
||||
ASSERT_TRUE(seq);
|
||||
ASSERT_TRUE(par);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(kotlin_typed_calls) {
|
||||
RUN_TEST(kotlin_property_receiver_binds_declared_type_both_venues);
|
||||
}
|
||||
@@ -1812,6 +1812,7 @@ extern void suite_ts_lsp(void);
|
||||
extern void suite_java_lsp(void);
|
||||
extern void suite_java_lsp_coverage(void);
|
||||
extern void suite_kotlin_lsp(void);
|
||||
extern void suite_kotlin_typed_calls(void);
|
||||
extern void suite_rust_lsp(void);
|
||||
extern void suite_store_arch(void);
|
||||
extern void suite_architecture_projection(void);
|
||||
@@ -2394,6 +2395,7 @@ int main(int argc, char **argv) {
|
||||
RUN_SELECTED_SUITE(perl_lsp);
|
||||
RUN_SELECTED_SUITE(py_lsp);
|
||||
RUN_SELECTED_SUITE(kotlin_lsp);
|
||||
RUN_SELECTED_SUITE(kotlin_typed_calls);
|
||||
RUN_SELECTED_SUITE(rust_lsp);
|
||||
RUN_SELECTED_SUITE_PERF(py_lsp_bench);
|
||||
RUN_SELECTED_SUITE(py_lsp_stress);
|
||||
|
||||
@@ -8868,6 +8868,180 @@ TEST(pipeline_go_rw_usage_never_cross_into_c) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The Rust registries were built from the mixed-language def set: a Perl
|
||||
* `sub new` became a Rust free function, and the project-wide
|
||||
* lsp_short_name_unique fallback bound `Store::new()` to it at 0.90 (found by
|
||||
* the generic-engine lane on its 12-language fixture). A Rust call never
|
||||
* targets another language's definition. pad_files > 0 forces the parallel
|
||||
* resolver, whose Rust registry is the shared all-defs build. */
|
||||
/* Pads a Rust fixture with pad_files filler modules (pad_files > 0 forces the
|
||||
* parallel resolver: CBM_WORKERS=4 for the run), runs the full pipeline into
|
||||
* <tmp>/<db_name> and opens the store. NULL store = the run failed. */
|
||||
static cbm_store_t *rust_fixture_run(const char *tmp, int pad_files, const char *db_name,
|
||||
cbm_pipeline_t **out_p) {
|
||||
for (int i = 0; i < pad_files; i++) {
|
||||
char name[64];
|
||||
char body[128];
|
||||
snprintf(name, sizeof(name), "src/filler%d.rs", i);
|
||||
snprintf(body, sizeof(body), "pub fn filler%d() -> u32 {\n %d\n}\n", i, i);
|
||||
write_temp_file(tmp, name, body);
|
||||
}
|
||||
char *saved = NULL;
|
||||
if (pad_files > 0) {
|
||||
char *old_workers = getenv("CBM_WORKERS");
|
||||
saved = old_workers ? strdup(old_workers) : NULL;
|
||||
cbm_setenv("CBM_WORKERS", "4", 1);
|
||||
}
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/%s", tmp, db_name);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
int rc = p ? cbm_pipeline_run(p) : -1;
|
||||
if (pad_files > 0) {
|
||||
if (saved) {
|
||||
cbm_setenv("CBM_WORKERS", saved, 1);
|
||||
free(saved);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
}
|
||||
*out_p = p;
|
||||
return rc == 0 ? cbm_store_open_path(db_path) : NULL;
|
||||
}
|
||||
|
||||
static int rust_registry_rust_only_case(int pad_files) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_rust_only_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_temp_file(tmp, "Cargo.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n");
|
||||
write_temp_file(tmp, "src/store.rs",
|
||||
"pub struct Store {\n"
|
||||
" n: u32,\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"impl Store {\n"
|
||||
" pub fn new() -> Store {\n"
|
||||
" Store { n: 0 }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" pub fn get(&self, k: u32) -> u32 {\n"
|
||||
" self.n + k\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "src/main.rs",
|
||||
"mod store;\n"
|
||||
"\n"
|
||||
"use crate::store::Store;\n"
|
||||
"\n"
|
||||
"fn run() -> u32 {\n"
|
||||
" let s = Store::new();\n"
|
||||
" s.get(1)\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"fn main() {\n"
|
||||
" run();\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "perlsrc/Lib.pm",
|
||||
"package Lib;\n"
|
||||
"\n"
|
||||
"sub new {\n"
|
||||
" my ($class) = @_;\n"
|
||||
" return bless {}, $class;\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"1;\n");
|
||||
cbm_pipeline_t *p = NULL;
|
||||
cbm_store_t *s = rust_fixture_run(tmp, pad_files, "rust_only.db", &p);
|
||||
ASSERT_NOT_NULL(s);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
|
||||
/* Never onto the Perl constructor ... */
|
||||
ASSERT_EQ(named_edge_to_file_count(s, project, "CALLS", "run", "new", "perlsrc/Lib.pm"), 0);
|
||||
/* ... and the Rust call still binds the Rust associated function. */
|
||||
ASSERT_GTE(named_edge_to_file_count(s, project, "CALLS", "run", "new", "src/store.rs"), 1);
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pipeline_rust_call_never_binds_another_language) {
|
||||
return rust_registry_rust_only_case(0);
|
||||
}
|
||||
|
||||
TEST(pipeline_rust_call_never_binds_another_language_parallel) {
|
||||
return rust_registry_rust_only_case(52);
|
||||
}
|
||||
|
||||
/* `Opts::write(true)` is an associated function. The LSP leaves it to the
|
||||
* registry (import_map -> fsx.Opts.write), but its free-function short-name
|
||||
* fallback bound it to the project's only free `write` (cache.write) first, and
|
||||
* that LSP row pre-empted the right answer. With the cross-language namesakes
|
||||
* gone (above), this hijack replaced 600+ correct qualified_suffix/import_map
|
||||
* edges on the Rust corpus. A type-qualified call never takes that fallback. */
|
||||
static int rust_assoc_call_free_namesake_case(int pad_files) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_rust_assoc_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_temp_file(tmp, "Cargo.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n");
|
||||
write_temp_file(tmp, "src/fsx.rs",
|
||||
"pub struct Opts {\n"
|
||||
" pub w: bool,\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"impl Opts {\n"
|
||||
" pub fn write(w: bool) -> Opts {\n"
|
||||
" Opts { w }\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "src/cache.rs",
|
||||
"pub fn write(p: &str) -> usize {\n"
|
||||
" p.len()\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "src/main.rs",
|
||||
"mod cache;\n"
|
||||
"mod fsx;\n"
|
||||
"\n"
|
||||
"use crate::fsx::Opts;\n"
|
||||
"\n"
|
||||
"fn run() -> bool {\n"
|
||||
" let o = Opts::write(true);\n"
|
||||
" o.w\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"fn main() {\n"
|
||||
" run();\n"
|
||||
" cache::write(\"x\");\n"
|
||||
"}\n");
|
||||
cbm_pipeline_t *p = NULL;
|
||||
cbm_store_t *s = rust_fixture_run(tmp, pad_files, "rust_assoc.db", &p);
|
||||
ASSERT_NOT_NULL(s);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
|
||||
/* The associated function, never the free namesake ... */
|
||||
ASSERT_GTE(named_edge_to_file_count(s, project, "CALLS", "run", "write", "src/fsx.rs"), 1);
|
||||
ASSERT_EQ(named_edge_to_file_count(s, project, "CALLS", "run", "write", "src/cache.rs"), 0);
|
||||
/* ... while the module-qualified free call keeps its target. */
|
||||
ASSERT_GTE(named_edge_to_file_count(s, project, "CALLS", "main", "write", "src/cache.rs"), 1);
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pipeline_rust_assoc_call_never_binds_free_namesake) {
|
||||
return rust_assoc_call_free_namesake_case(0);
|
||||
}
|
||||
|
||||
TEST(pipeline_rust_assoc_call_never_binds_free_namesake_parallel) {
|
||||
return rust_assoc_call_free_namesake_case(52);
|
||||
}
|
||||
|
||||
TEST(pipeline_go_rw_usage_never_cross_into_c_parallel) {
|
||||
/* Parallel twin of the test above: USAGE and WRITES/READS each have a
|
||||
* second, independent resolver in pass_parallel.c (resolve_file_usages /
|
||||
@@ -13628,6 +13802,123 @@ TEST(python_crossfile_typed_field_calls_issue1277_parallel) {
|
||||
return check_python_crossfile_typed_field_calls_issue1277(true);
|
||||
}
|
||||
|
||||
/* A base class spelled through a root-binding import (`import pkg.mod.nodes`
|
||||
* binds `pkg`; `class X(pkg.mod.nodes.Context)`) is the same class as the
|
||||
* from-imported spelling. The Python LSP's inheritance view never resolved the
|
||||
* dotted spelling, so a member inherited through it (self.m(), super().m(),
|
||||
* super().__init__()) fell to a weak name guess or to nothing. The plain
|
||||
* from-import control must keep resolving; the untyped receiver must not. */
|
||||
static int check_python_dotted_base_inheritance(bool parallel) {
|
||||
const char *nodes = "class Context:\n"
|
||||
" def __init__(self, line=-1):\n"
|
||||
" self.line = line\n\n"
|
||||
" def ctx_only_helper(self):\n"
|
||||
" return 1\n\n"
|
||||
" def describe(self):\n"
|
||||
" return 0\n";
|
||||
const char *types = "import pkg.mod.nodes\n"
|
||||
"from pkg.mod.nodes import Context\n\n"
|
||||
"class DottedType(pkg.mod.nodes.Context):\n"
|
||||
" def __init__(self, line=-1):\n"
|
||||
" super().__init__(line)\n\n"
|
||||
" def run(self):\n"
|
||||
" return self.ctx_only_helper()\n\n"
|
||||
" def describe(self):\n"
|
||||
" return super().describe()\n\n"
|
||||
"class PlainType(Context):\n"
|
||||
" def run(self):\n"
|
||||
" return self.ctx_only_helper()\n\n"
|
||||
"def untyped(thing):\n"
|
||||
" return thing.ctx_only_helper()\n";
|
||||
if (setup_usages_repo("pkg/mod/nodes.py", nodes, "pkg/types.py", types) != 0) {
|
||||
FAIL("failed to create temp dir");
|
||||
}
|
||||
char path[512];
|
||||
const char *inits[] = {"pkg/__init__.py", "pkg/mod/__init__.py"};
|
||||
for (size_t i = 0; i < sizeof(inits) / sizeof(inits[0]); i++) {
|
||||
snprintf(path, sizeof(path), "%s/%s", g_usages_tmpdir, inits[i]);
|
||||
if (th_write_file(path, "") != 0) {
|
||||
teardown_usages_repo();
|
||||
FAIL("failed to write package marker");
|
||||
}
|
||||
}
|
||||
if (parallel) {
|
||||
for (int i = 0; i < 50; i++) {
|
||||
snprintf(path, sizeof(path), "%s/dotted_pad_%02d.py", g_usages_tmpdir, i);
|
||||
if (th_write_file(path, "dotted_padding = 0\n") != 0) {
|
||||
teardown_usages_repo();
|
||||
FAIL("failed to write parallel-selection fixture");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char *old_workers = getenv("CBM_WORKERS");
|
||||
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
|
||||
const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
|
||||
char *saved_single = old_single ? strdup(old_single) : NULL;
|
||||
if ((old_workers && !saved_workers) || (old_single && !saved_single)) {
|
||||
free(saved_workers);
|
||||
free(saved_single);
|
||||
teardown_usages_repo();
|
||||
FAIL("failed to save pipeline environment");
|
||||
}
|
||||
int pin_workers_rc = parallel ? cbm_setenv("CBM_WORKERS", "4", 1) : 0;
|
||||
int pin_single_rc = parallel ? cbm_unsetenv("CBM_INDEX_SINGLE_THREAD") : 0;
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/test_dotted_base.db", g_usages_tmpdir);
|
||||
cbm_pipeline_t *p = pin_workers_rc == 0 && pin_single_rc == 0
|
||||
? cbm_pipeline_new(g_usages_tmpdir, db_path, CBM_MODE_FULL)
|
||||
: NULL;
|
||||
int run_rc = p ? cbm_pipeline_run(p) : -1;
|
||||
cbm_store_t *s = run_rc == 0 ? cbm_store_open_path(db_path) : NULL;
|
||||
bool store_opened = s != NULL;
|
||||
int dotted_self = -1, dotted_super = -1, dotted_init = -1, plain_self = -1, untyped = -1;
|
||||
if (s) {
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
dotted_self = count_lsp_calls(s, project, "pkg.types.DottedType.run",
|
||||
"pkg.mod.nodes.Context.ctx_only_helper");
|
||||
dotted_super = count_lsp_calls(s, project, "pkg.types.DottedType.describe",
|
||||
"pkg.mod.nodes.Context.describe");
|
||||
dotted_init = count_lsp_calls(s, project, "pkg.types.DottedType.__init__",
|
||||
"pkg.mod.nodes.Context.__init__");
|
||||
plain_self = count_lsp_calls(s, project, "pkg.types.PlainType.run",
|
||||
"pkg.mod.nodes.Context.ctx_only_helper");
|
||||
untyped = count_lsp_calls(s, project, "pkg.types.untyped", "ctx_only_helper");
|
||||
cbm_store_close(s);
|
||||
}
|
||||
|
||||
cbm_pipeline_free(p);
|
||||
int restore_workers_rc =
|
||||
saved_workers ? cbm_setenv("CBM_WORKERS", saved_workers, 1) : cbm_unsetenv("CBM_WORKERS");
|
||||
int restore_single_rc = saved_single ? cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1)
|
||||
: cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
|
||||
free(saved_workers);
|
||||
free(saved_single);
|
||||
teardown_usages_repo();
|
||||
|
||||
ASSERT_EQ(pin_workers_rc, 0);
|
||||
ASSERT_EQ(pin_single_rc, 0);
|
||||
ASSERT_EQ(restore_workers_rc, 0);
|
||||
ASSERT_EQ(restore_single_rc, 0);
|
||||
ASSERT_EQ(run_rc, 0);
|
||||
ASSERT_TRUE(store_opened);
|
||||
ASSERT_EQ(dotted_self, 1); /* self.m() inherited through a dotted base */
|
||||
ASSERT_EQ(dotted_super, 1); /* super().m() through a dotted base */
|
||||
ASSERT_EQ(dotted_init, 1); /* super().__init__() through a dotted base */
|
||||
ASSERT_EQ(plain_self, 1); /* control: from-imported base */
|
||||
ASSERT_EQ(untyped, 0); /* control: no receiver type, no LSP edge */
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(python_dotted_base_inheritance) {
|
||||
return check_python_dotted_base_inheritance(false);
|
||||
}
|
||||
|
||||
TEST(python_dotted_base_inheritance_parallel) {
|
||||
return check_python_dotted_base_inheritance(true);
|
||||
}
|
||||
|
||||
/* Bind the external-import veto specifically to #1277's typed-field fold.
|
||||
* No base classes or CALLS edges participate: a name-only call fallback
|
||||
* cannot turn this metadata assertion into a false positive. The unguarded
|
||||
@@ -24541,6 +24832,10 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_html_embedded_member_call_stays_unbound);
|
||||
RUN_TEST(pipeline_go_rw_usage_never_cross_into_c);
|
||||
RUN_TEST(pipeline_go_rw_usage_never_cross_into_c_parallel);
|
||||
RUN_TEST(pipeline_rust_call_never_binds_another_language);
|
||||
RUN_TEST(pipeline_rust_call_never_binds_another_language_parallel);
|
||||
RUN_TEST(pipeline_rust_assoc_call_never_binds_free_namesake);
|
||||
RUN_TEST(pipeline_rust_assoc_call_never_binds_free_namesake_parallel);
|
||||
RUN_TEST(pipeline_rust_std_receiver_never_binds_project_method);
|
||||
RUN_TEST(pipeline_rust_std_receiver_never_binds_project_method_parallel);
|
||||
RUN_TEST(pipeline_go_bare_ref_never_binds_field);
|
||||
@@ -24591,6 +24886,8 @@ SUITE(pipeline) {
|
||||
RUN_TEST(override_python_through_intermediate_class);
|
||||
RUN_TEST(python_crossfile_typed_field_calls_issue1277);
|
||||
RUN_TEST(python_crossfile_typed_field_calls_issue1277_parallel);
|
||||
RUN_TEST(python_dotted_base_inheritance);
|
||||
RUN_TEST(python_dotted_base_inheritance_parallel);
|
||||
RUN_TEST(python_typed_field_external_import_stays_unresolved);
|
||||
RUN_TEST(python_typed_field_project_class_resolves);
|
||||
RUN_TEST(usages_creates_edges);
|
||||
|
||||
@@ -591,6 +591,9 @@ TEST(rustlsp_shared_registry_resolves_like_per_file) {
|
||||
defs[1].receiver_type = "test.demo.Database";
|
||||
defs[1].def_module_qn = "test.demo";
|
||||
defs[1].return_types = "alloc.string.String";
|
||||
/* The shared build registers Rust defs only, as the pipeline tags them. */
|
||||
defs[0].lang = CBM_LANG_RUST;
|
||||
defs[1].lang = CBM_LANG_RUST;
|
||||
|
||||
const char *imp_names[] = {"demo"};
|
||||
const char *imp_qns[] = {"test::demo"};
|
||||
@@ -695,6 +698,7 @@ TEST(rustlsp_shared_registry_macro_hidden_call_has_carrier) {
|
||||
defs[0].label = "Function";
|
||||
defs[0].def_module_qn = "test.lib";
|
||||
defs[0].return_types = "str";
|
||||
defs[0].lang = CBM_LANG_RUST;
|
||||
|
||||
const char *imp_names[] = {"lib"};
|
||||
const char *imp_qns[] = {"test::lib"};
|
||||
|
||||
Reference in new issue
Block a user