Skip to content

feat: add Vue support - #66

Merged
colbymchenry merged 2 commits into
colbymchenry:mainfrom
abhijeetj100:feat/support-vue
May 8, 2026
Merged

feat: add Vue support#66
colbymchenry merged 2 commits into
colbymchenry:mainfrom
abhijeetj100:feat/support-vue

Conversation

@abhijeetj100

@abhijeetj100 abhijeetj100 commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

This pull request adds comprehensive support for Vue Single-File Components (SFCs) to the code extraction and analysis pipeline. It introduces a new VueExtractor class that can parse .vue files, extract component and function nodes from both standard and <script setup> blocks, and integrate them into the existing code relationship graph. Additionally, the PR updates language detection, grammar support, and framework resolution to recognize and process Vue files throughout the system. Extensive tests are included to verify correct extraction behavior for various Vue SFC patterns.

The most important changes are:

Vue Extraction Support

  • Added a new VueExtractor class to parse .vue files, extract component nodes, delegate script parsing to the TypeScript/JavaScript extractor, and create containment edges between components and their script nodes. This enables accurate modeling of Vue SFCs in the code graph.
  • Updated extractFromSource to use the new VueExtractor when a .vue file is detected, ensuring all Vue files are processed with the appropriate logic.

Language and Grammar Integration

  • Updated language detection, extension mapping, and grammar support to recognize .vue files as the vue language, and ensured all relevant utility functions (e.g., isLanguageSupported, isGrammarLoaded, getSupportedLanguages, getLanguageDisplayName) include vue. [1] [2] [3] [4] [5]

Framework Resolution

  • Added a vueResolver to the framework resolution system and included it in the list of available framework resolvers, enabling Vue-specific relationship resolution. [1] [2] [3]

Testing

  • Added a comprehensive suite of tests for Vue extraction, covering detection, extraction from different script block types, template-only components, and containment relationships between component and script/function nodes.

PS: This is my first contribution to the project. Please let me know if any part of this PR deviates from your established standards—I’m happy to make any necessary adjustments to align with the codebase.

CC: @colbymchenry

Related issue: #51

Conflict resolution after rebasing 6 weeks of main onto this PR:

- src/extraction/tree-sitter.ts: main moved Liquid/Svelte/Dfm extractors
  out of this file into per-extractor modules. Did the same for
  VueExtractor — now lives in src/extraction/vue-extractor.ts. Added
  the import + dispatch line was already present in the PR's diff.
- src/types.ts: main converted Language to a runtime-iterable
  `as const` array (LANGUAGES) for parser/registry use. Kept that
  shape and inserted 'vue' between 'svelte' and 'liquid' so the
  contributor's Vue language work continues to typecheck.
- __tests__/extraction.test.ts: kept both the PR's Vue Extraction
  describe block AND main's new Instantiates+Decorates describe
  block; they don't overlap.
- Removed Sentry's captureException call in the extracted
  VueExtractor — sentry was removed from main, so the import would
  have broken the build.

