claude-code-workflow 6.3.13 → 6.3.15
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.
- package/.claude/agents/issue-plan-agent.md +57 -103
- package/.claude/agents/issue-queue-agent.md +69 -120
- package/.claude/commands/issue/new.md +217 -473
- package/.claude/commands/issue/plan.md +76 -154
- package/.claude/commands/issue/queue.md +208 -259
- package/.claude/skills/issue-manage/SKILL.md +63 -22
- package/.claude/workflows/cli-templates/schemas/discovery-finding-schema.json +3 -3
- package/.claude/workflows/cli-templates/schemas/issues-jsonl-schema.json +3 -3
- package/.claude/workflows/cli-templates/schemas/queue-schema.json +0 -5
- package/.codex/prompts/issue-plan.md +16 -19
- package/.codex/prompts/issue-queue.md +0 -1
- package/README.md +1 -0
- package/ccw/dist/cli.d.ts.map +1 -1
- package/ccw/dist/cli.js +3 -1
- package/ccw/dist/cli.js.map +1 -1
- package/ccw/dist/commands/cli.d.ts.map +1 -1
- package/ccw/dist/commands/cli.js +45 -3
- package/ccw/dist/commands/cli.js.map +1 -1
- package/ccw/dist/commands/issue.d.ts +3 -1
- package/ccw/dist/commands/issue.d.ts.map +1 -1
- package/ccw/dist/commands/issue.js +383 -30
- package/ccw/dist/commands/issue.js.map +1 -1
- package/ccw/dist/core/routes/issue-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/issue-routes.js +77 -16
- package/ccw/dist/core/routes/issue-routes.js.map +1 -1
- package/ccw/dist/tools/cli-executor.d.ts.map +1 -1
- package/ccw/dist/tools/cli-executor.js +117 -4
- package/ccw/dist/tools/cli-executor.js.map +1 -1
- package/ccw/dist/tools/litellm-executor.d.ts +4 -0
- package/ccw/dist/tools/litellm-executor.d.ts.map +1 -1
- package/ccw/dist/tools/litellm-executor.js +54 -1
- package/ccw/dist/tools/litellm-executor.js.map +1 -1
- package/ccw/dist/tools/ui-generate-preview.d.ts +18 -0
- package/ccw/dist/tools/ui-generate-preview.d.ts.map +1 -1
- package/ccw/dist/tools/ui-generate-preview.js +26 -10
- package/ccw/dist/tools/ui-generate-preview.js.map +1 -1
- package/ccw/src/cli.ts +3 -1
- package/ccw/src/commands/cli.ts +47 -3
- package/ccw/src/commands/issue.ts +442 -34
- package/ccw/src/core/routes/issue-routes.ts +82 -16
- package/ccw/src/tools/cli-executor.ts +125 -4
- package/ccw/src/tools/litellm-executor.ts +107 -24
- package/ccw/src/tools/ui-generate-preview.js +60 -37
- package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/__pycache__/entities.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/config.py +25 -2
- package/codex-lens/src/codexlens/entities.py +5 -1
- package/codex-lens/src/codexlens/indexing/__pycache__/symbol_extractor.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/indexing/symbol_extractor.py +243 -243
- package/codex-lens/src/codexlens/parsers/__pycache__/factory.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/parsers/__pycache__/treesitter_parser.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/parsers/factory.py +256 -256
- package/codex-lens/src/codexlens/parsers/treesitter_parser.py +335 -335
- package/codex-lens/src/codexlens/search/__pycache__/chain_search.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/search/__pycache__/hybrid_search.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/search/chain_search.py +30 -1
- package/codex-lens/src/codexlens/semantic/__pycache__/__init__.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/semantic/__pycache__/embedder.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/semantic/__pycache__/reranker.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/semantic/__pycache__/vector_store.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/semantic/embedder.py +6 -9
- package/codex-lens/src/codexlens/semantic/vector_store.py +271 -200
- package/codex-lens/src/codexlens/storage/__pycache__/dir_index.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/storage/__pycache__/index_tree.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/storage/__pycache__/sqlite_store.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/storage/sqlite_store.py +184 -108
- package/package.json +6 -1
- package/.claude/commands/issue/manage.md +0 -113
|
@@ -1,335 +1,335 @@
|
|
|
1
|
-
"""Tree-sitter based parser for CodexLens.
|
|
2
|
-
|
|
3
|
-
Provides precise AST-level parsing with fallback to regex-based parsing.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import List, Optional
|
|
10
|
-
|
|
11
|
-
try:
|
|
12
|
-
from tree_sitter import Language as TreeSitterLanguage
|
|
13
|
-
from tree_sitter import Node as TreeSitterNode
|
|
14
|
-
from tree_sitter import Parser as TreeSitterParser
|
|
15
|
-
TREE_SITTER_AVAILABLE = True
|
|
16
|
-
except ImportError:
|
|
17
|
-
TreeSitterLanguage = None # type: ignore[assignment]
|
|
18
|
-
TreeSitterNode = None # type: ignore[assignment]
|
|
19
|
-
TreeSitterParser = None # type: ignore[assignment]
|
|
20
|
-
TREE_SITTER_AVAILABLE = False
|
|
21
|
-
|
|
22
|
-
from codexlens.entities import IndexedFile, Symbol
|
|
23
|
-
from codexlens.parsers.tokenizer import get_default_tokenizer
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class TreeSitterSymbolParser:
|
|
27
|
-
"""Parser using tree-sitter for AST-level symbol extraction."""
|
|
28
|
-
|
|
29
|
-
def __init__(self, language_id: str, path: Optional[Path] = None) -> None:
|
|
30
|
-
"""Initialize tree-sitter parser for a language.
|
|
31
|
-
|
|
32
|
-
Args:
|
|
33
|
-
language_id: Language identifier (python, javascript, typescript, etc.)
|
|
34
|
-
path: Optional file path for language variant detection (e.g., .tsx)
|
|
35
|
-
"""
|
|
36
|
-
self.language_id = language_id
|
|
37
|
-
self.path = path
|
|
38
|
-
self._parser: Optional[object] = None
|
|
39
|
-
self._language: Optional[TreeSitterLanguage] = None
|
|
40
|
-
self._tokenizer = get_default_tokenizer()
|
|
41
|
-
|
|
42
|
-
if TREE_SITTER_AVAILABLE:
|
|
43
|
-
self._initialize_parser()
|
|
44
|
-
|
|
45
|
-
def _initialize_parser(self) -> None:
|
|
46
|
-
"""Initialize tree-sitter parser and language."""
|
|
47
|
-
if TreeSitterParser is None or TreeSitterLanguage is None:
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
try:
|
|
51
|
-
# Load language grammar
|
|
52
|
-
if self.language_id == "python":
|
|
53
|
-
import tree_sitter_python
|
|
54
|
-
self._language = TreeSitterLanguage(tree_sitter_python.language())
|
|
55
|
-
elif self.language_id == "javascript":
|
|
56
|
-
import tree_sitter_javascript
|
|
57
|
-
self._language = TreeSitterLanguage(tree_sitter_javascript.language())
|
|
58
|
-
elif self.language_id == "typescript":
|
|
59
|
-
import tree_sitter_typescript
|
|
60
|
-
# Detect TSX files by extension
|
|
61
|
-
if self.path is not None and self.path.suffix.lower() == ".tsx":
|
|
62
|
-
self._language = TreeSitterLanguage(tree_sitter_typescript.language_tsx())
|
|
63
|
-
else:
|
|
64
|
-
self._language = TreeSitterLanguage(tree_sitter_typescript.language_typescript())
|
|
65
|
-
else:
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
# Create parser
|
|
69
|
-
self._parser = TreeSitterParser()
|
|
70
|
-
if hasattr(self._parser, "set_language"):
|
|
71
|
-
self._parser.set_language(self._language) # type: ignore[attr-defined]
|
|
72
|
-
else:
|
|
73
|
-
self._parser.language = self._language # type: ignore[assignment]
|
|
74
|
-
|
|
75
|
-
except Exception:
|
|
76
|
-
# Gracefully handle missing language bindings
|
|
77
|
-
self._parser = None
|
|
78
|
-
self._language = None
|
|
79
|
-
|
|
80
|
-
def is_available(self) -> bool:
|
|
81
|
-
"""Check if tree-sitter parser is available.
|
|
82
|
-
|
|
83
|
-
Returns:
|
|
84
|
-
True if parser is initialized and ready
|
|
85
|
-
"""
|
|
86
|
-
return self._parser is not None and self._language is not None
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def parse_symbols(self, text: str) -> Optional[List[Symbol]]:
|
|
90
|
-
"""Parse source code and extract symbols without creating IndexedFile.
|
|
91
|
-
|
|
92
|
-
Args:
|
|
93
|
-
text: Source code text
|
|
94
|
-
|
|
95
|
-
Returns:
|
|
96
|
-
List of symbols if parsing succeeds, None if tree-sitter unavailable
|
|
97
|
-
"""
|
|
98
|
-
if not self.is_available() or self._parser is None:
|
|
99
|
-
return None
|
|
100
|
-
|
|
101
|
-
try:
|
|
102
|
-
source_bytes = text.encode("utf8")
|
|
103
|
-
tree = self._parser.parse(source_bytes) # type: ignore[attr-defined]
|
|
104
|
-
root = tree.root_node
|
|
105
|
-
|
|
106
|
-
return self._extract_symbols(source_bytes, root)
|
|
107
|
-
except Exception:
|
|
108
|
-
# Gracefully handle parsing errors
|
|
109
|
-
return None
|
|
110
|
-
|
|
111
|
-
def parse(self, text: str, path: Path) -> Optional[IndexedFile]:
|
|
112
|
-
"""Parse source code and extract symbols.
|
|
113
|
-
|
|
114
|
-
Args:
|
|
115
|
-
text: Source code text
|
|
116
|
-
path: File path
|
|
117
|
-
|
|
118
|
-
Returns:
|
|
119
|
-
IndexedFile if parsing succeeds, None if tree-sitter unavailable
|
|
120
|
-
"""
|
|
121
|
-
if not self.is_available() or self._parser is None:
|
|
122
|
-
return None
|
|
123
|
-
|
|
124
|
-
try:
|
|
125
|
-
symbols = self.parse_symbols(text)
|
|
126
|
-
if symbols is None:
|
|
127
|
-
return None
|
|
128
|
-
|
|
129
|
-
return IndexedFile(
|
|
130
|
-
path=str(path.resolve()),
|
|
131
|
-
language=self.language_id,
|
|
132
|
-
symbols=symbols,
|
|
133
|
-
chunks=[],
|
|
134
|
-
)
|
|
135
|
-
except Exception:
|
|
136
|
-
# Gracefully handle parsing errors
|
|
137
|
-
return None
|
|
138
|
-
|
|
139
|
-
def _extract_symbols(self, source_bytes: bytes, root: TreeSitterNode) -> List[Symbol]:
|
|
140
|
-
"""Extract symbols from AST.
|
|
141
|
-
|
|
142
|
-
Args:
|
|
143
|
-
source_bytes: Source code as bytes
|
|
144
|
-
root: Root AST node
|
|
145
|
-
|
|
146
|
-
Returns:
|
|
147
|
-
List of extracted symbols
|
|
148
|
-
"""
|
|
149
|
-
if self.language_id == "python":
|
|
150
|
-
return self._extract_python_symbols(source_bytes, root)
|
|
151
|
-
elif self.language_id in {"javascript", "typescript"}:
|
|
152
|
-
return self._extract_js_ts_symbols(source_bytes, root)
|
|
153
|
-
else:
|
|
154
|
-
return []
|
|
155
|
-
|
|
156
|
-
def _extract_python_symbols(self, source_bytes: bytes, root: TreeSitterNode) -> List[Symbol]:
|
|
157
|
-
"""Extract Python symbols from AST.
|
|
158
|
-
|
|
159
|
-
Args:
|
|
160
|
-
source_bytes: Source code as bytes
|
|
161
|
-
root: Root AST node
|
|
162
|
-
|
|
163
|
-
Returns:
|
|
164
|
-
List of Python symbols (classes, functions, methods)
|
|
165
|
-
"""
|
|
166
|
-
symbols: List[Symbol] = []
|
|
167
|
-
|
|
168
|
-
for node in self._iter_nodes(root):
|
|
169
|
-
if node.type == "class_definition":
|
|
170
|
-
name_node = node.child_by_field_name("name")
|
|
171
|
-
if name_node is None:
|
|
172
|
-
continue
|
|
173
|
-
symbols.append(Symbol(
|
|
174
|
-
name=self._node_text(source_bytes, name_node),
|
|
175
|
-
kind="class",
|
|
176
|
-
range=self._node_range(node),
|
|
177
|
-
))
|
|
178
|
-
elif node.type in {"function_definition", "async_function_definition"}:
|
|
179
|
-
name_node = node.child_by_field_name("name")
|
|
180
|
-
if name_node is None:
|
|
181
|
-
continue
|
|
182
|
-
symbols.append(Symbol(
|
|
183
|
-
name=self._node_text(source_bytes, name_node),
|
|
184
|
-
kind=self._python_function_kind(node),
|
|
185
|
-
range=self._node_range(node),
|
|
186
|
-
))
|
|
187
|
-
|
|
188
|
-
return symbols
|
|
189
|
-
|
|
190
|
-
def _extract_js_ts_symbols(self, source_bytes: bytes, root: TreeSitterNode) -> List[Symbol]:
|
|
191
|
-
"""Extract JavaScript/TypeScript symbols from AST.
|
|
192
|
-
|
|
193
|
-
Args:
|
|
194
|
-
source_bytes: Source code as bytes
|
|
195
|
-
root: Root AST node
|
|
196
|
-
|
|
197
|
-
Returns:
|
|
198
|
-
List of JS/TS symbols (classes, functions, methods)
|
|
199
|
-
"""
|
|
200
|
-
symbols: List[Symbol] = []
|
|
201
|
-
|
|
202
|
-
for node in self._iter_nodes(root):
|
|
203
|
-
if node.type in {"class_declaration", "class"}:
|
|
204
|
-
name_node = node.child_by_field_name("name")
|
|
205
|
-
if name_node is None:
|
|
206
|
-
continue
|
|
207
|
-
symbols.append(Symbol(
|
|
208
|
-
name=self._node_text(source_bytes, name_node),
|
|
209
|
-
kind="class",
|
|
210
|
-
range=self._node_range(node),
|
|
211
|
-
))
|
|
212
|
-
elif node.type in {"function_declaration", "generator_function_declaration"}:
|
|
213
|
-
name_node = node.child_by_field_name("name")
|
|
214
|
-
if name_node is None:
|
|
215
|
-
continue
|
|
216
|
-
symbols.append(Symbol(
|
|
217
|
-
name=self._node_text(source_bytes, name_node),
|
|
218
|
-
kind="function",
|
|
219
|
-
range=self._node_range(node),
|
|
220
|
-
))
|
|
221
|
-
elif node.type == "variable_declarator":
|
|
222
|
-
name_node = node.child_by_field_name("name")
|
|
223
|
-
value_node = node.child_by_field_name("value")
|
|
224
|
-
if (
|
|
225
|
-
name_node is None
|
|
226
|
-
or value_node is None
|
|
227
|
-
or name_node.type not in {"identifier", "property_identifier"}
|
|
228
|
-
or value_node.type != "arrow_function"
|
|
229
|
-
):
|
|
230
|
-
continue
|
|
231
|
-
symbols.append(Symbol(
|
|
232
|
-
name=self._node_text(source_bytes, name_node),
|
|
233
|
-
kind="function",
|
|
234
|
-
range=self._node_range(node),
|
|
235
|
-
))
|
|
236
|
-
elif node.type == "method_definition" and self._has_class_ancestor(node):
|
|
237
|
-
name_node = node.child_by_field_name("name")
|
|
238
|
-
if name_node is None:
|
|
239
|
-
continue
|
|
240
|
-
name = self._node_text(source_bytes, name_node)
|
|
241
|
-
if name == "constructor":
|
|
242
|
-
continue
|
|
243
|
-
symbols.append(Symbol(
|
|
244
|
-
name=name,
|
|
245
|
-
kind="method",
|
|
246
|
-
range=self._node_range(node),
|
|
247
|
-
))
|
|
248
|
-
|
|
249
|
-
return symbols
|
|
250
|
-
|
|
251
|
-
def _python_function_kind(self, node: TreeSitterNode) -> str:
|
|
252
|
-
"""Determine if Python function is a method or standalone function.
|
|
253
|
-
|
|
254
|
-
Args:
|
|
255
|
-
node: Function definition node
|
|
256
|
-
|
|
257
|
-
Returns:
|
|
258
|
-
'method' if inside a class, 'function' otherwise
|
|
259
|
-
"""
|
|
260
|
-
parent = node.parent
|
|
261
|
-
while parent is not None:
|
|
262
|
-
if parent.type in {"function_definition", "async_function_definition"}:
|
|
263
|
-
return "function"
|
|
264
|
-
if parent.type == "class_definition":
|
|
265
|
-
return "method"
|
|
266
|
-
parent = parent.parent
|
|
267
|
-
return "function"
|
|
268
|
-
|
|
269
|
-
def _has_class_ancestor(self, node: TreeSitterNode) -> bool:
|
|
270
|
-
"""Check if node has a class ancestor.
|
|
271
|
-
|
|
272
|
-
Args:
|
|
273
|
-
node: AST node to check
|
|
274
|
-
|
|
275
|
-
Returns:
|
|
276
|
-
True if node is inside a class
|
|
277
|
-
"""
|
|
278
|
-
parent = node.parent
|
|
279
|
-
while parent is not None:
|
|
280
|
-
if parent.type in {"class_declaration", "class"}:
|
|
281
|
-
return True
|
|
282
|
-
parent = parent.parent
|
|
283
|
-
return False
|
|
284
|
-
|
|
285
|
-
def _iter_nodes(self, root: TreeSitterNode):
|
|
286
|
-
"""Iterate over all nodes in AST.
|
|
287
|
-
|
|
288
|
-
Args:
|
|
289
|
-
root: Root node to start iteration
|
|
290
|
-
|
|
291
|
-
Yields:
|
|
292
|
-
AST nodes in depth-first order
|
|
293
|
-
"""
|
|
294
|
-
stack = [root]
|
|
295
|
-
while stack:
|
|
296
|
-
node = stack.pop()
|
|
297
|
-
yield node
|
|
298
|
-
for child in reversed(node.children):
|
|
299
|
-
stack.append(child)
|
|
300
|
-
|
|
301
|
-
def _node_text(self, source_bytes: bytes, node: TreeSitterNode) -> str:
|
|
302
|
-
"""Extract text for a node.
|
|
303
|
-
|
|
304
|
-
Args:
|
|
305
|
-
source_bytes: Source code as bytes
|
|
306
|
-
node: AST node
|
|
307
|
-
|
|
308
|
-
Returns:
|
|
309
|
-
Text content of node
|
|
310
|
-
"""
|
|
311
|
-
return source_bytes[node.start_byte:node.end_byte].decode("utf8")
|
|
312
|
-
|
|
313
|
-
def _node_range(self, node: TreeSitterNode) -> tuple[int, int]:
|
|
314
|
-
"""Get line range for a node.
|
|
315
|
-
|
|
316
|
-
Args:
|
|
317
|
-
node: AST node
|
|
318
|
-
|
|
319
|
-
Returns:
|
|
320
|
-
(start_line, end_line) tuple, 1-based inclusive
|
|
321
|
-
"""
|
|
322
|
-
start_line = node.start_point[0] + 1
|
|
323
|
-
end_line = node.end_point[0] + 1
|
|
324
|
-
return (start_line, max(start_line, end_line))
|
|
325
|
-
|
|
326
|
-
def count_tokens(self, text: str) -> int:
|
|
327
|
-
"""Count tokens in text.
|
|
328
|
-
|
|
329
|
-
Args:
|
|
330
|
-
text: Text to count tokens for
|
|
331
|
-
|
|
332
|
-
Returns:
|
|
333
|
-
Token count
|
|
334
|
-
"""
|
|
335
|
-
return self._tokenizer.count_tokens(text)
|
|
1
|
+
"""Tree-sitter based parser for CodexLens.
|
|
2
|
+
|
|
3
|
+
Provides precise AST-level parsing with fallback to regex-based parsing.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from tree_sitter import Language as TreeSitterLanguage
|
|
13
|
+
from tree_sitter import Node as TreeSitterNode
|
|
14
|
+
from tree_sitter import Parser as TreeSitterParser
|
|
15
|
+
TREE_SITTER_AVAILABLE = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
TreeSitterLanguage = None # type: ignore[assignment]
|
|
18
|
+
TreeSitterNode = None # type: ignore[assignment]
|
|
19
|
+
TreeSitterParser = None # type: ignore[assignment]
|
|
20
|
+
TREE_SITTER_AVAILABLE = False
|
|
21
|
+
|
|
22
|
+
from codexlens.entities import IndexedFile, Symbol
|
|
23
|
+
from codexlens.parsers.tokenizer import get_default_tokenizer
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TreeSitterSymbolParser:
|
|
27
|
+
"""Parser using tree-sitter for AST-level symbol extraction."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, language_id: str, path: Optional[Path] = None) -> None:
|
|
30
|
+
"""Initialize tree-sitter parser for a language.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
language_id: Language identifier (python, javascript, typescript, etc.)
|
|
34
|
+
path: Optional file path for language variant detection (e.g., .tsx)
|
|
35
|
+
"""
|
|
36
|
+
self.language_id = language_id
|
|
37
|
+
self.path = path
|
|
38
|
+
self._parser: Optional[object] = None
|
|
39
|
+
self._language: Optional[TreeSitterLanguage] = None
|
|
40
|
+
self._tokenizer = get_default_tokenizer()
|
|
41
|
+
|
|
42
|
+
if TREE_SITTER_AVAILABLE:
|
|
43
|
+
self._initialize_parser()
|
|
44
|
+
|
|
45
|
+
def _initialize_parser(self) -> None:
|
|
46
|
+
"""Initialize tree-sitter parser and language."""
|
|
47
|
+
if TreeSitterParser is None or TreeSitterLanguage is None:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
# Load language grammar
|
|
52
|
+
if self.language_id == "python":
|
|
53
|
+
import tree_sitter_python
|
|
54
|
+
self._language = TreeSitterLanguage(tree_sitter_python.language())
|
|
55
|
+
elif self.language_id == "javascript":
|
|
56
|
+
import tree_sitter_javascript
|
|
57
|
+
self._language = TreeSitterLanguage(tree_sitter_javascript.language())
|
|
58
|
+
elif self.language_id == "typescript":
|
|
59
|
+
import tree_sitter_typescript
|
|
60
|
+
# Detect TSX files by extension
|
|
61
|
+
if self.path is not None and self.path.suffix.lower() == ".tsx":
|
|
62
|
+
self._language = TreeSitterLanguage(tree_sitter_typescript.language_tsx())
|
|
63
|
+
else:
|
|
64
|
+
self._language = TreeSitterLanguage(tree_sitter_typescript.language_typescript())
|
|
65
|
+
else:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
# Create parser
|
|
69
|
+
self._parser = TreeSitterParser()
|
|
70
|
+
if hasattr(self._parser, "set_language"):
|
|
71
|
+
self._parser.set_language(self._language) # type: ignore[attr-defined]
|
|
72
|
+
else:
|
|
73
|
+
self._parser.language = self._language # type: ignore[assignment]
|
|
74
|
+
|
|
75
|
+
except Exception:
|
|
76
|
+
# Gracefully handle missing language bindings
|
|
77
|
+
self._parser = None
|
|
78
|
+
self._language = None
|
|
79
|
+
|
|
80
|
+
def is_available(self) -> bool:
|
|
81
|
+
"""Check if tree-sitter parser is available.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
True if parser is initialized and ready
|
|
85
|
+
"""
|
|
86
|
+
return self._parser is not None and self._language is not None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def parse_symbols(self, text: str) -> Optional[List[Symbol]]:
|
|
90
|
+
"""Parse source code and extract symbols without creating IndexedFile.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
text: Source code text
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
List of symbols if parsing succeeds, None if tree-sitter unavailable
|
|
97
|
+
"""
|
|
98
|
+
if not self.is_available() or self._parser is None:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
source_bytes = text.encode("utf8")
|
|
103
|
+
tree = self._parser.parse(source_bytes) # type: ignore[attr-defined]
|
|
104
|
+
root = tree.root_node
|
|
105
|
+
|
|
106
|
+
return self._extract_symbols(source_bytes, root)
|
|
107
|
+
except Exception:
|
|
108
|
+
# Gracefully handle parsing errors
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def parse(self, text: str, path: Path) -> Optional[IndexedFile]:
|
|
112
|
+
"""Parse source code and extract symbols.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
text: Source code text
|
|
116
|
+
path: File path
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
IndexedFile if parsing succeeds, None if tree-sitter unavailable
|
|
120
|
+
"""
|
|
121
|
+
if not self.is_available() or self._parser is None:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
symbols = self.parse_symbols(text)
|
|
126
|
+
if symbols is None:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
return IndexedFile(
|
|
130
|
+
path=str(path.resolve()),
|
|
131
|
+
language=self.language_id,
|
|
132
|
+
symbols=symbols,
|
|
133
|
+
chunks=[],
|
|
134
|
+
)
|
|
135
|
+
except Exception:
|
|
136
|
+
# Gracefully handle parsing errors
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
def _extract_symbols(self, source_bytes: bytes, root: TreeSitterNode) -> List[Symbol]:
|
|
140
|
+
"""Extract symbols from AST.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
source_bytes: Source code as bytes
|
|
144
|
+
root: Root AST node
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
List of extracted symbols
|
|
148
|
+
"""
|
|
149
|
+
if self.language_id == "python":
|
|
150
|
+
return self._extract_python_symbols(source_bytes, root)
|
|
151
|
+
elif self.language_id in {"javascript", "typescript"}:
|
|
152
|
+
return self._extract_js_ts_symbols(source_bytes, root)
|
|
153
|
+
else:
|
|
154
|
+
return []
|
|
155
|
+
|
|
156
|
+
def _extract_python_symbols(self, source_bytes: bytes, root: TreeSitterNode) -> List[Symbol]:
|
|
157
|
+
"""Extract Python symbols from AST.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
source_bytes: Source code as bytes
|
|
161
|
+
root: Root AST node
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
List of Python symbols (classes, functions, methods)
|
|
165
|
+
"""
|
|
166
|
+
symbols: List[Symbol] = []
|
|
167
|
+
|
|
168
|
+
for node in self._iter_nodes(root):
|
|
169
|
+
if node.type == "class_definition":
|
|
170
|
+
name_node = node.child_by_field_name("name")
|
|
171
|
+
if name_node is None:
|
|
172
|
+
continue
|
|
173
|
+
symbols.append(Symbol(
|
|
174
|
+
name=self._node_text(source_bytes, name_node),
|
|
175
|
+
kind="class",
|
|
176
|
+
range=self._node_range(node),
|
|
177
|
+
))
|
|
178
|
+
elif node.type in {"function_definition", "async_function_definition"}:
|
|
179
|
+
name_node = node.child_by_field_name("name")
|
|
180
|
+
if name_node is None:
|
|
181
|
+
continue
|
|
182
|
+
symbols.append(Symbol(
|
|
183
|
+
name=self._node_text(source_bytes, name_node),
|
|
184
|
+
kind=self._python_function_kind(node),
|
|
185
|
+
range=self._node_range(node),
|
|
186
|
+
))
|
|
187
|
+
|
|
188
|
+
return symbols
|
|
189
|
+
|
|
190
|
+
def _extract_js_ts_symbols(self, source_bytes: bytes, root: TreeSitterNode) -> List[Symbol]:
|
|
191
|
+
"""Extract JavaScript/TypeScript symbols from AST.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
source_bytes: Source code as bytes
|
|
195
|
+
root: Root AST node
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
List of JS/TS symbols (classes, functions, methods)
|
|
199
|
+
"""
|
|
200
|
+
symbols: List[Symbol] = []
|
|
201
|
+
|
|
202
|
+
for node in self._iter_nodes(root):
|
|
203
|
+
if node.type in {"class_declaration", "class"}:
|
|
204
|
+
name_node = node.child_by_field_name("name")
|
|
205
|
+
if name_node is None:
|
|
206
|
+
continue
|
|
207
|
+
symbols.append(Symbol(
|
|
208
|
+
name=self._node_text(source_bytes, name_node),
|
|
209
|
+
kind="class",
|
|
210
|
+
range=self._node_range(node),
|
|
211
|
+
))
|
|
212
|
+
elif node.type in {"function_declaration", "generator_function_declaration"}:
|
|
213
|
+
name_node = node.child_by_field_name("name")
|
|
214
|
+
if name_node is None:
|
|
215
|
+
continue
|
|
216
|
+
symbols.append(Symbol(
|
|
217
|
+
name=self._node_text(source_bytes, name_node),
|
|
218
|
+
kind="function",
|
|
219
|
+
range=self._node_range(node),
|
|
220
|
+
))
|
|
221
|
+
elif node.type == "variable_declarator":
|
|
222
|
+
name_node = node.child_by_field_name("name")
|
|
223
|
+
value_node = node.child_by_field_name("value")
|
|
224
|
+
if (
|
|
225
|
+
name_node is None
|
|
226
|
+
or value_node is None
|
|
227
|
+
or name_node.type not in {"identifier", "property_identifier"}
|
|
228
|
+
or value_node.type != "arrow_function"
|
|
229
|
+
):
|
|
230
|
+
continue
|
|
231
|
+
symbols.append(Symbol(
|
|
232
|
+
name=self._node_text(source_bytes, name_node),
|
|
233
|
+
kind="function",
|
|
234
|
+
range=self._node_range(node),
|
|
235
|
+
))
|
|
236
|
+
elif node.type == "method_definition" and self._has_class_ancestor(node):
|
|
237
|
+
name_node = node.child_by_field_name("name")
|
|
238
|
+
if name_node is None:
|
|
239
|
+
continue
|
|
240
|
+
name = self._node_text(source_bytes, name_node)
|
|
241
|
+
if name == "constructor":
|
|
242
|
+
continue
|
|
243
|
+
symbols.append(Symbol(
|
|
244
|
+
name=name,
|
|
245
|
+
kind="method",
|
|
246
|
+
range=self._node_range(node),
|
|
247
|
+
))
|
|
248
|
+
|
|
249
|
+
return symbols
|
|
250
|
+
|
|
251
|
+
def _python_function_kind(self, node: TreeSitterNode) -> str:
|
|
252
|
+
"""Determine if Python function is a method or standalone function.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
node: Function definition node
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
'method' if inside a class, 'function' otherwise
|
|
259
|
+
"""
|
|
260
|
+
parent = node.parent
|
|
261
|
+
while parent is not None:
|
|
262
|
+
if parent.type in {"function_definition", "async_function_definition"}:
|
|
263
|
+
return "function"
|
|
264
|
+
if parent.type == "class_definition":
|
|
265
|
+
return "method"
|
|
266
|
+
parent = parent.parent
|
|
267
|
+
return "function"
|
|
268
|
+
|
|
269
|
+
def _has_class_ancestor(self, node: TreeSitterNode) -> bool:
|
|
270
|
+
"""Check if node has a class ancestor.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
node: AST node to check
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
True if node is inside a class
|
|
277
|
+
"""
|
|
278
|
+
parent = node.parent
|
|
279
|
+
while parent is not None:
|
|
280
|
+
if parent.type in {"class_declaration", "class"}:
|
|
281
|
+
return True
|
|
282
|
+
parent = parent.parent
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
def _iter_nodes(self, root: TreeSitterNode):
|
|
286
|
+
"""Iterate over all nodes in AST.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
root: Root node to start iteration
|
|
290
|
+
|
|
291
|
+
Yields:
|
|
292
|
+
AST nodes in depth-first order
|
|
293
|
+
"""
|
|
294
|
+
stack = [root]
|
|
295
|
+
while stack:
|
|
296
|
+
node = stack.pop()
|
|
297
|
+
yield node
|
|
298
|
+
for child in reversed(node.children):
|
|
299
|
+
stack.append(child)
|
|
300
|
+
|
|
301
|
+
def _node_text(self, source_bytes: bytes, node: TreeSitterNode) -> str:
|
|
302
|
+
"""Extract text for a node.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
source_bytes: Source code as bytes
|
|
306
|
+
node: AST node
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Text content of node
|
|
310
|
+
"""
|
|
311
|
+
return source_bytes[node.start_byte:node.end_byte].decode("utf8")
|
|
312
|
+
|
|
313
|
+
def _node_range(self, node: TreeSitterNode) -> tuple[int, int]:
|
|
314
|
+
"""Get line range for a node.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
node: AST node
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
(start_line, end_line) tuple, 1-based inclusive
|
|
321
|
+
"""
|
|
322
|
+
start_line = node.start_point[0] + 1
|
|
323
|
+
end_line = node.end_point[0] + 1
|
|
324
|
+
return (start_line, max(start_line, end_line))
|
|
325
|
+
|
|
326
|
+
def count_tokens(self, text: str) -> int:
|
|
327
|
+
"""Count tokens in text.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
text: Text to count tokens for
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Token count
|
|
334
|
+
"""
|
|
335
|
+
return self._tokenizer.count_tokens(text)
|
|
Binary file
|