mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-29 06:32:28 +00:00
* fix(vue): F89 JSDoc fix, F90 dual-script merge, F92 lang plumbing (#1936) * fix(vue): reviewer fixes — P1 lang routing, P2 lineOffset, P2/P3 pipeline tests * fix(vue): add jsx to lang routing condition * fix(vue): update F90/F92 fixtures and test assertions for CI --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
This commit is contained in:
parent
2bbb8bf5de
commit
4231c96018
7 changed files with 170 additions and 19 deletions
|
|
@ -22,6 +22,7 @@
|
|||
import type { CaptureMatch } from 'gitnexus-shared';
|
||||
import { extractVueScript } from '../../vue-sfc-extractor.js';
|
||||
import { emitTsScopeCaptures } from '../typescript/captures.js';
|
||||
import { emitJsScopeCaptures } from '../javascript/captures.js';
|
||||
|
||||
/**
|
||||
* Emit scope captures for a Vue SFC.
|
||||
|
|
@ -31,11 +32,11 @@ import { emitTsScopeCaptures } from '../typescript/captures.js';
|
|||
* 1. **Full SFC content** (sequential path, <15 files): `sourceText`
|
||||
* contains the whole `.vue` file with `<template>`, `<script>`, etc.
|
||||
* `extractVueScript` extracts the script block and we delegate to
|
||||
* `emitTsScopeCaptures` with that extracted content.
|
||||
* `emitTsScopeCaptures` or `emitJsScopeCaptures` based on `lang`.
|
||||
*
|
||||
* 2. **Already-extracted script content** (worker-mode path, ≥15 files):
|
||||
* the parse worker calls `extractVueScript` itself before calling
|
||||
* `extractParsedFile`, so `sourceText` is already the bare TypeScript
|
||||
* `extractParsedFile`, so `sourceText` is already the bare script
|
||||
* text with no `<script>` tags. The caller marks this explicitly via
|
||||
* `sourceMeta.sourceKind === 'pre-extracted-script'`.
|
||||
*
|
||||
|
|
@ -48,7 +49,7 @@ export function emitVueScopeCaptures(
|
|||
sourceText: string,
|
||||
filePath: string,
|
||||
cachedTree?: unknown,
|
||||
sourceMeta?: { sourceKind?: 'full-file' | 'pre-extracted-script' },
|
||||
sourceMeta?: { sourceKind?: 'full-file' | 'pre-extracted-script'; setupLang?: string },
|
||||
): readonly CaptureMatch[] {
|
||||
// Vue resolver may include supporting TS/JS files in the same run to
|
||||
// preserve cross-file import/type context for `.vue` callers. These are
|
||||
|
|
@ -58,10 +59,19 @@ export function emitVueScopeCaptures(
|
|||
}
|
||||
|
||||
if (sourceMeta?.sourceKind === 'pre-extracted-script') {
|
||||
// Worker-mode path: the parse worker always uses TypeScript grammar for
|
||||
// .vue files. Lang-based grammar selection is a sequential-path feature.
|
||||
return emitTsScopeCaptures(sourceText, filePath, cachedTree);
|
||||
}
|
||||
|
||||
const extracted = extractVueScript(sourceText);
|
||||
if (extracted === null) return [];
|
||||
|
||||
// Select captures based on script lang attribute.
|
||||
// Use TS grammar unless ALL blocks explicitly request JS/JSX.
|
||||
// Mixed-lang: TS handles JS natively; JS grammar chokes on TS syntax.
|
||||
if (extracted.lang === 'js' || extracted.lang === 'jsx') {
|
||||
return emitJsScopeCaptures(extracted.scriptContent, filePath, cachedTree);
|
||||
}
|
||||
return emitTsScopeCaptures(extracted.scriptContent, filePath, cachedTree);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,13 @@
|
|||
export interface VueScriptExtraction {
|
||||
/** Extracted script content (TypeScript/JavaScript) */
|
||||
scriptContent: string;
|
||||
/** 0-based line number in the .vue file where the script content starts */
|
||||
/** 1-based line number in the .vue file where the script content starts
|
||||
* (used as offset from tree-sitter's 0-based row). */
|
||||
lineOffset: number;
|
||||
/** true if the primary block is <script setup> */
|
||||
/** true if at least one block is <script setup> */
|
||||
isSetup: boolean;
|
||||
/** Value of the `lang` attribute on the extracted script block (e.g. "ts", "js", "tsx", "jsx", or "" for default). */
|
||||
lang: string;
|
||||
}
|
||||
|
||||
interface ScriptBlock {
|
||||
|
|
@ -238,11 +241,9 @@ function parseScriptBlock(
|
|||
/**
|
||||
* Extract script content from a Vue SFC.
|
||||
*
|
||||
* When both <script> and <script setup> are present, returns only the
|
||||
* <script setup> block (the dominant pattern — 94% of Vue files in real
|
||||
* projects use setup). The <script> (non-setup) block typically contains
|
||||
* only `defineOptions` or legacy option merges and is less important for
|
||||
* the knowledge graph.
|
||||
* When both <script> and <script setup> are present, the content of all
|
||||
* blocks is combined (non-setup first, then setup) so the full script
|
||||
* surface is available to the knowledge graph.
|
||||
*/
|
||||
export function extractVueScript(vueContent: string): VueScriptExtraction | null {
|
||||
const blocks: ScriptBlock[] = [];
|
||||
|
|
@ -257,14 +258,22 @@ export function extractVueScript(vueContent: string): VueScriptExtraction | null
|
|||
|
||||
if (blocks.length === 0) return null;
|
||||
|
||||
// Prefer <script setup> if present
|
||||
const setupBlock = blocks.find((b) => b.isSetup);
|
||||
const primary = setupBlock ?? blocks[0];
|
||||
// Merge all blocks: non-setup first, then setup, so content order is stable.
|
||||
const nonSetupBlocks = blocks.filter((b) => !b.isSetup);
|
||||
const setupBlocks = blocks.filter((b) => b.isSetup);
|
||||
const ordered = [...nonSetupBlocks, ...setupBlocks];
|
||||
const combinedContent = ordered.map((b) => b.content).join('\n');
|
||||
const lineOffset = ordered[0].lineOffset;
|
||||
// Only use JavaScript grammar when ALL blocks are explicitly lang="js"
|
||||
// or lang="jsx". If any block omits lang or uses ts/tsx, TypeScript wins.
|
||||
const allBlocksJs = blocks.length > 0 && blocks.every((b) => b.lang === 'js' || b.lang === 'jsx');
|
||||
const lang = allBlocksJs ? 'js' : '';
|
||||
|
||||
return {
|
||||
scriptContent: primary.content,
|
||||
lineOffset: primary.lineOffset,
|
||||
isSetup: primary.isSetup,
|
||||
scriptContent: combinedContent,
|
||||
lineOffset,
|
||||
isSetup: blocks.some((b) => b.isSetup),
|
||||
lang,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
8
gitnexus/test/fixtures/lang-resolution/js-parsing-coverage/class-expression.ts
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/js-parsing-coverage/class-expression.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// F44: Class expression — should produce a Class scope for the expression body
|
||||
// On main: no @scope.class emitted → methods have no Class parent
|
||||
// On this branch: (class) @scope.class emitted → methods get Class scope
|
||||
export const instance = class {
|
||||
greet(): string {
|
||||
return 'hello';
|
||||
}
|
||||
};
|
||||
13
gitnexus/test/fixtures/vue-scope/vue-dual-script/App.vue
vendored
Normal file
13
gitnexus/test/fixtures/vue-scope/vue-dual-script/App.vue
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<div>Hello</div>
|
||||
</template>
|
||||
<script>
|
||||
function legacySetup() {
|
||||
return { count: 0 };
|
||||
}
|
||||
</script>
|
||||
<script setup lang="ts">
|
||||
function setupInit() {
|
||||
return 42;
|
||||
}
|
||||
</script>
|
||||
8
gitnexus/test/fixtures/vue-scope/vue-js-lang/App.vue
vendored
Normal file
8
gitnexus/test/fixtures/vue-scope/vue-js-lang/App.vue
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<template>
|
||||
<div>Hello</div>
|
||||
</template>
|
||||
<script lang="js">
|
||||
function greet(name) {
|
||||
return 'Hello ' + name;
|
||||
}
|
||||
</script>
|
||||
|
|
@ -473,3 +473,38 @@ describe('Vue cross-file composable and class resolution', () => {
|
|||
expect(files.some((f) => f.endsWith('models.ts'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// F90 — dual-script merge (<script> + <script setup>)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('F90 — dual-script (<script> + <script setup>)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(VUE_SCOPE_FIXTURES, 'vue-dual-script'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('includes symbols from both the non-setup and setup blocks', () => {
|
||||
const funcs = getNodesByLabel(result, 'Function');
|
||||
expect(funcs).toContain('legacySetup');
|
||||
expect(funcs).toContain('setupInit');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// F92 — JS-lang script block (<script lang="js">)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('F92 — JS-lang script block', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(VUE_SCOPE_FIXTURES, 'vue-js-lang'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('parses <script lang="js"> content and finds functions', () => {
|
||||
const funcs = getNodesByLabel(result, 'Function');
|
||||
expect(funcs).toContain('greet');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -45,13 +45,12 @@ export default {
|
|||
expect(result!.scriptContent).toContain('export default');
|
||||
});
|
||||
|
||||
it('prefers <script setup> when both blocks exist', () => {
|
||||
it('combines both blocks when <script> and <script setup> both exist', () => {
|
||||
const vue = `<script lang="ts">
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
const name = ref('test');
|
||||
|
|
@ -61,9 +60,78 @@ const name = ref('test');
|
|||
`;
|
||||
const result = extractVueScript(vue);
|
||||
expect(result).not.toBeNull();
|
||||
// F90: both blocks are combined
|
||||
expect(result!.isSetup).toBe(true);
|
||||
expect(result!.scriptContent).toContain("const name = ref('test')");
|
||||
expect(result!.scriptContent).not.toContain('inheritAttrs');
|
||||
expect(result!.scriptContent).toContain('inheritAttrs');
|
||||
});
|
||||
|
||||
it('returns lang from the script block', () => {
|
||||
const vue = `<script lang="js">
|
||||
export default {};
|
||||
</script>
|
||||
`;
|
||||
const result = extractVueScript(vue);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.lang).toBe('js');
|
||||
});
|
||||
|
||||
it('returns empty lang when no lang attribute', () => {
|
||||
const vue = `<script>
|
||||
export default {};
|
||||
</script>
|
||||
`;
|
||||
const result = extractVueScript(vue);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.lang).toBe('');
|
||||
});
|
||||
|
||||
it('jsx lang triggers JS grammar (maps to lang=js)', () => {
|
||||
const vue = `<script lang="jsx">
|
||||
export default {};
|
||||
</script>
|
||||
`;
|
||||
const result = extractVueScript(vue);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.lang).toBe('js');
|
||||
});
|
||||
|
||||
it('ts lang returns empty (only js/jsx triggers JS grammar)', () => {
|
||||
const vue = `<script lang="ts">
|
||||
export default {};
|
||||
</script>
|
||||
`;
|
||||
const result = extractVueScript(vue);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.lang).toBe('');
|
||||
});
|
||||
|
||||
it('mixed js + ts blocks return empty lang (TypeScript wins)', () => {
|
||||
const vue = `<script lang="js">
|
||||
export default {};
|
||||
</script>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
</script>
|
||||
`;
|
||||
const result = extractVueScript(vue);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.lang).toBe('');
|
||||
});
|
||||
|
||||
it('isSetup is true when at least one block is setup', () => {
|
||||
const vue = `<script lang="ts">
|
||||
export default {};
|
||||
</script>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
</script>
|
||||
`;
|
||||
const result = extractVueScript(vue);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.isSetup).toBe(true);
|
||||
// ts blocks — lang should be empty
|
||||
expect(result!.lang).toBe('');
|
||||
});
|
||||
|
||||
it('returns null for .vue files with no <script> block', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue