fix(swift): resolve calls on a singleton cached into a local var (#1604)
`let manager = NetworkManager.shared` followed by `manager.fetchData()` on a later line produced zero call edges. Two gaps: (1) local let/var bindings inside method bodies were never typed (only class-level properties and function params populated the per-file type table), and (2) a static-member initializer (`Type.shared`, a navigation_expression) wasn't recognized as typing the local even in the class-property path — only constructor calls (`Type()`) were. Add _swift_local_var_types (mirrors _cpp_local_var_types): walk each function body and type a local from a constructor OR a `Type.staticProp` access whose head is upper-cased. `x.method()` then resolves to the receiver type through the existing single-definition god-node guard. The class-property path also learns the Type.shared shape. Reported on a 23k-file iOS corpus where this idiom is the median call pattern. Verified: the repro resolves (loadIfNeeded -> fetchData/isLoading); constructor locals still resolve; a lowercase-head init (`other.child`) does NOT falsely type; and an ambiguous method name resolves to the receiver-typed class only. Swift + full suite 2789 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b70a6d7126
commit
44c0a5e33c
@@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Fix: Swift singleton cached into a local var now resolves later calls (#1604, thanks @jerryliurui). `let x = NetworkManager.shared` followed by `x.fetchData()` on a subsequent line produced zero call edges — local `let`/`var` bindings inside method bodies weren't typed (only class-level properties and params were), and a static-member init (`Type.shared`, a navigation expression) wasn't recognized even where locals were typed. Method-body locals are now typed from both constructor (`Type()`) and static-member (`Type.shared`) initializers, so `x.method()` resolves to the receiver type via the existing single-definition guard. This singleton-into-local idiom is one of the most common Swift call patterns.
|
||||
- Fix: the skill's Python-interpreter detection now accepts Homebrew `python@3.x` paths (#1586, thanks @SUDARSHANCHAUDHARI). The shebang allowlist rejected any path with a character outside `[a-zA-Z0-9/_.-]`, but Homebrew installs versioned Python under `python@3.13`, so a valid interpreter containing `@` was skipped and detection fell through to a bare `python3` that lacked graphify (every step then failed with `ModuleNotFoundError`). `@` is now allowed across all skill variants (matching the #473 hooks.py fix); injection characters are still rejected.
|
||||
- Fix: `graphify merge-graphs` no longer crashes on inputs that disagree on graph type (#1606, thanks @AdrianRusan). Per-repo `graph.json` files don't always share the same `directed` / `multigraph` flags, and `compose` requires one uniform type, so a mixed set raised an unhandled `NetworkXError`. All inputs are now normalized to a plain undirected graph (which the cross-repo merged view already is) before composing.
|
||||
- Fix: type-reference / inheritance edge gaps closed across seven languages (all thanks @Synvoya):
|
||||
|
||||
@@ -2221,6 +2221,45 @@ def _cpp_local_var_types(body_node, source: bytes, table: dict[str, str]) -> Non
|
||||
stack.append(c)
|
||||
|
||||
|
||||
def _swift_local_var_types(body_node, source: bytes, table: dict[str, str]) -> None:
|
||||
"""Collect ``var -> Type`` from local ``let``/``var`` bindings in a Swift
|
||||
function body, so a member call on the local (``x.method()``) resolves to Type
|
||||
in the cross-file member-call pass (#1604).
|
||||
|
||||
Two initializer shapes are recorded, PRECISION over recall:
|
||||
- a constructor call ``let x = Type()`` (``_swift_constructor_type``);
|
||||
- a static-member access ``let x = Type.shared`` (a navigation_expression
|
||||
with an upper-cased head) — the singleton-cached-into-a-local idiom, one
|
||||
of the most common Swift call patterns and previously resolved to nothing.
|
||||
Nested function declarations are not descended into (their locals are scoped
|
||||
away); the first binding for a name wins, so a class property of the same name
|
||||
already in the table is not overwritten.
|
||||
"""
|
||||
stack = [body_node]
|
||||
while stack:
|
||||
n = stack.pop()
|
||||
if n.type == "function_declaration" and n is not body_node:
|
||||
continue
|
||||
if n.type == "property_declaration":
|
||||
prop_type: str | None = None
|
||||
for child in n.children:
|
||||
if child.type == "call_expression":
|
||||
prop_type = _swift_constructor_type(child, source)
|
||||
break
|
||||
if child.type == "navigation_expression":
|
||||
head = child.children[0] if child.children else None
|
||||
if head is not None and head.type == "simple_identifier":
|
||||
htext = _read_text(head, source)
|
||||
if htext and htext[:1].isupper():
|
||||
prop_type = htext
|
||||
break
|
||||
name = _swift_property_name(n, source)
|
||||
if name and prop_type and name not in table:
|
||||
table[name] = prop_type
|
||||
for c in n.children:
|
||||
stack.append(c)
|
||||
|
||||
|
||||
def _objc_local_var_types(body_node, source: bytes, table: dict[str, str]) -> None:
|
||||
"""Collect ``var -> ClassName`` from ObjC local declarations (``Foo *f = ...;``)
|
||||
in a method body, for receiver typing in the cross-file message-send pass
|
||||
@@ -3932,6 +3971,18 @@ def _extract_generic(
|
||||
ctor = _swift_constructor_type(child, source)
|
||||
if ctor is not None:
|
||||
prop_type = ctor
|
||||
# #1604 Stage 2b: `let x = Type.shared` (or any `Type.staticProp`)
|
||||
# binds x to Type via a static-member access, which is a
|
||||
# navigation_expression, not a constructor call. Infer x's type from
|
||||
# the uppercase head so later `x.method()` calls resolve to Type. This
|
||||
# is the singleton idiom (`Type.shared`) cached into a local var and
|
||||
# called on a subsequent line — extremely common in Swift.
|
||||
elif child.type == "navigation_expression" and prop_type is None:
|
||||
head = child.children[0] if child.children else None
|
||||
if head is not None and head.type == "simple_identifier":
|
||||
htext = _read_text(head, source)
|
||||
if htext and htext[:1].isupper():
|
||||
prop_type = htext
|
||||
prop_name = _swift_property_name(node, source)
|
||||
if prop_name and prop_type:
|
||||
type_table[prop_name] = prop_type
|
||||
@@ -5040,6 +5091,13 @@ def _extract_generic(
|
||||
for _caller_nid, body_node in function_bodies:
|
||||
_cpp_local_var_types(body_node, source, type_table)
|
||||
|
||||
# Swift: type local `let x = Type()` / `let x = Type.shared` bindings inside
|
||||
# method bodies so `x.method()` on a later line resolves — class-level
|
||||
# properties are typed in the walk, but method-body locals were not (#1604).
|
||||
if config.ts_module == "tree_sitter_swift":
|
||||
for _caller_nid, body_node in function_bodies:
|
||||
_swift_local_var_types(body_node, source, type_table)
|
||||
|
||||
for caller_nid, body_node in function_bodies:
|
||||
walk_calls(body_node, caller_nid)
|
||||
|
||||
|
||||
@@ -152,3 +152,28 @@ def test_swift_unknown_receiver_emits_no_edge(tmp_path: Path):
|
||||
|
||||
edges = _edge_labels(result, relations=("calls",))
|
||||
assert (".run()", "calls", ".help()") not in edges
|
||||
|
||||
|
||||
def test_deferred_singleton_local_var_resolves(tmp_path):
|
||||
"""#1604: `let x = Type.shared` cached into a local var, then `x.method()` on a
|
||||
later line, must resolve to Type's method. This static-member (navigation) init
|
||||
was previously untyped, so the singleton-into-local idiom produced zero edges.
|
||||
The constructor form `let x = Type()` is exercised alongside it."""
|
||||
base = tmp_path / "src"
|
||||
_write(base / "NetworkManager.swift",
|
||||
"class NetworkManager {\n static let shared = NetworkManager()\n"
|
||||
" func fetchData() { }\n func isLoading() -> Bool { return false }\n}\n")
|
||||
_write(base / "ViewController.swift",
|
||||
"class ViewControllerA {\n func loadIfNeeded() {\n"
|
||||
" let manager = NetworkManager.shared\n"
|
||||
" if manager.isLoading() { return }\n"
|
||||
" manager.fetchData()\n }\n"
|
||||
" func makeFresh() {\n let m = NetworkManager()\n m.fetchData()\n }\n}\n")
|
||||
result = extract(sorted(base.glob("*.swift")), cache_root=tmp_path / "cache", parallel=False)
|
||||
calls = {(s, t) for s, r, t in _edge_labels(result, ("calls",))}
|
||||
# deferred singleton local var -> both later member calls resolve (method
|
||||
# labels carry a leading dot, e.g. ".loadIfNeeded()")
|
||||
assert any("loadIfNeeded" in s and "fetchData" in t for s, t in calls)
|
||||
assert any("loadIfNeeded" in s and "isLoading" in t for s, t in calls)
|
||||
# constructor-into-local still resolves
|
||||
assert any("makeFresh" in s and "fetchData" in t for s, t in calls)
|
||||
|
||||
Reference in New Issue
Block a user