Skip to content

fix(extraction): attribute Swift lifecycle calls - #2395

Open
Studnicky wants to merge 18 commits into
DeusData:mainfrom
Studnicky:fix/swift-init-extraction
Open

Studnicky wants to merge 18 commits into
DeusData:mainfrom
Studnicky:fix/swift-init-extraction

Conversation

@Studnicky

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes #2379. Swift init and deinit declarations are emitted as methods named init and deinit, so calls in their bodies retain the lifecycle method as their enclosing function instead of falling back to module scope.

The regression coverage exercises struct, class, convenience, failable enum, extension, ordinary-method, and deinitializer cases.

This contribution is written with Codex under the account holder's direction.

Validation

  • make -f Makefile.cbm test — 8,198 passed, 10 skipped
  • make -j3 -f Makefile.cbm lint-ci lint-tidy-diff — passed
  • scripts/security-audit.sh — passed
  • build/c/test-runner extraction — 386 passed

Checklist

  • Every commit is signed off (git commit -s)
  • Tests pass locally (make -f Makefile.cbm test)
  • Lint passes (make -f Makefile.cbm lint-ci)
  • New behavior is covered by a test

Signed-off-by: Andrew Studnicky <a.j.studnicky@gmail.com>
@Studnicky
Studnicky requested a review from DeusData as a code owner September 28, 2026 02:34
@github-actions

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

@DeusData DeusData left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, @Studnicky, both for the precise report in #2379 (the minimal repro and the query_graph output made the gap obvious) and for turning it into a fix with eight focused tests. They cover struct, class, convenience, failable, extension and deinit, plus a guard that ordinary methods keep their attribution. Welcome back; #1470 and #1471 are still doing their job.

The extraction side is right: init/deinit become Type.init/Type.deinit Methods, and calls in their bodies now come from them. Because those nodes are also new call targets, we need to see the edge-level result before merging:

  1. super.init(...). Callees like super.init and self.init have a lower-case receiver, so receiver_chain_admits (src/pipeline/registry.c:1179) lets them through, and resolve_name_lookup then picks the nearest node whose short name is init. Before this PR there was no Swift init to bind to. Now a UIViewController subclass's super.init(nibName:bundle:) could resolve to its own init (a self-loop) or to a sibling type's. We keep the graph to true edges only, so could you add a pipeline-level test that asserts the exact CALLS set? Use the #2379 repro plus class Sub: Base { init() { super.init() } }. If super.init mis-binds, please handle it, for example by resolving it through the class's base, or dropping it when the base isn't in the project.
  2. A before/after on a real Swift project (Alamofire, for example): CALLS counts plus a sample of the new *.init edges. That's the evidence we use to judge graph-quality changes.
  3. Gate the naming on Swift in one place. func_name_node (extract_defs.c:212) has no language parameter, and the Slang grammar also has an init_declaration node. A lang == CBM_LANG_SWIFT branch in cbm_resolve_func_name / resolve_method_name, like the Kotlin secondary_constructor branch in extract_class_methods, keeps this explicit.

