From d67c676c3e0311de088c9c94647684ed4d8e8bb8 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Thu, 26 Mar 2026 11:58:45 +0000 Subject: [PATCH] feat(swift): Phase 8/9 integration tests for field-type and call-result binding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Swift field-type resolution and call-result binding integration tests with fixtures, plus merge-conflict fixes for the FieldExtractor code. **Swift integration tests:** - `swift-field-types/` fixture (Models.swift + App.swift) — tests HAS_PROPERTY edges, field-chain CALLS resolution (user.address.save() → Address#save), and ACCESSES edges for field reads. - `swift-call-result-binding/` fixture — tests call-result binding (let user = getUser(); user.save() → User#save). - 2 new describe blocks in swift.test.ts with skipIf(!swiftAvailable). **Swift arity fix:** - extractMethodSignature fallback counts direct `parameter` children when no wrapper list node exists (Swift's tree-sitter grammar places parameters as direct children of function_declaration). Without this, all Swift functions had parameterCount: 0 and the arity filter rejected valid call targets. **FieldExtractor merge-conflict fixes:** - field-extractor.ts: update import from removed ./utils.js to ./utils/ast-helpers.js; use typeEnv.fileScope() instead of .get(''). - field-extractors/typescript.ts: same import fix. - field-types.ts: alias TypeEnvironment as TypeEnv (renamed on main). - field-extraction.test.ts: mock TypeEnvironment interface properly. --- .../src/core/ingestion/field-extractor.ts | 10 +-- .../ingestion/field-extractors/typescript.ts | 3 +- gitnexus/src/core/ingestion/field-types.ts | 4 +- .../src/core/ingestion/utils/ast-helpers.ts | 12 +++ .../swift-call-result-binding/App.swift | 4 + .../swift-call-result-binding/Models.swift | 11 +++ .../swift-field-types/App.swift | 3 + .../swift-field-types/Models.swift | 16 ++++ .../test/integration/resolvers/swift.test.ts | 89 ++++++++++++++++++- gitnexus/test/unit/field-extraction.test.ts | 10 ++- 10 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 gitnexus/test/fixtures/lang-resolution/swift-call-result-binding/App.swift create mode 100644 gitnexus/test/fixtures/lang-resolution/swift-call-result-binding/Models.swift create mode 100644 gitnexus/test/fixtures/lang-resolution/swift-field-types/App.swift create mode 100644 gitnexus/test/fixtures/lang-resolution/swift-field-types/Models.swift diff --git a/gitnexus/src/core/ingestion/field-extractor.ts b/gitnexus/src/core/ingestion/field-extractor.ts index c8f1b87f1..079228369 100644 --- a/gitnexus/src/core/ingestion/field-extractor.ts +++ b/gitnexus/src/core/ingestion/field-extractor.ts @@ -1,6 +1,6 @@ // gitnexus/src/core/ingestion/field-extractor.ts -import type { SyntaxNode } from './utils.js'; +import type { SyntaxNode } from './utils/ast-helpers.js'; import { SupportedLanguages } from '../../config/supported-languages.js'; import type { FieldExtractorContext, @@ -62,11 +62,9 @@ export abstract class BaseFieldExtractor implements FieldExtractor { const { typeEnv, symbolTable, filePath } = context; // Try to find in type environment (check file scope first) - const fileEnv = typeEnv.get(''); - if (fileEnv) { - const local = fileEnv.get(typeName); - if (local) return local; - } + const fileEnv = typeEnv.fileScope(); + const local = fileEnv.get(typeName); + if (local) return local; // Try symbol table lookup in current file const symbols = symbolTable.lookupExactAll(filePath, typeName); diff --git a/gitnexus/src/core/ingestion/field-extractors/typescript.ts b/gitnexus/src/core/ingestion/field-extractors/typescript.ts index b58106fe0..6282fecf7 100644 --- a/gitnexus/src/core/ingestion/field-extractors/typescript.ts +++ b/gitnexus/src/core/ingestion/field-extractors/typescript.ts @@ -1,10 +1,9 @@ // gitnexus/src/core/ingestion/field-extractors/typescript.ts -import type { SyntaxNode } from '../utils.js'; +import type { SyntaxNode } from '../utils/ast-helpers.js'; import { SupportedLanguages } from '../../../config/supported-languages.js'; import { BaseFieldExtractor } from '../field-extractor.js'; import type { FieldExtractorContext, ExtractedFields, FieldInfo } from '../field-types.js'; -import { extractSimpleTypeName } from '../type-extractors/shared.js'; /** * TypeScript field extractor for class and interface declarations. diff --git a/gitnexus/src/core/ingestion/field-types.ts b/gitnexus/src/core/ingestion/field-types.ts index eb6d765c2..6c5fe84e4 100644 --- a/gitnexus/src/core/ingestion/field-types.ts +++ b/gitnexus/src/core/ingestion/field-types.ts @@ -1,6 +1,6 @@ // gitnexus/src/core/ingestion/field-types.ts -import type { TypeEnv } from './type-env.js'; +import type { TypeEnvironment } from './type-env.js'; import type { SymbolTable } from './symbol-table.js'; import { SupportedLanguages } from '../../config/supported-languages.js'; @@ -34,7 +34,7 @@ export type FieldTypeMap = Map; */ export interface FieldExtractorContext { /** Type environment for resolution */ - typeEnv: TypeEnv; + typeEnv: TypeEnvironment; /** Symbol table for FQN lookups */ symbolTable: SymbolTable; /** Current file path */ diff --git a/gitnexus/src/core/ingestion/utils/ast-helpers.ts b/gitnexus/src/core/ingestion/utils/ast-helpers.ts index 778699de0..057990da5 100644 --- a/gitnexus/src/core/ingestion/utils/ast-helpers.ts +++ b/gitnexus/src/core/ingestion/utils/ast-helpers.ts @@ -601,6 +601,18 @@ export const extractMethodSignature = (node: SyntaxNode | null | undefined): Met } } + // Swift fallback: tree-sitter-swift places `parameter` nodes as direct children of + // function_declaration without a wrapping parameters/function_parameters list node. + // When no parameter list was found, count direct `parameter` children on the node. + if (!parameterList && parameterCount === 0) { + for (const child of node.namedChildren) { + if (child.type === 'parameter') { + if (!hasDefaultValue(child)) requiredCount++; + parameterCount++; + } + } + } + // Return type extraction — language-specific field names // Go: 'result' field is either a type_identifier or parameter_list (multi-return) const goResult = node.childForFieldName?.('result'); diff --git a/gitnexus/test/fixtures/lang-resolution/swift-call-result-binding/App.swift b/gitnexus/test/fixtures/lang-resolution/swift-call-result-binding/App.swift new file mode 100644 index 000000000..edf1600d9 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/swift-call-result-binding/App.swift @@ -0,0 +1,4 @@ +func processUser() { + let user = getUser(name: "alice") + user.save() +} diff --git a/gitnexus/test/fixtures/lang-resolution/swift-call-result-binding/Models.swift b/gitnexus/test/fixtures/lang-resolution/swift-call-result-binding/Models.swift new file mode 100644 index 000000000..fbf620bcb --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/swift-call-result-binding/Models.swift @@ -0,0 +1,11 @@ +class User { + var name: String = "" + + func save() -> Bool { + return true + } +} + +func getUser(name: String) -> User { + return User() +} diff --git a/gitnexus/test/fixtures/lang-resolution/swift-field-types/App.swift b/gitnexus/test/fixtures/lang-resolution/swift-field-types/App.swift new file mode 100644 index 000000000..43e6eca46 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/swift-field-types/App.swift @@ -0,0 +1,3 @@ +func processUser(user: User) { + user.address.save() +} diff --git a/gitnexus/test/fixtures/lang-resolution/swift-field-types/Models.swift b/gitnexus/test/fixtures/lang-resolution/swift-field-types/Models.swift new file mode 100644 index 000000000..01de26dbb --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/swift-field-types/Models.swift @@ -0,0 +1,16 @@ +class Address { + var city: String = "" + + func save() { + // persist address + } +} + +class User { + var name: String = "" + var address: Address = Address() + + func greet() -> String { + return name + } +} diff --git a/gitnexus/test/integration/resolvers/swift.test.ts b/gitnexus/test/integration/resolvers/swift.test.ts index e773ce56e..307f2e62a 100644 --- a/gitnexus/test/integration/resolvers/swift.test.ts +++ b/gitnexus/test/integration/resolvers/swift.test.ts @@ -9,7 +9,7 @@ import { describe, it, expect, beforeAll } from 'vitest'; import path from 'path'; import { - FIXTURES, getRelationships, getNodesByLabel, + FIXTURES, getRelationships, getNodesByLabel, edgeSet, runPipelineFromRepo, type PipelineResult, } from './helpers.js'; import { isLanguageAvailable } from '../../../src/core/tree-sitter/parser-loader.js'; @@ -500,3 +500,90 @@ describe.skipIf(!swiftAvailable)('Swift for-in loop element type inference', () expect(imports.length).toBeGreaterThan(0); }); }); + +// ── Phase 8: Field-type resolution ────────────────────────────────────── + +describe.skipIf(!swiftAvailable)('Swift field-type resolution', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'swift-field-types'), + () => {}, + ); + }, 60000); + + it('detects classes and their properties', () => { + expect(getNodesByLabel(result, 'Class')).toEqual( + expect.arrayContaining(['Address', 'User']), + ); + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('address'); + expect(properties).toContain('city'); + expect(properties).toContain('name'); + }); + + it('emits HAS_PROPERTY edges from class to field', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(edgeSet(propEdges)).toEqual( + expect.arrayContaining([ + 'User → address', + 'Address → city', + ]), + ); + }); + + it('resolves field-chain call user.address.save() → Address#save', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter( + (c) => c.target === 'save' && c.source === 'processUser', + ); + expect(saveCalls.length).toBe(1); + expect(saveCalls[0]!.targetFilePath).toContain('Models.swift'); + }); + + it('emits ACCESSES edges for field reads in chains', () => { + const accesses = getRelationships(result, 'ACCESSES'); + const addressReads = accesses.filter( + (e) => e.target === 'address' && e.rel.reason === 'read', + ); + expect(addressReads.length).toBeGreaterThanOrEqual(1); + expect(addressReads[0]!.source).toBe('processUser'); + expect(addressReads[0]!.targetLabel).toBe('Property'); + }); +}); + +// ── Phase 9: Call-result binding ──────────────────────────────────────── + +describe.skipIf(!swiftAvailable)('Swift call-result binding', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'swift-call-result-binding'), + () => {}, + ); + }, 60000); + + it('resolves call-result-bound method call user.save() → User#save', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter( + (c) => c.target === 'save' && c.source === 'processUser', + ); + expect(saveCalls.length).toBe(1); + expect(saveCalls[0]!.targetFilePath).toContain('Models.swift'); + }); + + it('getUser() is present as a defined function', () => { + expect(getNodesByLabel(result, 'Function')).toContain('getUser'); + }); + + it('emits processUser -> getUser CALLS edge for let-assigned free function call', () => { + const calls = getRelationships(result, 'CALLS'); + const getUserCall = calls.find(c => + c.target === 'getUser' && c.source === 'processUser', + ); + expect(getUserCall).toBeDefined(); + expect(getUserCall!.targetFilePath).toContain('Models.swift'); + }); +}); diff --git a/gitnexus/test/unit/field-extraction.test.ts b/gitnexus/test/unit/field-extraction.test.ts index f9c409c6e..93861ee1d 100644 --- a/gitnexus/test/unit/field-extraction.test.ts +++ b/gitnexus/test/unit/field-extraction.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { TypeScriptFieldExtractor } from '../../src/core/ingestion/field-extractors/typescript.js'; import type { FieldExtractorContext, ExtractedFields } from '../../src/core/ingestion/field-types.js'; -import type { TypeEnv } from '../../src/core/ingestion/type-env.js'; +import type { TypeEnvironment } from '../../src/core/ingestion/type-env.js'; import { createSymbolTable } from '../../src/core/ingestion/symbol-table.js'; import Parser from 'tree-sitter'; import TypeScript from 'tree-sitter-typescript'; @@ -16,7 +16,13 @@ const parse = (code: string) => { // Mock context for tests const createMockContext = (): FieldExtractorContext => ({ - typeEnv: new Map() as TypeEnv, + typeEnv: { + lookup: () => undefined, + constructorBindings: [], + fileScope: () => new Map(), + allScopes: () => new Map(), + constructorTypeMap: new Map(), + } as TypeEnvironment, symbolTable: createSymbolTable(), filePath: 'test.ts', language: SupportedLanguages.TypeScript,