Verified live on real Vue codebases:
  - vuejs/pinia (36 .vue files): 1,019 nodes, 1,033 edges, no errors
  - vuejs/router (111 .vue files): 3,095 nodes, 2,899 edges, no errors
  Component nodes, script-block extraction, and containment edges all
  produce correct output. Full test suite: 423/423 pass (was 416, +7
  Vue tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 8, 2026 02:56
@colbymchenry

Copy link
Copy Markdown
Owner

Reviewed, rebased, and merging — thanks for the contribution, Abhijeet!

Pushed a follow-up commit to your branch resolving the conflicts that accumulated over 6 weeks:

  • src/extraction/tree-sitter.ts had the inline LiquidExtractor/SvelteExtractor/DfmExtractor classes that main has since moved into per-extractor files. Mirrored that for VueExtractor — it now lives in src/extraction/vue-extractor.ts matching the structure of svelte-extractor.ts.
  • src/types.ts was converted to a runtime-iterable LANGUAGES as const array on main; kept that shape and added 'vue' to it.
  • __tests__/extraction.test.ts kept both your Vue Extraction tests and main's new Instantiates+Decorates tests.
  • Removed the captureException call from VueExtractor since Sentry was removed from main.

Validated live on two popular Vue codebases:

  • vuejs/pinia (36 .vue files): 1,019 nodes, 1,033 edges, no errors
  • vuejs/router (111 .vue files): 3,095 nodes, 2,899 edges, no errors

Component nodes, script-block extraction, and containment edges all produce correct output. Full test suite: 423/423 pass (was 416 + your 7 Vue tests).

@colbymchenry
colbymchenry merged commit 5ab8174 into colbymchenry:main May 8, 2026
2 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class Vue Single-File Component (SFC) support to the extraction + resolution pipeline so .vue files can participate in the code relationship graph similarly to other frontend component formats (e.g., Svelte).

Changes:

  • Introduces a VueExtractor that delegates <script> / <script setup> parsing to the existing JS/TS tree-sitter extractor and adds component containment edges.
  • Extends language detection / supported-language utilities to recognize .vue as vue and includes .vue in default include globs.
  • Registers a new vueResolver framework resolver (Vue/Nuxt conventions) and adds Vue extraction tests.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/types.ts Adds vue to supported languages and includes **/*.vue in default config globs.
src/resolution/frameworks/vue.ts New Vue/Nuxt framework resolver for macro/auto-import handling and Nuxt route/middleware heuristics.
src/resolution/frameworks/index.ts Registers and re-exports the new vueResolver.
src/extraction/vue-extractor.ts New Vue SFC extractor delegating script parsing to the existing tree-sitter extractor and emitting containment edges.
src/extraction/tree-sitter.ts Routes .vue files to VueExtractor in extractFromSource.
src/extraction/grammars.ts Maps .vue to vue and marks vue as supported via custom extractor path.
tests/extraction.test.ts Adds Vue extraction tests for detection, node creation, and containment edges.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +197 to +201
// Detect Nuxt page routes (pages/ directory)
const pagesIndex = normalized.indexOf('/pages/');
if (pagesIndex !== -1 && normalized.endsWith('.vue')) {
const routePath = filePathToNuxtRoute(normalized, pagesIndex + '/pages/'.length);
if (routePath !== null) {
Comment on lines +282 to +314
const allFiles = context.getAllFiles();
const vueFiles = allFiles.filter((f) => f.endsWith('.vue'));

// Check for exact name match (Button -> Button.vue)
for (const file of vueFiles) {
const fileName = file.split(/[/\\]/).pop() || '';
const componentName = fileName.replace(/\.vue$/, '');
if (componentName === name) {
const nodes = context.getNodesInFile(file);
const component = nodes.find((n) => n.kind === 'component' && n.name === name);
if (component) {
return component.id;
}
}
}

// Check same directory first for better specificity
const fromDir = fromFile.substring(0, fromFile.lastIndexOf('/'));
for (const file of vueFiles) {
if (file.startsWith(fromDir)) {
const fileName = file.split(/[/\\]/).pop() || '';
const componentName = fileName.replace(/\.vue$/, '');
if (componentName === name) {
const nodes = context.getNodesInFile(file);
const component = nodes.find((n) => n.kind === 'component');
if (component) {
return component.id;
}
}
}
}

return null;
Comment on lines +154 to +172
// Delegate to TreeSitterExtractor
const extractor = new TreeSitterExtractor(this.filePath, block.content, scriptLanguage);
const result = extractor.extract();

// Offset line numbers from script block back to .vue file positions
for (const node of result.nodes) {
node.startLine += block.startLine;
node.endLine += block.startLine;
node.language = 'vue'; // Mark as vue, not TS/JS

this.nodes.push(node);

// Add containment edge from component to this node
this.edges.push({
source: componentNodeId,
target: node.id,
kind: 'contains',
});
}
} catch (error) {
this.errors.push({
message: `Vue extraction error: ${error instanceof Error ? error.message : String(error)}`,
severity: 'error',
colbymchenry added a commit to timomeara/codegraph that referenced this pull request May 8, 2026
Conflict resolution after rebasing main onto this PR:

- src/extraction/tree-sitter.ts: main added VueExtractor (new file
  src/extraction/vue-extractor.ts via colbymchenry#66). The PR's restructured
  if/else chain in extractFromSource gets a new vue branch alongside
  svelte/liquid/dfm so the framework-extract pipeline runs uniformly
  for vue files too.
- src/resolution/frameworks/vue.ts: vue resolver still used the dead
  extractNodes(): Node[] interface that this PR replaced. Migrated to
  extract(): { nodes, references } matching the other 13 resolvers —
  Vue's nuxt route detection (pages/, server/api/, middleware/) keeps
  working, just emits no references (matches react.ts shape).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
colbymchenry added a commit that referenced this pull request May 8, 2026
Followup to #66 — Vue support shipped but the README languages table
was never updated.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jorgerobles pushed a commit to jorgerobles/codegraph that referenced this pull request Jun 1, 2026
Co-authored-by: Colby McHenry <me@colbymchenry.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jorgerobles pushed a commit to jorgerobles/codegraph that referenced this pull request Jun 1, 2026
)

Followup to colbymchenry#66 — Vue support shipped but the README languages table
was never updated.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
inth3shadows added a commit to inth3shadows/codegraph that referenced this pull request Sep 6, 2026
…on.rs

The TS extractor keeps a python call's receiver text as a qualifier when the
receiver is not a plain identifier — an attribute chain (`self.data.append`),
a subscript (`d[k].append`) or a call chain (`d.setdefault(k, []).append`) —
so a bare `append` can never exact-match an unrelated project function of that
name (colbymchenry#66). `codegraph-kernel/src/python.rs` still collapsed all three to the
bare method name.

Python is in the kernel's DEFAULT_ROUTED set and every published bundle ships
the .node, so the TS-only fix never ran where it mattered: on the installed
1.5.0 build, `self.data.append(...)` and `rows["k"].append(2)` both fabricated
a `calls` edge onto an unrelated module-level `append`, while the real
`ledger.append(row)` was missing. A from-source checkout has no .node, so the
existing coverage silently exercised the wasm arm and stayed green.

Mirrored the branch, with a `collapse_js_whitespace` helper rather than
`char::is_whitespace`: the sets differ (U+0085 in one, U+FEFF in the other),
and the parity sweep compares the two arms byte for byte.

torture.py gains the subscript and call-chain shapes; the attribute-chain
shape (`self.registry.lookup`) was already there and is what makes
kernel-tsjs-parity fail without this commit. The new test asserts the
end-to-end property on the kernel arm specifically, and skips when no .node is
staged, like the parity suites.

Verified: kernel-tsjs-parity 17/17 with the mirror, 2 failures without it
(rebuilt both ways); the new suite passes against a freshly built kernel.
inth3shadows added a commit to inth3shadows/codegraph that referenced this pull request Sep 6, 2026
…on.rs

The TS extractor keeps a python call's receiver text as a qualifier when the
receiver is not a plain identifier — an attribute chain (`self.data.append`),
a subscript (`d[k].append`) or a call chain (`d.setdefault(k, []).append`) —
so a bare `append` can never exact-match an unrelated project function of that
name (colbymchenry#66). `codegraph-kernel/src/python.rs` still collapsed all three to the
bare method name.

Python is in the kernel's DEFAULT_ROUTED set and every published bundle ships
the .node, so the TS-only fix never ran where it mattered: on the installed
1.5.0 build, `self.data.append(...)` and `rows["k"].append(2)` both fabricated
a `calls` edge onto an unrelated module-level `append`, while the real
`ledger.append(row)` was missing. A from-source checkout has no .node, so the
existing coverage silently exercised the wasm arm and stayed green.

Mirrored the branch, with a `collapse_js_whitespace` helper rather than
`char::is_whitespace`: the sets differ (U+0085 in one, U+FEFF in the other),
and the parity sweep compares the two arms byte for byte.

torture.py gains the subscript and call-chain shapes; the attribute-chain
shape (`self.registry.lookup`) was already there and is what makes
kernel-tsjs-parity fail without this commit. The new test asserts the
end-to-end property on the kernel arm specifically, and skips when no .node is
staged, like the parity suites.

Verified: kernel-tsjs-parity 17/17 with the mirror, 2 failures without it
(rebuilt both ways); the new suite passes against a freshly built kernel.
inth3shadows added a commit to inth3shadows/codegraph that referenced this pull request Sep 7, 2026
…odule

The escape added for colbymchenry#66 asked only whether SOME import bound the receiver's
local name. Every import produces a mapping — stdlib and PyPI included — so it
was also true for `os`, `requests`, `np`. That opened the built-in-method
filter for them; `resolveViaImport` then found no project file, resolution fell
through to the bare-name strategy, and the call bound to whatever project
method happened to share the name. The escape hatch reintroduced the exact
fabrication class the filter exists to prevent.

Verified before the fix, on a project with `Store.remove` / `Store.get`:

    import os; import requests
    cleanup -> Store.remove   refName "os.remove"
    cleanup -> Store.get      refName "requests.get"

Two wrong edges where 1.6.0 produced none.

The escape now resolves the import specifier and opens only when it names a
file in this project — the same question `resolveViaImport` asks next, so a
receiver that passes is one the qualified path can actually serve. `from . import
mod` and `import pkg.mod as m` are both handled; anything else stays a silent
miss rather than a wrong edge.

`__tests__/python-import-gate.test.ts` pins both directions, and fails on the
first without this change (`['remove@store.py','get@store.py']` vs `[]`).
resolution + extraction + frameworks + kernel parity: 855 tests, all pass.
inth3shadows added a commit to inth3shadows/codegraph that referenced this pull request Sep 7, 2026
…odule

The escape added for colbymchenry#66 asked only whether SOME import bound the receiver's
local name. Every import produces a mapping — stdlib and PyPI included — so it
was also true for `os`, `requests`, `np`. That opened the built-in-method
filter for them; `resolveViaImport` then found no project file, resolution fell
through to the bare-name strategy, and the call bound to whatever project
method happened to share the name. The escape hatch reintroduced the exact
fabrication class the filter exists to prevent.

Verified before the fix, on a project with `Store.remove` / `Store.get`:

    import os; import requests
    cleanup -> Store.remove   refName "os.remove"
    cleanup -> Store.get      refName "requests.get"

Two wrong edges where 1.6.0 produced none.

The escape now resolves the import specifier and opens only when it names a
file in this project — the same question `resolveViaImport` asks next, so a
receiver that passes is one the qualified path can actually serve. `from . import
mod` and `import pkg.mod as m` are both handled; anything else stays a silent
miss rather than a wrong edge.

`__tests__/python-import-gate.test.ts` pins both directions, and fails on the
first without this change (`['remove@store.py','get@store.py']` vs `[]`).
resolution + extraction + frameworks + kernel parity: 855 tests, all pass.
inth3shadows added a commit to inth3shadows/codegraph that referenced this pull request Sep 7, 2026
The colbymchenry#66 escape opened the built-in-method filter for ANY imported local name,
stdlib and PyPI included, so `os.remove(p)` bound to a project method named
`remove` — the fabrication class the filter exists to prevent, arriving through
its own escape. Verified: 2 wrong edges where 1.6.0 produced none.

Found by code review of the em.2 build.
inth3shadows added a commit to inth3shadows/codegraph that referenced this pull request Sep 7, 2026
Since colbymchenry#66 kept the receiver's text, `self.data.append(1)` reaches the resolver as
`self.data.append` — which `matchMethodCall`'s dotMatch splits into receiver
`self.data` + method `append`, and the bare-name strategies then bound it to any
project method of that name. The comment on colbymchenry#66 claimed the qualifier prevented
exactly this. It did not; only receivers with non-word characters (subscript,
call chain) got the promised silent miss.

This is the discipline Go (colbymchenry#1276), Rust (colbymchenry#1585) and PHP's `this->prop.method`
already have in this same function: a dotted python receiver resolves through
validated inference or not at all.

**It costs recall, and the cost is measured rather than waved at.** Indexing the
tracked .py of four real projects: three unchanged (278 / 643 / 301 call edges),
and a 249-file one 2605 -> 2564. Of the 41 dropped, 38 were fabrications — 21 x a
dict `.update` bound to a service's `update`, 16 x application code bound to a
`get` defined in a TEST file, and `self._model.transcribe` on an external Whisper
model bound to the file's own `transcribe` — and 3 were genuine
`self._capture.stop()` hops onto the class the constructor assigns.

Those 3 are recoverable: python names an attribute's type in the class body
(`self.x: T`, `self.x = T()`, a typed `__init__` parameter, a class-level
annotation, a base class). A first attempt read those with regexes over the
class's source lines and review killed it — with only `#` stripped it took a type
out of a DOCSTRING and turned a correct edge into a wrong one, read a nested
class's `__init__` as the outer class's, and stripped the package off
`requests.Session()` to bind an external object to a project class. Doing it
right needs the AST, and it is its own change. Until then this shape is a silent
miss, which is the trade this file makes everywhere else.

The three negative tests each use a DISTRACTOR — a second project symbol with the
same method name. Without one the old fallback found the right target by
single-candidate luck and the test passed on both arms, proving nothing; that is
how the first version of this suite was vacuous. Verified: all three fail against
the pre-change resolver, and the two boundary guards (a single-segment inferable
receiver, a module-qualified call) pass on both arms.

resolution + extraction + frameworks + kernel parity: 860 tests, all pass.
Plan: ~/.claude/plans/codegraph-python-attribute-chain-receiver.md
inth3shadows added a commit to inth3shadows/codegraph that referenced this pull request Sep 7, 2026
The colbymchenry#66 extractor change kept the receiver's text but the resolver still split it
and fell through to the bare-name strategies, so `self.data.append(1)` bound to
an unrelated class's `append`. Now validated-inference-or-nothing, the rule Go,
Rust and PHP already follow in that function.

Measured: 38 fabrications dropped, 3 genuine edges lost, on a 249-file python
repo. Attribute type inference would recover the 3 and needs the AST — a regex
version was reviewed and rejected for reading types out of docstrings.
inth3shadows added a commit to inth3shadows/codegraph that referenced this pull request Sep 7, 2026
Since colbymchenry#66 kept the receiver's text, `self.data.append(1)` reaches the resolver as
`self.data.append` — which `matchMethodCall`'s dotMatch splits into receiver
`self.data` + method `append`, and the bare-name strategies then bound it to any
project method of that name. The comment on colbymchenry#66 claimed the qualifier prevented
exactly this. It did not; only receivers with non-word characters (subscript,
call chain) got the promised silent miss.

This is the discipline Go (colbymchenry#1276), Rust (colbymchenry#1585) and PHP's `this->prop.method`
already have in this same function: a dotted python receiver resolves through
validated inference or not at all.

**It costs recall, and the cost is measured rather than waved at.** Indexing the
tracked .py of four real projects: three unchanged (278 / 643 / 301 call edges),
and a 249-file one 2605 -> 2564. Of the 41 dropped, 38 were fabrications — 21 x a
dict `.update` bound to a service's `update`, 16 x application code bound to a
`get` defined in a TEST file, and `self._model.transcribe` on an external Whisper
model bound to the file's own `transcribe` — and 3 were genuine
`self._capture.stop()` hops onto the class the constructor assigns.

Those 3 are recoverable: python names an attribute's type in the class body
(`self.x: T`, `self.x = T()`, a typed `__init__` parameter, a class-level
annotation, a base class). A first attempt read those with regexes over the
class's source lines and review killed it — with only `#` stripped it took a type
out of a DOCSTRING and turned a correct edge into a wrong one, read a nested
class's `__init__` as the outer class's, and stripped the package off
`requests.Session()` to bind an external object to a project class. Doing it
right needs the AST, and it is its own change. Until then this shape is a silent
miss, which is the trade this file makes everywhere else.

The three negative tests each use a DISTRACTOR — a second project symbol with the
same method name. Without one the old fallback found the right target by
single-candidate luck and the test passed on both arms, proving nothing; that is
how the first version of this suite was vacuous. Verified: all three fail against
the pre-change resolver, and the two boundary guards (a single-segment inferable
receiver, a module-qualified call) pass on both arms.

resolution + extraction + frameworks + kernel parity: 860 tests, all pass.
Plan: ~/.claude/plans/codegraph-python-attribute-chain-receiver.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants