Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python 3.12
- name: Set up Python 3.12 (Linux)
if: runner.os == 'Linux'
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Set up Python 3.12 (Windows/Mac)
if: runner.os != 'Linux'
uses: actions/setup-python@v5
with:
python-version: '3.12'
Expand Down Expand Up @@ -57,9 +64,14 @@ jobs:
/bin/bash -c "
set -e
cd /src
/opt/python/cp312-cp312/bin/python -m pip install --upgrade pip
/opt/python/cp312-cp312/bin/pip install . pyinstaller
/opt/python/cp312-cp312/bin/pyinstaller cgc.spec --clean
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH=/root/.local/bin:\$PATH
uv python install 3.12
uv venv --python 3.12 --seed /tmp/venv
. /tmp/venv/bin/activate
python -m pip install --upgrade pip
pip install . pyinstaller
pyinstaller cgc.spec --clean
"
sudo chown -R $USER:$USER dist build

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ A powerful **MCP server** and **CLI toolkit** that indexes local code into a gra
- **Live File Watching:** Watch directories for changes and automatically update the graph in real-time (`cgc watch`).
- **Interactive Setup:** A user-friendly command-line wizard for easy setup.
- **Dual Mode:** Works as a standalone **CLI toolkit** for developers and as an **MCP server** for AI agents.
- **Multi-Language Support:** Full support for 14 programming languages.
- **Multi-Language Support:** Full support for 16 programming languages.
- **Flexible Database Backend:** KùzuDB (default, zero-config for all platforms), FalkorDB Lite (Unix-only), FalkorDB Remote, or Neo4j (all platforms via Docker/native).

---
Expand All @@ -144,7 +144,8 @@ CodeGraphContext provides comprehensive parsing and analysis for the following l
| ☕ | **Java** | 🏗️ | **C / C++** | #️⃣ | **C#** |
| 🐹 | **Go** | 🦀 | **Rust** | 💎 | **Ruby** |
| 🐘 | **PHP** | 🍎 | **Swift** | 🎨 | **Kotlin** |
| 🎯 | **Dart** | 🐪 | **Perl** | | |
| 🎯 | **Dart** | 🐪 | **Perl** | 🧩 | **Vue** |
| 🔥 | **Svelte** | | | | |

Each language parser extracts functions, classes, methods, parameters, inheritance relationships, function calls, and imports to build a comprehensive code graph.

Expand Down
5 changes: 3 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
- **实时文件监控:** 监控目录更改并实时自动更新代码图 (`cgc watch`)。
- **交互式设置:** 用户友好的命令行向导,轻松完成设置。
- **双重模式:** 既可以作为开发者的独立 **CLI 工具包**,也可以作为 AI 智能体的 **MCP 服务端**。
- **多语言支持:** 全面支持 14 种编程语言。
- **多语言支持:** 全面支持 16 种编程语言。
- **灵活的数据库后端:** KùzuDB(默认,所有平台零配置)、FalkorDB Lite(仅限 Unix)、FalkorDB Remote 或 Neo4j(通过 Docker/原生支持所有平台)。

---
Expand All @@ -144,7 +144,8 @@ CodeGraphContext 为以下语言提供全面的解析和分析:
| ☕ | **Java** | 🏗️ | **C / C++** | #️⃣ | **C#** |
| 🐹 | **Go** | 🦀 | **Rust** | 💎 | **Ruby** |
| 🐘 | **PHP** | 🍎 | **Swift** | 🎨 | **Kotlin** |
| 🎯 | **Dart** | 🐪 | **Perl** | | |
| 🎯 | **Dart** | 🐪 | **Perl** | 🧩 | **Vue** |
| 🔥 | **Svelte** | | | | |

每个语言解析器都会提取函数、类、方法、参数、继承关系、函数调用和导入,以构建全面的代码图。

