Skip to content

fix(swift): bind self and super calls within the caller's class chain - #4030

Closed
Cintu07 wants to merge 3 commits into
Graphify-Labs:v8from
Cintu07:fix/self-calls-bind-own-class-swift
Closed

Cintu07 wants to merge 3 commits into
Graphify-Labs:v8from
Cintu07:fix/self-calls-bind-own-class-swift

Conversation

@Cintu07

@Cintu07 Cintu07 commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

what does this pr do?

stacked on #4011 (python) and #4012 (js), it reuses their _self_call_target. only the last commit is new, happy to rebase once those land.

self.save(), a bare save() (implicit self) and super.ping() in a swift method go through the file-wide label_to_nid, which keeps only the last definition of each name, so they land on whichever class the file declares last, as EXTRACTED.

on Alamofire at bda9ed5 (Source/, 44 files, 665 calls edges) 46 calls edges changed: 33 wrong ones moved to the right method and 13 wrong ones dropped. DataRequest.init, UploadRequest.reset, WebSocketRequest.cancel and friends calling super were recorded as calling themselves. URLEncodedFormEncoder's keyed container calls went to the unkeyed container, and DataRequest.serializingResponse went to DownloadTask.response.

swift inherits edges exist at bind time, so this uses the same in-file chain walk as python: own class first, then bases nearest first, super skips the class itself. a method of a class outside the chain is never the target. free functions, constructors and calls from an extension of the same type resolve exactly as before.

one gap that was already there: an extension of a nested type (extension URLEncodedFormEncoder.KeyedContainer) gets a different node id than the nested class, so those calls now get no in-file edge instead of the wrong one.

type of change

  • Bug fix
  • New feature
  • Documentation
  • Tests or CI
  • Refactor
  • Security fix

verification & invariants

a self, super or implicit-self call never binds to a method of a class outside the callers own class chain. calls that resolve to something that is not a method of another class keep their old target.

  • Read the CONTRIBUTING.md guide.
  • Reproduced the issue and identified the invariant.
  • Made the smallest fix necessary.
  • Added a regression test (if bug fix) or isolated boundary test.
  • Kept the PR description synchronized with the final implementation.
  • Documented any limitations / unsupported cases explicitly.

how was this tested?

windows 11 arm64, python 3.14, venv from uv sync --all-extras.

python -m pytest tests/test_extract.py -k swift_          2 passed, the chain test fails without this commit, the boundary one passes on both
python -m pytest <swift/objc + extract/languages/multilang/cross-repo test files>    867 passed, 2 failed (both fail on v8 too: windows MAX_PATH and cp1252 locale)
python -m ruff check .                                    all checks passed

graphify-specific checklist

  • I updated generated skill artifacts (uv run python -m tools.skillgen --bless) when changing their source fragments.
  • I confirmed that AST/structural extraction remains deterministic (no ambient state dependencies like ENV variables).
  • I reviewed changes for security implications (no unsafe interpolation into shell/Python).
  • I confirmed no API keys or local-only graph data are included.
  • (If applicable) I disclosed AI authorship in my commit messages.

self.m(), cls.m() and super().m() are exempt from the receiver-typed
defer because the receiver is the caller's own instance, but the lookup
they fell through to was the file-wide name map, which keeps only the
last definition of each name. Two classes in one file that both define
m() sent every self-call to whichever was declared last, as EXTRACTED.
On httpx that wires the sync Client's request/send chain into
AsyncClient, and every super().__init__() in _exceptions.py into
RequestNotRead.__init__.

Resolve these against the enclosing class, then its in-file bases
nearest first (super skips the class itself), the same order the ObjC
resolver uses for self. A tie between two bases on one level binds
nothing, and a method of a class outside the chain is never the target.
A module-level callable stored on the instance still binds as before.
this.save() in one JS/TS class was resolved through the file-wide name
map, which keeps only the last definition of each name, so a class that
calls this.save() was wired to the save() of whichever class the file
declared last, as EXTRACTED. In zod's v3 types.ts every ZodString,
ZodNumber and ZodBigInt check method landed on ZodDate._addCheck.

