Merge pull request #408 from marxo126/fix/swift-query-and-patch-script

feat: complete Swift support — query fix, export detection, implicit imports, constructor resolution
This commit is contained in:
Gergő Magyar 2026-03-23 13:53:25 +00:00 committed by GitHub
commit db6b302fee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 994 additions and 42 deletions

View file

@ -26,7 +26,7 @@
"mnemonist": "^0.39.0",
"onnxruntime-node": "^1.24.0",
"pandemonium": "^2.4.0",
"tree-sitter": "^0.21.0",
"tree-sitter": "0.22.4",
"tree-sitter-c": "^0.21.0",
"tree-sitter-c-sharp": "^0.21.0",
"tree-sitter-cpp": "^0.22.0",
@ -60,7 +60,7 @@
},
"optionalDependencies": {
"tree-sitter-kotlin": "^0.3.8",
"tree-sitter-swift": "^0.6.0"
"tree-sitter-swift": "0.7.1"
}
},
"node_modules/@babel/helper-string-parser": {
@ -3130,6 +3130,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@ -4184,6 +4185,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -4958,14 +4960,15 @@
}
},
"node_modules/tree-sitter": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz",
"integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==",
"version": "0.22.4",
"resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.22.4.tgz",
"integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"node-addon-api": "^8.0.0",
"node-gyp-build": "^4.8.0"
"node-addon-api": "^8.3.0",
"node-gyp-build": "^4.8.4"
}
},
"node_modules/tree-sitter-c": {
@ -5284,9 +5287,9 @@
"license": "MIT"
},
"node_modules/tree-sitter-swift": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tree-sitter-swift/-/tree-sitter-swift-0.6.0.tgz",
"integrity": "sha512-9vOJZes4/UFjBr4COHtp6ZHVuZYwfChSQbpneXQog04dAstfx5px3ybVX2cN+ylvLqsvVpmXLpidxxgF2rDQ7A==",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/tree-sitter-swift/-/tree-sitter-swift-0.7.1.tgz",
"integrity": "sha512-pneKVTuGamaBsqqqfB9BvNQjktzh/0IVPR54jLB5Fq/JTDQwYHd0Wo6pVyZ5jAYpbztzq+rJ/rpL9ruxTmSoKw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@ -5297,7 +5300,7 @@
"which": "2.0.2"
},
"peerDependencies": {
"tree-sitter": "^0.21.1"
"tree-sitter": "^0.22.1"
},
"peerDependenciesMeta": {
"tree_sitter": {
@ -5365,6 +5368,7 @@
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
@ -5485,6 +5489,7 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@ -5560,6 +5565,7 @@
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/expect": "4.0.18",
"@vitest/mocker": "4.0.18",
@ -5843,6 +5849,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View file

@ -66,7 +66,7 @@
"mnemonist": "^0.39.0",
"onnxruntime-node": "^1.24.0",
"pandemonium": "^2.4.0",
"tree-sitter": "^0.21.0",
"tree-sitter": "0.22.4",
"tree-sitter-c": "^0.21.0",
"tree-sitter-c-sharp": "^0.21.0",
"tree-sitter-cpp": "^0.22.0",
@ -82,7 +82,7 @@
},
"optionalDependencies": {
"tree-sitter-kotlin": "^0.3.8",
"tree-sitter-swift": "^0.6.0"
"tree-sitter-swift": "0.7.1"
},
"devDependencies": {
"@types/cli-progress": "^3.11.6",
@ -99,7 +99,8 @@
"overrides": {
"@huggingface/transformers": {
"onnxruntime-node": "$onnxruntime-node"
}
},
"tree-sitter": "0.22.4"
},
"engines": {
"node": ">=18.0.0"

View file

@ -41,8 +41,10 @@ try {
let needsRebuild = false;
if (content.includes('"actions"')) {
// Strip Python-style comments (#) before JSON parsing
const cleaned = content.replace(/#[^\n]*/g, '');
// Strip Python-style comments (#) and trailing commas before JSON parsing
const cleaned = content
.replace(/#[^\n]*/g, '') // Remove # comments
.replace(/,(\s*[\]}])/g, '$1'); // Remove trailing commas before ] or }
const gyp = JSON.parse(cleaned);
if (gyp.targets && gyp.targets[0] && gyp.targets[0].actions) {

View file

@ -830,6 +830,18 @@ const resolveCallTarget = (
let filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, call.callForm);
// Swift/Kotlin: constructor calls look like free function calls (no `new` keyword).
// If free-form filtering found no callable candidates but the symbol resolves to a
// Class/Struct, retry with constructor form so CONSTRUCTOR_TARGET_TYPES applies.
if (filteredCandidates.length === 0 && call.callForm === 'free') {
const hasTypeTarget = tiered.candidates.some(c =>
c.type === 'Class' || c.type === 'Struct' || c.type === 'Enum',
);
if (hasTypeTarget) {
filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, 'constructor');
}
}
// Module-qualified constructor pattern: e.g. Python `import models; models.User()`.
// The attribute access gives callForm='member', but the callee may be a Class — a valid
// constructor target. Re-try with constructor-form filtering so that `module.ClassName()`
@ -904,7 +916,20 @@ const resolveCallTarget = (
if (disambiguated) return toResolveResult(disambiguated, tiered.tier);
}
if (filteredCandidates.length !== 1) return null;
if (filteredCandidates.length !== 1) {
// Deduplicate: Swift extensions create multiple Class nodes with the same name.
// When all candidates share the same type and differ only by file (extension vs
// primary definition), they represent the same symbol. Prefer the primary
// definition (shortest file path: Product.swift over ProductExtension.swift).
if (filteredCandidates.length > 1) {
const allSameType = filteredCandidates.every(c => c.type === filteredCandidates[0].type);
if (allSameType && (filteredCandidates[0].type === 'Class' || filteredCandidates[0].type === 'Struct')) {
const sorted = [...filteredCandidates].sort((a, b) => a.filePath.length - b.filePath.length);
return toResolveResult(sorted[0], tiered.tier);
}
}
return null;
}
return toResolveResult(filteredCandidates[0], tiered.tier);
};

View file

@ -192,17 +192,27 @@ const phpExportChecker: ExportChecker = (node, _name) => {
return true;
};
/** Swift: check for 'public' or 'open' access modifiers. */
/**
* Swift: treat symbols as exported unless explicitly marked private/fileprivate.
*
* Swift's default access level is `internal`, which means visible to all files
* in the same module/target. Since GitNexus indexes at the target level,
* `internal` symbols should be treated as exported (cross-file visible).
* Only `private` and `fileprivate` symbols are truly file-scoped.
*/
const swiftExportChecker: ExportChecker = (node, _name) => {
let current: SyntaxNode | null = node;
while (current) {
if (current.type === 'modifiers' || current.type === 'visibility_modifier') {
const text = current.text || '';
if (text.includes('public') || text.includes('open')) return true;
// Exclude private(set)/fileprivate(set) — only the setter is restricted,
// the symbol itself is still readable cross-file.
if (/\b(private|fileprivate)\b(?!\s*\()/.test(text)) return false;
}
current = current.parent;
}
return false;
// Default (internal), public, and open are all cross-file visible
return true;
};
// ============================================================================

View file

@ -5,6 +5,8 @@ import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/pa
import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
import { generateId } from '../../lib/utils.js';
import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop } from './utils.js';
import { SupportedLanguages } from '../../config/supported-languages.js';
import type { SwiftPackageConfig } from './language-config.js';
import type { ExtractedImport } from './workers/parse-worker.js';
import { getTreeSitterBufferSize } from './constants.js';
import { loadImportConfigs } from './language-config.js';
@ -97,6 +99,82 @@ function createImportEdgeHelpers(graph: KnowledgeGraph, importMap: ImportMap) {
return { addImportEdge, addImportGraphEdge, getResolvedCount: () => totalImportsResolved };
}
/**
* Group Swift files by target for implicit module visibility.
*
* If SwiftPackageConfig is available, use SPM target directory mappings.
* Otherwise, group all Swift files under a single "default" target
* (assumes a single-module Xcode project).
*/
function groupSwiftFilesByTarget(
swiftFiles: string[],
swiftPackageConfig: SwiftPackageConfig | null,
): Map<string, string[]> {
const groups = new Map<string, string[]>();
if (swiftPackageConfig && swiftPackageConfig.targets.size > 0) {
for (const file of swiftFiles) {
const normalized = file.replace(/\\/g, '/');
let assigned = false;
for (const [targetName, targetDir] of swiftPackageConfig.targets) {
const dirPrefix = targetDir + '/';
const idx = normalized.indexOf(dirPrefix);
if (idx === 0 || (idx > 0 && normalized[idx - 1] === '/')) {
if (!groups.has(targetName)) groups.set(targetName, []);
groups.get(targetName)!.push(file);
assigned = true;
break;
}
}
if (!assigned) {
if (!groups.has('__default__')) groups.set('__default__', []);
groups.get('__default__')!.push(file);
}
}
} else {
groups.set('__default__', [...swiftFiles]);
}
return groups;
}
/**
* Add implicit IMPORTS edges between all Swift files in the same module/target.
* Swift has no file-level imports all files in a module see each other.
*/
function addSwiftImplicitImports(
files: string[] | { path: string }[],
swiftPackageConfig: SwiftPackageConfig | null,
importMap: Map<string, Set<string>>,
addImportEdge: (src: string, target: string) => void,
logSuffix = '',
): void {
const paths = typeof files[0] === 'string'
? files as string[]
: (files as { path: string }[]).map(f => f.path);
const swiftFiles = paths
.filter(f => getLanguageFromFilename(f) === SupportedLanguages.Swift);
if (swiftFiles.length <= 1) return;
const targetGroups = groupSwiftFilesByTarget(swiftFiles, swiftPackageConfig);
for (const group of targetGroups.values()) {
for (const srcFile of group) {
const existing = importMap.get(srcFile);
for (const otherFile of group) {
if (srcFile === otherFile) continue;
if (existing?.has(otherFile)) continue;
addImportEdge(srcFile, otherFile);
}
}
}
if (isDev) {
console.log(`📊 Swift: ${swiftFiles.length} files in ${targetGroups.size} target group(s), implicit imports added${logSuffix}`);
}
}
/**
* Apply an ImportResult: emit graph edges and update ImportMap/PackageMap.
* If namedBindings are provided and the import resolves to a single file,
@ -326,6 +404,8 @@ export const processImports = async (
// Tree is now owned by the LRU cache — no manual delete needed
}
addSwiftImplicitImports(allFileList, configs.swiftPackageConfig, importMap, addImportEdge);
if (skippedByLang && skippedByLang.size > 0) {
for (const [lang, count] of skippedByLang.entries()) {
console.warn(
@ -396,6 +476,8 @@ export const processImportsFromExtracted = async (
onProgress?.(totalFiles, totalFiles);
addSwiftImplicitImports(files, configs.swiftPackageConfig, importMap, addImportEdge, ' (fast path)');
if (isDev) {
console.log(`📊 Import processing (fast path): ${getResolvedCount()}/${totalImportsFound} imports resolved to graph edges`);
}

View file

@ -895,6 +895,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
@ -917,13 +920,14 @@ export const SWIFT_QUERIES = `
(class_declaration "extension" name: (user_type (type_identifier) @heritage.class)
(inheritance_specifier inherits_from: (user_type (type_identifier) @heritage.extends))) @heritage
; Write access: obj.field = value
; Write access: obj.field = value (tree-sitter-swift 0.7.1 uses named fields)
(assignment
(directly_assignable_expression
(_) @assignment.receiver
(navigation_suffix
(simple_identifier) @assignment.property))
(_)) @assignment
target: (directly_assignable_expression
(navigation_expression
target: (_) @assignment.receiver
suffix: (navigation_suffix
suffix: (simple_identifier) @assignment.property)))
result: (_)) @assignment
`;

View file

@ -854,6 +854,18 @@ export const buildTypeEnv = (
}
}
}
// Swift: property_declaration has type_annotation as a direct child (not a 'type' field).
// Extract the inner type node (array_type, user_type, etc.) for declarationTypeNodes.
if (!typeNode) {
for (let i = 0; i < node.namedChildCount; i++) {
const c = node.namedChild(i);
if (c?.type === 'type_annotation') {
// Use the inner type (array_type, user_type) rather than the annotation wrapper
typeNode = c.firstNamedChild ?? c;
break;
}
}
}
}
if (typeNode) {
const nameNode = node.childForFieldName('name')

View file

@ -317,6 +317,11 @@ export const extractVarName = (node: SyntaxNode): string | undefined => {
const inner = node.firstNamedChild;
if (inner) return extractVarName(inner);
}
// Swift: pattern node wraps a simple_identifier
if (node.type === 'pattern') {
const inner = node.firstNamedChild;
if (inner) return extractVarName(inner);
}
return undefined;
};

View file

@ -1,12 +1,33 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner } 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,10 +155,302 @@ 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:
* let user = getUser() callResult
* 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;
// Find the variable name from the pattern child
let lhs: string | undefined;
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child?.type === 'pattern') {
lhs = child.text;
break;
}
}
if (!lhs || scopeEnv.has(lhs)) return undefined;
// Find the value expression (last meaningful named child after pattern)
let valueNode: SyntaxNode | null = null;
for (let i = node.namedChildCount - 1; i >= 0; i--) {
const child = node.namedChild(i);
if (!child) continue;
if (child.type === 'pattern' || child.type === 'value_binding_pattern' || child.type === 'type_annotation') continue;
valueNode = child;
break;
}
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 };
}
// let name = user.name → 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, receiver: receiver.text, field: field.text };
}
}
return undefined;
}
// Call expressions
if (valueNode.type === 'call_expression') {
const callee = valueNode.firstNamedChild;
if (!callee) return undefined;
// let user = getUser() → callResult
if (callee.type === 'simple_identifier') {
return { kind: 'callResult', lhs, callee: callee.text };
}
// let result = user.save() → methodCallResult
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, receiver: receiver.text, method: method.text };
}
}
}
}
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) {
// Extract a simple identifier from the pattern. Skip non-trivial patterns
// (e.g. tuple destructuring `for (a, b) in ...`) to avoid polluting scopeEnv.
const varName = extractVarName(child) ?? (child.type === 'simple_identifier' ? child.text : undefined);
if (!varName) return; // Non-simple pattern — bail out
loopVarName = varName;
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

@ -0,0 +1,9 @@
func processAwait() async {
let user = await fetchUser()
user.save()
}
func processTry() throws {
let repo = try parseRepo("main")
repo.save()
}

View file

@ -0,0 +1,15 @@
class User {
func save() {}
}
class Repo {
func save() {}
}
func fetchUser() async -> User {
return User()
}
func parseRepo(_ name: String) throws -> Repo {
return Repo()
}

View file

@ -0,0 +1,4 @@
func scan() {
let ocr = OCRService()
ocr.recognize()
}

View file

@ -0,0 +1,5 @@
class OCRService {
func recognize() -> String {
return "text"
}
}

View file

@ -0,0 +1,7 @@
func main() {
let svc = PublicService()
svc.doWork()
internalHelper()
secretHelper()
fileOnlyHelper()
}

View file

@ -0,0 +1,15 @@
class PublicService {
func doWork() {}
}
func internalHelper() -> String {
return "help"
}
private func secretHelper() -> String {
return "secret"
}
fileprivate func fileOnlyHelper() -> String {
return "fileonly"
}

View file

@ -0,0 +1,4 @@
func process() {
let product = Product(name: "Widget")
product.save()
}

View file

@ -0,0 +1,5 @@
class Product {
var name: String
init(name: String) { self.name = name }
func save() -> Bool { return true }
}

View file

@ -0,0 +1,5 @@
extension Product {
func displayName() -> String {
return name.uppercased()
}
}

View file

@ -0,0 +1,6 @@
func processAll() {
let users: [User] = []
for user in users {
user.save()
}
}

View file

@ -0,0 +1,7 @@
class User {
func save() {}
}
class Repo {
func save() {}
}

View file

@ -0,0 +1,10 @@
func processIfLet() {
if let user = findUser() {
user.save()
}
}
func processGuardLet() {
guard let repo = findRepo() else { return }
repo.save()
}

View file

@ -0,0 +1,15 @@
class User {
func save() {}
}
class Repo {
func save() {}
}
func findUser() -> User? {
return User()
}
func findRepo() -> Repo? {
return Repo()
}

View file

@ -0,0 +1,4 @@
func main() {
let service = UserService()
service.fetchUser()
}

View file

@ -0,0 +1,5 @@
class UserService {
func fetchUser() -> String {
return "alice"
}
}

View file

@ -163,10 +163,10 @@ describe('parsing', () => {
expect(isNodeExported(nameNode, 'doStuff', 'swift')).toBe(true);
});
it('non-public function is not exported', () => {
it('non-public (internal) function is exported (Swift default is module-scoped)', () => {
const fnDecl = mockNode('function_declaration', 'func helper() {}');
const nameNode = mockNode('identifier', 'helper', fnDecl);
expect(isNodeExported(nameNode, 'helper', 'swift')).toBe(false);
expect(isNodeExported(nameNode, 'helper', 'swift')).toBe(true);
});
});
@ -660,10 +660,10 @@ describe('parsing', () => {
// Swift edge cases
describe('swift edge cases', () => {
it('internal function is not exported (Swift default)', () => {
it('internal function is exported (Swift internal = module-scoped visibility)', () => {
const visMod = mockNode('visibility_modifier', 'internal');
const nameNode = mockNode('identifier', 'setup', visMod);
expect(isNodeExported(nameNode, 'setup', 'swift')).toBe(false);
expect(isNodeExported(nameNode, 'setup', 'swift')).toBe(true);
});
it('private function is not exported', () => {

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 () => {
@ -224,3 +224,279 @@ describe.skipIf(!swiftAvailable)('Swift return-type inference via function retur
expect(saveCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Implicit imports: Swift files in the same module see each other without
// explicit import statements. This is the foundation of all cross-file
// resolution — without addSwiftImplicitImports, Tier 2a lookups fail.
// ---------------------------------------------------------------------------
describe.skipIf(!swiftAvailable)('Swift implicit imports (cross-file visibility)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'swift-implicit-imports'),
() => {},
);
}, 60000);
it('detects UserService class in Models.swift', () => {
expect(getNodesByLabel(result, 'Class')).toContain('UserService');
});
it('resolves UserService() constructor call across files (no explicit import)', () => {
const calls = getRelationships(result, 'CALLS');
const ctorCall = calls.find(c =>
c.target === 'UserService' && c.targetFilePath === 'Models.swift',
);
expect(ctorCall).toBeDefined();
});
it('resolves service.fetchUser() member call across files', () => {
const calls = getRelationships(result, 'CALLS');
const memberCall = calls.find(c =>
c.target === 'fetchUser' && c.targetFilePath === 'Models.swift',
);
expect(memberCall).toBeDefined();
});
it('creates IMPORTS edges between files in the same module', () => {
const imports = getRelationships(result, 'IMPORTS');
const crossFileImport = imports.find(c =>
(c.sourceFilePath === 'App.swift' && c.targetFilePath === 'Models.swift')
|| (c.sourceFilePath === 'Models.swift' && c.targetFilePath === 'App.swift'),
);
expect(crossFileImport).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Extension deduplication: Swift extensions create multiple Class nodes
// with the same name. The resolver should deduplicate and prefer the
// primary definition (shortest file path).
// ---------------------------------------------------------------------------
describe.skipIf(!swiftAvailable)('Swift extension deduplication', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'swift-extension-dedup'),
() => {},
);
}, 60000);
it('detects Product class', () => {
expect(getNodesByLabel(result, 'Class')).toContain('Product');
});
it('resolves Product() constructor despite extension creating duplicate class node', () => {
const calls = getRelationships(result, 'CALLS');
const ctorCall = calls.find(c =>
c.target === 'Product' && c.source === 'process',
);
expect(ctorCall).toBeDefined();
});
it('resolves product.save() to Product.swift (primary definition)', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(c =>
c.target === 'save' && c.source === 'process' && c.targetFilePath === 'Product.swift',
);
expect(saveCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Constructor fallback: Swift constructors look like free function calls
// (no `new` keyword). The resolver retries with constructor form when
// free-form finds no callable but the name resolves to a Class/Struct.
// ---------------------------------------------------------------------------
describe.skipIf(!swiftAvailable)('Swift constructor call fallback (no new keyword)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'swift-constructor-fallback'),
() => {},
);
}, 60000);
it('resolves OCRService() as constructor call across files', () => {
const calls = getRelationships(result, 'CALLS');
const ctorCall = calls.find(c =>
c.target === 'OCRService' && c.targetFilePath === 'Service.swift',
);
expect(ctorCall).toBeDefined();
});
it('resolves ocr.recognize() member call via constructor-inferred type', () => {
const calls = getRelationships(result, 'CALLS');
const memberCall = calls.find(c =>
c.target === 'recognize' && c.targetFilePath === 'Service.swift',
);
expect(memberCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Export visibility: internal (default) symbols are cross-file visible,
// private/fileprivate are not. Verifies the export detection inversion.
// ---------------------------------------------------------------------------
describe.skipIf(!swiftAvailable)('Swift export visibility (internal vs private)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'swift-export-visibility'),
() => {},
);
}, 60000);
it('resolves PublicService() constructor across files', () => {
const calls = getRelationships(result, 'CALLS');
const ctorCall = calls.find(c =>
c.target === 'PublicService' && c.targetFilePath === 'Visible.swift',
);
expect(ctorCall).toBeDefined();
});
it('resolves internalHelper() across files (internal = module-scoped)', () => {
const calls = getRelationships(result, 'CALLS');
const helperCall = calls.find(c =>
c.target === 'internalHelper' && c.targetFilePath === 'Visible.swift',
);
expect(helperCall).toBeDefined();
});
// NOTE: private/fileprivate symbols are marked as unexported, which prevents
// Tier 2a (import-scoped) resolution. However, Tier 3 (global) still resolves
// them — export filtering at global scope is a separate enhancement.
// These tests verify the symbols ARE marked correctly in export detection
// (covered by parsing.test.ts mock tests), not end-to-end call blocking.
});
// ---------------------------------------------------------------------------
// if let / guard let optional binding resolution:
// Swift's most common unwrap patterns — extractIfGuardBinding extracts the
// variable name and infers type from the RHS call result.
// ---------------------------------------------------------------------------
describe.skipIf(!swiftAvailable)('Swift if let / guard let binding resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'swift-if-let-guard-let'),
() => {},
);
}, 60000);
it('detects User and Repo classes', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
});
it('resolves user.save() inside if-let to User#save', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(c =>
c.target === 'save' && c.source === 'processIfLet' && c.targetFilePath === 'Models.swift',
);
expect(saveCall).toBeDefined();
});
it('resolves repo.save() inside guard-let to Repo#save', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(c =>
c.target === 'save' && c.source === 'processGuardLet' && c.targetFilePath === 'Models.swift',
);
expect(saveCall).toBeDefined();
});
it('user.save() in if-let does NOT resolve to Repo#save', () => {
const calls = getRelationships(result, 'CALLS');
const wrongSave = calls.find(c =>
c.target === 'save' && c.source === 'processIfLet',
);
if (wrongSave) {
// If resolved, it should be to User's save (in Models.swift), not Repo's
expect(wrongSave.targetFilePath).toBe('Models.swift');
}
});
});
// ---------------------------------------------------------------------------
// await / try expression unwrapping:
// Swift's await_expression and try_expression wrap call_expression nodes.
// extractPendingAssignment must unwrap these to find the inner call.
// ---------------------------------------------------------------------------
describe.skipIf(!swiftAvailable)('Swift await / try expression unwrapping', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'swift-await-try'),
() => {},
);
}, 60000);
it('resolves user.save() via await fetchUser() return type', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(c =>
c.target === 'save' && c.source === 'processAwait' && c.targetFilePath === 'Models.swift',
);
expect(saveCall).toBeDefined();
});
it('resolves repo.save() via try parseRepo() return type', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(c =>
c.target === 'save' && c.source === 'processTry' && c.targetFilePath === 'Models.swift',
);
expect(saveCall).toBeDefined();
});
it('detects fetchUser and parseRepo as functions', () => {
const fns = getNodesByLabel(result, 'Function');
expect(fns).toContain('fetchUser');
expect(fns).toContain('parseRepo');
});
});
// ---------------------------------------------------------------------------
// For-in loop element type inference: extractForLoopBinding derives element
// type from the iterable's declared type annotation (e.g., [User] → User).
//
// KNOWN GAP: The type-env correctly stores declarationTypeNodes for Swift
// array types ([User]), but the call-processor's re-parse path doesn't
// propagate the for-loop binding to receiver resolution. The type-env
// infrastructure (extractForLoopBinding, extractSwiftElementTypeFromTypeNode,
// declarationTypeNodes population for type_annotation) is in place — the
// integration gap is in how processCalls rebuilds TypeEnv for call resolution.
// Fixture: swift-for-loop-inference/ (ready for when this is wired up).
// ---------------------------------------------------------------------------
describe.skipIf(!swiftAvailable)('Swift for-in loop element type inference', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'swift-for-loop-inference'),
() => {},
);
}, 60000);
it('detects User and Repo classes', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
});
it('creates implicit import edges between files', () => {
const imports = getRelationships(result, 'IMPORTS');
expect(imports.length).toBeGreaterThan(0);
});
});

80
swift-ingestion-gaps.md Normal file
View file

@ -0,0 +1,80 @@
# Swift Ingestion Gaps
Tracks missing Swift features in the GitNexus ingestion pipeline. Organized by priority.
## 🔴 High Priority
### Type Inference
| Gap | Description | Impact |
|-----|-------------|--------|
| `if let` / `guard let` inside for-loop bodies | Type-env binds the variable correctly but call-processor's re-parse path doesn't propagate for-loop element bindings to receiver resolution | Calls inside `for item in collection` are unresolved |
| `while let` binding | `while let x = iter.next()` not in `DECLARATION_NODE_TYPES` | Uncommon but valid Swift pattern |
### Call Resolution
| Gap | Description | Impact |
|-----|-------------|--------|
| `await expr` / `try expr` as call wrappers | `await_expression` and `try_expression` wrap `call_expression` — call extraction queries match but the outer wrapper can interfere with receiver resolution in some paths | Most cases work via `unwrapSwiftExpression` but edge cases remain |
| Multi-hop chains | `a.b.c()` — only single-hop `receiver.method()` resolved | Common in UIKit/SwiftUI code |
| Trailing closures | `items.map { $0.save() }``$0` type not inferrable | Functional-style Swift code |
## 🟡 Medium Priority
### Symbol Extraction
| Gap | Description | Impact |
|-----|-------------|--------|
| Enum `case` as callable | `MyEnum.case` calls are member-form, not caught by constructor fallback | Enum-heavy code (Result, State enums) |
| Subscript declarations | `subscript(i:) -> T` not captured | Protocol conformance tracking |
| Operator overloads | `static func + (lhs:, rhs:)` not captured | Mathematical types |
| `deinit` | `deinit {}` not captured | Minor — rarely called explicitly |
| Macro declarations | `@macro` / `#macro` (Swift 5.9+) not captured | Swift macro ecosystem is growing |
### Heritage / Inheritance
| Gap | Description | Impact |
|-----|-------------|--------|
| Multiple inheritance specifiers | `class Foo: Bar, P1, P2` — only first specifier captured | Missing protocol conformance edges |
| Generic constraints | `class Foo<T: Equatable>` — bounds not tracked | Advanced generics |
| Conditional conformance | `extension Array: P where Element: Q``where` clause not processed | Cross-platform code |
| Protocol composition | `typealias Codable = Encodable & Decodable` — not expanded | Type alias resolution |
### Export / Visibility
| Gap | Description | Impact |
|-----|-------------|--------|
| Nested function declarations | Inner `func` marked as exported — should be private | Conservative resolution still correct (over-exports) |
### Module / Import
| Gap | Description | Impact |
|-----|-------------|--------|
| `@testable import` | Test target imports treated as opaque | Test file cross-references |
| Cross-package SPM imports | External package symbols not resolved | Only affects multi-package repos |
| `@_exported import` | Module re-exports not tracked | Framework wrapper patterns |
## 🟢 Low Priority
### Type Inference
| Gap | Description | Impact |
|-----|-------------|--------|
| `switch` / `case` pattern binding | `case let x as Foo:` not tracked | Enum pattern matching |
| Tuple destructuring | `let (a, b) = fn()` not handled | Uncommon pattern |
| `@Environment` / `@EnvironmentObject` | SwiftUI dependency injection — no AST representation | Would need heuristic resolution |
| `@Query` (SwiftData) | Property wrapper types not inferrable from AST | SwiftData-specific |
| `#if canImport(...)` | Conditional compilation not evaluated | Cross-platform projects |
## ✅ Resolved
| Gap | Resolution | Commit |
|-----|-----------|--------|
| Cross-chunk implicit imports | `addSwiftImplicitImports` now uses `allFileList` instead of chunk-only `files` | `956dfd0` |
| `private(set)` false positive | Regex excludes `private(set)` / `fileprivate(set)` from unexported check | `0a3cdce` |
| `if let` / `guard let` binding | `extractIfGuardBinding` handles optional bindings | `16b1a63` |
| `await` / `try` unwrapping | `unwrapSwiftExpression` strips wrappers before RHS analysis | `16b1a63` |
| For-loop element type extraction | `extractForLoopBinding` + `extractSwiftElementTypeFromTypeNode` + type_annotation population in type-env | `956dfd0` |
| `self` / `super` resolution | `lookupInEnv` handles `self`/`super` via AST walk | `16b1a63` |
| Optional chaining `obj?.method()` | Handled via `optional_chaining_expression` | `16b1a63` |
| Multi-inheritance specifiers | First specifier captured via `inheritance_specifier` query | `16b1a63` |

View file

@ -381,15 +381,15 @@ So return-type-aware receiver inference already exists in a constrained downstre
| Parameters | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Initializer / constructor inference | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Constructor binding scan | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| For-loop element types | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes | Yes |
| Pattern binding | Yes | Yes | Yes | Yes | No | Yes | Yes | No | No | No | No | No | No |
| For-loop element types | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes††† | Yes | Yes |
| Pattern binding | Yes | Yes | Yes | Yes | No | Yes | Yes | No | No | No | Partial‡‡‡ | No | No |
| Assignment chains | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes |
| Field/property type resolution | Yes | No† | Yes | Yes | Yes | Yes | Yes | Yes* | Yes | YARD | No | Yes | No‡ |
| Comment-based types | JSDoc | JSDoc | No | No | No | No | No | No | PHPDoc | YARD | No | No | No |
| Return type extraction | JSDoc | JSDoc | No | No | No | No | No | No | PHPDoc | YARD | No | No | No |
| Call-result variable binding | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes¶ | No | Yes | No |
| Field access binding | Yes | No† | Yes | Yes | Yes | Yes | Yes | No‖ | Yes | N/A | No | Yes | No |
| Method-call-result binding | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes¶ | No | Yes | No |
| Call-result variable binding | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes¶ | Yes††† | Yes | No |
| Field access binding | Yes | No† | Yes | Yes | Yes | Yes | Yes | No‖ | Yes | N/A | Yes††† | Yes | No |
| Method-call-result binding | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes¶ | Yes††† | Yes | No |
| Write access (ACCESSES write) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes§ | Yes | Yes | Yes | No |
| Parameter types extracted | Yes** | No | Yes | Yes | Yes | Yes | Yes | Partial†† | No | No | No | Yes | No |
| Method overload disambiguation | Yes** | No | Yes | Yes | Yes | No | No | No | No | No | No | Yes | No |
@ -423,6 +423,10 @@ So return-type-aware receiver inference already exists in a constrained downstre
¶¶ C#: `using static NS.Type;` now captured (last segment as class binding). Non-alias `using NS;` still unsupported — namespace imports can't be reduced to per-symbol bindings without type inference.
††† Swift: `extractPendingAssignment` handles `callResult`, `methodCallResult`, `fieldAccess`, and `copy` bindings. `if let` / `guard let` optional bindings supported via `extractIfGuardBinding`. `await` / `try` expression wrappers are unwrapped before RHS analysis. For-loop element type extraction supports `[User]` array sugar and `Array<User>` generics. See `swift-ingestion-gaps.md` for remaining limitations.
‡‡‡ Swift: `if let` / `guard let` optional bindings supported. `while let`, `switch` / `case` pattern matching, and tuple destructuring not yet implemented.
\*\*\* Whole-module-import languages (Go, Ruby, C/C++, Swift): namedImportMap entries synthesized from graph-exported symbols via `synthesizeWildcardImportBindings()`. Not from import AST node extraction.
---
@ -458,7 +462,7 @@ Important gaps still remain:
- no general cross-file propagation of inferred bindings
- `this`/`self`/`$this` receivers are not resolved in the fixpoint loop (resolved on-demand at call sites via AST walk instead)
- limited branch-sensitive narrowing outside selected pattern constructs
- limited Swift support compared with other languages
- limited Swift support compared with other languages (see `swift-ingestion-gaps.md`)
- no complete destructuring-based field typing
- no MRO/inheritance walking for field lookups (`lookupFieldByOwner` is direct-only)
- for-loop variables bound at walk time cannot see fixpoint-resolved types (Phase 9B gap)