GitNexus/gitnexus/test/integration/qualified-class-lookups.test.ts
Andrey Golanov 63f2f8e590
fix(ingestion): retain named class-expression owners (#3725)
* fix(ingestion): retain class-expression owners

Keep named JS/TS expressions, members and scope declarations on matching
graph identities. Distinguish expressions that reuse a local class name.
Avoid root enumeration when no file-scope types are configured.

1088 focused tests pass. Full suite: 28407 passed, one pre-existing
SSH-signing-sensitive golden failure, 91 skipped.

* fix(ingestion): preserve type-alias owners

Keep method-local type-alias member edges on the existing simple
TypeAlias IDs when JS/TS class qualification is enabled. Otherwise
native COPY drops both method and property relationships.

Cover TS, TSX and Vue workers plus direct native-persistence reads.

Refs #3725

* fix(bench): align JS/TS capture baselines

Record the fingerprints introduced by qualified class and member identities. Same-corpus comparisons attribute every changed byte to the new qualified-name tags; removing those tags restores both previous hashes. Keep capture counts and scaling budgets unchanged.

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
2026-10-26 07:57:55 +00:00

94 lines
3.6 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { parseFilesWithWorkers } from '../helpers/worker-parse.js';
describe('qualified class lookups', () => {
it('preserves nested names with and without file-level package scopes', async () => {
const { graph, model } = await parseFilesWithWorkers([
{
path: 'nested.ts',
content:
'class Outer { make() { class Inner { value = 1; run() { return this.value; } } return Inner; } }',
},
{
path: 'Nested.java',
content: 'package example.models; class Outer { class Inner {} }',
},
{
path: 'nested.rb',
content: 'module Root\n class Outer\n class Inner\n end\n end\nend\n',
},
]);
for (const name of ['Outer.Inner', 'example.models.Outer.Inner', 'Root.Outer.Inner']) {
expect(model.types.lookupClassByQualifiedName(name)).toHaveLength(1);
}
const ownerId = 'Class:nested.ts:Outer.Inner';
expect(graph.getNode(ownerId)).toBeDefined();
const members = graph.relationships.filter(
(edge) =>
edge.sourceId === ownerId && (edge.type === 'HAS_METHOD' || edge.type === 'HAS_PROPERTY'),
);
expect(members.map((edge) => graph.getNode(edge.targetId)?.properties.name).sort()).toEqual([
'run',
'value',
]);
for (const edge of members) expect(graph.getNode(edge.targetId)).toBeDefined();
});
it('derives canonical dot-separated names from namespaces, packages, and modules', async () => {
const { model } = await parseFilesWithWorkers([
{
path: 'src/Services/User.cs',
content: 'namespace Services.Auth;\npublic class User {}\n',
},
{
path: 'src/Data/User.cs',
content: 'namespace Data.Auth;\npublic class User {}\n',
},
{
path: 'src/models/Config.java',
content: 'package com.example.models;\nclass Config {}\n',
},
{
path: 'lib/admin/user.rb',
content: 'module Admin\n class User\n end\nend\n',
},
]);
const userMatches = model.types.lookupClassByName('User');
expect(userMatches).toHaveLength(3);
expect(userMatches.map((match) => match.qualifiedName).sort()).toEqual(
['Admin.User', 'Data.Auth.User', 'Services.Auth.User'].sort(),
);
const servicesUser = model.types.lookupClassByQualifiedName('Services.Auth.User');
expect(servicesUser).toHaveLength(1);
expect(servicesUser[0].filePath).toBe('src/Services/User.cs');
expect(servicesUser[0].qualifiedName).toBe('Services.Auth.User');
const dataUser = model.types.lookupClassByQualifiedName('Data.Auth.User');
expect(dataUser).toHaveLength(1);
expect(dataUser[0].filePath).toBe('src/Data/User.cs');
const javaConfig = model.types.lookupClassByQualifiedName('com.example.models.Config');
expect(javaConfig).toHaveLength(1);
expect(javaConfig[0].qualifiedName).toBe('com.example.models.Config');
const rubyUser = model.types.lookupClassByQualifiedName('Admin.User');
expect(rubyUser).toHaveLength(1);
expect(rubyUser[0].qualifiedName).toBe('Admin.User');
});
it('falls back to the simple name for top-level class-like symbols', async () => {
const { model } = await parseFilesWithWorkers([
{ path: 'src/plain-user.ts', content: 'export class User {}\n' },
]);
const simpleMatches = model.types.lookupClassByName('User');
expect(simpleMatches).toHaveLength(1);
expect(simpleMatches[0].qualifiedName).toBe('User');
const matches = model.types.lookupClassByQualifiedName('User');
expect(matches).toHaveLength(1);
expect(matches[0].qualifiedName).toBe('User');
});
});