feat: 7 Swift features — if/guard let, await/try, for-in, enum cases, self/super, optional chaining, multi-inheritance

Covers all high and medium impact gaps from the Swift feature coverage
analysis:

1. if let / guard let bindings: add if_statement and guard_statement
   to DECLARATION_NODE_TYPES, extract varName and value for Tier 2
   return-type propagation (callResult, copy, fieldAccess, methodCallResult)

2. await / try expression unwrapping: add unwrapSwiftExpression() that
   strips await_expression and try_expression wrappers before checking
   for call_expression. Applied in extractPendingAssignment,
   extractInitializer, and scanConstructorBinding.

3. for item in collection: add extractForLoopBinding for Swift with
   extractSwiftElementTypeFromTypeNode that handles [User] array sugar
   and Array<User> generic types. Registered in typeConfig.

4. Multiple inheritance specifiers: already working — tree-sitter
   queries match all inheritance_specifier occurrences automatically.
   Verified, no code changes needed.

5. Enum case extraction: add (enum_entry (simple_identifier) @name)
   @definition.property query to SWIFT_QUERIES.

6. self/super resolution: unskipped both describe.skip test suites
   (tree-sitter-swift 0.7.1 ships prebuilds, Node 22 build issue
   resolved). Both pass — 5 previously-skipped tests now running.

7. Optional chaining obj?.method(): already working — tree-sitter-swift
   parses the ? transparently. Verified, no code changes needed.

Tests: 3,603 → 3,608 (5 unskipped self/super tests)
Swift tests: 23 → 28 passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
marxo126 2026-03-22 09:53:59 +01:00
parent 99f0aaaea5
commit 16b1a63134
3 changed files with 254 additions and 6 deletions

View file

