mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-07 08:26:11 +00:00
feat: full Swift cross-file resolution (export, imports, constructors)
Three changes that together enable cross-file call resolution for Swift: 1. export-detection.ts: Treat internal (default) Swift symbols as exported. Swift's default access level is `internal` (module-scoped, visible to all files in the same target). Only private/fileprivate are file-scoped. Previously all non-public/open symbols were marked unexported. 2. import-processor.ts: Add implicit import edges between all Swift files in the same module/target. Swift has no file-level imports — all files see each other automatically. Without these edges, the tiered resolver can't find cross-file symbols at Tier 2a (import-scoped). Supports SPM targets via Package.swift; falls back to single-module for Xcode projects without SPM. 3. call-processor.ts: Add constructor fallback for free-form calls. Swift constructors look like free function calls (no `new` keyword): `let ocr = OCRService()`. The call form is inferred as `free`, which filters out Class/Struct targets. Now retries with `constructor` form when free-form finds no callable but the name resolves to a type. Tested on 61-file iOS 26 project (PricePal): - Before: 0 cross-file CALLS edges - After: full cross-file resolution (OCRService traced from ScanViewModel) - 3,099 nodes, 10,449 edges, 246 clusters, 243 flows Related: #406, #407 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0c8ec952ee
commit
65bc99c448
3 changed files with 76 additions and 4 deletions
|
|
@ -827,7 +827,19 @@ const resolveCallTarget = (
|
|||
const tiered = ctx.resolve(call.calledName, currentFile);
|
||||
if (!tiered) return null;
|
||||
|
||||
const filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, call.callForm);
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
// D. Receiver-type filtering: for member calls with a known receiver type,
|
||||
// resolve the type through the same tiered import infrastructure, then
|
||||
|
|
|
|||
|
|
@ -192,17 +192,25 @@ 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;
|
||||
if (text.includes('private') || text.includes('fileprivate')) return false;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return false;
|
||||
// Default (internal), public, and open are all cross-file visible
|
||||
return true;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -306,6 +306,35 @@ export const processImports = async (
|
|||
// Tree is now owned by the LRU cache — no manual delete needed
|
||||
}
|
||||
|
||||
// ---- Swift: implicit module-level visibility ----
|
||||
// In Swift, all files in the same module/target see each other without explicit imports.
|
||||
// Add implicit import edges between all Swift files so the call resolver can find
|
||||
// cross-file symbols at Tier 2a (import-scoped) instead of falling to Tier 3 (global).
|
||||
const swiftFiles = files
|
||||
.filter(f => getLanguageFromFilename(f.path) === SupportedLanguages.Swift)
|
||||
.map(f => f.path);
|
||||
|
||||
if (swiftFiles.length > 1) {
|
||||
// Group Swift files by target directory (SPM target or common root)
|
||||
const targetGroups = groupSwiftFilesByTarget(swiftFiles, configs.swiftPackageConfig);
|
||||
|
||||
for (const group of targetGroups.values()) {
|
||||
for (const srcFile of group) {
|
||||
for (const otherFile of group) {
|
||||
if (srcFile === otherFile) continue;
|
||||
// Only add if not already imported (from explicit `import TargetName`)
|
||||
if (importMap.has(srcFile) && importMap.get(srcFile)!.has(otherFile)) continue;
|
||||
addImportEdge(srcFile, otherFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDev) {
|
||||
const totalGroups = targetGroups.size;
|
||||
console.log(`📊 Swift: ${swiftFiles.length} files in ${totalGroups} target group(s), implicit imports added`);
|
||||
}
|
||||
}
|
||||
|
||||
if (skippedByLang && skippedByLang.size > 0) {
|
||||
for (const [lang, count] of skippedByLang.entries()) {
|
||||
console.warn(
|
||||
|
|
@ -375,6 +404,29 @@ export const processImportsFromExtracted = async (
|
|||
|
||||
onProgress?.(totalFiles, totalFiles);
|
||||
|
||||
// ---- Swift: implicit module-level visibility (fast path) ----
|
||||
const swiftFilePaths = files
|
||||
.filter(f => getLanguageFromFilename(f.path) === SupportedLanguages.Swift)
|
||||
.map(f => f.path);
|
||||
|
||||
if (swiftFilePaths.length > 1) {
|
||||
const targetGroups = groupSwiftFilesByTarget(swiftFilePaths, configs.swiftPackageConfig);
|
||||
|
||||
for (const group of targetGroups.values()) {
|
||||
for (const srcFile of group) {
|
||||
for (const otherFile of group) {
|
||||
if (srcFile === otherFile) continue;
|
||||
if (importMap.has(srcFile) && importMap.get(srcFile)!.has(otherFile)) continue;
|
||||
addImportEdge(srcFile, otherFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDev) {
|
||||
console.log(`📊 Swift: ${swiftFilePaths.length} files in ${targetGroups.size} target group(s), implicit imports added (fast path)`);
|
||||
}
|
||||
}
|
||||
|
||||
if (isDev) {
|
||||
console.log(`📊 Import processing (fast path): ${getResolvedCount()}/${totalImportsFound} imports resolved to graph edges`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue