velune-cli 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- velune/__init__.py +5 -0
- velune/__main__.py +6 -0
- velune/cli/__init__.py +5 -0
- velune/cli/app.py +208 -0
- velune/cli/autocomplete.py +80 -0
- velune/cli/banner.py +60 -0
- velune/cli/commands/__init__.py +32 -0
- velune/cli/commands/ask.py +175 -0
- velune/cli/commands/base.py +16 -0
- velune/cli/commands/chat.py +228 -0
- velune/cli/commands/config.py +224 -0
- velune/cli/commands/daemon.py +88 -0
- velune/cli/commands/doctor.py +721 -0
- velune/cli/commands/init.py +170 -0
- velune/cli/commands/mcp.py +82 -0
- velune/cli/commands/memory.py +293 -0
- velune/cli/commands/models.py +683 -0
- velune/cli/commands/preflight.py +95 -0
- velune/cli/commands/run.py +270 -0
- velune/cli/commands/setup.py +184 -0
- velune/cli/commands/workspace.py +249 -0
- velune/cli/context.py +36 -0
- velune/cli/councilmodel_ui.py +199 -0
- velune/cli/display/council_view.py +254 -0
- velune/cli/display/memory_view.py +126 -0
- velune/cli/display/panels.py +35 -0
- velune/cli/display/progress.py +25 -0
- velune/cli/display/themes.py +25 -0
- velune/cli/main.py +15 -0
- velune/cli/model_selector.py +51 -0
- velune/cli/modes.py +86 -0
- velune/cli/pull_ui.py +123 -0
- velune/cli/registry.py +80 -0
- velune/cli/rendering/__init__.py +5 -0
- velune/cli/rendering/error_panel.py +79 -0
- velune/cli/rendering/markdown.py +63 -0
- velune/cli/repl.py +1855 -0
- velune/cli/session_manager.py +71 -0
- velune/cli/slash_commands.py +37 -0
- velune/cli/theme.py +8 -0
- velune/cognition/__init__.py +23 -0
- velune/cognition/agents/__init__.py +7 -0
- velune/cognition/agents/coder.py +209 -0
- velune/cognition/agents/planner.py +156 -0
- velune/cognition/agents/reviewer.py +195 -0
- velune/cognition/arbitrator.py +220 -0
- velune/cognition/architecture.py +415 -0
- velune/cognition/budget.py +65 -0
- velune/cognition/council/__init__.py +47 -0
- velune/cognition/council/base.py +217 -0
- velune/cognition/council/challenger.py +74 -0
- velune/cognition/council/coder.py +79 -0
- velune/cognition/council/critic_agent.py +43 -0
- velune/cognition/council/critic_configs.py +111 -0
- velune/cognition/council/critics.py +41 -0
- velune/cognition/council/debate.py +46 -0
- velune/cognition/council/factory.py +140 -0
- velune/cognition/council/messages.py +56 -0
- velune/cognition/council/planner.py +124 -0
- velune/cognition/council/reviewer.py +74 -0
- velune/cognition/council/synthesizer.py +67 -0
- velune/cognition/council/tiers.py +188 -0
- velune/cognition/council_orchestrator.py +282 -0
- velune/cognition/firewall.py +354 -0
- velune/cognition/module.py +46 -0
- velune/cognition/orchestrator.py +1205 -0
- velune/cognition/personality.py +238 -0
- velune/cognition/state.py +104 -0
- velune/cognition/style_resolver.py +64 -0
- velune/cognition/verification.py +205 -0
- velune/context/__init__.py +28 -0
- velune/context/assembler.py +240 -0
- velune/context/budget.py +97 -0
- velune/context/extractive.py +95 -0
- velune/context/prompt_adaptation.py +480 -0
- velune/context/sections.py +99 -0
- velune/context/token_counter.py +134 -0
- velune/context/utilization.py +33 -0
- velune/context/window.py +63 -0
- velune/core/__init__.py +89 -0
- velune/core/background.py +5 -0
- velune/core/config/__init__.py +37 -0
- velune/core/errors/__init__.py +90 -0
- velune/core/errors/catalog.py +188 -0
- velune/core/errors/execution.py +31 -0
- velune/core/errors/memory.py +25 -0
- velune/core/errors/orchestration.py +31 -0
- velune/core/errors/provider.py +37 -0
- velune/core/event_loop.py +35 -0
- velune/core/logging.py +83 -0
- velune/core/paths.py +165 -0
- velune/core/runtime.py +113 -0
- velune/core/startup_profiler.py +56 -0
- velune/core/task_registry.py +117 -0
- velune/core/trace.py +83 -0
- velune/core/types/__init__.py +48 -0
- velune/core/types/agent.py +53 -0
- velune/core/types/context.py +42 -0
- velune/core/types/inference.py +38 -0
- velune/core/types/memory.py +42 -0
- velune/core/types/model.py +70 -0
- velune/core/types/provider.py +62 -0
- velune/core/types/repository.py +38 -0
- velune/core/types/task.py +61 -0
- velune/core/types/workspace.py +28 -0
- velune/daemon/client.py +13 -0
- velune/daemon/server.py +127 -0
- velune/daemon/transport.py +179 -0
- velune/events.py +204 -0
- velune/execution/__init__.py +22 -0
- velune/execution/benchmarker.py +315 -0
- velune/execution/cancellation.py +53 -0
- velune/execution/checkpointer.py +130 -0
- velune/execution/command_spec.py +165 -0
- velune/execution/diff_preview.py +197 -0
- velune/execution/executor.py +181 -0
- velune/execution/module.py +18 -0
- velune/execution/multi_diff.py +67 -0
- velune/execution/path_guard.py +74 -0
- velune/execution/planner.py +91 -0
- velune/execution/rollback.py +89 -0
- velune/execution/sandbox.py +268 -0
- velune/execution/validator.py +115 -0
- velune/hardware/__init__.py +1 -0
- velune/hardware/detector.py +192 -0
- velune/kernel/__init__.py +55 -0
- velune/kernel/bootstrap.py +125 -0
- velune/kernel/config.py +426 -0
- velune/kernel/entrypoint.py +78 -0
- velune/kernel/health.py +54 -0
- velune/kernel/lifecycle.py +143 -0
- velune/kernel/module.py +17 -0
- velune/kernel/modules.py +23 -0
- velune/kernel/registry.py +96 -0
- velune/kernel/schemas.py +28 -0
- velune/main.py +9 -0
- velune/mcp/__init__.py +9 -0
- velune/mcp/client.py +115 -0
- velune/mcp/config.py +19 -0
- velune/mcp/server.py +624 -0
- velune/memory/__init__.py +32 -0
- velune/memory/compaction.py +506 -0
- velune/memory/embedding_pipeline.py +241 -0
- velune/memory/lifecycle.py +680 -0
- velune/memory/module.py +218 -0
- velune/memory/prioritizer.py +67 -0
- velune/memory/storage/episodic_schema.sql +53 -0
- velune/memory/storage/lancedb_store.py +282 -0
- velune/memory/storage/sqlite_manager.py +369 -0
- velune/memory/storage/sqlite_pool.py +149 -0
- velune/memory/tiers/episodic.py +588 -0
- velune/memory/tiers/graph.py +378 -0
- velune/memory/tiers/lineage.py +416 -0
- velune/memory/tiers/semantic.py +475 -0
- velune/memory/tiers/working.py +168 -0
- velune/memory/vitality.py +132 -0
- velune/models/__init__.py +15 -0
- velune/models/family.py +76 -0
- velune/models/module.py +20 -0
- velune/models/probes.py +192 -0
- velune/models/profile_cache.py +84 -0
- velune/models/profiler.py +108 -0
- velune/models/registry.py +251 -0
- velune/models/scorer.py +233 -0
- velune/models/specializations.py +205 -0
- velune/orchestration/__init__.py +19 -0
- velune/orchestration/engine.py +239 -0
- velune/orchestration/module.py +15 -0
- velune/orchestration/role_assignments.py +82 -0
- velune/orchestration/schemas.py +98 -0
- velune/plugins/__init__.py +20 -0
- velune/plugins/hooks.py +50 -0
- velune/plugins/loader.py +161 -0
- velune/plugins/registry.py +56 -0
- velune/plugins/schemas.py +21 -0
- velune/providers/__init__.py +23 -0
- velune/providers/adapters/anthropic.py +257 -0
- velune/providers/adapters/fireworks.py +115 -0
- velune/providers/adapters/google.py +234 -0
- velune/providers/adapters/groq.py +151 -0
- velune/providers/adapters/huggingface.py +210 -0
- velune/providers/adapters/llamacpp.py +208 -0
- velune/providers/adapters/lmstudio.py +175 -0
- velune/providers/adapters/ollama.py +233 -0
- velune/providers/adapters/openai.py +213 -0
- velune/providers/adapters/openrouter.py +81 -0
- velune/providers/adapters/together.py +134 -0
- velune/providers/adapters/xai.py +60 -0
- velune/providers/base.py +86 -0
- velune/providers/benchmarker.py +138 -0
- velune/providers/discovery/__init__.py +33 -0
- velune/providers/discovery/anthropic.py +79 -0
- velune/providers/discovery/benchmarks.py +44 -0
- velune/providers/discovery/classifier.py +69 -0
- velune/providers/discovery/fireworks.py +95 -0
- velune/providers/discovery/gguf.py +88 -0
- velune/providers/discovery/google.py +95 -0
- velune/providers/discovery/gpu.py +117 -0
- velune/providers/discovery/groq.py +21 -0
- velune/providers/discovery/huggingface.py +67 -0
- velune/providers/discovery/lmstudio.py +80 -0
- velune/providers/discovery/ollama.py +162 -0
- velune/providers/discovery/openai.py +96 -0
- velune/providers/discovery/openrouter.py +113 -0
- velune/providers/discovery/scanner.py +115 -0
- velune/providers/discovery/together.py +114 -0
- velune/providers/discovery/xai.py +57 -0
- velune/providers/health.py +67 -0
- velune/providers/health_monitor.py +169 -0
- velune/providers/keystore.py +142 -0
- velune/providers/local_paths.py +49 -0
- velune/providers/local_resolver.py +229 -0
- velune/providers/module.py +51 -0
- velune/providers/ollama_manager.py +193 -0
- velune/providers/registry.py +220 -0
- velune/providers/router.py +255 -0
- velune/providers/task_classifier.py +288 -0
- velune/py.typed +0 -0
- velune/repository/__init__.py +33 -0
- velune/repository/analyzer.py +127 -0
- velune/repository/ast_parser.py +822 -0
- velune/repository/blast_radius.py +298 -0
- velune/repository/boundary_classifier.py +295 -0
- velune/repository/cognition.py +316 -0
- velune/repository/grapher.py +179 -0
- velune/repository/import_graph.py +263 -0
- velune/repository/incremental_indexer.py +275 -0
- velune/repository/index_state.py +96 -0
- velune/repository/indexer.py +243 -0
- velune/repository/module.py +17 -0
- velune/repository/parser.py +474 -0
- velune/repository/project_type.py +300 -0
- velune/repository/rename_journal.py +287 -0
- velune/repository/scanner.py +193 -0
- velune/repository/schemas.py +102 -0
- velune/repository/symbol_registry.py +365 -0
- velune/repository/tracker.py +252 -0
- velune/retrieval/__init__.py +27 -0
- velune/retrieval/cache.py +110 -0
- velune/retrieval/fast_path.py +391 -0
- velune/retrieval/graph.py +124 -0
- velune/retrieval/hybrid.py +271 -0
- velune/retrieval/keyword.py +131 -0
- velune/retrieval/module.py +26 -0
- velune/retrieval/pipeline.py +303 -0
- velune/retrieval/reranker.py +102 -0
- velune/retrieval/schemas.py +59 -0
- velune/retrieval/slow_path.py +364 -0
- velune/retrieval/vector.py +203 -0
- velune/telemetry/__init__.py +59 -0
- velune/telemetry/cognition.py +267 -0
- velune/telemetry/cost_estimator.py +92 -0
- velune/telemetry/debug.py +304 -0
- velune/telemetry/doctor.py +244 -0
- velune/telemetry/logging.py +286 -0
- velune/telemetry/spans.py +277 -0
- velune/telemetry/token_tracker.py +140 -0
- velune/telemetry/usage_tracker.py +340 -0
- velune/tools/__init__.py +41 -0
- velune/tools/base/registry.py +87 -0
- velune/tools/base/tool.py +63 -0
- velune/tools/code/navigate.py +116 -0
- velune/tools/code/search.py +123 -0
- velune/tools/filesystem/read.py +75 -0
- velune/tools/filesystem/search.py +136 -0
- velune/tools/filesystem/write.py +163 -0
- velune/tools/git/history.py +177 -0
- velune/tools/git/operations.py +122 -0
- velune/tools/git/state.py +121 -0
- velune/tools/module.py +81 -0
- velune/tools/terminal/execute.py +72 -0
- velune/tools/terminal/history.py +47 -0
- velune/tools/web/fetch.py +55 -0
- velune/tools/web/validator.py +122 -0
- velune_cli-0.9.0.dist-info/METADATA +518 -0
- velune_cli-0.9.0.dist-info/RECORD +279 -0
- velune_cli-0.9.0.dist-info/WHEEL +4 -0
- velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
- velune_cli-0.9.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
"""Tree-sitter-based AST parsing for symbol extraction.
|
|
2
|
+
|
|
3
|
+
Tree-sitter ships compiled C extensions (.pyd/.so). Importing them at module
|
|
4
|
+
load time forces Windows to load several DLLs synchronously — each triggering
|
|
5
|
+
a Defender real-time scan — adding seconds to *every* startup, even when no
|
|
6
|
+
parsing is requested. We therefore defer all tree-sitter imports until the
|
|
7
|
+
first actual parse via ``_ensure_tree_sitter()``.
|
|
8
|
+
|
|
9
|
+
``HAS_TREE_SITTER`` starts ``None`` (unknown) and becomes ``True``/``False``
|
|
10
|
+
after the first lazy load attempt. It remains importable for callers/tests
|
|
11
|
+
that reference it, but no import cost is paid until parsing happens.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import logging
|
|
18
|
+
import uuid
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from enum import StrEnum
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import TYPE_CHECKING, Any
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from tree_sitter import Tree
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger("velune.repository.ast_parser")
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Lazy tree-sitter loader — no DLL cost until first parse
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
#: ``None`` = not yet probed; ``True`` = available; ``False`` = unavailable.
|
|
34
|
+
HAS_TREE_SITTER: bool | None = None
|
|
35
|
+
_TS_LANGUAGES: dict[str, Any] = {}
|
|
36
|
+
# We do NOT cache a singleton Parser — each parse invocation creates a fresh
|
|
37
|
+
# Parser (cheap object) and sets its language, which is safe in a thread pool.
|
|
38
|
+
_TS_PARSER_CLS: Any = None # tree_sitter.Parser class, set after lazy load
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _ensure_tree_sitter() -> bool:
|
|
42
|
+
"""Lazily import tree-sitter grammars on first use. Returns availability."""
|
|
43
|
+
global HAS_TREE_SITTER, _TS_PARSER_CLS
|
|
44
|
+
if HAS_TREE_SITTER is not None:
|
|
45
|
+
return HAS_TREE_SITTER
|
|
46
|
+
try:
|
|
47
|
+
import tree_sitter_go
|
|
48
|
+
import tree_sitter_python
|
|
49
|
+
import tree_sitter_rust
|
|
50
|
+
import tree_sitter_typescript
|
|
51
|
+
from tree_sitter import Language, Parser
|
|
52
|
+
|
|
53
|
+
_TS_PARSER_CLS = Parser
|
|
54
|
+
for lang_name, factory in (
|
|
55
|
+
("python", tree_sitter_python.language),
|
|
56
|
+
("typescript", tree_sitter_typescript.language_typescript),
|
|
57
|
+
("javascript", tree_sitter_typescript.language_typescript),
|
|
58
|
+
("go", tree_sitter_go.language),
|
|
59
|
+
("rust", tree_sitter_rust.language),
|
|
60
|
+
):
|
|
61
|
+
try:
|
|
62
|
+
lang_obj = Language(factory())
|
|
63
|
+
_TS_LANGUAGES[lang_name] = lang_obj
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
logger.debug("Failed to load %s grammar: %s", lang_name, exc)
|
|
66
|
+
HAS_TREE_SITTER = True
|
|
67
|
+
except ImportError:
|
|
68
|
+
HAS_TREE_SITTER = False
|
|
69
|
+
return HAS_TREE_SITTER
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class SymbolKind(StrEnum):
|
|
73
|
+
"""Type of code symbol."""
|
|
74
|
+
|
|
75
|
+
FUNCTION = "function"
|
|
76
|
+
CLASS = "class"
|
|
77
|
+
METHOD = "method"
|
|
78
|
+
IMPORT = "import"
|
|
79
|
+
VARIABLE = "variable"
|
|
80
|
+
CONSTANT = "constant"
|
|
81
|
+
INTERFACE = "interface"
|
|
82
|
+
ENUM = "enum"
|
|
83
|
+
ASYNC_FUNCTION = "async_function"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class Symbol:
|
|
88
|
+
"""Extracted code symbol from AST."""
|
|
89
|
+
|
|
90
|
+
id: str # Stable UUID
|
|
91
|
+
name: str
|
|
92
|
+
kind: SymbolKind
|
|
93
|
+
file_path: str # Relative to workspace root
|
|
94
|
+
line_start: int
|
|
95
|
+
line_end: int
|
|
96
|
+
docstring: str | None = None
|
|
97
|
+
parameters: list[str] = field(default_factory=list)
|
|
98
|
+
return_type: str | None = None
|
|
99
|
+
is_exported: bool = False
|
|
100
|
+
source_text: str | None = None # For reconstruction/debugging
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class ParsedFile:
|
|
105
|
+
"""Result of parsing a single file."""
|
|
106
|
+
|
|
107
|
+
file_path: Path
|
|
108
|
+
language: str
|
|
109
|
+
symbols: list[Symbol]
|
|
110
|
+
error: str | None = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ASTParser:
|
|
114
|
+
"""Tree-sitter AST parser for multiple languages."""
|
|
115
|
+
|
|
116
|
+
LANGUAGE_MAP = {
|
|
117
|
+
"python": "tree_sitter_python",
|
|
118
|
+
"javascript": "tree_sitter_typescript",
|
|
119
|
+
"typescript": "tree_sitter_typescript",
|
|
120
|
+
"rust": "tree_sitter_rust",
|
|
121
|
+
"go": "tree_sitter_go",
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
def __init__(self, languages: list[str] | None = None) -> None:
|
|
125
|
+
"""Initialize parser with language grammars.
|
|
126
|
+
|
|
127
|
+
Grammars are loaded lazily on first call to :meth:`parse_file` via
|
|
128
|
+
:func:`_ensure_tree_sitter`. This avoids Windows Defender DLL scans
|
|
129
|
+
at startup when no parsing will be performed.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
languages: List of language names (python, javascript, typescript,
|
|
133
|
+
rust, go). If None, defaults to all supported languages.
|
|
134
|
+
"""
|
|
135
|
+
self._requested_languages: list[str] = (
|
|
136
|
+
list(self.LANGUAGE_MAP.keys()) if languages is None else languages
|
|
137
|
+
)
|
|
138
|
+
# languages and parser are populated lazily by _ensure_loaded()
|
|
139
|
+
self.languages: dict[str, Any] = {}
|
|
140
|
+
self.parser: Any = None
|
|
141
|
+
self._loaded: bool = False
|
|
142
|
+
|
|
143
|
+
def _ensure_loaded(self) -> bool:
|
|
144
|
+
"""Populate ``self.languages`` from the lazy cache.
|
|
145
|
+
|
|
146
|
+
Returns True if at least one grammar is available.
|
|
147
|
+
"""
|
|
148
|
+
if self._loaded:
|
|
149
|
+
return bool(self.languages)
|
|
150
|
+
self._loaded = True
|
|
151
|
+
if _ensure_tree_sitter():
|
|
152
|
+
# Restrict to languages the caller requested
|
|
153
|
+
for lang in self._requested_languages:
|
|
154
|
+
lang_lower = lang.lower()
|
|
155
|
+
if lang_lower in _TS_LANGUAGES:
|
|
156
|
+
self.languages[lang_lower] = _TS_LANGUAGES[lang_lower]
|
|
157
|
+
return bool(self.languages)
|
|
158
|
+
|
|
159
|
+
async def parse_file(self, path: Path) -> ParsedFile | None:
|
|
160
|
+
"""Parse a file asynchronously without blocking event loop.
|
|
161
|
+
|
|
162
|
+
Grammars are loaded on first call (lazy). Runs the actual parse in a
|
|
163
|
+
thread pool to avoid blocking the event loop.
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
# Detect language from file extension
|
|
167
|
+
language = self._detect_language(path)
|
|
168
|
+
if not language:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
# Trigger lazy grammar load on first call
|
|
172
|
+
if not self._ensure_loaded() or language not in self.languages:
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
# Read file content
|
|
176
|
+
source = path.read_text(encoding="utf-8")
|
|
177
|
+
|
|
178
|
+
# Parse in thread to avoid blocking
|
|
179
|
+
loop = asyncio.get_running_loop()
|
|
180
|
+
tree = await loop.run_in_executor(
|
|
181
|
+
None,
|
|
182
|
+
self._parse_sync,
|
|
183
|
+
source,
|
|
184
|
+
language,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if not tree:
|
|
188
|
+
return ParsedFile(path, language, [], error="Parse failed")
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
rel_path = str(path.relative_to(Path.cwd()))
|
|
192
|
+
except ValueError:
|
|
193
|
+
rel_path = path.name
|
|
194
|
+
|
|
195
|
+
# Extract symbols in thread
|
|
196
|
+
symbols = await loop.run_in_executor(
|
|
197
|
+
None,
|
|
198
|
+
self.extract_symbols,
|
|
199
|
+
tree,
|
|
200
|
+
source,
|
|
201
|
+
language,
|
|
202
|
+
rel_path,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return ParsedFile(path, language, symbols)
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error(f"Error parsing {path}: {e}")
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
def _parse_sync(self, source: str, language: str) -> Any:
|
|
212
|
+
"""Synchronous parse operation (run in executor).
|
|
213
|
+
|
|
214
|
+
Creates a fresh tree_sitter.Parser per call to avoid language-switching
|
|
215
|
+
races when multiple files are parsed concurrently in the thread pool.
|
|
216
|
+
"""
|
|
217
|
+
if not _TS_PARSER_CLS or language not in self.languages:
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
p = _TS_PARSER_CLS()
|
|
222
|
+
p.language = self.languages[language]
|
|
223
|
+
tree = p.parse(source.encode("utf-8"))
|
|
224
|
+
return tree
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.debug(f"Parse error: {e}")
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
def extract_symbols(
|
|
230
|
+
self,
|
|
231
|
+
tree: Tree,
|
|
232
|
+
source: str,
|
|
233
|
+
language: str,
|
|
234
|
+
file_path: str,
|
|
235
|
+
) -> list[Symbol]:
|
|
236
|
+
"""Extract symbols from AST tree."""
|
|
237
|
+
symbols: list[Symbol] = []
|
|
238
|
+
|
|
239
|
+
if language == "python":
|
|
240
|
+
symbols.extend(self._extract_python_symbols(tree, source, file_path))
|
|
241
|
+
elif language in ("javascript", "typescript"):
|
|
242
|
+
symbols.extend(self._extract_js_symbols(tree, source, file_path, language))
|
|
243
|
+
elif language == "rust":
|
|
244
|
+
symbols.extend(self._extract_rust_symbols(tree, source, file_path))
|
|
245
|
+
elif language == "go":
|
|
246
|
+
symbols.extend(self._extract_go_symbols(tree, source, file_path))
|
|
247
|
+
|
|
248
|
+
return symbols
|
|
249
|
+
|
|
250
|
+
def _extract_python_symbols(self, tree: Tree, source: str, file_path: str) -> list[Symbol]:
|
|
251
|
+
"""Extract symbols from Python AST."""
|
|
252
|
+
symbols: list[Symbol] = []
|
|
253
|
+
source_lines = source.split("\n")
|
|
254
|
+
|
|
255
|
+
def visit(node: Any) -> None:
|
|
256
|
+
if node.type == "function_definition":
|
|
257
|
+
sym = self._parse_python_function(node, source_lines, file_path)
|
|
258
|
+
if sym:
|
|
259
|
+
symbols.append(sym)
|
|
260
|
+
elif node.type == "class_definition":
|
|
261
|
+
sym = self._parse_python_class(node, source_lines, file_path)
|
|
262
|
+
if sym:
|
|
263
|
+
symbols.append(sym)
|
|
264
|
+
# Also extract methods from class
|
|
265
|
+
for child in node.children:
|
|
266
|
+
if child.type == "block":
|
|
267
|
+
for subchild in child.children:
|
|
268
|
+
if subchild.type == "function_definition":
|
|
269
|
+
method = self._parse_python_function(
|
|
270
|
+
subchild, source_lines, file_path, is_method=True
|
|
271
|
+
)
|
|
272
|
+
if method:
|
|
273
|
+
symbols.append(method)
|
|
274
|
+
elif node.type in ("import_statement", "import_from_statement"):
|
|
275
|
+
sym = self._parse_python_import(node, source_lines, file_path)
|
|
276
|
+
if sym:
|
|
277
|
+
symbols.append(sym)
|
|
278
|
+
|
|
279
|
+
for child in node.children:
|
|
280
|
+
visit(child)
|
|
281
|
+
|
|
282
|
+
visit(tree.root_node)
|
|
283
|
+
return symbols
|
|
284
|
+
|
|
285
|
+
def _parse_python_function(
|
|
286
|
+
self,
|
|
287
|
+
node: Any,
|
|
288
|
+
source_lines: list[str],
|
|
289
|
+
file_path: str,
|
|
290
|
+
is_method: bool = False,
|
|
291
|
+
) -> Symbol | None:
|
|
292
|
+
"""Parse a Python function node."""
|
|
293
|
+
try:
|
|
294
|
+
name_node = None
|
|
295
|
+
params_node = None
|
|
296
|
+
is_async = False
|
|
297
|
+
|
|
298
|
+
# Check if async
|
|
299
|
+
for child in node.children:
|
|
300
|
+
if child.type == "async":
|
|
301
|
+
is_async = True
|
|
302
|
+
|
|
303
|
+
# Find name and parameters
|
|
304
|
+
for child in node.children:
|
|
305
|
+
if child.type == "identifier":
|
|
306
|
+
name_node = child
|
|
307
|
+
elif child.type == "parameters":
|
|
308
|
+
params_node = child
|
|
309
|
+
|
|
310
|
+
if not name_node:
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
name = self._get_node_text(name_node, source_lines)
|
|
314
|
+
params = self._extract_parameters(params_node, source_lines) if params_node else []
|
|
315
|
+
docstring = self._extract_python_docstring(node, source_lines)
|
|
316
|
+
|
|
317
|
+
kind = (
|
|
318
|
+
SymbolKind.ASYNC_FUNCTION
|
|
319
|
+
if is_async
|
|
320
|
+
else SymbolKind.METHOD
|
|
321
|
+
if is_method
|
|
322
|
+
else SymbolKind.FUNCTION
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return Symbol(
|
|
326
|
+
id=str(uuid.uuid4()),
|
|
327
|
+
name=name,
|
|
328
|
+
kind=kind,
|
|
329
|
+
file_path=file_path,
|
|
330
|
+
line_start=node.start_point[0] + 1,
|
|
331
|
+
line_end=node.end_point[0] + 1,
|
|
332
|
+
docstring=docstring,
|
|
333
|
+
parameters=params,
|
|
334
|
+
)
|
|
335
|
+
except Exception as e:
|
|
336
|
+
logger.debug(f"Error parsing Python function: {e}")
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
def _parse_python_class(
|
|
340
|
+
self, node: Any, source_lines: list[str], file_path: str
|
|
341
|
+
) -> Symbol | None:
|
|
342
|
+
"""Parse a Python class node."""
|
|
343
|
+
try:
|
|
344
|
+
name_node = None
|
|
345
|
+
for child in node.children:
|
|
346
|
+
if child.type == "identifier":
|
|
347
|
+
name_node = child
|
|
348
|
+
break
|
|
349
|
+
|
|
350
|
+
if not name_node:
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
name = self._get_node_text(name_node, source_lines)
|
|
354
|
+
docstring = self._extract_python_docstring(node, source_lines)
|
|
355
|
+
|
|
356
|
+
return Symbol(
|
|
357
|
+
id=str(uuid.uuid4()),
|
|
358
|
+
name=name,
|
|
359
|
+
kind=SymbolKind.CLASS,
|
|
360
|
+
file_path=file_path,
|
|
361
|
+
line_start=node.start_point[0] + 1,
|
|
362
|
+
line_end=node.end_point[0] + 1,
|
|
363
|
+
docstring=docstring,
|
|
364
|
+
)
|
|
365
|
+
except Exception as e:
|
|
366
|
+
logger.debug(f"Error parsing Python class: {e}")
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
def _parse_python_import(
|
|
370
|
+
self, node: Any, source_lines: list[str], file_path: str
|
|
371
|
+
) -> Symbol | None:
|
|
372
|
+
"""Parse a Python import statement."""
|
|
373
|
+
try:
|
|
374
|
+
text = self._get_node_text(node, source_lines).strip()
|
|
375
|
+
# Extract imported name
|
|
376
|
+
if "from" in text:
|
|
377
|
+
parts = text.split("import")
|
|
378
|
+
if len(parts) > 1:
|
|
379
|
+
name = parts[-1].strip().split()[0]
|
|
380
|
+
else:
|
|
381
|
+
return None
|
|
382
|
+
else:
|
|
383
|
+
name = text.replace("import", "").strip().split()[0]
|
|
384
|
+
|
|
385
|
+
return Symbol(
|
|
386
|
+
id=str(uuid.uuid4()),
|
|
387
|
+
name=name,
|
|
388
|
+
kind=SymbolKind.IMPORT,
|
|
389
|
+
file_path=file_path,
|
|
390
|
+
line_start=node.start_point[0] + 1,
|
|
391
|
+
line_end=node.end_point[0] + 1,
|
|
392
|
+
source_text=text,
|
|
393
|
+
)
|
|
394
|
+
except Exception as e:
|
|
395
|
+
logger.debug(f"Error parsing Python import: {e}")
|
|
396
|
+
return None
|
|
397
|
+
|
|
398
|
+
def _extract_js_symbols(
|
|
399
|
+
self,
|
|
400
|
+
tree: Tree,
|
|
401
|
+
source: str,
|
|
402
|
+
file_path: str,
|
|
403
|
+
language: str,
|
|
404
|
+
) -> list[Symbol]:
|
|
405
|
+
"""Extract symbols from JavaScript/TypeScript AST."""
|
|
406
|
+
symbols: list[Symbol] = []
|
|
407
|
+
source_lines = source.split("\n")
|
|
408
|
+
|
|
409
|
+
def visit(node: Any) -> None:
|
|
410
|
+
if node.type in ("function_declaration", "generator_function_declaration"):
|
|
411
|
+
sym = self._parse_js_function(node, source_lines, file_path, is_async=False)
|
|
412
|
+
if sym:
|
|
413
|
+
symbols.append(sym)
|
|
414
|
+
elif node.type == "class_declaration":
|
|
415
|
+
sym = self._parse_js_class(node, source_lines, file_path)
|
|
416
|
+
if sym:
|
|
417
|
+
symbols.append(sym)
|
|
418
|
+
# Extract methods
|
|
419
|
+
for child in node.children:
|
|
420
|
+
if child.type == "class_body":
|
|
421
|
+
for method_node in child.children:
|
|
422
|
+
if method_node.type in ("method_definition", "function_definition"):
|
|
423
|
+
method = self._parse_js_function(
|
|
424
|
+
method_node, source_lines, file_path, is_method=True
|
|
425
|
+
)
|
|
426
|
+
if method:
|
|
427
|
+
symbols.append(method)
|
|
428
|
+
elif node.type in ("import_statement", "import_specifier"):
|
|
429
|
+
sym = self._parse_js_import(node, source_lines, file_path)
|
|
430
|
+
if sym:
|
|
431
|
+
symbols.append(sym)
|
|
432
|
+
elif node.type == "export_statement":
|
|
433
|
+
# Extract what's being exported
|
|
434
|
+
for child in node.children:
|
|
435
|
+
if child.type in ("function_declaration", "class_declaration"):
|
|
436
|
+
visit(child)
|
|
437
|
+
|
|
438
|
+
for child in node.children:
|
|
439
|
+
visit(child)
|
|
440
|
+
|
|
441
|
+
visit(tree.root_node)
|
|
442
|
+
return symbols
|
|
443
|
+
|
|
444
|
+
def _parse_js_function(
|
|
445
|
+
self,
|
|
446
|
+
node: Any,
|
|
447
|
+
source_lines: list[str],
|
|
448
|
+
file_path: str,
|
|
449
|
+
is_async: bool = False,
|
|
450
|
+
is_method: bool = False,
|
|
451
|
+
) -> Symbol | None:
|
|
452
|
+
"""Parse a JS/TS function node."""
|
|
453
|
+
try:
|
|
454
|
+
name_node = None
|
|
455
|
+
params_node = None
|
|
456
|
+
|
|
457
|
+
for child in node.children:
|
|
458
|
+
if child.type == "identifier":
|
|
459
|
+
name_node = child
|
|
460
|
+
elif child.type == "formal_parameters":
|
|
461
|
+
params_node = child
|
|
462
|
+
|
|
463
|
+
if not name_node:
|
|
464
|
+
return None
|
|
465
|
+
|
|
466
|
+
name = self._get_node_text(name_node, source_lines)
|
|
467
|
+
params = self._extract_parameters(params_node, source_lines) if params_node else []
|
|
468
|
+
|
|
469
|
+
kind = (
|
|
470
|
+
SymbolKind.ASYNC_FUNCTION
|
|
471
|
+
if is_async
|
|
472
|
+
else SymbolKind.METHOD
|
|
473
|
+
if is_method
|
|
474
|
+
else SymbolKind.FUNCTION
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
return Symbol(
|
|
478
|
+
id=str(uuid.uuid4()),
|
|
479
|
+
name=name,
|
|
480
|
+
kind=kind,
|
|
481
|
+
file_path=file_path,
|
|
482
|
+
line_start=node.start_point[0] + 1,
|
|
483
|
+
line_end=node.end_point[0] + 1,
|
|
484
|
+
parameters=params,
|
|
485
|
+
)
|
|
486
|
+
except Exception as e:
|
|
487
|
+
logger.debug(f"Error parsing JS function: {e}")
|
|
488
|
+
return None
|
|
489
|
+
|
|
490
|
+
def _parse_js_class(self, node: Any, source_lines: list[str], file_path: str) -> Symbol | None:
|
|
491
|
+
"""Parse a JS/TS class node."""
|
|
492
|
+
try:
|
|
493
|
+
name_node = None
|
|
494
|
+
for child in node.children:
|
|
495
|
+
if child.type == "identifier":
|
|
496
|
+
name_node = child
|
|
497
|
+
break
|
|
498
|
+
|
|
499
|
+
if not name_node:
|
|
500
|
+
return None
|
|
501
|
+
|
|
502
|
+
name = self._get_node_text(name_node, source_lines)
|
|
503
|
+
|
|
504
|
+
return Symbol(
|
|
505
|
+
id=str(uuid.uuid4()),
|
|
506
|
+
name=name,
|
|
507
|
+
kind=SymbolKind.CLASS,
|
|
508
|
+
file_path=file_path,
|
|
509
|
+
line_start=node.start_point[0] + 1,
|
|
510
|
+
line_end=node.end_point[0] + 1,
|
|
511
|
+
)
|
|
512
|
+
except Exception as e:
|
|
513
|
+
logger.debug(f"Error parsing JS class: {e}")
|
|
514
|
+
return None
|
|
515
|
+
|
|
516
|
+
def _parse_js_import(self, node: Any, source_lines: list[str], file_path: str) -> Symbol | None:
|
|
517
|
+
"""Parse a JS/TS import statement."""
|
|
518
|
+
try:
|
|
519
|
+
# Find imported identifiers
|
|
520
|
+
text = self._get_node_text(node, source_lines).strip()
|
|
521
|
+
parts = text.split("from")[0].replace("import", "").strip()
|
|
522
|
+
name = parts.split(",")[0].strip().replace("{", "").replace("}", "").strip()
|
|
523
|
+
|
|
524
|
+
if not name:
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
return Symbol(
|
|
528
|
+
id=str(uuid.uuid4()),
|
|
529
|
+
name=name,
|
|
530
|
+
kind=SymbolKind.IMPORT,
|
|
531
|
+
file_path=file_path,
|
|
532
|
+
line_start=node.start_point[0] + 1,
|
|
533
|
+
line_end=node.end_point[0] + 1,
|
|
534
|
+
source_text=text,
|
|
535
|
+
)
|
|
536
|
+
except Exception as e:
|
|
537
|
+
logger.debug(f"Error parsing JS import: {e}")
|
|
538
|
+
return None
|
|
539
|
+
|
|
540
|
+
def _extract_rust_symbols(self, tree: Tree, source: str, file_path: str) -> list[Symbol]:
|
|
541
|
+
"""Extract symbols from Rust AST."""
|
|
542
|
+
symbols: list[Symbol] = []
|
|
543
|
+
source_lines = source.split("\n")
|
|
544
|
+
|
|
545
|
+
def visit(node: Any) -> None:
|
|
546
|
+
if node.type == "function_item":
|
|
547
|
+
sym = self._parse_rust_function(node, source_lines, file_path)
|
|
548
|
+
if sym:
|
|
549
|
+
symbols.append(sym)
|
|
550
|
+
elif node.type == "struct_item":
|
|
551
|
+
sym = self._parse_rust_struct(node, source_lines, file_path)
|
|
552
|
+
if sym:
|
|
553
|
+
symbols.append(sym)
|
|
554
|
+
elif node.type == "enum_item":
|
|
555
|
+
sym = self._parse_rust_enum(node, source_lines, file_path)
|
|
556
|
+
if sym:
|
|
557
|
+
symbols.append(sym)
|
|
558
|
+
elif node.type == "impl_item":
|
|
559
|
+
# Extract methods from impl block
|
|
560
|
+
for child in node.children:
|
|
561
|
+
if child.type == "function_item":
|
|
562
|
+
method = self._parse_rust_function(
|
|
563
|
+
child, source_lines, file_path, is_method=True
|
|
564
|
+
)
|
|
565
|
+
if method:
|
|
566
|
+
symbols.append(method)
|
|
567
|
+
|
|
568
|
+
for child in node.children:
|
|
569
|
+
visit(child)
|
|
570
|
+
|
|
571
|
+
visit(tree.root_node)
|
|
572
|
+
return symbols
|
|
573
|
+
|
|
574
|
+
def _parse_rust_function(
|
|
575
|
+
self,
|
|
576
|
+
node: Any,
|
|
577
|
+
source_lines: list[str],
|
|
578
|
+
file_path: str,
|
|
579
|
+
is_method: bool = False,
|
|
580
|
+
) -> Symbol | None:
|
|
581
|
+
"""Parse a Rust function node."""
|
|
582
|
+
try:
|
|
583
|
+
name_node = None
|
|
584
|
+
for child in node.children:
|
|
585
|
+
if child.type == "identifier":
|
|
586
|
+
name_node = child
|
|
587
|
+
break
|
|
588
|
+
|
|
589
|
+
if not name_node:
|
|
590
|
+
return None
|
|
591
|
+
|
|
592
|
+
name = self._get_node_text(name_node, source_lines)
|
|
593
|
+
|
|
594
|
+
return Symbol(
|
|
595
|
+
id=str(uuid.uuid4()),
|
|
596
|
+
name=name,
|
|
597
|
+
kind=SymbolKind.METHOD if is_method else SymbolKind.FUNCTION,
|
|
598
|
+
file_path=file_path,
|
|
599
|
+
line_start=node.start_point[0] + 1,
|
|
600
|
+
line_end=node.end_point[0] + 1,
|
|
601
|
+
)
|
|
602
|
+
except Exception as e:
|
|
603
|
+
logger.debug(f"Error parsing Rust function: {e}")
|
|
604
|
+
return None
|
|
605
|
+
|
|
606
|
+
def _parse_rust_struct(
|
|
607
|
+
self, node: Any, source_lines: list[str], file_path: str
|
|
608
|
+
) -> Symbol | None:
|
|
609
|
+
"""Parse a Rust struct node."""
|
|
610
|
+
try:
|
|
611
|
+
name_node = None
|
|
612
|
+
for child in node.children:
|
|
613
|
+
if child.type == "type_identifier":
|
|
614
|
+
name_node = child
|
|
615
|
+
break
|
|
616
|
+
|
|
617
|
+
if not name_node:
|
|
618
|
+
return None
|
|
619
|
+
|
|
620
|
+
name = self._get_node_text(name_node, source_lines)
|
|
621
|
+
|
|
622
|
+
return Symbol(
|
|
623
|
+
id=str(uuid.uuid4()),
|
|
624
|
+
name=name,
|
|
625
|
+
kind=SymbolKind.CLASS,
|
|
626
|
+
file_path=file_path,
|
|
627
|
+
line_start=node.start_point[0] + 1,
|
|
628
|
+
line_end=node.end_point[0] + 1,
|
|
629
|
+
)
|
|
630
|
+
except Exception as e:
|
|
631
|
+
logger.debug(f"Error parsing Rust struct: {e}")
|
|
632
|
+
return None
|
|
633
|
+
|
|
634
|
+
def _parse_rust_enum(self, node: Any, source_lines: list[str], file_path: str) -> Symbol | None:
|
|
635
|
+
"""Parse a Rust enum node."""
|
|
636
|
+
try:
|
|
637
|
+
name_node = None
|
|
638
|
+
for child in node.children:
|
|
639
|
+
if child.type == "type_identifier":
|
|
640
|
+
name_node = child
|
|
641
|
+
break
|
|
642
|
+
|
|
643
|
+
if not name_node:
|
|
644
|
+
return None
|
|
645
|
+
|
|
646
|
+
name = self._get_node_text(name_node, source_lines)
|
|
647
|
+
|
|
648
|
+
return Symbol(
|
|
649
|
+
id=str(uuid.uuid4()),
|
|
650
|
+
name=name,
|
|
651
|
+
kind=SymbolKind.ENUM,
|
|
652
|
+
file_path=file_path,
|
|
653
|
+
line_start=node.start_point[0] + 1,
|
|
654
|
+
line_end=node.end_point[0] + 1,
|
|
655
|
+
)
|
|
656
|
+
except Exception as e:
|
|
657
|
+
logger.debug(f"Error parsing Rust enum: {e}")
|
|
658
|
+
return None
|
|
659
|
+
|
|
660
|
+
def _extract_go_symbols(self, tree: Tree, source: str, file_path: str) -> list[Symbol]:
|
|
661
|
+
"""Extract symbols from Go AST."""
|
|
662
|
+
symbols: list[Symbol] = []
|
|
663
|
+
source_lines = source.split("\n")
|
|
664
|
+
|
|
665
|
+
def visit(node: Any) -> None:
|
|
666
|
+
if node.type == "function_declaration":
|
|
667
|
+
sym = self._parse_go_function(node, source_lines, file_path)
|
|
668
|
+
if sym:
|
|
669
|
+
symbols.append(sym)
|
|
670
|
+
elif node.type == "method_declaration":
|
|
671
|
+
sym = self._parse_go_function(node, source_lines, file_path, is_method=True)
|
|
672
|
+
if sym:
|
|
673
|
+
symbols.append(sym)
|
|
674
|
+
elif node.type == "type_declaration":
|
|
675
|
+
# Handle struct/interface definitions
|
|
676
|
+
for child in node.children:
|
|
677
|
+
if child.type in ("struct_type", "interface_type"):
|
|
678
|
+
kind = (
|
|
679
|
+
SymbolKind.INTERFACE
|
|
680
|
+
if child.type == "interface_type"
|
|
681
|
+
else SymbolKind.CLASS
|
|
682
|
+
)
|
|
683
|
+
sym = self._parse_go_type(child, source_lines, file_path, kind)
|
|
684
|
+
if sym:
|
|
685
|
+
symbols.append(sym)
|
|
686
|
+
|
|
687
|
+
for child in node.children:
|
|
688
|
+
visit(child)
|
|
689
|
+
|
|
690
|
+
visit(tree.root_node)
|
|
691
|
+
return symbols
|
|
692
|
+
|
|
693
|
+
def _parse_go_function(
|
|
694
|
+
self,
|
|
695
|
+
node: Any,
|
|
696
|
+
source_lines: list[str],
|
|
697
|
+
file_path: str,
|
|
698
|
+
is_method: bool = False,
|
|
699
|
+
) -> Symbol | None:
|
|
700
|
+
"""Parse a Go function node."""
|
|
701
|
+
try:
|
|
702
|
+
name_node = None
|
|
703
|
+
for child in node.children:
|
|
704
|
+
if child.type == "identifier":
|
|
705
|
+
name_node = child
|
|
706
|
+
break
|
|
707
|
+
|
|
708
|
+
if not name_node:
|
|
709
|
+
return None
|
|
710
|
+
|
|
711
|
+
name = self._get_node_text(name_node, source_lines)
|
|
712
|
+
|
|
713
|
+
return Symbol(
|
|
714
|
+
id=str(uuid.uuid4()),
|
|
715
|
+
name=name,
|
|
716
|
+
kind=SymbolKind.METHOD if is_method else SymbolKind.FUNCTION,
|
|
717
|
+
file_path=file_path,
|
|
718
|
+
line_start=node.start_point[0] + 1,
|
|
719
|
+
line_end=node.end_point[0] + 1,
|
|
720
|
+
)
|
|
721
|
+
except Exception as e:
|
|
722
|
+
logger.debug(f"Error parsing Go function: {e}")
|
|
723
|
+
return None
|
|
724
|
+
|
|
725
|
+
def _parse_go_type(
|
|
726
|
+
self,
|
|
727
|
+
node: Any,
|
|
728
|
+
source_lines: list[str],
|
|
729
|
+
file_path: str,
|
|
730
|
+
kind: SymbolKind,
|
|
731
|
+
) -> Symbol | None:
|
|
732
|
+
"""Parse a Go struct/interface type."""
|
|
733
|
+
try:
|
|
734
|
+
# Go types are usually preceded by their name in a type_declaration
|
|
735
|
+
# This is a simplification
|
|
736
|
+
text = self._get_node_text(node, source_lines).strip()
|
|
737
|
+
# Extract name from type definition
|
|
738
|
+
name = text.split("{")[0].split("(")[0].strip()
|
|
739
|
+
|
|
740
|
+
if not name:
|
|
741
|
+
return None
|
|
742
|
+
|
|
743
|
+
return Symbol(
|
|
744
|
+
id=str(uuid.uuid4()),
|
|
745
|
+
name=name,
|
|
746
|
+
kind=kind,
|
|
747
|
+
file_path=file_path,
|
|
748
|
+
line_start=node.start_point[0] + 1,
|
|
749
|
+
line_end=node.end_point[0] + 1,
|
|
750
|
+
)
|
|
751
|
+
except Exception as e:
|
|
752
|
+
logger.debug(f"Error parsing Go type: {e}")
|
|
753
|
+
return None
|
|
754
|
+
|
|
755
|
+
def _get_node_text(self, node: Any, source_lines: list[str]) -> str:
|
|
756
|
+
"""Extract text of a node from source."""
|
|
757
|
+
try:
|
|
758
|
+
start_row = node.start_point[0]
|
|
759
|
+
start_col = node.start_point[1]
|
|
760
|
+
end_row = node.end_point[0]
|
|
761
|
+
end_col = node.end_point[1]
|
|
762
|
+
|
|
763
|
+
if start_row == end_row:
|
|
764
|
+
return source_lines[start_row][start_col:end_col]
|
|
765
|
+
|
|
766
|
+
lines = [source_lines[start_row][start_col:]]
|
|
767
|
+
for row in range(start_row + 1, end_row):
|
|
768
|
+
lines.append(source_lines[row])
|
|
769
|
+
lines.append(source_lines[end_row][:end_col])
|
|
770
|
+
|
|
771
|
+
return "\n".join(lines)
|
|
772
|
+
except Exception:
|
|
773
|
+
return ""
|
|
774
|
+
|
|
775
|
+
def _extract_parameters(self, params_node: Any, source_lines: list[str]) -> list[str]:
|
|
776
|
+
"""Extract parameter names from function parameters node."""
|
|
777
|
+
try:
|
|
778
|
+
text = self._get_node_text(params_node, source_lines)
|
|
779
|
+
# Simple extraction: split by comma, extract identifiers
|
|
780
|
+
params = []
|
|
781
|
+
for part in text.replace("(", "").replace(")", "").split(","):
|
|
782
|
+
part = part.strip()
|
|
783
|
+
if part:
|
|
784
|
+
# Extract identifier (first word before : or =)
|
|
785
|
+
name = part.split(":")[0].split("=")[0].strip()
|
|
786
|
+
if name:
|
|
787
|
+
params.append(name)
|
|
788
|
+
return params
|
|
789
|
+
except Exception:
|
|
790
|
+
return []
|
|
791
|
+
|
|
792
|
+
def _extract_python_docstring(self, node: Any, source_lines: list[str]) -> str | None:
|
|
793
|
+
"""Extract docstring from Python function or class."""
|
|
794
|
+
try:
|
|
795
|
+
# Look for string node after function/class definition
|
|
796
|
+
for child in node.children:
|
|
797
|
+
if child.type == "block":
|
|
798
|
+
for subchild in child.children:
|
|
799
|
+
if subchild.type == "expression_statement":
|
|
800
|
+
for subsubchild in subchild.children:
|
|
801
|
+
if subsubchild.type == "string":
|
|
802
|
+
text = self._get_node_text(subsubchild, source_lines)
|
|
803
|
+
# Remove quotes
|
|
804
|
+
if text.startswith('"""') or text.startswith("'''"):
|
|
805
|
+
return text[3:-3].strip()
|
|
806
|
+
return text.strip("\"'")
|
|
807
|
+
return None
|
|
808
|
+
except Exception:
|
|
809
|
+
return None
|
|
810
|
+
|
|
811
|
+
def _detect_language(self, path: Path) -> str | None:
|
|
812
|
+
"""Detect language from file extension."""
|
|
813
|
+
ext_map = {
|
|
814
|
+
".py": "python",
|
|
815
|
+
".js": "javascript",
|
|
816
|
+
".jsx": "javascript",
|
|
817
|
+
".ts": "typescript",
|
|
818
|
+
".tsx": "typescript",
|
|
819
|
+
".rs": "rust",
|
|
820
|
+
".go": "go",
|
|
821
|
+
}
|
|
822
|
+
return ext_map.get(path.suffix.lower())
|