-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
feat(vue): migrate Vue SFC to scope-based resolution (RFC #909 Ring 3, closes #940) #1950
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
magyargergo
merged 11 commits into
abhigyanpatwari:main
from
ReidenXerx:feat/vue-scope-resolution-940
Jun 3, 2026
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
d39ee5a
feat(vue): migrate Vue SFC to scope-based resolution (RFC #909 Ring 3…
ReidenXerx b0b6021
fix(vue): address P0/P1 review findings from #1950
ReidenXerx 85f2b51
feat(vue): template-derived edges + pipeline benchmark (#1950 review)
ReidenXerx d0ff425
feat(vue): BINDS_EVENT_HANDLER/EMITS_EVENT edges via ScopeResolver hook
ReidenXerx 2469ed0
fix(vue): close review gaps in scope/parity extraction
ReidenXerx 04a4b0d
fix(vue): address second review round — regex safety, emit coverage, …
ReidenXerx 3aba2f0
fix(vue): eliminate double file-read and per-file template re-scans
ReidenXerx ea63275
Merge branch 'main' into feat/vue-scope-resolution-940
magyargergo 4a75665
fix(parity): exclude TypeScript HOC/HOF/JSX scope-resolver tests from…
ReidenXerx abfd824
Merge branch 'main' into feat/vue-scope-resolution-940
magyargergo b9efb5a
chore(test): remove registry-primary-flag unit tests after migration …
magyargergo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| /** | ||
| * Vue SFC scope captures (RFC #909 Ring 3, issue #940). | ||
| * | ||
| * Extracts the `<script>` / `<script setup>` block from the SFC source | ||
| * and delegates to `emitTsScopeCaptures`. The parse-worker builds the | ||
| * cached tree from the extracted script content using the TypeScript | ||
| * grammar (see `[SupportedLanguages.Vue]: TypeScript.typescript` in | ||
| * `parse-worker.ts`), so passing that tree here keeps grammar identity | ||
| * consistent and avoids a redundant re-parse. | ||
| * | ||
| * Template expressions are intentionally out-of-scope: component- | ||
| * reference CALLS edges are already emitted by the legacy template | ||
| * extractor in the parse worker and would be double-counted here. | ||
| * | ||
| * Position note: all capture positions are relative to the *extracted* | ||
| * script block, not the full .vue file. This is consistent with the | ||
| * cached tree and with how the scope model uses positions (only for | ||
| * scope-containment walks within a single file), so no offset | ||
| * translation is required for graph-edge correctness. | ||
| */ | ||
|
|
||
| import type { CaptureMatch } from 'gitnexus-shared'; | ||
| import { extractVueScript } from '../../vue-sfc-extractor.js'; | ||
| import { emitTsScopeCaptures } from '../typescript/captures.js'; | ||
|
|
||
| /** | ||
| * Emit scope captures for a Vue SFC. | ||
| * | ||
| * Handles three call-site shapes: | ||
| * | ||
| * 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. | ||
| * | ||
| * 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 | ||
| * text with no `<script>` tags. The caller marks this explicitly via | ||
| * `sourceMeta.sourceKind === 'pre-extracted-script'`. | ||
| * | ||
| * 3. **Supporting TS/JS files** included in Vue scope-resolution runs: | ||
| * when `filePath` is not `.vue`, delegate straight to TypeScript captures. | ||
| * | ||
| * Returns an empty array for render-function-only SFCs (no `<script>` block). | ||
| */ | ||
| export function emitVueScopeCaptures( | ||
| sourceText: string, | ||
| filePath: string, | ||
| cachedTree?: unknown, | ||
| sourceMeta?: { sourceKind?: 'full-file' | 'pre-extracted-script' }, | ||
| ): 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 | ||
| // already plain script files, so no SFC extraction is needed. | ||
| if (!filePath.endsWith('.vue')) { | ||
| return emitTsScopeCaptures(sourceText, filePath, cachedTree); | ||
| } | ||
|
|
||
| if (sourceMeta?.sourceKind === 'pre-extracted-script') { | ||
| return emitTsScopeCaptures(sourceText, filePath, cachedTree); | ||
| } | ||
|
|
||
| const extracted = extractVueScript(sourceText); | ||
| if (extracted === null) return []; | ||
| return emitTsScopeCaptures(extracted.scriptContent, filePath, cachedTree); | ||
| } |
81 changes: 81 additions & 0 deletions
81
gitnexus/src/core/ingestion/languages/vue/import-target.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| /** | ||
| * Import-target resolver for Vue SFCs (RFC #909 Ring 3, issue #940). | ||
| * | ||
| * Vue `<script>` / `<script setup>` blocks are TypeScript (or plain | ||
| * JavaScript), so the resolver delegates to `resolveTsTarget` with | ||
| * `language: SupportedLanguages.TypeScript` to get: | ||
| * | ||
| * - tsconfig path-alias rewriting (Vue projects universally use TS) | ||
| * - `.ts` / `.tsx` / `.js` / `.jsx` extension-suffix fallback | ||
| * | ||
| * `.vue` imports are written with explicit extensions (`'./Button.vue'`), | ||
| * so no Vue-specific suffix guessing is required: the standard | ||
| * resolver finds them via the exact-path branch before any extension | ||
| * logic fires. | ||
| * | ||
| * Memoization mirrors the TypeScript adapter: workspace file-list | ||
| * arrays, the suffix index, and the per-pass resolve cache are rebuilt | ||
| * lazily when `allFilePaths` reference changes (once per workspace pass). | ||
| */ | ||
|
|
||
| import { SupportedLanguages } from 'gitnexus-shared'; | ||
| import { resolveTsTarget, type TsResolveContext } from '../typescript/import-target.js'; | ||
| import { buildSuffixIndex, type SuffixIndex } from '../../import-resolvers/utils.js'; | ||
| import type { TsconfigPaths } from '../../language-config.js'; | ||
|
|
||
| interface VueResolutionConfig { | ||
| readonly tsconfigPaths: TsconfigPaths | null; | ||
| } | ||
|
|
||
| interface PassCache { | ||
| readonly key: ReadonlySet<string>; | ||
| readonly allFilePaths: Set<string>; | ||
| readonly allFileList: readonly string[]; | ||
| readonly normalizedFileList: readonly string[]; | ||
| readonly index: SuffixIndex; | ||
| readonly resolveCache: Map<string, string | null>; | ||
| } | ||
|
|
||
| /** | ||
| * Build a memoized `resolveImportTarget` adapter for Vue SFCs. | ||
| * | ||
| * Uses `SupportedLanguages.TypeScript` so tsconfig path-alias resolution | ||
| * and `.ts`/`.tsx` extension guessing fire for relative and bare-specifier | ||
| * imports inside `<script>` blocks. | ||
| */ | ||
| export function makeVueResolveImportTarget(): ( | ||
| targetRaw: string, | ||
| fromFile: string, | ||
| allFilePaths: ReadonlySet<string>, | ||
| resolutionConfig?: unknown, | ||
| ) => string | readonly string[] | null { | ||
| let cached: PassCache | null = null; | ||
|
|
||
| return (targetRaw, fromFile, allFilePaths, resolutionConfig) => { | ||
| if (cached === null || cached.key !== allFilePaths) { | ||
| const allFileList = Array.from(allFilePaths); | ||
| const normalizedFileList = allFileList.map((f) => f.toLowerCase()); | ||
| cached = { | ||
| key: allFilePaths, | ||
| allFilePaths: new Set(allFilePaths), | ||
| allFileList, | ||
| normalizedFileList, | ||
| index: buildSuffixIndex(normalizedFileList, allFileList), | ||
| resolveCache: new Map(), | ||
| }; | ||
| } | ||
|
|
||
| const cfg = resolutionConfig as VueResolutionConfig | undefined; | ||
| const ws: TsResolveContext = { | ||
| fromFile, | ||
| language: SupportedLanguages.TypeScript, | ||
| allFilePaths: cached.allFilePaths, | ||
| allFileList: cached.allFileList, | ||
| normalizedFileList: cached.normalizedFileList, | ||
| index: cached.index, | ||
| resolveCache: cached.resolveCache, | ||
| tsconfigPaths: cfg?.tsconfigPaths ?? null, | ||
| }; | ||
| return resolveTsTarget(targetRaw, ws); | ||
| }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| /** | ||
| * Vue SFC scope-resolution hooks (RFC #909 Ring 3, issue #940). | ||
| * | ||
| * Public API barrel. Consumers should import from this file rather | ||
| * than the individual modules. | ||
| * | ||
| * Module layout (each file is a single concern): | ||
| * | ||
| * - `captures.ts` — `emitVueScopeCaptures` — extracts the | ||
| * `<script>` / `<script setup>` block and | ||
| * delegates to `emitTsScopeCaptures` (TypeScript | ||
| * grammar, same grammar the parse-worker uses). | ||
| * - `import-target.ts` — `makeVueResolveImportTarget` — memoized | ||
| * adapter using the TypeScript resolver with | ||
| * tsconfig path-alias support. | ||
| * - `scope-resolver.ts` — `vueScopeResolver` wiring object. | ||
| * | ||
| * ## Known limitations | ||
| * | ||
| * 1. **Template expressions** — Full template AST parsing is not performed. | ||
| * `vueScopeResolver.emitPostResolutionEdges` extracts five categories of | ||
| * template-derived edges via lightweight regex, all emitted after standard | ||
| * scope-resolution passes complete: | ||
| * - PascalCase/kebab-case component references → `vue-template-component` `CALLS` | ||
| * - `@event="handler"` on **native** elements → `vue-template-callback` `CALLS` | ||
| * - `@event="handler"` on **component** elements → `vue-event: @<name>` `BINDS_EVENT_HANDLER` | ||
| * - `emit(...)` / `this.$emit(...)` in script → `vue-emit: <name>` `EMITS_EVENT` | ||
| * - `:prop="varName"` single-identifier bindings → `vue-template-attribute` `ACCESSES` | ||
| * `BINDS_EVENT_HANDLER` and `EMITS_EVENT` are complementary "hanging" edges: | ||
| * a Cypher query joining on the shared component File node reveals which | ||
| * handlers receive which component's emitted events. | ||
| * Complex inline expressions (`@click="toggle(item)"`, `{{ a + b }}`, | ||
| * member-access bindings `:key="post.id"`) are intentionally excluded | ||
| * because they cannot be resolved to a single call/access target without | ||
| * a full template AST. Tracked in #1647. | ||
| * 2. **Options API `this` resolution** — `this.X()` in Options API | ||
| * components does not resolve through type-binding when the component | ||
| * uses a plain object literal rather than a class. `fieldFallbackOnMethodLookup` | ||
| * recovers common cases via field-name matching. | ||
| * 3. **`<script setup>` + `<script>` dual-block** — When both blocks are | ||
| * present, only `<script setup>` is processed (per `extractVueScript` | ||
| * priority). The non-setup block is skipped. | ||
| * 4. **JSX in `<template>`** — Vue's template compiler is not a | ||
| * tree-sitter grammar; JSX-style bindings inside templates are not | ||
| * processed by the scope-resolution pipeline. | ||
| */ | ||
|
|
||
| export { emitVueScopeCaptures } from './captures.js'; | ||
| export { makeVueResolveImportTarget } from './import-target.js'; | ||
| export { vueScopeResolver } from './scope-resolver.js'; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P0 · CI-reproduced]
vueProviderregistersemitScopeCapturesbut omits the four scope-resolution hooks the TypeScript provider supplies —interpretImport,interpretTypeBinding,bindingScopeFor,importOwningScope(typescript.ts:317–320). The legacyimportResolver/namedBindingExtractoron lines 77–78 feed the old DAG path, not the registry-primary scope path.pass3CollectImportsearly-returns wheninterpretImportis undefined (scope-extractor.ts:824), soparsedImportsstays empty → zero IMPORTS and zero cross-file CALLS edges for every Vue file. Reproduced by CI run 26712104950:vue.test.ts4 failures +vue-scope.test.ts20/38. Independently found by Codex and the correctness + adversarial lanes.Fix: add the four hooks (already exported from
languages/typescript) tovueProvider.[reproduced]