mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
feat: add Swift integration tests for if-let, await/try, for-loop + fix cross-chunk imports
- Add 3 new test fixtures: swift-if-let-guard-let, swift-await-try, swift-for-loop-inference
- Add integration tests for if let/guard let binding resolution (4 assertions)
- Add integration tests for await/try expression unwrapping (3 assertions)
- Add for-loop-inference fixture (documented as known gap — type-env infrastructure
is in place but call-processor re-parse path doesn't propagate the binding yet)
- Fix cross-chunk Swift implicit imports: standard processImports path now passes
allFileList instead of chunk-only files to addSwiftImplicitImports, matching
the fast-path behavior
- Add Swift type_annotation fallback in type-env declarationTypeNodes population
(handles [User] array sugar where childForFieldName('type') returns null)
- Handle Swift 'pattern' node in extractVarName fallback (pattern wraps simple_identifier)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
884b4acf84
commit
956dfd0bb4
9 changed files with 206 additions and 2 deletions
|
|
@ -382,7 +382,7 @@ export const processImports = async (
|
|||
// Tree is now owned by the LRU cache — no manual delete needed
|
||||
}
|
||||
|
||||
addSwiftImplicitImports(files, configs.swiftPackageConfig, importMap, addImportEdge);
|
||||
addSwiftImplicitImports(allFileList.map(p => ({ path: p })), configs.swiftPackageConfig, importMap, addImportEdge);
|
||||
|
||||
if (skippedByLang && skippedByLang.size > 0) {
|
||||
for (const [lang, count] of skippedByLang.entries()) {
|
||||
|
|
|
|||
|
|
@ -854,13 +854,27 @@ 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')
|
||||
?? node.childForFieldName('left')
|
||||
?? node.childForFieldName('pattern');
|
||||
if (nameNode) {
|
||||
const varName = extractVarName(nameNode);
|
||||
// Swift: pattern node wraps a simple_identifier — unwrap it
|
||||
const varName = extractVarName(nameNode)
|
||||
?? (nameNode.type === 'pattern' ? extractVarName(nameNode.firstNamedChild!) ?? nameNode.text : undefined);
|
||||
if (varName && !declarationTypeNodes.has(`${scope}\0${varName}`)) {
|
||||
declarationTypeNodes.set(`${scope}\0${varName}`, typeNode);
|
||||
}
|
||||
|
|
|
|||
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()
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
|
@ -378,3 +378,131 @@ describe.skipIf(!swiftAvailable)('Swift export visibility (internal vs private)'
|
|||
// 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).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue