fix(swift): resolve chained static-factory/fluent calls + nested-extension naming (#750) (#755)

Completes Swift in the #750 chained-call series (after Java #751, Kotlin #752,
C# #753, conformance #754). Two parts:

1. Swift chained-call resolution (the #645/#608 mechanism): capture Swift return
   types (positional, member types -> last segment), encode capitalized-receiver
   chains `Foo.make().draw()` / `Foo(args).draw()`, resolve+validate via the
   shared matchDottedCallChain (+ constructor branch). Fixes the decoy wrong-edge
   bug where a chained method dropped to a bare name and attached to a same-named
   method on an unrelated class.

2. Nested-type extension naming fix: `extension KF.Builder: KFOptionSetter` parsed
   as a class_declaration named `KF.Builder` (dot) — inconsistent with the type's
   own declaration `KF::Builder` (name `Builder`) — so the extension's conformances
   and members were invisible to a chained call on the type. A Swift resolveName
   now names a nested-type extension by its last segment (`Builder`), so its
   `implements`/`extends` edges and methods are found by the supertype walk
   (conformance #754) and the simple-name method match.

Validated: synthetic decoy + args + constructor + absent-method tests; full suite
green; nested-extension repro (`KF.url().onSuccess()` resolves via conformance to
the protocol method). Real-repo A/B vs main (conformance) — Alamofire and
Kingfisher both **0 added / 0 removed, node count unchanged**: NEUTRAL and SAFE.
The prior -168 Kingfisher regression (from the naming inconsistency) is eliminated;
Swift's unique-named fluent methods already resolved by bare name, so the chain
path lands the same edges — the value here is decoy-collision correctness, the
nested-extension naming fix, and consistency with the other four languages.
EXTRACTION_VERSION 9 -> 10.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-09 01:54:12 -04:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 48d4654e8d
commit 7c7f0dd56f
7 changed files with 163 additions and 25 deletions
+21 -9
View File
@@ -595,6 +595,13 @@ export function matchPhpCallChain(
return resolveMethodOnType(resolvedClass, method, ref, context, 0.85, 'instance-method');
}
/**
* Languages where an unprefixed capitalized call `Foo(args)` constructs the
* class (so a `Foo(args).method()` receiver's type is `Foo`). Java/C# need `new`,
* so a bare `Foo()` there is a method call, not construction — excluded.
*/
const CONSTRUCTS_VIA_BARE_CALL = new Set(['kotlin', 'swift']);
/**
* Resolve a dotted chained call whose receiver is a static factory / fluent call —
* `Foo.getInstance().bar()`, encoded by the extractor as `Foo.getInstance().bar`
@@ -603,7 +610,7 @@ export function matchPhpCallChain(
* it (resolveMethodOnType requires `Type::method` to exist), so a wrong inference
* yields no edge rather than a wrong one (e.g. a same-named `bar()` on an
* unrelated class is never matched). Shared by the dot-notation languages
* (Java, Kotlin, C#) — same receiver shape, same `Class::method` qualified names.
* (Java, Kotlin, C#, Swift) — same receiver shape, same `Class::method` qualified names.
*/
export function matchDottedCallChain(
ref: UnresolvedRef,
@@ -617,13 +624,13 @@ export function matchDottedCallChain(
// Constructor receiver `Foo(args).method()` (encoded `Foo().method`): a bare,
// capitalized inner is a class construction, so the receiver's type is the
// class itself — resolve the method on it. Kotlin only: there an unprefixed
// capitalized call constructs the class, whereas in Java a bare `Foo()` is a
// method call (constructors need `new`), so we must not assume construction.
// A lowercase bare inner is a top-level `factory().method()` whose type we
// can't recover — bail.
// class itself — resolve the method on it. Only in languages where an
// unprefixed capitalized call constructs the class (Kotlin, Swift); in Java/C#
// a bare `Foo()` is a method call (constructors need `new`), so we must not
// assume construction. A lowercase bare inner is a top-level `factory().method()`
// whose type we can't recover — bail.
if (lastDot <= 0) {
if (ref.language !== 'kotlin' || !/^[A-Z]/.test(inner)) return null;
if (!CONSTRUCTS_VIA_BARE_CALL.has(ref.language) || !/^[A-Z]/.test(inner)) return null;
return resolveMethodOnType(inner, method, ref, context, 0.85, 'instance-method', importedFqnOf(inner, ref, context));
}
@@ -1081,11 +1088,16 @@ export function matchReference(
if (result) return result;
}
// 1d. Dotted chained static-factory / fluent call (Java / Kotlin / C#) —
// 1d. Dotted chained static-factory / fluent call (Java / Kotlin / C# / Swift) —
// `Foo.getInstance().bar()` encoded as `Foo.getInstance().bar` (#645/#608
// mechanism). Resolve bar's class from getInstance's declared return type, then
// validate the method on it.
if (ref.language === 'java' || ref.language === 'kotlin' || ref.language === 'csharp') {
if (
ref.language === 'java' ||
ref.language === 'kotlin' ||
ref.language === 'csharp' ||
ref.language === 'swift'
) {
result = matchDottedCallChain(ref, context);
if (result) return result;
}