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>
139 lines
5.8 KiB
TypeScript
139 lines
5.8 KiB
TypeScript
import type { Node as SyntaxNode } from 'web-tree-sitter';
|
|
import { getNodeText, getChildByField } from '../tree-sitter-helpers';
|
|
import type { LanguageExtractor } from '../tree-sitter-types';
|
|
|
|
/**
|
|
* A Swift function's declared return type, normalized to the bare class name a
|
|
* chained `Foo.make().draw()` could be called on (the #645/#608 mechanism).
|
|
* tree-sitter-swift labels BOTH the function name (`simple_identifier`) and the
|
|
* return type (a `user_type`) with the field `name`, so `childForFieldName`
|
|
* returns the name; the return type is found positionally — the first type node
|
|
* after the `simple_identifier` name, before the body. Optionals (`Foo?`) are
|
|
* unwrapped; arrays/tuples/function types and `Void` yield undefined.
|
|
*/
|
|
function extractSwiftReturnType(node: SyntaxNode, source: string): string | undefined {
|
|
let seenName = false;
|
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
const child = node.namedChild(i);
|
|
if (!child) continue;
|
|
if (child.type === 'simple_identifier' && !seenName) {
|
|
seenName = true;
|
|
continue;
|
|
}
|
|
if (!seenName) continue;
|
|
if (child.type === 'function_body') return undefined; // body reached: no return type
|
|
let typeNode: SyntaxNode | null = null;
|
|
if (child.type === 'user_type') typeNode = child;
|
|
else if (child.type === 'optional_type') {
|
|
typeNode = child.namedChildren.find((c: SyntaxNode) => c.type === 'user_type') ?? null;
|
|
}
|
|
if (typeNode) {
|
|
// Use the whole type node's text, strip generics, then take the LAST
|
|
// dotted segment — a member type `KF.Builder` resolves to `Builder` (its
|
|
// first type_identifier is the OUTER `KF`, which would be wrong).
|
|
const name = getNodeText(typeNode, source).trim().replace(/<[^>]*>/g, '');
|
|
const last = name.split('.').pop()?.trim();
|
|
if (!last || !/^[A-Za-z_]\w*$/.test(last) || last === 'Void') return undefined;
|
|
return last;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export const swiftExtractor: LanguageExtractor = {
|
|
functionTypes: ['function_declaration'],
|
|
classTypes: ['class_declaration'],
|
|
methodTypes: ['function_declaration'], // Methods are functions inside classes
|
|
interfaceTypes: ['protocol_declaration'],
|
|
structTypes: ['struct_declaration'],
|
|
enumTypes: ['enum_declaration'],
|
|
enumMemberTypes: ['enum_entry'],
|
|
typeAliasTypes: ['typealias_declaration'],
|
|
importTypes: ['import_declaration'],
|
|
callTypes: ['call_expression'],
|
|
variableTypes: ['property_declaration', 'constant_declaration'],
|
|
nameField: 'name',
|
|
bodyField: 'body',
|
|
paramsField: 'parameter',
|
|
returnField: 'return_type',
|
|
getReturnType: extractSwiftReturnType,
|
|
resolveName: (node, source) => {
|
|
// A nested-type extension `extension KF.Builder { … }` parses as a
|
|
// class_declaration whose `name` is a multi-segment `user_type` (`KF.Builder`
|
|
// = type_identifiers `KF`, `Builder`). Name the node by the LAST segment
|
|
// (`Builder`) so it shares the simple name of the extended type's own
|
|
// declaration (`struct Builder` → `KF::Builder`) instead of becoming a
|
|
// distinct `KF.Builder` node. Without this, the extension's conformances and
|
|
// members are invisible to a chained call on the type — supertype lookup and
|
|
// method matching both key off the simple name (#750). Simple names (regular
|
|
// class/struct/enum, or `extension Plain`) fall through to default extraction.
|
|
if (node.type !== 'class_declaration') return undefined;
|
|
const nameNode = getChildByField(node, 'name');
|
|
if (!nameNode || nameNode.type !== 'user_type') return undefined;
|
|
const ids = nameNode.namedChildren.filter((c: SyntaxNode) => c.type === 'type_identifier');
|
|
return ids.length > 1 ? getNodeText(ids[ids.length - 1]!, source) : undefined;
|
|
},
|
|
getSignature: (node, source) => {
|
|
// Swift function signature: func name(params) -> ReturnType
|
|
const params = getChildByField(node, 'parameter');
|
|
const returnType = getChildByField(node, 'return_type');
|
|
if (!params) return undefined;
|
|
let sig = getNodeText(params, source);
|
|
if (returnType) {
|
|
sig += ' -> ' + getNodeText(returnType, source);
|
|
}
|
|
return sig;
|
|
},
|
|
getVisibility: (node) => {
|
|
// Check for visibility modifiers in Swift
|
|
for (let i = 0; i < node.childCount; i++) {
|
|
const child = node.child(i);
|
|
if (child?.type === 'modifiers') {
|
|
const text = child.text;
|
|
if (text.includes('public')) return 'public';
|
|
if (text.includes('private')) return 'private';
|
|
if (text.includes('internal')) return 'internal';
|
|
if (text.includes('fileprivate')) return 'private';
|
|
}
|
|
}
|
|
return 'internal'; // Swift defaults to internal
|
|
},
|
|
isStatic: (node) => {
|
|
for (let i = 0; i < node.childCount; i++) {
|
|
const child = node.child(i);
|
|
if (child?.type === 'modifiers') {
|
|
if (child.text.includes('static') || child.text.includes('class')) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
classifyClassNode: (node) => {
|
|
// Swift uses class_declaration for classes, structs, and enums
|
|
for (let i = 0; i < node.childCount; i++) {
|
|
const child = node.child(i);
|
|
if (child?.type === 'struct') return 'struct';
|
|
if (child?.type === 'enum') return 'enum';
|
|
}
|
|
return 'class';
|
|
},
|
|
isAsync: (node) => {
|
|
for (let i = 0; i < node.childCount; i++) {
|
|
const child = node.child(i);
|
|
if (child?.type === 'modifiers' && child.text.includes('async')) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
extractImport: (node, source) => {
|
|
const importText = source.substring(node.startIndex, node.endIndex).trim();
|
|
const identifier = node.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
|
|
if (identifier) {
|
|
return { moduleName: source.substring(identifier.startIndex, identifier.endIndex), signature: importText };
|
|
}
|
|
return null;
|
|
},
|
|
};
|