GitNexus/gitnexus/test/unit/scope-resolution/php/php-import-target.test.ts
Gergo Magyar 6b013e7d30 fix(scan): lock in Rust publish order and guard PHP suffix roots
Adds the missing #2481 Rust regression test (importer before definer),
fails closed when a PHP namespace suffix matches directories under
different roots, and points the baselines note at #2481/#2482.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 08:42:07 +00:00

159 lines
5.1 KiB
TypeScript

import type { ParsedFile, ParsedImport, SymbolDefinition } from 'gitnexus-shared';
import { describe, expect, it } from 'vitest';
import type { ComposerConfig } from '../../../../src/core/ingestion/language-config.js';
import { resolvePhpImportTargetInternal } from '../../../../src/core/ingestion/languages/php/import-target.js';
const composerConfig: ComposerConfig = { psr4: new Map([['App', 'app']]) };
function parsedFile(filePath: string, definitions: readonly SymbolDefinition[]): ParsedFile {
return { filePath, localDefs: definitions } as ParsedFile;
}
function definition(
filePath: string,
type: SymbolDefinition['type'],
name: string,
): SymbolDefinition {
return {
nodeId: `def:${filePath}:${type}:${name}`,
filePath,
type,
qualifiedName: name,
};
}
const functionImport: ParsedImport = {
kind: 'named',
localName: 'getUser',
importedName: 'getUser',
targetRaw: 'App\\Models\\getUser',
importedSymbolKind: 'function',
};
describe('resolvePhpImportTargetInternal declaration selection', () => {
it('finds a unique function declaration when the symbol name is not a filename', () => {
const user = '/repo/app/Models/User.php';
const factory = '/repo/app/Models/UserFactory.php';
const parsedFiles = [
parsedFile(user, [definition(user, 'Class', 'User')]),
parsedFile(factory, [definition(factory, 'Function', 'getUser')]),
];
expect(
resolvePhpImportTargetInternal(
functionImport.targetRaw,
'/repo/app/Main.php',
new Set(parsedFiles.map((parsed) => parsed.filePath)),
composerConfig,
{ parsedFiles, parsedImport: functionImport },
),
).toBe(factory);
});
it('reuses directory selection without leaking candidates across namespaces', () => {
const models = '/repo/app/Models/functions.php';
const services = '/repo/app/Services/functions.php';
const parsedFiles = [
parsedFile(models, [definition(models, 'Function', 'getUser')]),
parsedFile(services, [definition(services, 'Function', 'getUser')]),
];
const first = resolvePhpImportTargetInternal(
functionImport.targetRaw,
'/repo/app/Main.php',
new Set(parsedFiles.map((parsed) => parsed.filePath)),
composerConfig,
{ parsedFiles, parsedImport: functionImport },
);
const second = resolvePhpImportTargetInternal(
functionImport.targetRaw,
'/repo/app/Main.php',
new Set(parsedFiles.map((parsed) => parsed.filePath)),
composerConfig,
{ parsedFiles, parsedImport: functionImport },
);
expect(first).toBe(models);
expect(second).toBe(models);
});
it('fails closed when the namespace has duplicate function declarations', () => {
const first = '/repo/app/Models/First.php';
const second = '/repo/app/Models/Second.php';
const parsedFiles = [
parsedFile(first, [definition(first, 'Function', 'getUser')]),
parsedFile(second, [definition(second, 'Function', 'getUser')]),
];
expect(
resolvePhpImportTargetInternal(
functionImport.targetRaw,
'/repo/app/Main.php',
new Set(parsedFiles.map((parsed) => parsed.filePath)),
composerConfig,
{ parsedFiles, parsedImport: functionImport },
),
).toBeNull();
});
it('never resolves into a different root that shares a directory suffix', () => {
const app = '/repo/app/Models/functions.php';
const vendor = '/repo/vendor/pkg/app/Models/helpers.php';
const parsedFiles = [
parsedFile(app, []),
parsedFile(vendor, [definition(vendor, 'Function', 'getUser')]),
];
const result = resolvePhpImportTargetInternal(
functionImport.targetRaw,
'/repo/app/Main.php',
new Set(parsedFiles.map((parsed) => parsed.filePath)),
composerConfig,
{ parsedFiles, parsedImport: functionImport },
);
expect(result).not.toBe(vendor);
});
it('stays out of suffix-colliding roots even when both declare the function', () => {
const app = '/repo/app/Models/functions.php';
const vendor = '/repo/vendor/pkg/app/Models/helpers.php';
const parsedFiles = [
parsedFile(app, [definition(app, 'Function', 'getUser')]),
parsedFile(vendor, [definition(vendor, 'Function', 'getUser')]),
];
const result = resolvePhpImportTargetInternal(
functionImport.targetRaw,
'/repo/app/Main.php',
new Set(parsedFiles.map((parsed) => parsed.filePath)),
composerConfig,
{ parsedFiles, parsedImport: functionImport },
);
expect(result).not.toBe(vendor);
});
it('resolves a constant only when its namespace directory has one candidate file', () => {
const constants = '/repo/app/Config/constants.php';
const parsedFiles = [parsedFile(constants, [])];
const parsedImport: ParsedImport = {
kind: 'named',
localName: 'MAX_RETRIES',
importedName: 'MAX_RETRIES',
targetRaw: 'App\\Config\\MAX_RETRIES',
importedSymbolKind: 'const',
};
expect(
resolvePhpImportTargetInternal(
parsedImport.targetRaw,
'/repo/app/Main.php',
new Set([constants]),
composerConfig,
{ parsedFiles, parsedImport },
),
).toBe(constants);
});
});