feat(swift): Phase 8/9 integration tests for field-type and call-result binding

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.
This commit is contained in:
Gergo Magyar 2026-03-26 11:58:45 +00:00
parent da162332aa
commit d67c676c3e
10 changed files with 149 additions and 13 deletions

View file

@ -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);

View file

@ -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.

View file

@ -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<string, FieldInfo[]>;
*/
export interface FieldExtractorContext {
/** Type environment for resolution */
typeEnv: TypeEnv;
typeEnv: TypeEnvironment;
/** Symbol table for FQN lookups */
symbolTable: SymbolTable;
/** Current file path */

View file

@ -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');

View file

@ -0,0 +1,4 @@
func processUser() {
let user = getUser(name: "alice")
user.save()
}

View file

@ -0,0 +1,11 @@
class User {
var name: String = ""
func save() -> Bool {
return true
}
}
func getUser(name: String) -> User {
return User()
}

View file

@ -0,0 +1,3 @@
func processUser(user: User) {
user.address.save()
}

View file

@ -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
}
}

View file

@ -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');
});
});

View file

@ -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,