diff --git a/internal/cbm/extract_calls.c b/internal/cbm/extract_calls.c index 4eb02659c..b5cda6429 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -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 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 diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 865f56aa7..612161348 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -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"); diff --git a/internal/cbm/extract_unified.c b/internal/cbm/extract_unified.c index fbb7e12c3..f0163d4ff 100644 --- a/internal/cbm/extract_unified.c +++ b/internal/cbm/extract_unified.c @@ -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}; diff --git a/internal/cbm/extract_unified.h b/internal/cbm/extract_unified.h index f80237229..8e40d6a2c 100644 --- a/internal/cbm/extract_unified.h +++ b/internal/cbm/extract_unified.h @@ -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 diff --git a/internal/cbm/helpers.c b/internal/cbm/helpers.c index f03c0c940..53b12d44c 100644 --- a/internal/cbm/helpers.c +++ b/internal/cbm/helpers.c @@ -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; diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 738e5b932..b41530cba 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -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 diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 302e76faa..0f71f38d3 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -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; diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 83b888ec6..42c465431 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -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 ".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, 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); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 7b868a7c1..8d4aaddc6 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -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);