Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f200d74
fix(extraction): attribute Swift lifecycle calls
Studnicky Sep 27, 2026
468102c
Merge branch 'main' into fix/swift-init-extraction
DeusData Oct 20, 2026
71ab015
fix(extraction): keep Swift initializer edges to true targets
DeusData Oct 20, 2026
cd87073
Merge branch 'main' into fix/swift-init-extraction
DeusData Oct 20, 2026
b5b16e0
Merge origin/main into fix/swift-init-extraction
DeusData Oct 26, 2026
f2c9080
Merge origin/main into fix/swift-init-extraction
DeusData Oct 27, 2026
b11aef0
Merge origin/main into fix/swift-init-extraction
DeusData Oct 28, 2026
3fbe17f
Merge origin/main into fix/swift-init-extraction
DeusData Oct 28, 2026
ebe2e6f
Merge origin/main into fix/swift-init-extraction
DeusData Oct 29, 2026
5efa564
Merge origin/main into fix/swift-init-extraction
DeusData Oct 29, 2026
97628c0
Merge origin/main into fix/swift-init-extraction
DeusData Oct 29, 2026
ca1f750
Merge origin/main into fix/swift-init-extraction
DeusData Oct 30, 2026
f6100d9
Merge origin/main into fix/swift-init-extraction
DeusData Oct 31, 2026
2e8865c
Merge origin/main into fix/swift-init-extraction
DeusData Oct 31, 2026
176991c
Merge origin/main into fix/swift-init-extraction
DeusData Oct 31, 2026
7b39877
Merge origin/main into fix/swift-init-extraction
DeusData Nov 1, 2026
b2edaef
Merge origin/main into fix/swift-init-extraction
DeusData Nov 2, 2026
6d5cb97
Merge origin/main into fix/swift-init-extraction
DeusData Nov 2, 2026
dedd429
Merge origin/main into fix/swift-init-extraction
DeusData Nov 3, 2026
a4869bf
Merge origin/main into fix/swift-init-extraction
DeusData Nov 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions internal/cbm/extract_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -4358,6 +4358,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
Expand All @@ -4368,6 +4432,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;
}
Expand Down Expand Up @@ -4591,6 +4656,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
Expand Down
15 changes: 15 additions & 0 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,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) {
Expand Down Expand Up @@ -9205,6 +9214,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");
Expand Down
8 changes: 8 additions & 0 deletions internal/cbm/extract_unified.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -2823,6 +2830,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};
Expand Down
2 changes: 2 additions & 0 deletions internal/cbm/extract_unified.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions internal/cbm/helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1187,6 +1197,16 @@ bool cbm_is_c_preprocessor_lang(CBMLanguage lang) {
// const", and a member lookup for "operator bool" (the implicit call in
// `if (obj)`) misses.
char *cbm_func_name_node_text(CBMArena *a, TSNode name_node, const char *source, CBMLanguage lang) {
if (lang == CBM_LANG_SWIFT) {
const char *kind = ts_node_type(name_node);
if (strcmp(kind, "init_declaration") == 0) {
return cbm_arena_strdup(a, "init");
}
if (strcmp(kind, "deinit_declaration") == 0) {
return cbm_arena_strdup(a, "deinit");
}
}

char *text = cbm_node_text(a, name_node, source);
/* A C keyword is never a function name: the definition is an error-recovery
* artifact. No name means no def and no call scope (calls inside fall back
Expand Down
3 changes: 2 additions & 1 deletion internal/cbm/lang_specs.c
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,8 @@ static const char *objc_assign_types[] = {"assignment_expression", NULL};
// well as in the name resolvers — extract_class_methods gates on this set, so
// without the entry the requirement is walked and then discarded.
static const char *swift_func_types[] = {"function_declaration", "protocol_function_declaration",
"macro_declaration", NULL};
"macro_declaration", "init_declaration",
"deinit_declaration", NULL};
// KNOWN GAP: struct_declaration and enum_declaration are not node types in the
// vendored tree-sitter-swift grammar — it models both as class_declaration — so
// these two entries never match anything, and a bare Swift `enum` is labeled
Expand Down
6 changes: 5 additions & 1 deletion src/pipeline/pass_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -1274,7 +1274,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
Expand Down
4 changes: 3 additions & 1 deletion src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -3510,7 +3510,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;
Expand Down
Loading
Loading