mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
feat: expand field-type tests to 6 languages, fix Go ownerId and Kotlin navigation_expression
- Add integration test fixtures for Java, C#, Go, Kotlin, PHP (alongside existing TS) - Fix Go: add type_declaration handling in findEnclosingClassId for struct fields (field_declaration → field_declaration_list → struct_type → type_spec → type_declaration) - Fix Kotlin: add navigation_expression handling in field-access resolution (Kotlin uses navigation_expression + navigation_suffix, not member_expression) - Add extractMemberAccessParts helper in call-processor for cross-language member access - All 24 field-type tests pass across 6 languages, 181 Go+Kotlin tests pass with no regressions
This commit is contained in:
parent
69a611e0f5
commit
9681fa20da
16 changed files with 472 additions and 29 deletions
|
|
@ -276,26 +276,17 @@ export const processCalls = async (
|
|||
if (callForm === 'member' && !receiverTypeName && !receiverName) {
|
||||
const receiverNode = extractReceiverNode(nameNode);
|
||||
if (receiverNode && !CALL_EXPRESSION_TYPES.has(receiverNode.type)) {
|
||||
// receiverNode is a member_expression — extract object.property
|
||||
const objectNode = receiverNode.childForFieldName?.('object')
|
||||
?? receiverNode.childForFieldName?.('value')
|
||||
?? receiverNode.childForFieldName?.('operand')
|
||||
?? receiverNode.childForFieldName?.('expression');
|
||||
const propertyNode = receiverNode.childForFieldName?.('property')
|
||||
?? receiverNode.childForFieldName?.('field')
|
||||
?? receiverNode.childForFieldName?.('name');
|
||||
if (objectNode && propertyNode) {
|
||||
const objectName = objectNode.text;
|
||||
const propertyName = propertyNode.text;
|
||||
const parts = extractMemberAccessParts(receiverNode);
|
||||
if (parts) {
|
||||
// Resolve the object's type from TypeEnv
|
||||
let objectType = typeEnv ? typeEnv.lookup(objectName, callNode) : undefined;
|
||||
let objectType = typeEnv ? typeEnv.lookup(parts.objectName, callNode) : undefined;
|
||||
if (!objectType && verifiedReceivers.size > 0) {
|
||||
const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
|
||||
const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : '';
|
||||
objectType = lookupReceiverType(verifiedReceivers, funcName, objectName);
|
||||
objectType = lookupReceiverType(verifiedReceivers, funcName, parts.objectName);
|
||||
}
|
||||
if (objectType) {
|
||||
receiverTypeName = resolveFieldAccessType(objectType, propertyName, file.path, ctx);
|
||||
receiverTypeName = resolveFieldAccessType(objectType, parts.propertyName, file.path, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -612,6 +603,66 @@ const lookupReceiverType = (
|
|||
*
|
||||
* @returns The resolved type of the deepest field access, or undefined if resolution fails.
|
||||
*/
|
||||
/**
|
||||
* Extract object and property names from a member-access AST node.
|
||||
* Handles cross-language AST variations:
|
||||
* - TS/JS: member_expression with `object`/`property` fields
|
||||
* - C#: member_access_expression with `expression`/`name` fields
|
||||
* - Go: selector_expression with `operand`/`field` fields
|
||||
* - Rust/C++: field_expression with `value`/`field` fields
|
||||
* - Kotlin: navigation_expression with first child as object and navigation_suffix child containing property
|
||||
* - Python: attribute with `object`/`attribute` fields
|
||||
*/
|
||||
const extractMemberAccessParts = (
|
||||
node: any,
|
||||
): { objectName: string; propertyName: string } | undefined => {
|
||||
// Kotlin/Swift: navigation_expression — object is first child, property is inside navigation_suffix
|
||||
if (node.type === 'navigation_expression') {
|
||||
let objectNode: any = null;
|
||||
let propertyNode: any = null;
|
||||
for (const child of node.children ?? []) {
|
||||
if (child.type === 'navigation_suffix') {
|
||||
// The property identifier is inside the suffix
|
||||
for (const sc of child.children ?? []) {
|
||||
if (sc.isNamed && sc.type !== '.') {
|
||||
propertyNode = sc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (child.isNamed && !objectNode) {
|
||||
objectNode = child;
|
||||
}
|
||||
}
|
||||
if (objectNode && propertyNode) {
|
||||
return { objectName: objectNode.text, propertyName: propertyNode.text };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Python: attribute node — `object` and `attribute` fields
|
||||
if (node.type === 'attribute') {
|
||||
const objectNode = node.childForFieldName?.('object');
|
||||
const attrNode = node.childForFieldName?.('attribute');
|
||||
if (objectNode && attrNode) {
|
||||
return { objectName: objectNode.text, propertyName: attrNode.text };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// General: try standard field names used across grammars
|
||||
const objectNode = node.childForFieldName?.('object')
|
||||
?? node.childForFieldName?.('value')
|
||||
?? node.childForFieldName?.('operand')
|
||||
?? node.childForFieldName?.('expression');
|
||||
const propertyNode = node.childForFieldName?.('property')
|
||||
?? node.childForFieldName?.('field')
|
||||
?? node.childForFieldName?.('name');
|
||||
if (objectNode && propertyNode) {
|
||||
return { objectName: objectNode.text, propertyName: propertyNode.text };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const resolveFieldAccessType = (
|
||||
receiverName: string,
|
||||
fieldName: string,
|
||||
|
|
|
|||
|
|
@ -318,6 +318,21 @@ export const findEnclosingClassId = (node: any, filePath: string): string | null
|
|||
}
|
||||
}
|
||||
}
|
||||
// Go: type_declaration wrapping a struct_type (type User struct { ... })
|
||||
// field_declaration → field_declaration_list → struct_type → type_spec → type_declaration
|
||||
if (current.type === 'type_declaration') {
|
||||
const typeSpec = current.children?.find((c: any) => c.type === 'type_spec');
|
||||
if (typeSpec) {
|
||||
const typeBody = typeSpec.childForFieldName?.('type');
|
||||
if (typeBody?.type === 'struct_type' || typeBody?.type === 'interface_type') {
|
||||
const nameNode = typeSpec.childForFieldName?.('name');
|
||||
if (nameNode) {
|
||||
const label = typeBody.type === 'struct_type' ? 'Struct' : 'Interface';
|
||||
return generateId(label, `${filePath}:${nameNode.text}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (CLASS_CONTAINER_TYPES.has(current.type)) {
|
||||
// Rust impl_item: for `impl Trait for Struct {}`, pick the type after `for`
|
||||
if (current.type === 'impl_item') {
|
||||
|
|
|
|||
|
|
@ -1097,16 +1097,34 @@ const processFileGroup = (
|
|||
} else if (receiverNode) {
|
||||
// Receiver is a member_expression (field access like user.address.save()).
|
||||
// Extract object and property so processCallsFromExtracted can resolve the field type.
|
||||
const objectNode = receiverNode.childForFieldName?.('object')
|
||||
?? receiverNode.childForFieldName?.('value')
|
||||
?? receiverNode.childForFieldName?.('operand')
|
||||
?? receiverNode.childForFieldName?.('expression');
|
||||
const propertyNode = receiverNode.childForFieldName?.('property')
|
||||
?? receiverNode.childForFieldName?.('field')
|
||||
?? receiverNode.childForFieldName?.('name');
|
||||
if (objectNode && propertyNode) {
|
||||
const objectName = objectNode.text;
|
||||
const fieldName = propertyNode.text;
|
||||
let objectName: string | undefined;
|
||||
let fieldName: string | undefined;
|
||||
|
||||
// Kotlin/Swift: navigation_expression — object is first child, property inside navigation_suffix
|
||||
if (receiverNode.type === 'navigation_expression') {
|
||||
for (const child of receiverNode.children ?? []) {
|
||||
if (child.type === 'navigation_suffix') {
|
||||
for (const sc of child.children ?? []) {
|
||||
if (sc.isNamed && sc.type !== '.') { fieldName = sc.text; break; }
|
||||
}
|
||||
} else if (child.isNamed && !objectName) {
|
||||
objectName = child.text;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// General: try standard field names used across grammars
|
||||
const objectNode = receiverNode.childForFieldName?.('object')
|
||||
?? receiverNode.childForFieldName?.('value')
|
||||
?? receiverNode.childForFieldName?.('operand')
|
||||
?? receiverNode.childForFieldName?.('expression');
|
||||
const propertyNode = receiverNode.childForFieldName?.('property')
|
||||
?? receiverNode.childForFieldName?.('field')
|
||||
?? receiverNode.childForFieldName?.('name');
|
||||
if (objectNode) objectName = objectNode.text;
|
||||
if (propertyNode) fieldName = propertyNode.text;
|
||||
}
|
||||
|
||||
if (objectName && fieldName) {
|
||||
receiverFieldAccess = { objectName, fieldName };
|
||||
// Try resolving the object's type immediately from TypeEnv
|
||||
const objectType = typeEnv.lookup(objectName, callNode);
|
||||
|
|
|
|||
22
gitnexus/test/fixtures/lang-resolution/csharp-field-types/Models.cs
vendored
Normal file
22
gitnexus/test/fixtures/lang-resolution/csharp-field-types/Models.cs
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace FieldTypes;
|
||||
|
||||
public class Address
|
||||
{
|
||||
public string City { get; set; }
|
||||
|
||||
public void Save()
|
||||
{
|
||||
// persist address
|
||||
}
|
||||
}
|
||||
|
||||
public class User
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public Address Address { get; set; }
|
||||
|
||||
public string Greet()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/csharp-field-types/Service.cs
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/csharp-field-types/Service.cs
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace FieldTypes;
|
||||
|
||||
public class Service
|
||||
{
|
||||
public static void ProcessUser(User user)
|
||||
{
|
||||
// Field-access chain: user.Address → Address, then .Save() → Address#Save
|
||||
user.Address.Save();
|
||||
}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/go-field-types/cmd/main.go
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/go-field-types/cmd/main.go
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package main
|
||||
|
||||
import "example.com/go-field-types/models"
|
||||
|
||||
func processUser(user models.User) {
|
||||
// Field-access chain: user.Address → Address, then .Save() → Address#Save
|
||||
user.Address.Save()
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/go-field-types/go.mod
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/go-field-types/go.mod
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module example.com/go-field-types
|
||||
|
||||
go 1.21
|
||||
18
gitnexus/test/fixtures/lang-resolution/go-field-types/models/models.go
vendored
Normal file
18
gitnexus/test/fixtures/lang-resolution/go-field-types/models/models.go
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package models
|
||||
|
||||
type Address struct {
|
||||
City string
|
||||
}
|
||||
|
||||
func (a *Address) Save() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
Address Address
|
||||
}
|
||||
|
||||
func (u *User) Greet() string {
|
||||
return u.Name
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/java-field-types/App.java
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/java-field-types/App.java
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import models.User;
|
||||
|
||||
public class App {
|
||||
public static void processUser(User user) {
|
||||
// Field-access chain: user.address → Address, then .save() → Address#save
|
||||
user.address.save();
|
||||
}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/java-field-types/models/Address.java
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/java-field-types/models/Address.java
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package models;
|
||||
|
||||
public class Address {
|
||||
public String city;
|
||||
|
||||
public void save() {
|
||||
// persist address
|
||||
}
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/java-field-types/models/User.java
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/java-field-types/models/User.java
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package models;
|
||||
|
||||
public class User {
|
||||
public String name;
|
||||
public Address address;
|
||||
|
||||
public String greet() {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
16
gitnexus/test/fixtures/lang-resolution/kotlin-field-types/Models.kt
vendored
Normal file
16
gitnexus/test/fixtures/lang-resolution/kotlin-field-types/Models.kt
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
class Address {
|
||||
var city: String = ""
|
||||
|
||||
fun save() {
|
||||
// persist address
|
||||
}
|
||||
}
|
||||
|
||||
class User {
|
||||
var name: String = ""
|
||||
var address: Address = Address()
|
||||
|
||||
fun greet(): String {
|
||||
return name
|
||||
}
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/kotlin-field-types/Service.kt
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/kotlin-field-types/Service.kt
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
fun processUser(user: User) {
|
||||
// Field-access chain: user.address → Address, then .save() → Address#save
|
||||
user.address.save()
|
||||
}
|
||||
22
gitnexus/test/fixtures/lang-resolution/php-field-types/Models.php
vendored
Normal file
22
gitnexus/test/fixtures/lang-resolution/php-field-types/Models.php
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
class Address {
|
||||
/** @var string */
|
||||
public string $city;
|
||||
|
||||
public function save(): void {
|
||||
// persist address
|
||||
}
|
||||
}
|
||||
|
||||
class User {
|
||||
/** @var string */
|
||||
public string $name;
|
||||
|
||||
/** @var Address */
|
||||
public Address $address;
|
||||
|
||||
public function greet(): string {
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/php-field-types/Service.php
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/php-field-types/Service.php
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
class Service {
|
||||
public function processUser(User $user): void {
|
||||
// Field-access chain: $user->address → Address, then ->save() → Address#save
|
||||
$user->address->save();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
/**
|
||||
* Phase 8: Field/property type resolution — verifies that chained member access
|
||||
* through typed fields resolves correctly (e.g. user.address.save() → Address#save).
|
||||
*
|
||||
* Per-language fixtures test:
|
||||
* 1. Property nodes are extracted with correct ownerId linkage
|
||||
* 2. HAS_METHOD edges link properties to their owning classes
|
||||
* 3. Field-access chain resolution resolves user.address.save() → Address#save
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import path from 'path';
|
||||
|
|
@ -9,6 +14,10 @@ import {
|
|||
runPipelineFromRepo, type PipelineResult,
|
||||
} from './helpers.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TypeScript
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Field type resolution (TypeScript)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
|
|
@ -25,8 +34,6 @@ describe('Field type resolution (TypeScript)', () => {
|
|||
|
||||
it('detects Property nodes for typed fields', () => {
|
||||
const properties = getNodesByLabel(result, 'Property');
|
||||
// Should capture: address, city, name (from User + Address classes)
|
||||
// DEFAULT is static and may or may not be captured depending on tree-sitter query
|
||||
expect(properties).toContain('address');
|
||||
expect(properties).toContain('name');
|
||||
expect(properties).toContain('city');
|
||||
|
|
@ -35,7 +42,6 @@ describe('Field type resolution (TypeScript)', () => {
|
|||
it('emits HAS_METHOD edges linking properties to classes', () => {
|
||||
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
||||
const propEdges = hasMethod.filter(e => e.targetLabel === 'Property');
|
||||
// At minimum: User.address, User.name, Address.city
|
||||
expect(propEdges.length).toBeGreaterThanOrEqual(3);
|
||||
expect(edgeSet(propEdges)).toContain('User → address');
|
||||
expect(edgeSet(propEdges)).toContain('User → name');
|
||||
|
|
@ -44,11 +50,226 @@ describe('Field type resolution (TypeScript)', () => {
|
|||
|
||||
it('resolves user.address.save() → Address#save via field type', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
// processUser should call save() with receiver type Address
|
||||
const saveCalls = calls.filter(e => e.target === 'save');
|
||||
// The save method belongs to Address, so the target should be Address's save
|
||||
const addressSave = saveCalls.find(e => e.targetFilePath.includes('models'));
|
||||
expect(addressSave).toBeDefined();
|
||||
expect(addressSave!.source).toBe('processUser');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Java
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Field type resolution (Java)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'java-field-types'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects classes: Address, App, User', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'App', 'User']);
|
||||
});
|
||||
|
||||
it('detects Property nodes for Java fields', () => {
|
||||
const properties = getNodesByLabel(result, 'Property');
|
||||
expect(properties).toContain('address');
|
||||
expect(properties).toContain('name');
|
||||
expect(properties).toContain('city');
|
||||
});
|
||||
|
||||
it('emits HAS_METHOD edges linking properties to classes', () => {
|
||||
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
||||
const propEdges = hasMethod.filter(e => e.targetLabel === 'Property');
|
||||
expect(propEdges.length).toBeGreaterThanOrEqual(3);
|
||||
expect(edgeSet(propEdges)).toContain('User → address');
|
||||
expect(edgeSet(propEdges)).toContain('User → name');
|
||||
expect(edgeSet(propEdges)).toContain('Address → city');
|
||||
});
|
||||
|
||||
it('resolves user.address.save() → Address#save via field type', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCalls = calls.filter(e => e.target === 'save');
|
||||
const addressSave = saveCalls.find(
|
||||
e => e.source === 'processUser' && e.targetFilePath.includes('Address'),
|
||||
);
|
||||
expect(addressSave).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// C#
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Field type resolution (C#)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'csharp-field-types'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects classes: Address, Service, User', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'Service', 'User']);
|
||||
});
|
||||
|
||||
it('detects Property nodes for C# properties', () => {
|
||||
const properties = getNodesByLabel(result, 'Property');
|
||||
// C# property_declaration already captured before Phase 8
|
||||
expect(properties).toContain('Address');
|
||||
expect(properties).toContain('Name');
|
||||
expect(properties).toContain('City');
|
||||
});
|
||||
|
||||
it('emits HAS_METHOD edges linking properties to classes', () => {
|
||||
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
||||
const propEdges = hasMethod.filter(e => e.targetLabel === 'Property');
|
||||
expect(propEdges.length).toBeGreaterThanOrEqual(3);
|
||||
expect(edgeSet(propEdges)).toContain('User → Address');
|
||||
expect(edgeSet(propEdges)).toContain('User → Name');
|
||||
expect(edgeSet(propEdges)).toContain('Address → City');
|
||||
});
|
||||
|
||||
it('resolves user.Address.Save() → Address#Save via field type', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCalls = calls.filter(e => e.target === 'Save');
|
||||
const addressSave = saveCalls.find(
|
||||
e => e.source === 'ProcessUser' && e.targetFilePath.includes('Models'),
|
||||
);
|
||||
expect(addressSave).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Go
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Field type resolution (Go)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'go-field-types'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects structs: Address, User', () => {
|
||||
expect(getNodesByLabel(result, 'Struct')).toEqual(['Address', 'User']);
|
||||
});
|
||||
|
||||
it('detects Property nodes for Go struct fields', () => {
|
||||
const properties = getNodesByLabel(result, 'Property');
|
||||
expect(properties).toContain('Address');
|
||||
expect(properties).toContain('Name');
|
||||
expect(properties).toContain('City');
|
||||
});
|
||||
|
||||
it('emits HAS_METHOD edges linking struct fields to structs', () => {
|
||||
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
||||
const propEdges = hasMethod.filter(e => e.targetLabel === 'Property');
|
||||
expect(propEdges.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('resolves user.Address.Save() → Address#Save via field type', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCalls = calls.filter(e => e.target === 'Save');
|
||||
const addressSave = saveCalls.find(
|
||||
e => e.source === 'processUser' && e.targetFilePath.includes('models'),
|
||||
);
|
||||
expect(addressSave).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Kotlin
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Field type resolution (Kotlin)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'kotlin-field-types'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects classes: Address, User', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'User']);
|
||||
});
|
||||
|
||||
it('detects Property nodes for Kotlin properties', () => {
|
||||
const properties = getNodesByLabel(result, 'Property');
|
||||
// Kotlin property_declaration was already captured pre-Phase 8
|
||||
expect(properties).toContain('address');
|
||||
expect(properties).toContain('name');
|
||||
expect(properties).toContain('city');
|
||||
});
|
||||
|
||||
it('emits HAS_METHOD edges linking properties to classes', () => {
|
||||
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
||||
const propEdges = hasMethod.filter(e => e.targetLabel === 'Property');
|
||||
expect(propEdges.length).toBeGreaterThanOrEqual(3);
|
||||
expect(edgeSet(propEdges)).toContain('User → address');
|
||||
expect(edgeSet(propEdges)).toContain('User → name');
|
||||
expect(edgeSet(propEdges)).toContain('Address → city');
|
||||
});
|
||||
|
||||
it('resolves user.address.save() → Address#save via field type', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCalls = calls.filter(e => e.target === 'save');
|
||||
const addressSave = saveCalls.find(
|
||||
e => e.source === 'processUser' && e.targetFilePath.includes('Models'),
|
||||
);
|
||||
expect(addressSave).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PHP
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Field type resolution (PHP)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'php-field-types'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects classes: Address, Service, User', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'Service', 'User']);
|
||||
});
|
||||
|
||||
it('detects Property nodes for PHP properties', () => {
|
||||
const properties = getNodesByLabel(result, 'Property');
|
||||
// PHP property_declaration was already captured pre-Phase 8
|
||||
expect(properties).toContain('address');
|
||||
expect(properties).toContain('name');
|
||||
expect(properties).toContain('city');
|
||||
});
|
||||
|
||||
it('emits HAS_METHOD edges linking properties to classes', () => {
|
||||
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
||||
const propEdges = hasMethod.filter(e => e.targetLabel === 'Property');
|
||||
expect(propEdges.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
|
||||
it('resolves $user->address->save() → Address#save via field type', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCalls = calls.filter(e => e.target === 'save');
|
||||
const addressSave = saveCalls.find(
|
||||
e => e.source === 'processUser' && e.targetFilePath.includes('Models'),
|
||||
);
|
||||
expect(addressSave).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue