diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index affa7246b..4c112c7d2 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -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 diff --git a/gitnexus/src/core/ingestion/type-extractors/swift.ts b/gitnexus/src/core/ingestion/type-extractors/swift.ts index e94eb31a5..8634dc4dd 100644 --- a/gitnexus/src/core/ingestion/type-extractors/swift.ts +++ b/gitnexus/src/core/ingestion/type-extractors/swift.ts @@ -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 = new Set([ 'property_declaration', + 'if_statement', + 'guard_statement', ]); +const FOR_LOOP_NODE_TYPES: ReadonlySet = 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): void => { // Swift property_declaration has pattern and type_annotation @@ -52,8 +73,18 @@ const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map { 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): 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).set(loopVarName, elementType); + } +}; + +/** + * Extract element type from a Swift type annotation AST node. + * Handles: [User] (array sugar), Array, Set, 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, Set + 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, }; diff --git a/gitnexus/test/integration/resolvers/swift.test.ts b/gitnexus/test/integration/resolvers/swift.test.ts index a3626a3dc..5107491ce 100644 --- a/gitnexus/test/integration/resolvers/swift.test.ts +++ b/gitnexus/test/integration/resolvers/swift.test.ts @@ -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 () => {