Expand Down
2 changes: 1 addition & 1 deletion docs/MCP_TOOLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Performs a one-time scan of a local folder to add its code to the graph. Ideal f
Add an external package to the graph by discovering its location.
- **Args**: `package_name` (string), `language` (string), `is_dependency` (boolean)
- **Returns**: Job ID
- **Supported Languages**: python, javascript, typescript, java, c, go, ruby, php, cpp
- **Supported Languages**: python, javascript, typescript, java, c, go, ruby, php, cpp, vue, svelte

### `list_indexed_repositories`
List all repositories currently indexed in the graph.
Expand Down
25 changes: 24 additions & 1 deletion scripts/test_all_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,27 @@ class MyClass {
print "world\\n";
}
1;
''',
'vue': '''
<template>
<div>{{ msg }}</div>
</template>
<script lang="ts">
import { greet } from './helpers'
function hello(name: string) {
return greet(name)
}
</script>
''',
'svelte': '''
<script>
import { onMount } from 'svelte'
function hello() {
console.log('world')
}
</script>

<h1>Hello</h1>
'''
}

Expand All @@ -127,7 +148,9 @@ class MyClass {
'ruby': '.rb',
'c_sharp': '.cs',
'dart': '.dart',
'perl': '.pl'
'perl': '.pl',
'vue': '.vue',
'svelte': '.svelte'
}

results = {}
Expand Down
2 changes: 1 addition & 1 deletion src/codegraphcontext/tool_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"type": "object",
"properties": {
"package_name": {"type": "string", "description": "Name of the package to add (e.g., 'requests', 'express', 'moment', 'lodash')."},
"language": {"type": "string", "description": "The programming language of the package.", "enum": ["python", "javascript", "typescript", "java", "c", "go", "ruby", "php","cpp"]},
"language": {"type": "string", "description": "The programming language of the package.", "enum": ["python", "javascript", "typescript", "java", "c", "go", "ruby", "php", "cpp", "vue", "svelte"]},
"is_dependency": {"type": "boolean", "description": "Mark as a dependency.", "default": True}
},
"required": ["package_name", "language"]
Expand Down
29 changes: 24 additions & 5 deletions src/codegraphcontext/tools/graph_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,16 @@ class TreeSitterParser:
def __init__(self, language_name: str):
self.language_name = language_name
self.ts_manager = get_tree_sitter_manager()

# Get the language (cached) and create a new parser for this instance
self.language: Language = self.ts_manager.get_language_safe(language_name)
# In tree-sitter 0.25+, Parser takes language in constructor
self.parser = Parser(self.language)

# Vue/Svelte parsers extract script blocks and delegate parsing to JS/TS parsers.
# They don't need a dedicated tree-sitter parser initialized here.
self.language: Optional[Language] = None
self.parser: Optional[Parser] = None
if self.language_name not in {'vue', 'svelte'}:
# Get the language (cached) and create a new parser for this instance
self.language = self.ts_manager.get_language_safe(language_name)
# In tree-sitter 0.25+, Parser takes language in constructor
self.parser = Parser(self.language)

self.language_specific_parser = None
if self.language_name == 'python':
Expand Down Expand Up @@ -109,6 +114,12 @@ def __init__(self, language_name: str):
elif self.language_name == 'elixir':
from .languages.elixir import ElixirTreeSitterParser
self.language_specific_parser = ElixirTreeSitterParser(self)
elif self.language_name == 'vue':
from .languages.vue import VueTreeSitterParser
self.language_specific_parser = VueTreeSitterParser(self)
elif self.language_name == 'svelte':
from .languages.svelte import SvelteTreeSitterParser
self.language_specific_parser = SvelteTreeSitterParser(self)



Expand Down Expand Up @@ -158,6 +169,8 @@ def __init__(self, db_manager: DatabaseManager, job_manager: JobManager, loop: a
'.pm': 'perl',
'.ex': 'elixir',
'.exs': 'elixir',
'.vue': 'vue',
'.svelte': 'svelte',
}
self._parsed_cache = {}
self.create_schema()
Expand Down Expand Up @@ -368,6 +381,12 @@ def _pre_scan_for_imports(self, files: list[Path]) -> dict:
if '.exs' in files_by_lang:
from .languages import elixir as elixir_lang_module
imports_map.update(elixir_lang_module.pre_scan_elixir(files_by_lang['.exs'], self.get_parser('.exs')))
if '.vue' in files_by_lang:
from .languages import vue as vue_lang_module
imports_map.update(vue_lang_module.pre_scan_vue(files_by_lang['.vue'], self.get_parser('.vue')))
if '.svelte' in files_by_lang:
from .languages import svelte as svelte_lang_module
imports_map.update(svelte_lang_module.pre_scan_svelte(files_by_lang['.svelte'], self.get_parser('.svelte')))

return imports_map

Expand Down
175 changes: 175 additions & 0 deletions src/codegraphcontext/tools/languages/sfc_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
from pathlib import Path
from typing import Any, Dict, Type
import re

from codegraphcontext.utils.debug_log import warning_logger
from codegraphcontext.utils.tree_sitter_manager import get_tree_sitter_manager

from .javascript import JavascriptTreeSitterParser
from .typescript import TypescriptTreeSitterParser


_SCRIPT_TAG_RE = re.compile(
r"<script(?P<attrs>[^>]*)>(?P<content>.*?)</script\s*>",
re.IGNORECASE | re.DOTALL,
)
_LANG_TS_RE = re.compile(
r"\blang\s*=\s*(?:\"|')?(?:ts|tsx|typescript)(?:\"|')?\b",
re.IGNORECASE,
)
_LINE_FIELDS = {"line_number", "end_line", "function_line_number"}


class _ParserWrapper:
"""Lightweight wrapper to build language parsers outside GraphBuilder."""

def __init__(self, language_name: str):
manager = get_tree_sitter_manager()
self.language_name = language_name
self.language = manager.get_language_safe(language_name)
self.parser = manager.create_parser(language_name)


def _extract_script_blocks(source_code: str) -> list[dict[str, Any]]:
"""Extract <script> blocks and metadata from SFC source text."""
script_blocks = []

for match in _SCRIPT_TAG_RE.finditer(source_code):
attrs = match.group("attrs") or ""
content = match.group("content") or ""

script_blocks.append(
{
"content": content,
"is_typescript": bool(_LANG_TS_RE.search(attrs)),
# Count how many lines appear before the script body starts.
"line_offset": source_code.count("\n", 0, match.start("content")),
}
)

return script_blocks


def _apply_line_offset(value: Any, line_offset: int) -> None:
"""Recursively shift line-based fields by an offset."""
if not line_offset:
return

if isinstance(value, dict):
for key, item in value.items():
if key in _LINE_FIELDS and isinstance(item, int):
value[key] = item + line_offset
elif key == "context" and isinstance(item, tuple) and len(item) == 3 and isinstance(item[2], int):
value[key] = (item[0], item[1], item[2] + line_offset)
else:
_apply_line_offset(item, line_offset)
elif isinstance(value, list):
for item in value:
_apply_line_offset(item, line_offset)


class SingleFileComponentTreeSitterParser:
"""Shared parser for Vue/Svelte single-file components."""

def __init__(self, generic_parser_wrapper, component_language: str):
self.generic_parser_wrapper = generic_parser_wrapper
self.language_name = component_language

self._javascript_parser = JavascriptTreeSitterParser(_ParserWrapper("javascript"))
self._typescript_parser = TypescriptTreeSitterParser(_ParserWrapper("typescript"))

def _parse_script_block(
self,
script_source: str,
is_typescript: bool,
index_source: bool,
) -> Dict[str, list[Dict[str, Any]]]:
if is_typescript:
parser = self._typescript_parser
parser.index_source = index_source
tree = parser.parser.parse(bytes(script_source, "utf8"))
root = tree.root_node
return {
"functions": parser._find_functions(root),
"classes": parser._find_classes(root),
"interfaces": parser._find_interfaces(root),
"type_aliases": parser._find_type_aliases(root),
"variables": parser._find_variables(root),
"imports": parser._find_imports(root),
"function_calls": parser._find_calls(root),
}

parser = self._javascript_parser
parser.index_source = index_source
tree = parser.parser.parse(bytes(script_source, "utf8"))
root = tree.root_node
return {
"functions": parser._find_functions(root),
"classes": parser._find_classes(root),
"variables": parser._find_variables(root),
"imports": parser._find_imports(root),
"function_calls": parser._find_calls(root),
}

def parse(self, path: Path, is_dependency: bool = False, index_source: bool = False) -> Dict[str, Any]:
with open(path, "r", encoding="utf-8") as f:
source_code = f.read()

result: Dict[str, Any] = {
"path": str(path),
"functions": [],
"classes": [],
"interfaces": [],
"type_aliases": [],
"variables": [],
"imports": [],
"function_calls": [],
"is_dependency": is_dependency,
"lang": self.language_name,
}

for block in _extract_script_blocks(source_code):
parsed_block = self._parse_script_block(
block["content"],
is_typescript=block["is_typescript"],
index_source=index_source,
)
_apply_line_offset(parsed_block, block["line_offset"])

for key, values in parsed_block.items():
if key not in result:
result[key] = []
result[key].extend(values)

return result


def pre_scan_sfc(
files: list[Path],
parser_wrapper,
parser_cls: Type[SingleFileComponentTreeSitterParser],
) -> dict[str, list[str]]:
"""Generic pre-scan for SFC files, collecting symbols for import resolution."""
imports_map: dict[str, list[str]] = {}
parser = parser_cls(parser_wrapper)

for path in files:
try:
file_data = parser.parse(path, is_dependency=False, index_source=False)

for item_key in ("functions", "classes", "interfaces", "type_aliases"):
for item in file_data.get(item_key, []):
name = item.get("name")
if not name:
continue

if name not in imports_map:
imports_map[name] = []

resolved_path = str(path.resolve())
if resolved_path not in imports_map[name]:
imports_map[name].append(resolved_path)
except Exception as e:
warning_logger(f"Tree-sitter pre-scan failed for {path}: {e}")

return imports_map
15 changes: 15 additions & 0 deletions src/codegraphcontext/tools/languages/svelte.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from pathlib import Path

from .sfc_common import SingleFileComponentTreeSitterParser, pre_scan_sfc


class SvelteTreeSitterParser(SingleFileComponentTreeSitterParser):
"""Tree-sitter parser for Svelte single-file components (.svelte)."""

def __init__(self, generic_parser_wrapper):
super().__init__(generic_parser_wrapper, component_language="svelte")


def pre_scan_svelte(files: list[Path], parser_wrapper) -> dict[str, list[str]]:
"""Pre-scan Svelte files for symbol-to-file import resolution."""
return pre_scan_sfc(files, parser_wrapper, SvelteTreeSitterParser)
15 changes: 15 additions & 0 deletions src/codegraphcontext/tools/languages/vue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from pathlib import Path

from .sfc_common import SingleFileComponentTreeSitterParser, pre_scan_sfc


class VueTreeSitterParser(SingleFileComponentTreeSitterParser):
"""Tree-sitter parser for Vue single-file components (.vue)."""

def __init__(self, generic_parser_wrapper):
super().__init__(generic_parser_wrapper, component_language="vue")


def pre_scan_vue(files: list[Path], parser_wrapper) -> dict[str, list[str]]:
"""Pre-scan Vue files for symbol-to-file import resolution."""
return pre_scan_sfc(files, parser_wrapper, VueTreeSitterParser)
Loading
Loading