mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-08 22:22:52 +00:00
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:
commit
db6b302fee
29 changed files with 994 additions and 42 deletions
29
gitnexus/package-lock.json
generated
29
gitnexus/package-lock.json
generated
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
`;
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
9
gitnexus/test/fixtures/lang-resolution/swift-await-try/App.swift
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/swift-await-try/App.swift
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
func processAwait() async {
|
||||
let user = await fetchUser()
|
||||
user.save()
|
||||
}
|
||||
|
||||
func processTry() throws {
|
||||
let repo = try parseRepo("main")
|
||||
repo.save()
|
||||
}
|
||||
15
gitnexus/test/fixtures/lang-resolution/swift-await-try/Models.swift
vendored
Normal file
15
gitnexus/test/fixtures/lang-resolution/swift-await-try/Models.swift
vendored
Normal 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()
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/swift-constructor-fallback/App.swift
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/swift-constructor-fallback/App.swift
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
func scan() {
|
||||
let ocr = OCRService()
|
||||
ocr.recognize()
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/swift-constructor-fallback/Service.swift
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/swift-constructor-fallback/Service.swift
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class OCRService {
|
||||
func recognize() -> String {
|
||||
return "text"
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/swift-export-visibility/App.swift
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/swift-export-visibility/App.swift
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
func main() {
|
||||
let svc = PublicService()
|
||||
svc.doWork()
|
||||
internalHelper()
|
||||
secretHelper()
|
||||
fileOnlyHelper()
|
||||
}
|
||||
15
gitnexus/test/fixtures/lang-resolution/swift-export-visibility/Visible.swift
vendored
Normal file
15
gitnexus/test/fixtures/lang-resolution/swift-export-visibility/Visible.swift
vendored
Normal 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"
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/swift-extension-dedup/App.swift
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/swift-extension-dedup/App.swift
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
func process() {
|
||||
let product = Product(name: "Widget")
|
||||
product.save()
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/swift-extension-dedup/Product.swift
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/swift-extension-dedup/Product.swift
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class Product {
|
||||
var name: String
|
||||
init(name: String) { self.name = name }
|
||||
func save() -> Bool { return true }
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/swift-extension-dedup/ProductExtensions.swift
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/swift-extension-dedup/ProductExtensions.swift
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
extension Product {
|
||||
func displayName() -> String {
|
||||
return name.uppercased()
|
||||
}
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/swift-for-loop-inference/App.swift
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/swift-for-loop-inference/App.swift
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
func processAll() {
|
||||
let users: [User] = []
|
||||
for user in users {
|
||||
user.save()
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/swift-for-loop-inference/Models.swift
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/swift-for-loop-inference/Models.swift
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
class User {
|
||||
func save() {}
|
||||
}
|
||||
|
||||
class Repo {
|
||||
func save() {}
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/swift-if-let-guard-let/App.swift
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/swift-if-let-guard-let/App.swift
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
func processIfLet() {
|
||||
if let user = findUser() {
|
||||
user.save()
|
||||
}
|
||||
}
|
||||
|
||||
func processGuardLet() {
|
||||
guard let repo = findRepo() else { return }
|
||||
repo.save()
|
||||
}
|
||||
15
gitnexus/test/fixtures/lang-resolution/swift-if-let-guard-let/Models.swift
vendored
Normal file
15
gitnexus/test/fixtures/lang-resolution/swift-if-let-guard-let/Models.swift
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
class User {
|
||||
func save() {}
|
||||
}
|
||||
|
||||
class Repo {
|
||||
func save() {}
|
||||
}
|
||||
|
||||
func findUser() -> User? {
|
||||
return User()
|
||||
}
|
||||
|
||||
func findRepo() -> Repo? {
|
||||
return Repo()
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/swift-implicit-imports/App.swift
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/swift-implicit-imports/App.swift
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
func main() {
|
||||
let service = UserService()
|
||||
service.fetchUser()
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/swift-implicit-imports/Models.swift
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/swift-implicit-imports/Models.swift
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class UserService {
|
||||
func fetchUser() -> String {
|
||||
return "alice"
|
||||
}
|
||||
}
|
||||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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
80
swift-ingestion-gaps.md
Normal 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` |
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue