mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(extraction): keep Swift initializer edges to true targets
Follow-up to the review of #2395 (issue #2379). Making init/deinit Method nodes gives every type an `init` target, and the short-name registry strategies then bound initializer calls to whichever `init` was nearest. On Alamofire (bda9ed57) all 8 new CALLS edges into an initializer were false, and so were the 40 new USAGE edges. - Name init/deinit in one Swift-gated place, cbm_resolve_func_name; resolve_method_name delegates there. The language-agnostic func_name_node no longer special-cases the node kinds (the Slang grammar has an init_declaration too). - super.init(...) is emitted as <Superclass>.init, from the first entry of the enclosing class's inheritance clause. The class node is carried by the unified walk (enclosing_class_node, saved and restored with the other frame state) instead of found by climbing parents. Without a clause, or with an entry that does not read as a type name, there is no call. - self.init(...) forms no call. Initializer overloads share one node, so the only possible edge is a self-loop, and that is false whenever the overload it delegates to lives outside the project. - T.init(...) calls are flagged is_method, and Swift joins the weak-member guard in pass_calls.c and pass_parallel.c (in lockstep), so only qualified, same-module or import placements survive. No other Swift call carries the flag. - init and deinit are Swift keywords for cbm_is_keyword, so references such as .init(...) and Foo.init are not usages resolved by name. Tests: the exact CALLS set for the #2379 reproduction plus initializer delegation in a SwiftPM package, through the sequential and the parallel resolver; extraction tests for the superclass rewrite, the no-superclass and lower-case cases, the scope of the call flag and init references. Review nits: renamed the overload test, removed the stacked helper comment. Alamofire, main / PR head / this commit: CALLS 5472 / 5487 / 5485; CALLS into an initializer 0 / 8 (all false) / 6 (all superclass initializers); USAGE into an initializer 0 / 40 / 0. Co-authored-by: Andrew Studnicky <a.j.studnicky@gmail.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
468102c707
commit
71ab015483
9 files changed
+492
-14
No files matched your search
@@ -4278,6 +4278,70 @@ static TSNode language_specific_callee_leaf(CBMLanguage language, TSNode expr,
|
||||
return terminal_callee_leaf(ctx, expr);
|
||||
}
|
||||
|
||||
/* `Base.init` for a `super.init(...)` call: Base is the first entry of the
|
||||
* enclosing class's inheritance clause, where Swift requires the superclass to
|
||||
* stand. The class node is carried by the walk, so no ancestor climb. NULL (no
|
||||
* call) outside a class, without a clause, or when the entry does not read as
|
||||
* a plain type name -- a lower-case or attributed root would pass the
|
||||
* registry's receiver-chain check and bind by short name, which is the false
|
||||
* edge this exists to prevent. */
|
||||
static const char *swift_superclass_init_callee(CBMExtractCtx *ctx, const WalkState *state) {
|
||||
TSNode cls = state ? state->enclosing_class_node : (TSNode){0};
|
||||
if (ts_node_is_null(cls) || strcmp(ts_node_type(cls), "class_declaration") != 0) {
|
||||
return NULL;
|
||||
}
|
||||
TSNode spec = cbm_find_child_by_kind(cls, "inheritance_specifier");
|
||||
char *base = ts_node_is_null(spec) ? NULL : cbm_node_text(ctx->arena, spec, ctx->source);
|
||||
if (!base || base[0] < 'A' || base[0] > 'Z') {
|
||||
return NULL;
|
||||
}
|
||||
size_t len = strcspn(base, "<"); /* Base<T> names Base */
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (!isalnum((unsigned char)base[i]) && base[i] != '_' && base[i] != '.') {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
base[len] = '\0';
|
||||
return cbm_arena_sprintf(ctx->arena, "%s.init", base);
|
||||
}
|
||||
|
||||
/* True for a Swift initializer callee `T.init` (T non-empty). */
|
||||
static bool swift_callee_is_initializer(const char *callee) {
|
||||
static const char suffix[] = ".init";
|
||||
size_t n = callee ? strlen(callee) : 0;
|
||||
return n > sizeof(suffix) - 1 && strcmp(callee + n - (sizeof(suffix) - 1), suffix) == 0;
|
||||
}
|
||||
|
||||
/* Swift initializer delegation (#2379). `super.init(...)` and `self.init(...)`
|
||||
* name no type, and a lower-case receiver passes the registry's receiver-chain
|
||||
* check, so a short-name match binds them to whichever project `init` is
|
||||
* nearest: a nested type's, a sibling type's, or the caller's own. On
|
||||
* Alamofire all 8 such edges were false.
|
||||
*
|
||||
* `super.init` is rewritten to name the superclass (`Base.init`), so the
|
||||
* qualified strategies place it on the project's Base.init. When Base is not
|
||||
* in the project (UIViewController, NSObject) nothing places it, and the
|
||||
* initializer guard in handle_calls keeps a short-name guess from doing so.
|
||||
*
|
||||
* `self.init` delegates to another initializer of the caller's own type.
|
||||
* Initializer overloads share one node (#2061), so the only edge it could form
|
||||
* is a self-loop, and that one is false whenever the overload it calls lives
|
||||
* outside the project (`extension Notification { init(...) { self.init(...) } }`
|
||||
* calls Foundation's). It forms no CALLS edge. */
|
||||
static const char *resolve_swift_delegating_init(CBMExtractCtx *ctx, const WalkState *state,
|
||||
const char *callee) {
|
||||
if (ctx->language != CBM_LANG_SWIFT || !callee) {
|
||||
return callee;
|
||||
}
|
||||
if (strcmp(callee, "self.init") == 0) {
|
||||
return NULL;
|
||||
}
|
||||
if (strcmp(callee, "super.init") == 0) {
|
||||
return swift_superclass_init_callee(ctx, state);
|
||||
}
|
||||
return callee;
|
||||
}
|
||||
|
||||
// Produce the textual callee and the AST occurrence from one operation. The
|
||||
// generic fallback retains the former field/terminal behavior, while grammars
|
||||
// whose extractor uses a sibling, a special field, or a synthetic literal are
|
||||
@@ -4288,6 +4352,7 @@ static CBMPrimaryCalleeSelection select_primary_callee(CBMExtractCtx *ctx, TSNod
|
||||
CBMPrimaryCalleeSelection selection = {0};
|
||||
selection.name = extract_callee_name(ctx->arena, node, ctx->source, ctx->language);
|
||||
selection.name = resolve_objectscript_callee(ctx, node, state, selection.name);
|
||||
selection.name = resolve_swift_delegating_init(ctx, state, selection.name);
|
||||
if (!selection.name || !selection.name[0]) {
|
||||
return selection;
|
||||
}
|
||||
@@ -4511,6 +4576,16 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML
|
||||
call.is_method = true;
|
||||
}
|
||||
}
|
||||
// Swift initializer guard (#2379; same direction). Flag `T.init(...)`,
|
||||
// as written or as a rewritten super.init. Nearly every type has an
|
||||
// `init`, so a short-name match onto one is a guess: with a project
|
||||
// module imported, `UIViewController.init` bound an unrelated
|
||||
// project initializer. The registry's qualified / same-module /
|
||||
// import strategies still place T.init when T is a project type.
|
||||
// No other Swift call carries the flag.
|
||||
if (ctx->language == CBM_LANG_SWIFT && swift_callee_is_initializer(call.callee_name)) {
|
||||
call.is_method = true;
|
||||
}
|
||||
|
||||
TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
|
||||
/* tree-sitter-elixir attaches NO field name to a call's arguments
|
||||
|
||||
@@ -236,11 +236,6 @@ static void extract_elixir_call(CBMExtractCtx *ctx, TSNode node, const CBMLangSp
|
||||
|
||||
// Get "name" field from a node
|
||||
static TSNode func_name_node(TSNode node) {
|
||||
const char *node_type = ts_node_type(node);
|
||||
if (strcmp(node_type, "init_declaration") == 0 ||
|
||||
strcmp(node_type, "deinit_declaration") == 0) {
|
||||
return node;
|
||||
}
|
||||
TSNode name = ts_node_child_by_field_name(node, TS_FIELD("name"));
|
||||
if (ts_node_is_null(name)) {
|
||||
/* Protobuf rpc: name is in rpc_name child, not "name" field */
|
||||
@@ -774,6 +769,15 @@ TSNode cbm_resolve_func_name(TSNode node, CBMLanguage lang) {
|
||||
return cbm_find_child_by_kind(node, "query_name");
|
||||
}
|
||||
|
||||
/* Swift init/deinit (#2379) have no name node at all. The declaration
|
||||
* itself stands in for it, and cbm_func_name_node_text maps that
|
||||
* node kind to "init" / "deinit". Gated on Swift: Slang's grammar
|
||||
* also has an init_declaration node. */
|
||||
if (lang == CBM_LANG_SWIFT &&
|
||||
(strcmp(kind, "init_declaration") == 0 || strcmp(kind, "deinit_declaration") == 0)) {
|
||||
return node;
|
||||
}
|
||||
|
||||
TSNode name = func_name_node(node);
|
||||
|
||||
if (lang == CBM_LANG_R && strcmp(kind, "function_definition") == 0) {
|
||||
@@ -8674,6 +8678,12 @@ static TSNode resolve_method_name(TSNode child, CBMLanguage lang) {
|
||||
return cbm_find_child_by_kind(child, "simple_identifier");
|
||||
}
|
||||
|
||||
// Swift init/deinit: named in one place, cbm_resolve_func_name (#2379).
|
||||
if (lang == CBM_LANG_SWIFT &&
|
||||
(strcmp(ck, "init_declaration") == 0 || strcmp(ck, "deinit_declaration") == 0)) {
|
||||
return cbm_resolve_func_name(child, lang);
|
||||
}
|
||||
|
||||
// Squirrel: function_declaration's name is a plain `identifier` child.
|
||||
if (lang == CBM_LANG_SQUIRREL && strcmp(ck, "function_declaration") == 0) {
|
||||
return cbm_find_child_by_kind(child, "identifier");
|
||||
|
||||
@@ -324,6 +324,7 @@ static bool push_scope(WalkState *state, uint8_t kind, uint32_t depth, const cha
|
||||
* CBMWalkScope comment for the quadratic recompute this replaces. */
|
||||
f->prev_enclosing_func_qn = state->enclosing_func_qn;
|
||||
f->prev_enclosing_class_qn = state->enclosing_class_qn;
|
||||
f->prev_enclosing_class_node = state->enclosing_class_node;
|
||||
f->prev_invocation_kind = state->invocation_kind;
|
||||
f->prev_callee_expr = state->callee_expr;
|
||||
f->prev_callee_leaf = state->callee_leaf;
|
||||
@@ -337,6 +338,8 @@ static bool push_scope(WalkState *state, uint8_t kind, uint32_t depth, const cha
|
||||
case SCOPE_CLASS:
|
||||
case SCOPE_NAMESPACE:
|
||||
state->enclosing_class_qn = qn;
|
||||
/* push_lexical_scope records the node; a node-less push has none. */
|
||||
state->enclosing_class_node = (TSNode){0};
|
||||
break;
|
||||
case SCOPE_CALL:
|
||||
/* The invocation triple is applied by push_call_scope once the caller
|
||||
@@ -364,6 +367,9 @@ static bool push_lexical_scope(WalkState *state, uint8_t walk_kind, uint32_t dep
|
||||
state->lexical_binding_tracking_failed = true;
|
||||
return false;
|
||||
}
|
||||
if (walk_kind == SCOPE_CLASS || walk_kind == SCOPE_NAMESPACE) {
|
||||
state->enclosing_class_node = node;
|
||||
}
|
||||
state->scopes[state->scope_top - SKIP_ONE].lexical_scope_id =
|
||||
add_lexical_scope(state, node, lexical_kind);
|
||||
return true;
|
||||
@@ -442,6 +448,7 @@ static void pop_expired_scopes(WalkState *state, uint32_t cur_depth) {
|
||||
const CBMWalkScope *f = &state->scopes[--state->scope_top];
|
||||
state->enclosing_func_qn = f->prev_enclosing_func_qn;
|
||||
state->enclosing_class_qn = f->prev_enclosing_class_qn;
|
||||
state->enclosing_class_node = f->prev_enclosing_class_node;
|
||||
state->invocation_kind = f->prev_invocation_kind;
|
||||
state->callee_expr = f->prev_callee_expr;
|
||||
state->callee_leaf = f->prev_callee_leaf;
|
||||
@@ -2671,6 +2678,7 @@ void cbm_extract_unified(CBMExtractCtx *ctx) {
|
||||
* recompute_state call): module scope, nothing else active. */
|
||||
state.enclosing_func_qn = ctx->module_qn;
|
||||
state.enclosing_class_qn = NULL;
|
||||
state.enclosing_class_node = (TSNode){0};
|
||||
state.invocation_kind = CBM_INVOCATION_NONE;
|
||||
state.callee_expr = (TSNode){0};
|
||||
state.callee_leaf = (TSNode){0};
|
||||
|
||||
@@ -78,6 +78,7 @@ typedef struct {
|
||||
* suite-budget kills on every non-M4 venue). */
|
||||
const char *prev_enclosing_func_qn;
|
||||
const char *prev_enclosing_class_qn;
|
||||
TSNode prev_enclosing_class_node;
|
||||
CBMInvocationKind prev_invocation_kind;
|
||||
TSNode prev_callee_expr;
|
||||
TSNode prev_callee_leaf;
|
||||
@@ -133,6 +134,7 @@ typedef struct {
|
||||
typedef struct {
|
||||
const char *enclosing_func_qn; // current function QN (module_qn at top level)
|
||||
const char *enclosing_class_qn; // current class QN (NULL outside class)
|
||||
TSNode enclosing_class_node; // that class scope's node (null when not known)
|
||||
const TSTreeCursor *current_cursor; // unified walk cursor at the current node
|
||||
TSTreeCursor *occurrence_cursor; // reusable parent-preserving classifier cursor
|
||||
CBMInvocationKind invocation_kind; // exact active invocation/reference role
|
||||
|
||||
@@ -301,6 +301,16 @@ bool cbm_is_keyword(const char *name, CBMLanguage lang) {
|
||||
case CBM_LANG_PUPPET:
|
||||
keywords = puppet_keywords;
|
||||
break;
|
||||
case CBM_LANG_SWIFT:
|
||||
/* `init` and `deinit` are Swift keywords. A reference to one
|
||||
* (`.init(...)`, `Foo.init`) names an initializer of a type the bare
|
||||
* name does not carry, so resolving it by name can only guess among
|
||||
* every type's initializer (#2379). Otherwise the generic set. */
|
||||
if (strcmp(name, "init") == 0 || strcmp(name, "deinit") == 0) {
|
||||
return true;
|
||||
}
|
||||
keywords = generic_keywords;
|
||||
break;
|
||||
default:
|
||||
keywords = generic_keywords;
|
||||
break;
|
||||
|
||||
@@ -1133,7 +1133,11 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
|
||||
* to a docs bundle (2026-09-16 probe: 4,207 junk
|
||||
* edges on JetBrains/Exposed). */
|
||||
lang == CBM_LANG_HTML || lang == CBM_LANG_VUE ||
|
||||
lang == CBM_LANG_SVELTE || lang == CBM_LANG_ASTRO;
|
||||
lang == CBM_LANG_SVELTE || lang == CBM_LANG_ASTRO ||
|
||||
/* Swift flags only initializer calls `T.init(...)`
|
||||
* (#2379, extract_calls.c); every other Swift call
|
||||
* keeps is_method=false and is unaffected. */
|
||||
lang == CBM_LANG_SWIFT;
|
||||
/* Bare-call local-binding suppression. A member call has a receiver the
|
||||
* guard above can reason about; a bare `run()` has none, so that guard
|
||||
* cannot see this class at all. Python-only today because the extraction
|
||||
|
||||
@@ -3401,7 +3401,9 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
lang == CBM_LANG_ARKTS ||
|
||||
/* embedded-script hosts — see pass_calls.c */
|
||||
lang == CBM_LANG_HTML || lang == CBM_LANG_VUE ||
|
||||
lang == CBM_LANG_SVELTE || lang == CBM_LANG_ASTRO;
|
||||
lang == CBM_LANG_SVELTE || lang == CBM_LANG_ASTRO ||
|
||||
/* Swift initializer calls only — see pass_calls.c */
|
||||
lang == CBM_LANG_SWIFT;
|
||||
/* Bare-call local-binding suppression — see the note in pass_calls.c.
|
||||
* This gate MUST stay identical to the one there. */
|
||||
bool suppress_weak_local_binding = lang == CBM_LANG_PYTHON;
|
||||
|
||||
+125
-7
@@ -79,8 +79,6 @@ static int has_def_qn(CBMFileResult *r, const char *qn) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Return the definition with the given qualified name, or NULL. Distinct from
|
||||
* find_def_by_name for the same reason has_def_qn is distinct from has_def. */
|
||||
/* Find a definition by its qualified name relative to the file's module QN
|
||||
* (e.g. "Box.init" resolves to "<module_qn>.Box.init"). */
|
||||
static const CBMDefinition *find_def_in_module(CBMFileResult *r, const char *rel_qn) {
|
||||
@@ -2599,9 +2597,11 @@ TEST(swift_init_convenience_delegates_issue2379) {
|
||||
const CBMDefinition *init_def = find_def_in_module(r, "Holder.init");
|
||||
ASSERT_NOT_NULL(init_def);
|
||||
|
||||
/* self.init(...) is a plain member call today, same as any other
|
||||
* `receiver.member(...)` invocation -- no dedicated delegation handling. */
|
||||
ASSERT(has_call(r, "self.init"));
|
||||
/* self.init(...) delegates to another overload of Holder.init, which
|
||||
* shares this node (#2061): it forms no call, because the only edge it
|
||||
* could produce is a self-loop. Its argument call stays. */
|
||||
ASSERT_FALSE(has_call(r, "self.init"));
|
||||
ASSERT_FALSE(has_call(r, "Holder.init"));
|
||||
|
||||
const CBMCall *call = find_call_by_callee(r, "other");
|
||||
ASSERT_NOT_NULL(call);
|
||||
@@ -2619,7 +2619,7 @@ TEST(swift_init_convenience_delegates_issue2379) {
|
||||
* Swift methods onto one node (DeusData/codebase-memory-mcp#2061), and an
|
||||
* init is no exception; distinguishing overloads by signature is that
|
||||
* issue's concern, not this one's. */
|
||||
TEST(swift_init_multiple_not_collapsed_issue2379) {
|
||||
TEST(swift_init_overloads_share_one_node_issue2379) {
|
||||
CBMFileResult *r = extract("func shared() -> Int { 3 }\n"
|
||||
"final class Holder {\n"
|
||||
" let value: Int\n"
|
||||
@@ -2761,6 +2761,120 @@ TEST(swift_deinit_issue2379) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #2379: `super.init(...)` names no type. Left as written, the registry binds it
|
||||
* by short name to whichever project `init` is nearest (on Alamofire: a nested
|
||||
* `Options.init`, a sibling decoder's). It is emitted as the superclass's
|
||||
* initializer instead -- the first inheritance entry, generic arguments
|
||||
* dropped -- and stays attributed to the calling initializer. */
|
||||
TEST(swift_super_init_names_superclass_issue2379) {
|
||||
CBMFileResult *r = extract("final class Sub: Base<Int>, Sendable {\n"
|
||||
" override init() {\n"
|
||||
" super.init()\n"
|
||||
" }\n"
|
||||
"}\n",
|
||||
CBM_LANG_SWIFT, "t", "Sub.swift");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
|
||||
const CBMDefinition *init_def = find_def_in_module(r, "Sub.init");
|
||||
ASSERT_NOT_NULL(init_def);
|
||||
ASSERT_FALSE(has_call(r, "super.init"));
|
||||
const CBMCall *call = find_call_by_callee(r, "Base.init");
|
||||
ASSERT_NOT_NULL(call);
|
||||
ASSERT_NOT_NULL(call->enclosing_func_qn);
|
||||
ASSERT_STR_EQ(call->enclosing_func_qn, init_def->qualified_name);
|
||||
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #2379: without a superclass to name -- no inheritance clause, or a first entry
|
||||
* that does not read as a type -- `super.init` forms no call at all. A
|
||||
* lower-case root would pass the registry's receiver-chain check and bind by
|
||||
* short name again. */
|
||||
TEST(swift_super_init_without_superclass_no_call_issue2379) {
|
||||
CBMFileResult *r = extract("class Root {\n"
|
||||
" init() {\n"
|
||||
" super.init()\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"class Odd: base {\n"
|
||||
" init() {\n"
|
||||
" super.init()\n"
|
||||
" }\n"
|
||||
"}\n",
|
||||
CBM_LANG_SWIFT, "t", "Root.swift");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_NOT_NULL(find_def_in_module(r, "Root.init"));
|
||||
ASSERT_NOT_NULL(find_def_in_module(r, "Odd.init"));
|
||||
for (int i = 0; i < r->calls.count; i++) {
|
||||
const char *callee = r->calls.items[i].callee_name;
|
||||
ASSERT(!callee || !strstr(callee, "init"));
|
||||
}
|
||||
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #2379: an initializer call `T.init(...)` is flagged so the resolvers'
|
||||
* weak-member guard keeps a short-name guess from placing it (nearly every
|
||||
* type has an `init`); every other Swift call stays unflagged and resolves
|
||||
* exactly as before. */
|
||||
TEST(swift_initializer_calls_flagged_issue2379) {
|
||||
CBMFileResult *r = extract("func run(obj: Worker) {\n"
|
||||
" _ = UIAlertAction.init(title: nil)\n"
|
||||
" helper()\n"
|
||||
" obj.start()\n"
|
||||
"}\n",
|
||||
CBM_LANG_SWIFT, "t", "Run.swift");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
const CBMCall *init_call = find_call_by_callee(r, "UIAlertAction.init");
|
||||
const CBMCall *bare = find_call_by_callee(r, "helper");
|
||||
const CBMCall *member = find_call_by_callee(r, "obj.start");
|
||||
ASSERT_NOT_NULL(init_call);
|
||||
ASSERT_NOT_NULL(bare);
|
||||
ASSERT_NOT_NULL(member);
|
||||
ASSERT_TRUE(init_call->is_method);
|
||||
ASSERT_FALSE(bare->is_method);
|
||||
ASSERT_FALSE(member->is_method);
|
||||
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #2379: `init` is a Swift keyword. A reference to one (`.init(...)`,
|
||||
* `Box.init`) names the initializer of a type the bare name does not carry,
|
||||
* so it is not recorded as a usage -- by name it could only be guessed among
|
||||
* every type's initializer (on Alamofire, 26 references landed on one nested
|
||||
* `Output.init`). The type in `Box.init` is still a usage. */
|
||||
TEST(swift_init_reference_is_not_a_usage_issue2379) {
|
||||
CBMFileResult *r = extract("struct Box {\n"
|
||||
" init(raw: Int) {}\n"
|
||||
"}\n"
|
||||
"func make() -> Box {\n"
|
||||
" .init(raw: 1)\n"
|
||||
"}\n"
|
||||
"func maker() -> (Int) -> Box {\n"
|
||||
" Box.init\n"
|
||||
"}\n",
|
||||
CBM_LANG_SWIFT, "t", "Make.swift");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
int init_usages = 0;
|
||||
int box_usages = 0;
|
||||
for (int i = 0; i < r->usages.count; i++) {
|
||||
const char *ref = r->usages.items[i].ref_name;
|
||||
init_usages += ref && strcmp(ref, "init") == 0;
|
||||
box_usages += ref && strcmp(ref, "Box") == 0;
|
||||
}
|
||||
ASSERT_EQ(init_usages, 0);
|
||||
ASSERT_GT(box_usages, 0);
|
||||
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* --- Objective-C --- */
|
||||
TEST(objc_interface) {
|
||||
CBMFileResult *r =
|
||||
@@ -13488,11 +13602,15 @@ SUITE(extraction) {
|
||||
RUN_TEST(swift_init_struct_issue2379);
|
||||
RUN_TEST(swift_init_class_designated_issue2379);
|
||||
RUN_TEST(swift_init_convenience_delegates_issue2379);
|
||||
RUN_TEST(swift_init_multiple_not_collapsed_issue2379);
|
||||
RUN_TEST(swift_init_overloads_share_one_node_issue2379);
|
||||
RUN_TEST(swift_init_failable_enum_issue2379);
|
||||
RUN_TEST(swift_init_extension_issue2379);
|
||||
RUN_TEST(swift_method_still_attributed_issue2379);
|
||||
RUN_TEST(swift_deinit_issue2379);
|
||||
RUN_TEST(swift_super_init_names_superclass_issue2379);
|
||||
RUN_TEST(swift_super_init_without_superclass_no_call_issue2379);
|
||||
RUN_TEST(swift_initializer_calls_flagged_issue2379);
|
||||
RUN_TEST(swift_init_reference_is_not_a_usage_issue2379);
|
||||
RUN_TEST(swift_call_string_arg_issue1892);
|
||||
RUN_TEST(swift_nested_url_constructor_issue1892);
|
||||
RUN_TEST(swift_nested_url_no_bang_issue1892);
|
||||
|
||||
@@ -18979,6 +18979,253 @@ TEST(pipeline_swift_cross_package_import) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #2379 at graph level, as a SwiftPM package. Sources/Kit/Inits.swift is the
|
||||
* issue's reproduction. Sources/App/Sub.swift imports the Kit target -- which
|
||||
* sends name resolution down the import-aware path -- and calls initializers
|
||||
* the way real code does: `super.init` to a project superclass in another
|
||||
* target, `super.init` to a superclass outside the project, an explicit
|
||||
* `T.init(...)` on a project type and on a type outside the project, `self.init`
|
||||
* from an extension of a type outside the project, and an implicit `.init(...)`.
|
||||
* Each of those can attract a false CALLS edge now that initializers are nodes. */
|
||||
static const char *const SWIFT_INIT_PACKAGE_SRC =
|
||||
"let package = Package(\n"
|
||||
" name: \"Kit\",\n"
|
||||
" products: [.library(name: \"Kit\", targets: [\"Kit\"])],\n"
|
||||
" targets: [\n"
|
||||
" .target(name: \"Kit\", dependencies: []),\n"
|
||||
" .target(name: \"App\", dependencies: [\"Kit\"])\n"
|
||||
" ]\n"
|
||||
")\n";
|
||||
|
||||
static const char *const SWIFT_INIT_INITS_SRC = "func helper() -> Int { 1 }\n"
|
||||
"func other() -> Int { 2 }\n"
|
||||
"\n"
|
||||
"struct Box {\n"
|
||||
" let value: Int\n"
|
||||
" init() {\n"
|
||||
" value = helper()\n"
|
||||
" }\n"
|
||||
" func read() -> Int {\n"
|
||||
" other()\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"final class Holder {\n"
|
||||
" let value: Int\n"
|
||||
" init(seed: Int) {\n"
|
||||
" value = helper() + seed\n"
|
||||
" }\n"
|
||||
" convenience init() {\n"
|
||||
" self.init(seed: other())\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"enum Mode {\n"
|
||||
" case on\n"
|
||||
" init?(flag: Bool) {\n"
|
||||
" if helper() > 0 { self = .on } else { "
|
||||
"return nil }\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"extension Box {\n"
|
||||
" init(raw: Int) {\n"
|
||||
" value = other()\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
|
||||
static const char *const SWIFT_INIT_BASE_SRC = "open class Base {\n"
|
||||
" public init() {}\n"
|
||||
"}\n";
|
||||
|
||||
static const char *const SWIFT_INIT_SUB_SRC = "import Kit\n"
|
||||
"import UIKit\n"
|
||||
"\n"
|
||||
"class Sub: Base {\n"
|
||||
" override init() {\n"
|
||||
" super.init()\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"class Screen: UIViewController {\n"
|
||||
" init() {\n"
|
||||
" super.init(nibName: nil, bundle: nil)\n"
|
||||
" }\n"
|
||||
" func alert() -> UIAlertAction {\n"
|
||||
" UIAlertAction.init(title: nil, style: "
|
||||
".default)\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"extension Notification {\n"
|
||||
" init(tag: String) {\n"
|
||||
" self.init(name: Notification.Name(tag))\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"func make() -> Box {\n"
|
||||
" .init(raw: 1)\n"
|
||||
"}\n"
|
||||
"func explicit() -> Box {\n"
|
||||
" Box.init(raw: 2)\n"
|
||||
"}\n";
|
||||
|
||||
enum { SWIFT_INIT_EDGE_CAP = 32, SWIFT_INIT_EDGE_LEN = 192 };
|
||||
|
||||
typedef struct {
|
||||
int run_rc;
|
||||
int calls; /* CALLS edges seen; -1 if the store was not read */
|
||||
int init_usages; /* USAGE edges into a node named init/deinit */
|
||||
char edges[SWIFT_INIT_EDGE_CAP][SWIFT_INIT_EDGE_LEN]; /* "src -> tgt", sorted */
|
||||
} SwiftInitObservation;
|
||||
|
||||
static int swift_init_edge_cmp(const void *a, const void *b) {
|
||||
return strcmp((const char *)a, (const char *)b);
|
||||
}
|
||||
|
||||
/* Qualified name relative to the project ("Inits.Box.init"). */
|
||||
static const char *swift_init_rel_qn(const char *qn, const char *project) {
|
||||
size_t plen = strlen(project);
|
||||
return strncmp(qn, project, plen) == 0 && qn[plen] == '.' ? qn + plen + 1 : qn;
|
||||
}
|
||||
|
||||
static void swift_init_observe(cbm_store_t *s, const char *project, SwiftInitObservation *o) {
|
||||
cbm_edge_t *edges = NULL;
|
||||
int count = 0;
|
||||
o->calls = 0;
|
||||
if (cbm_store_find_edges_by_type(s, project, "CALLS", &edges, &count) == CBM_STORE_OK) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
cbm_node_t src = {0};
|
||||
cbm_node_t tgt = {0};
|
||||
if (cbm_store_find_node_by_id(s, edges[i].source_id, &src) == CBM_STORE_OK &&
|
||||
cbm_store_find_node_by_id(s, edges[i].target_id, &tgt) == CBM_STORE_OK &&
|
||||
o->calls < SWIFT_INIT_EDGE_CAP) {
|
||||
snprintf(o->edges[o->calls], SWIFT_INIT_EDGE_LEN, "%s -> %s",
|
||||
swift_init_rel_qn(src.qualified_name, project),
|
||||
swift_init_rel_qn(tgt.qualified_name, project));
|
||||
}
|
||||
o->calls++;
|
||||
cbm_node_free_fields(&src);
|
||||
cbm_node_free_fields(&tgt);
|
||||
}
|
||||
if (edges) {
|
||||
cbm_store_free_edges(edges, count);
|
||||
}
|
||||
}
|
||||
int kept = o->calls < SWIFT_INIT_EDGE_CAP ? o->calls : SWIFT_INIT_EDGE_CAP;
|
||||
qsort(o->edges, (size_t)kept, sizeof(o->edges[0]), swift_init_edge_cmp);
|
||||
|
||||
edges = NULL;
|
||||
count = 0;
|
||||
o->init_usages = 0;
|
||||
if (cbm_store_find_edges_by_type(s, project, "USAGE", &edges, &count) == CBM_STORE_OK) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
cbm_node_t tgt = {0};
|
||||
if (cbm_store_find_node_by_id(s, edges[i].target_id, &tgt) == CBM_STORE_OK &&
|
||||
tgt.name && (strcmp(tgt.name, "init") == 0 || strcmp(tgt.name, "deinit") == 0)) {
|
||||
o->init_usages++;
|
||||
}
|
||||
cbm_node_free_fields(&tgt);
|
||||
}
|
||||
if (edges) {
|
||||
cbm_store_free_edges(edges, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* `pad` filler files push the run past MIN_FILES_FOR_PARALLEL so that, with
|
||||
* `parallel`, it goes through pass_parallel.c rather than pass_calls.c. */
|
||||
static void swift_init_case(bool parallel, int pad, SwiftInitObservation *o) {
|
||||
memset(o, 0, sizeof(*o));
|
||||
o->run_rc = -1;
|
||||
o->calls = -1;
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_swift_init_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
return;
|
||||
}
|
||||
char path[512];
|
||||
bool written = true;
|
||||
const char *names[] = {"Package.swift", "Sources/Kit/Inits.swift", "Sources/Kit/Base.swift",
|
||||
"Sources/App/Sub.swift"};
|
||||
const char *sources[] = {SWIFT_INIT_PACKAGE_SRC, SWIFT_INIT_INITS_SRC, SWIFT_INIT_BASE_SRC,
|
||||
SWIFT_INIT_SUB_SRC};
|
||||
for (int i = 0; i < 4 && written; i++) {
|
||||
snprintf(path, sizeof(path), "%s/%s", tmp, names[i]);
|
||||
written = th_write_file(path, sources[i]) == 0;
|
||||
}
|
||||
for (int i = 0; i < pad && written; i++) {
|
||||
char source[96];
|
||||
snprintf(path, sizeof(path), "%s/Pad%02d.swift", tmp, i);
|
||||
snprintf(source, sizeof(source), "func pad%02d() -> Int { %d }\n", i, i);
|
||||
written = th_write_file(path, source) == 0;
|
||||
}
|
||||
if (written) {
|
||||
XlangEnvSave save = xlang_env_select(parallel);
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/swift_init.db", tmp);
|
||||
cbm_pipeline_t *pipeline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
if (pipeline) {
|
||||
o->run_rc = cbm_pipeline_run(pipeline);
|
||||
const char *project = cbm_pipeline_project_name(pipeline);
|
||||
cbm_store_t *store = cbm_store_open_path(db_path);
|
||||
if (store && project) {
|
||||
swift_init_observe(store, project, o);
|
||||
}
|
||||
if (store) {
|
||||
cbm_store_close(store);
|
||||
}
|
||||
cbm_pipeline_free(pipeline);
|
||||
}
|
||||
xlang_env_restore(&save);
|
||||
}
|
||||
th_rmtree(tmp);
|
||||
}
|
||||
|
||||
/* The exact CALLS set. Every initializer's own calls come from that
|
||||
* initializer (the repro's file-level collapse is gone); Sub.init reaches
|
||||
* Base.init in the other target and `Box.init(raw:)` reaches Box.init.
|
||||
* UIViewController's and UIAlertAction's initializers, Notification's own and
|
||||
* the implicit `.init(...)` are outside the project and form no edge -- in
|
||||
* particular none to a project `init` that merely shares the name. Holder's
|
||||
* convenience `self.init` is not a self-loop. */
|
||||
static int swift_init_assert_exact(const SwiftInitObservation *o) {
|
||||
static const char *const expected[] = {
|
||||
"Sources.App.Sub.Sub.init -> Sources.Kit.Base.Base.init",
|
||||
"Sources.App.Sub.explicit -> Sources.Kit.Inits.Box.init",
|
||||
"Sources.Kit.Inits.Box.init -> Sources.Kit.Inits.helper",
|
||||
"Sources.Kit.Inits.Box.init -> Sources.Kit.Inits.other",
|
||||
"Sources.Kit.Inits.Box.read -> Sources.Kit.Inits.other",
|
||||
"Sources.Kit.Inits.Holder.init -> Sources.Kit.Inits.helper",
|
||||
"Sources.Kit.Inits.Holder.init -> Sources.Kit.Inits.other",
|
||||
"Sources.Kit.Inits.Mode.init -> Sources.Kit.Inits.helper",
|
||||
};
|
||||
enum { EXPECTED_CALLS = sizeof(expected) / sizeof(expected[0]) };
|
||||
ASSERT_EQ(o->run_rc, 0);
|
||||
ASSERT_EQ(o->init_usages, 0);
|
||||
ASSERT_EQ(o->calls, (int)EXPECTED_CALLS);
|
||||
for (int i = 0; i < (int)EXPECTED_CALLS; i++) {
|
||||
ASSERT_STR_EQ(o->edges[i], expected[i]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
TEST(pipeline_swift_init_exact_calls_sequential_issue2379) {
|
||||
SwiftInitObservation o;
|
||||
swift_init_case(false, 0, &o);
|
||||
if (swift_init_assert_exact(&o) != 0) {
|
||||
return 1;
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Same input through pass_parallel.c: 55 fillers exceed MIN_FILES_FOR_PARALLEL. */
|
||||
TEST(pipeline_swift_init_exact_calls_parallel_issue2379) {
|
||||
SwiftInitObservation o;
|
||||
swift_init_case(true, 55, &o);
|
||||
if (swift_init_assert_exact(&o) != 0) {
|
||||
return 1;
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pipeline_python_cross_module_call) {
|
||||
/* Port of TestPythonCrossModuleCallViaImport */
|
||||
const char *files[] = {"utils.py", "main.py"};
|
||||
@@ -27638,6 +27885,8 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_imports_multi_symbol_edges);
|
||||
RUN_TEST(pipeline_go_cross_package_call);
|
||||
RUN_TEST(pipeline_swift_cross_package_import);
|
||||
RUN_TEST(pipeline_swift_init_exact_calls_sequential_issue2379);
|
||||
RUN_TEST(pipeline_swift_init_exact_calls_parallel_issue2379);
|
||||
RUN_TEST(pipeline_python_cross_module_call);
|
||||
RUN_TEST(pipeline_cross_language_same_name_does_not_share_calls_issue725);
|
||||
RUN_TEST(pipeline_go_type_classification);
|
||||
|
||||
Reference in new issue
Block a user