Nits:

  • swift_init_multiple_not_collapsed_issue2379 asserts that the inits do share one node (correctly, per #2061), so something like ..._overloads_share_one_node_... would avoid confusion.
  • find_def_in_module has two stacked header comments (test_extraction.c:72-75), and the first describes a different helper.

Thanks again!

@DeusData DeusData added bug Something isn't working parsing/quality Graph extraction bugs, false positives, missing edges priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker. labels Oct 19, 2026
@DeusData

Copy link
Copy Markdown
Owner

Thanks again for the Swift lifecycle attribution work. Following up on the review: the exact super.init CALLS test, real-project edge comparison, and Swift-only naming gate remain the requested next steps. Please let us know if any point needs clarification.

DeusData and others added 2 commits October 20, 2026 19:30
Brings the branch up to date with main (a222715) so the Swift
initializer fix is reviewed and tested against the current tree.
No conflicts.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Follow-up to the review of DeusData#2395 (issue DeusData#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 DeusData#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>
@DeusData

Copy link
Copy Markdown
Owner

Thank you, @Studnicky, for this fix and for the careful report in #2379. Your extraction change is exactly right: init and deinit now become Type.init / Type.deinit Methods, and the calls in their bodies come from them instead of collapsing onto the file. Your eight tests cover every initializer form and made the follow-up easy to check.

Since you allowed maintainer edits, we took care of the points from the September review ourselves, so the PR does not have to wait for another round trip. We merged current main into the branch (no conflicts) and added one commit on top. Your commit is unchanged. Here is what our commit does, and why:

  1. super.init(...) now targets the superclass. Once initializers became nodes, super.init and self.init were resolved by their short name init and landed on whichever init was nearest. On Alamofire all 8 new edges into an initializer were wrong (for example DownloadRequest.init -> DownloadRequest.Options.init). A super.init call is now emitted as <Superclass>.init, taken from the first entry of the enclosing class's inheritance clause, so it reaches the project's superclass initializer (DownloadRequest.init -> Request.init). When the superclass is not part of the project (UIViewController), no edge is created.
  2. self.init(...) creates no edge. Initializer overloads share one node (Swift: overloaded methods collapse into one node, producing fake callers in trace_path #2061), so the only edge it could produce is a self-loop. That self-loop is also wrong whenever the overload being called lives outside the project, for example an extension Notification initializer that calls Foundation's.
  3. Explicit T.init(...) calls only bind to a project type T. Nearly every type has an init, so guessing by the short name is never safe here. These calls are now marked the way Python, TypeScript and Go member calls already are, and Swift joins that guard in both resolvers. Box.init(raw:) still reaches Box.init, but UIAlertAction.init(...) in a file that imports a project module no longer binds to an unrelated project initializer.
  4. References to init / deinit are not resolved as usages by name. They are Swift keywords, and a reference like .init(...) or Foo.init does not say which type's initializer it means. On Alamofire, 40 such USAGE edges appeared with this branch, 26 of them on one nested Output.init.
  5. The Swift naming is gated in one place, as the review asked: cbm_resolve_func_name has an explicit CBM_LANG_SWIFT branch, resolve_method_name delegates to it, and the language-agnostic func_name_node no longer special-cases the node kinds (the Slang grammar has an init_declaration too).
  6. Tests. A pipeline-level test asserts the exact CALLS set for your Swift: initializers are not extracted — calls inside init are attributed to the file #2379 reproduction plus initializer delegation in a small SwiftPM package, through both the sequential and the parallel resolver. New extraction tests cover the superclass rewrite, a class without a superclass, the call marking, and init references. swift_init_convenience_delegates_issue2379 now asserts that self.init emits no call.
  7. Review nits: swift_init_multiple_not_collapsed_issue2379 is renamed to swift_init_overloads_share_one_node_issue2379, and the stacked header comment above find_def_in_module is cleaned up.

Before and after on Alamofire (main / your branch / with our commit):

  • CALLS edges: 5472 / 5487 / 5485
  • CALLS into an initializer: 0 / 8 (all wrong) / 6 (all superclass initializers: DataRequest, DownloadRequest and WebSocketRequest to Request.init; UploadRequest and a test request to DataRequest.init; ConnectionLostRetryPolicy to RetryPolicy.init)
  • USAGE into an initializer: 0 / 40 / 0
  • Calls attributed to a file instead of a function: 244 / 205 / 204

On Kingfisher, CALLS into an initializer go from 15 on your branch (14 of them wrong) to 2, both correct.

CI will run on the updated branch, and we will follow up here. Thanks again for a really useful contribution!

@DeusData

Copy link
Copy Markdown
Owner

Thank you again for the Swift lifecycle fix. The October 20 update records the naming gate and initializer-resolution follow-ups as completed with the additional regressions. The remaining follow-up is ours; sorry for the delay, and I’ll follow up soon.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you again, @Studnicky, for this fix and for the clear reproduction in #2379. Turning Swift init and deinit into real Method nodes is exactly what lets every call inside an initializer land on the right node in the graph, and your tests made each initializer form easy to check.

A short status note: the CI run after our October 20 update stopped early, before any test suite ran, on a check of the published language counts that was broken on main at the time. That check has since been repaired on main, so we merged current main into the branch again (one signed-off merge commit, b5b16e0; no conflicts, your commit is unchanged). We will merge as soon as CI is green. If you plan to push anything else here, please pull first. Thanks again for a really useful contribution!

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thanks again, @Studnicky. Several other pull requests landed on main this evening, including changes in files this one also touches, so we merged current main into your branch once more (one signed-off merge commit, f2c9080; your changes are untouched). That keeps the required checks honest against today's tree; we will merge once they are green. Please pull before pushing again.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you for your patience, @Studnicky. CI went green on this branch, but tonight's merges on main touched tests/test_extraction.c and tests/test_pipeline.c, which this pull request also changes, so that green no longer covered today's main. We merged current main into the branch once more (one merge commit, b11aef0; your changes are untouched) and will merge once the fresh checks are green. Please pull before pushing again.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you for your patience, @Studnicky. CI went green on this branch, but this evening's merges on main touched files this pull request also changes, so that green no longer covered today's main. We merged current main into the branch once more (one merge commit, 3fbe17f; your changes are untouched) and will merge once the fresh checks are green. Please pull before pushing again.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you again, @Studnicky, for your patience with this one. Two pull requests merged into main a few minutes ago (#2754 and #2090), and they touch internal/cbm/lang_specs.c and src/pipeline/pass_parallel.c, which this pull request also changes. So we merged current main into your branch once more (one merge commit, ebe2e6f; it merged cleanly and your own change is untouched), checked that the merged files still compile, and started a fresh CI run. We will merge as soon as the required checks are green. Please pull before pushing again so the merge commit is kept.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you for your patience, @Studnicky. A pull request that merged into main a few minutes ago (#2769) changes internal/cbm/extract_defs.c, which this pull request also changes, so the CI run in progress here could no longer vouch for today's main. We merged current main into your branch once more (one merge commit, 5efa564; it merged cleanly and your own change is untouched) and started a fresh run. We will merge once the required checks are green. Please pull before pushing again.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you for your patience, @Studnicky. A pull request that merged into main a few minutes ago (#1710) changes internal/cbm/extract_calls.c, which this pull request also changes, so the CI run in progress here could no longer vouch for today's main. We merged current main into your branch once more (one merge commit, 97628c0; it merged cleanly and your own change is untouched) and started a fresh run. We will merge once the required checks are green. Please pull before pushing again.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you for your patience, @Studnicky. Main has moved since the last CI run on this branch, and some of the new commits change internal/cbm/extract_defs.c and src/pipeline/pass_calls.c, which this pull request also changes, so that run can no longer vouch for today's main. Separately, the Windows leg of that run failed with an intermittent crash at the start of a few test suites that we are tracking on our side; it is not caused by your change. We merged current main into your branch (one merge commit, ca1f750; it merged cleanly and your own change is untouched) and started a fresh run. We will merge once the required checks are green. Please pull before pushing again.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you for your patience, @Studnicky. The macOS legs of the last CI run here were cancelled by GitHub itself: it is retiring its macOS 14 runners on November 2 and ran scheduled brownouts beforehand, so those jobs never got a runner. Nothing in your change caused it. Separately, the Windows leg hit an intermittent crash at the start of a few test suites that we are tracking on our side; that is not your change either. Main now runs CI on macOS 15 (#2849), so we merged current main into your branch (one merge commit, f6100d9; it merged cleanly and your own change is untouched) to get a run on the new runners. Please pull before pushing again.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you for your patience, @Studnicky. Your change was green, but several pull requests merged yesterday touch src/pipeline/pass_calls.c, src/pipeline/pass_parallel.c and tests/test_pipeline.c, which this pull request also changes, so that run could no longer vouch for today's main. We merged current main into your branch (no conflicts, your own change is unchanged) to get a fresh CI run on the combined tree. Please pull before pushing again. We will merge as soon as the required checks are green.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

DeusData commented Nov 2, 2026

Copy link
Copy Markdown
Owner

Thank you for your patience, @Studnicky. A quick status note: the last CI run failed on the Linux GCC x86 leg because that job hung in apt-get update on the runner before it ever built anything. That was a GitHub runner/mirror problem, not your change (we have since made those steps fail fast instead of hanging). Main had also moved on in the meantime, including a change to extract_defs.c, so we merged current main into your branch (b2edaef, signed off, no conflicts; your own change is byte-for-byte the same). CI is running again on that head.

If you push to this branch again, please pull first so the merge commit is kept. Thanks again for the Swift initializer work; it is next in line once this run is green.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working parsing/quality Graph extraction bugs, false positives, missing edges priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Swift: initializers are not extracted — calls inside init are attributed to the file

2 participants