mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
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:
parent
da162332aa
commit
d67c676c3e
10 changed files with 149 additions and 13 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
4
gitnexus/test/fixtures/lang-resolution/swift-call-result-binding/App.swift
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/swift-call-result-binding/App.swift
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
func processUser() {
|
||||
let user = getUser(name: "alice")
|
||||
user.save()
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/swift-call-result-binding/Models.swift
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/swift-call-result-binding/Models.swift
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
class User {
|
||||
var name: String = ""
|
||||
|
||||
func save() -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func getUser(name: String) -> User {
|
||||
return User()
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/swift-field-types/App.swift
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/swift-field-types/App.swift
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
func processUser(user: User) {
|
||||
user.address.save()
|
||||
}
|
||||
16
gitnexus/test/fixtures/lang-resolution/swift-field-types/Models.swift
vendored
Normal file
16
gitnexus/test/fixtures/lang-resolution/swift-field-types/Models.swift
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue