mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-28 06:22:22 +00:00
163 lines
5.9 KiB
TypeScript
163 lines
5.9 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { vueProvider } from '../../../../src/core/ingestion/languages/vue.js';
|
|
import { getTsParser } from '../../../../src/core/ingestion/languages/typescript/query.js';
|
|
import { extractParsedFile } from '../../../../src/core/ingestion/scope-extractor-bridge.js';
|
|
import { extractVueScript } from '../../../../src/core/ingestion/vue-sfc-extractor.js';
|
|
import {
|
|
getRelationships,
|
|
runPipelineFromRepo,
|
|
writeFixtureRepo,
|
|
type PipelineResult,
|
|
} from '../../../integration/resolvers/helpers.js';
|
|
|
|
const sources = Object.fromEntries(
|
|
['jsx', 'tsx', 'ts'].map((lang) => [
|
|
lang,
|
|
`<script setup lang="${lang}">
|
|
import { Widget } from './target';
|
|
function setupCaller() { const ns = require('./target'); return ns.run(); }
|
|
${lang === 'ts' ? '' : `function render(value${lang === 'tsx' ? ': number' : ''}) { return <Widget value={value} />; }`}
|
|
</script>
|
|
<template>
|
|
<div />
|
|
</template>
|
|
<script lang="${lang}">
|
|
function normalCaller() { const ns = require('./target'); return ns.run(); }
|
|
</script>`,
|
|
]),
|
|
);
|
|
|
|
describe('Vue embedded script grammar and source positions', () => {
|
|
let repo: string;
|
|
let result: PipelineResult;
|
|
beforeAll(async () => {
|
|
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-vue-embedded-'));
|
|
writeFixtureRepo(repo, {
|
|
'target.ts':
|
|
'export function run() { return 1; }\nexport function Widget(value) { return value; }',
|
|
...Object.fromEntries(
|
|
Object.entries(sources).map(([lang, source]) => [`${lang}.vue`, source]),
|
|
),
|
|
});
|
|
result = await runPipelineFromRepo(repo, () => {}, {
|
|
workerPoolSize: 1,
|
|
skipGraphPhases: true,
|
|
pdg: true,
|
|
});
|
|
}, 120_000);
|
|
afterAll(() => fs.rmSync(repo, { recursive: true, force: true }));
|
|
|
|
it.each(['jsx', 'tsx', 'ts'])(
|
|
'maps reordered %s script declarations to original source rows in both paths',
|
|
(lang) => {
|
|
const source = sources[lang];
|
|
const extracted = extractVueScript(source)!;
|
|
expect(extracted.lang).toBe(lang);
|
|
expect(extracted.sourceLineMap).toHaveLength(extracted.scriptContent.split('\n').length);
|
|
const full = extractParsedFile(vueProvider, source, `${lang}.vue`)!;
|
|
const worker = extractParsedFile(
|
|
vueProvider,
|
|
extracted.scriptContent,
|
|
`${lang}.vue`,
|
|
undefined,
|
|
undefined,
|
|
'pre-extracted-script',
|
|
undefined,
|
|
extracted.lineOffset,
|
|
extracted.lang,
|
|
extracted.sourceLineMap,
|
|
)!;
|
|
expect(worker.localDefs).toEqual(full.localDefs);
|
|
for (const name of ['setupCaller', 'normalCaller']) {
|
|
const originalRow = source
|
|
.split('\n')
|
|
.findIndex((line) => line.startsWith(`function ${name}`));
|
|
const definition = full.localDefs.find((def) => def.qualifiedName === name)!;
|
|
expect(definition.graphPosition).toEqual({ startLine: originalRow + 1, startCol: 0 });
|
|
const graphNode = result.graph.getNode(`Function:${lang}.vue:${name}`)!;
|
|
expect(graphNode.properties.startLine).toBe(originalRow);
|
|
const blocks = [...result.graph.iterNodes()].filter(
|
|
(node) =>
|
|
node.label === 'BasicBlock' &&
|
|
node.properties.filePath === `${lang}.vue` &&
|
|
node.properties.text?.includes('ns.run()'),
|
|
);
|
|
expect(blocks.some((block) => block.properties.startLine === originalRow + 1)).toBe(true);
|
|
expect(
|
|
blocks.find((block) => block.properties.startLine === originalRow + 1)?.properties
|
|
.calleeIds,
|
|
).toBe('Function:target.ts:run');
|
|
expect(
|
|
getRelationships(result, 'CALLS')
|
|
.filter(
|
|
(edge) =>
|
|
edge.sourceFilePath === `${lang}.vue` &&
|
|
edge.source === name &&
|
|
edge.target === 'run',
|
|
)
|
|
.map((edge) => edge.rel.targetId),
|
|
).toEqual(['Function:target.ts:run']);
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each(['jsx', 'tsx'])(
|
|
'retains %s component calls through direct extraction and real workers',
|
|
(lang) => {
|
|
const parsed = extractParsedFile(vueProvider, sources[lang], `${lang}.vue`)!;
|
|
expect(
|
|
parsed.referenceSites.some((site) => site.name === 'Widget' && site.kind === 'call'),
|
|
).toBe(true);
|
|
expect(
|
|
getRelationships(result, 'CALLS')
|
|
.filter(
|
|
(edge) =>
|
|
edge.sourceFilePath === `${lang}.vue` &&
|
|
edge.source === 'render' &&
|
|
edge.target === 'Widget',
|
|
)
|
|
.map((edge) => edge.rel.targetId),
|
|
).toEqual(['Function:target.ts:Widget']);
|
|
},
|
|
);
|
|
|
|
it('reparses a cached TypeScript tree for a TSX embedded script', () => {
|
|
const extracted = extractVueScript(sources.tsx)!;
|
|
const wrongTree = getTsParser('wrong.ts').parse(extracted.scriptContent);
|
|
const parsed = extractParsedFile(
|
|
vueProvider,
|
|
extracted.scriptContent,
|
|
'tsx.vue',
|
|
undefined,
|
|
wrongTree,
|
|
'pre-extracted-script',
|
|
undefined,
|
|
extracted.lineOffset,
|
|
extracted.lang,
|
|
extracted.sourceLineMap,
|
|
)!;
|
|
expect(
|
|
parsed.referenceSites.some((site) => site.name === 'Widget' && site.kind === 'call'),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('selects TSX when TypeScript and JSX blocks are combined', () => {
|
|
expect(
|
|
extractVueScript(
|
|
'<script lang="ts">const n: number = 1;</script><script setup lang="jsx">const node = <Widget />;</script>',
|
|
)!.lang,
|
|
).toBe('tsx');
|
|
});
|
|
|
|
it('rejects a native TypeScript tree when extracting JSX scope captures', () => {
|
|
const extracted = extractVueScript(sources.jsx)!;
|
|
const wrongTree = getTsParser('wrong.ts').parse(extracted.scriptContent);
|
|
const parsed = extractParsedFile(vueProvider, sources.jsx, 'jsx.vue', undefined, wrongTree)!;
|
|
expect(
|
|
parsed.referenceSites.some((site) => site.name === 'Widget' && site.kind === 'call'),
|
|
).toBe(true);
|
|
});
|
|
});
|