@ -889,6 +889,9 @@ export const SWIFT_QUERIES = `
; Properties (stored and computed)
(property_declaration (pattern (simple_identifier) @name)) @definition.property
; Enum cases
(enum_entry (simple_identifier) @name) @definition.property
; Imports
(import_declaration (identifier (simple_identifier) @import.source)) @import

View file

@ -1,12 +1,33 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor, PendingAssignment } from './types.js';
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation } from './shared.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor, PendingAssignment, ForLoopExtractor } from './types.js';
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, extractElementTypeFromString, resolveIterableElementType } from './shared.js';
import { findChild } from '../resolvers/utils.js';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
'property_declaration',
'if_statement',
'guard_statement',
]);
const FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set([
'for_statement',
]);
/**
* Unwrap Swift `await_expression` and `try_expression` nodes to find the inner
* call_expression or other value node. `try` nodes contain a `try_operator` child
* that must be skipped.
*/
function unwrapSwiftExpression(node: SyntaxNode): SyntaxNode {
if (node.type === 'await_expression' || node.type === 'try_expression') {
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child && child.type !== 'try_operator') return unwrapSwiftExpression(child);
}
}
return node;
}
/** Swift: let x: Foo = ... */
const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
// Swift property_declaration has pattern and type_annotation
@ -52,8 +73,18 @@ const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<str
if (!pattern) return;
const varName = extractVarName(pattern) ?? pattern.text;
if (!varName || env.has(varName)) return;
// Find call_expression in the value
const callExpr = findChild(node, 'call_expression');
// Find call_expression in the value (unwrap await/try)
let callExpr = findChild(node, 'call_expression');
if (!callExpr) {
// Check for await_expression or try_expression wrapping a call_expression
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child && (child.type === 'await_expression' || child.type === 'try_expression')) {
const unwrapped = unwrapSwiftExpression(child);
if (unwrapped.type === 'call_expression') { callExpr = unwrapped; break; }
}
}
}
if (!callExpr) return;
const callee = callExpr.firstNamedChild;
if (!callee) return;
@ -90,6 +121,11 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child?.type === 'call_expression') { callExpr = child; break; }
// Unwrap await/try to find inner call_expression
if (child && (child.type === 'await_expression' || child.type === 'try_expression')) {
const unwrapped = unwrapSwiftExpression(child);
if (unwrapped.type === 'call_expression') { callExpr = unwrapped; break; }
}
}
if (!callExpr) return undefined;
const callee = callExpr.firstNamedChild;
@ -119,6 +155,82 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
return undefined;
};
/**
* Extract the variable name from an if_statement or guard_statement with optional binding.
* Pattern: `if let varName = expr` / `guard let varName = expr`
* AST: if_statement/guard_statement contains value_binding_pattern, then simple_identifier (varName),
* then call_expression/simple_identifier/navigation_expression (value).
*/
function extractIfGuardBinding(node: SyntaxNode, scopeEnv: ReadonlyMap<string, string>): PendingAssignment | undefined {
// Find value_binding_pattern to confirm this is an optional binding
let hasValueBinding = false;
let varName: string | undefined;
let valueNode: SyntaxNode | null = null;
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (!child) continue;
if (child.type === 'value_binding_pattern') {
hasValueBinding = true;
continue;
}
if (hasValueBinding && !varName && child.type === 'simple_identifier') {
varName = child.text;
continue;
}
if (varName && !valueNode) {
// Skip type annotations and binding operators
if (child.type === 'type_annotation') continue;
valueNode = child;
break;
}
}
if (!hasValueBinding || !varName || !valueNode || scopeEnv.has(varName)) return undefined;
// Unwrap await/try
valueNode = unwrapSwiftExpression(valueNode);
// simple_identifier → copy
if (valueNode.type === 'simple_identifier') {
return { kind: 'copy', lhs: varName, rhs: valueNode.text };
}
// navigation_expression → fieldAccess
if (valueNode.type === 'navigation_expression') {
const receiver = valueNode.firstNamedChild;
const suffix = valueNode.lastNamedChild;
if (receiver?.type === 'simple_identifier' && suffix?.type === 'navigation_suffix') {
const field = suffix.lastNamedChild;
if (field?.type === 'simple_identifier') {
return { kind: 'fieldAccess', lhs: varName, receiver: receiver.text, field: field.text };
}
}
return undefined;
}
// call_expression → callResult or methodCallResult
if (valueNode.type === 'call_expression') {
const callee = valueNode.firstNamedChild;
if (!callee) return undefined;
if (callee.type === 'simple_identifier') {
return { kind: 'callResult', lhs: varName, callee: callee.text };
}
if (callee.type === 'navigation_expression') {
const receiver = callee.firstNamedChild;
const suffix = callee.lastNamedChild;
if (receiver?.type === 'simple_identifier' && suffix?.type === 'navigation_suffix') {
const method = suffix.lastNamedChild;
if (method?.type === 'simple_identifier') {
return { kind: 'methodCallResult', lhs: varName, receiver: receiver.text, method: method.text };
}
}
}
}
return undefined;
}
/**
* Swift: extract pending assignments for Tier 2 return-type propagation.
* Handles:
@ -126,8 +238,17 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
* let result = user.save() → methodCallResult
* let name = user.name → fieldAccess
* let copy = user → copy
* let user = await getUser() → callResult (unwrapped)
* let user = try getUser() → callResult (unwrapped)
* if let user = getUser() → callResult (optional binding)
* guard let user = getUser() → callResult (optional binding)
*/
const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
// Handle if_statement and guard_statement optional bindings
if (node.type === 'if_statement' || node.type === 'guard_statement') {
return extractIfGuardBinding(node, scopeEnv);
}
if (node.type !== 'property_declaration') return undefined;
// Skip if type annotation exists — extractDeclaration handles it
if (hasTypeAnnotation(node)) return undefined;
@ -154,6 +275,9 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
}
if (!valueNode) return undefined;
// Unwrap await/try expressions (Feature 2)
valueNode = unwrapSwiftExpression(valueNode);
// let copy = user → copy
if (valueNode.type === 'simple_identifier') {
return { kind: 'copy', lhs, rhs: valueNode.text };
@ -198,11 +322,132 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
return undefined;
};
/**
* Swift: extract loop variable type binding from `for item in collection`.
* AST: for_statement with pattern > simple_identifier (loop var) and
* a simple_identifier/call_expression (collection).
*/
const extractForLoopBinding: ForLoopExtractor = (node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup }): void => {
if (node.type !== 'for_statement') return;
// Find the loop variable from the pattern child
let loopVarName: string | undefined;
let iterableNode: SyntaxNode | null = null;
// for_statement children: pattern (loop var), then the iterable expression, then the body
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (!child) continue;
if (child.type === 'pattern' || child.type === 'simple_identifier') {
if (!loopVarName) {
// The loop variable - may be inside a pattern node or a direct simple_identifier
loopVarName = child.type === 'pattern' ? child.text : child.text;
continue;
}
}
// After we found the loop var, the next expression-like node is the iterable
if (loopVarName && !iterableNode) {
if (child.type === 'simple_identifier' || child.type === 'call_expression' ||
child.type === 'navigation_expression') {
iterableNode = child;
break;
}
}
}
if (!loopVarName || !iterableNode) return;
let iterableName: string | undefined;
let callExprElementType: string | undefined;
if (iterableNode.type === 'simple_identifier') {
iterableName = iterableNode.text;
} else if (iterableNode.type === 'navigation_expression') {
// collection.property
const suffix = iterableNode.lastNamedChild;
if (suffix?.type === 'navigation_suffix') {
const prop = suffix.lastNamedChild;
if (prop?.type === 'simple_identifier') iterableName = prop.text;
} else if (suffix?.type === 'simple_identifier') {
iterableName = suffix.text;
}
} else if (iterableNode.type === 'call_expression') {
// getItems() or collection.values()
const fn = iterableNode.firstNamedChild;
let callee: string | undefined;
if (fn?.type === 'simple_identifier') {
callee = fn.text;
} else if (fn?.type === 'navigation_expression') {
const obj = fn.firstNamedChild;
const suffix = fn.lastNamedChild;
if (obj?.type === 'simple_identifier') iterableName = obj.text;
if (suffix?.type === 'navigation_suffix') {
const m = suffix.lastNamedChild;
if (m?.type === 'simple_identifier') callee = m.text;
} else if (suffix?.type === 'simple_identifier') {
callee = suffix.text;
}
}
if (callee) {
const rawReturn = returnTypeLookup.lookupRawReturnType(callee);
if (rawReturn) callExprElementType = extractElementTypeFromString(rawReturn);
}
}
if (!iterableName && !callExprElementType) return;
let elementType: string | undefined;
if (callExprElementType) {
elementType = callExprElementType;
} else if (iterableName) {
// Try to resolve element type from the iterable's declared type
elementType = resolveIterableElementType(
iterableName, node, scopeEnv, declarationTypeNodes, scope,
extractSwiftElementTypeFromTypeNode,
);
}
if (elementType && !scopeEnv.has(loopVarName)) {
(scopeEnv as Map<string, string>).set(loopVarName, elementType);
}
};
/**
* Extract element type from a Swift type annotation AST node.
* Handles: [User] (array sugar), Array<User>, Set<User>, etc.
*/
function extractSwiftElementTypeFromTypeNode(typeNode: SyntaxNode): string | undefined {
// Swift array sugar: [User] — parsed as array_type > user_type > type_identifier
if (typeNode.type === 'array_type') {
const inner = typeNode.firstNamedChild;
if (inner) return extractSimpleTypeName(inner);
}
// Generic type: Array<User>, Set<User>
if (typeNode.type === 'user_type') {
// Check for generic args: user_type > type_identifier + type_arguments
for (let i = 0; i < typeNode.namedChildCount; i++) {
const child = typeNode.namedChild(i);
if (child?.type === 'type_arguments') {
const lastArg = child.lastNamedChild;
if (lastArg) return extractSimpleTypeName(lastArg);
}
}
}
// type_annotation wrapping
if (typeNode.type === 'type_annotation') {
const inner = typeNode.firstNamedChild;
if (inner) return extractSwiftElementTypeFromTypeNode(inner);
}
return undefined;
}
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
extractDeclaration,
extractParameter,
extractInitializer,
scanConstructorBinding,
extractPendingAssignment,
extractForLoopBinding,
};

View file

@ -61,7 +61,7 @@ describe.skipIf(!swiftAvailable)('Swift constructor-inferred type resolution', (
// The self/super resolution code already exists in type-env.ts lookupInEnv (lines 56-66).
// ---------------------------------------------------------------------------
describe.skip('Swift self resolution', () => {
describe.skipIf(!swiftAvailable)('Swift self resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
@ -91,7 +91,7 @@ describe.skip('Swift self resolution', () => {
// findEnclosingParentClassName in type-env.ts already has Swift inheritance_specifier handler.
// ---------------------------------------------------------------------------
describe.skip('Swift parent resolution', () => {
describe.skipIf(!swiftAvailable)('Swift parent resolution', () => {
let result: PipelineResult;
beforeAll(async () => {