Route this.m() and super.m() through the self-call lookup the Python
fix added, renamed _self_call_target. It binds to the caller's own
class when that class defines m(). JS/TS extends edges are only emitted
by the later symbol pass, so the base chain is unknown at this point;
anything the own class does not define keeps the plain lookup, which
leaves inherited this.m() edges exactly as they were.
self.save(), a bare save() (implicit self) and super.ping() in a Swift
method went through the file-wide name map, which keeps only the last
definition of each name, so they landed on whichever class the file
declares last, as EXTRACTED. In Alamofire, DataRequest.init calling
super.init() was recorded as DataRequest.init calling itself, and
URLEncodedFormEncoder's keyed container calls went to the unkeyed one.

Route them through _self_call_target with the in-file inherits walk,
which Swift supports because its inherits edges exist at this point.
Free functions, constructors and calls from an extension of the same
type resolve exactly as before.
@Cintu07
Cintu07 requested a review from safishamsi as a code owner October 3, 2026 10:34
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown

Thanks for the pull request, @Cintu07. A maintainer will review it soon.

Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions.

A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.

Formal verification. PR-changed functions: 0/1 verified (0 proven, 0 may-equivalent, 0 distinguished) · 1 not verified (1 unsupported).

Not verified on this run: \_extract\_generic (unsupported).


Graphify review — findings

Fixes self.m() / cls.m() / super().m() in Python, this.m() / super.m() in JS/TS, and Swift's explicit and implicit self calls so they bind to the caller's own class instead of whichever class in the file declared m last. _self_call_target walks in-file bases nearest-first, skipping the caller's own class for super, and binds nothing on a multiple-inheritance tie or when m exists only on an unrelated class. Module-level callables stored on the instance still resolve through the file-wide lookup, and JS/TS stop after the caller's own class and keep the old lookup, because extends edges aren't known yet at this point.

Worth a look

  • Python super().method() is not routed through super resolution — graphify/extractors/engine.py:6838 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 1140 functions depend on the 700 functions this change touches.

Health — this change adds coupling hotspots:

  • new: _extract_generic() — 18 callers, 30 callees
  • new: extract_js() — 87 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: extract_objc() — 27 callers, 9 callees
  • new: extract_julia() — 19 callers, 7 callees
  • new: extract_cpp() — 31 callers, 3 callees
  • new: extract_vue() — 10 callers, 7 callees
  • new: walk() — 1 callers, 67 callees
  • …and 9 more — each is listed as a finding

Verification — 1140 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 1077 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

25 of 318 test file(s) selected (8%) via static blast radius.

  • tests/test_astro_extraction.py — impact
  • tests/test_build.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_dotnet.py — impact
  • tests/test_extract.py — impact, changed-test
  • tests/test_extract_php_closures.py — impact
  • tests/test_import_extension_resolution.py — impact
  • tests/test_indirect_call_block_scoped_shadow.py — impact
  • tests/test_indirect_dispatch.py — impact
  • tests/test_indirect_dispatch_assign_return.py — impact
  • tests/test_indirect_dispatch_getattr.py — impact
  • tests/test_js_exported_scalar_bindings.py — impact
  • tests/test_languages.py — impact
  • tests/test_multilang.py — impact
  • tests/test_python_underscore_resolution.py — impact
  • tests/test_rationale.py — impact
  • tests/test_ruby_resolution.py — impact
  • tests/test_scala_self_type.py — impact
  • tests/test_swift_computed_properties.py — impact
  • tests/test_swift_protocol_requirements.py — impact
  • tests/test_trailing_newline_not_a_syntax_error.py — impact
  • tests/test_ts_new_expression_calls.py — impact
  • tests/test_typescript_module_extensions.py — impact
  • tests/test_vue_extraction.py — impact

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

Could not verify: Could not verify \_extract\_generic.

The verifier did not have enough to check \_extract\_generic, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: parameter `config` is annotated `LanguageConfig` — outside the synthesizable primitive/collection set

· 17 more finding(s) on lines outside this diff (see the check run).

@safishamsi

Copy link
Copy Markdown
Member

Shipped in v0.9.75 (live on PyPI as graphifyy==0.9.75). Landed on v8 via an authorship-preserving cherry-pick, so your original commit authorship is kept. Thanks @Cintu07 for Swift self/super call binding 🙏

Closing as shipped.

@safishamsi safishamsi closed this Oct 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants