descry-codegraph 0.1.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.
- descry/__init__.py +4 -0
- descry/_env.py +30 -0
- descry/_graph.py +36 -0
- descry/ast_grep.py +382 -0
- descry/cli.py +457 -0
- descry/cross_lang.py +404 -0
- descry/embeddings.py +593 -0
- descry/generate.py +3579 -0
- descry/git_history.py +1631 -0
- descry/handlers.py +1907 -0
- descry/mcp_server.py +355 -0
- descry/query.py +2345 -0
- descry/scip/__init__.py +0 -0
- descry/scip/cache.py +1047 -0
- descry/scip/parser.py +531 -0
- descry/scip/pb2.py +92 -0
- descry/scip/support.py +174 -0
- descry/tree_sitter_parser.py +356 -0
- descry/web/__init__.py +0 -0
- descry/web/server.py +1457 -0
- descry/web/web/index.html +2444 -0
- descry/web/web/style.css +1791 -0
- descry_codegraph-0.1.0.dist-info/METADATA +345 -0
- descry_codegraph-0.1.0.dist-info/RECORD +27 -0
- descry_codegraph-0.1.0.dist-info/WHEEL +4 -0
- descry_codegraph-0.1.0.dist-info/entry_points.txt +5 -0
- descry_codegraph-0.1.0.dist-info/licenses/LICENSE +21 -0
descry/scip/support.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""SCIP support with auto-discovery for multiple languages.
|
|
2
|
+
|
|
3
|
+
Provides functions to detect SCIP indexer availability and manage
|
|
4
|
+
SCIP-based symbol resolution for improved descry call resolution.
|
|
5
|
+
|
|
6
|
+
SCIP (Source Code Index Protocol) is a format from Sourcegraph that
|
|
7
|
+
provides type-aware symbol information from language servers.
|
|
8
|
+
|
|
9
|
+
Supported indexers:
|
|
10
|
+
- rust-analyzer: Rust
|
|
11
|
+
- scip-typescript: TypeScript, JavaScript
|
|
12
|
+
- scip-python: Python
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import shutil
|
|
20
|
+
import subprocess
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
from descry._env import safe_env
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from typing import Dict
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# Global state (lazy-initialized)
|
|
31
|
+
_indexer_cache: Dict[str, dict] = {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _check_indexer(name: str, command: str, version_flag: str = "--version") -> dict:
|
|
35
|
+
"""Check if an indexer is available.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
name: Indexer name for logging
|
|
39
|
+
command: Command to check for
|
|
40
|
+
version_flag: Flag to get version info
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Dict with available, path, version keys
|
|
44
|
+
"""
|
|
45
|
+
result = {"available": False, "path": None, "version": None}
|
|
46
|
+
|
|
47
|
+
path = shutil.which(command)
|
|
48
|
+
if path:
|
|
49
|
+
try:
|
|
50
|
+
proc = subprocess.run(
|
|
51
|
+
[path, version_flag],
|
|
52
|
+
capture_output=True,
|
|
53
|
+
text=True,
|
|
54
|
+
timeout=10,
|
|
55
|
+
env=safe_env(),
|
|
56
|
+
)
|
|
57
|
+
if proc.returncode == 0:
|
|
58
|
+
result["available"] = True
|
|
59
|
+
result["path"] = path
|
|
60
|
+
result["version"] = proc.stdout.strip() or proc.stderr.strip()
|
|
61
|
+
logger.debug(f"SCIP: {name} available at {path}")
|
|
62
|
+
else:
|
|
63
|
+
logger.debug(f"SCIP: {name} found but failed to run")
|
|
64
|
+
except subprocess.TimeoutExpired:
|
|
65
|
+
logger.debug(f"SCIP: {name} version check timed out")
|
|
66
|
+
except (OSError, FileNotFoundError) as e:
|
|
67
|
+
logger.debug(f"SCIP: {name} check failed: {e}")
|
|
68
|
+
else:
|
|
69
|
+
logger.debug(f"SCIP: {name} not found in PATH")
|
|
70
|
+
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def rust_analyzer_available() -> bool:
|
|
75
|
+
"""Check if rust-analyzer is available for Rust SCIP generation."""
|
|
76
|
+
if os.environ.get("DESCRY_NO_SCIP"):
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
if "rust-analyzer" not in _indexer_cache:
|
|
80
|
+
_indexer_cache["rust-analyzer"] = _check_indexer(
|
|
81
|
+
"rust-analyzer", "rust-analyzer", "--version"
|
|
82
|
+
)
|
|
83
|
+
return _indexer_cache["rust-analyzer"]["available"]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def scip_typescript_available() -> bool:
|
|
87
|
+
"""Check if scip-typescript is available for TypeScript/JavaScript SCIP generation."""
|
|
88
|
+
if os.environ.get("DESCRY_NO_SCIP"):
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
if "scip-typescript" not in _indexer_cache:
|
|
92
|
+
_indexer_cache["scip-typescript"] = _check_indexer(
|
|
93
|
+
"scip-typescript", "scip-typescript", "--version"
|
|
94
|
+
)
|
|
95
|
+
return _indexer_cache["scip-typescript"]["available"]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def scip_python_available() -> bool:
|
|
99
|
+
"""Check if scip-python is available for Python SCIP generation.
|
|
100
|
+
|
|
101
|
+
scip-python is a Sourcegraph-maintained indexer distributed via npm
|
|
102
|
+
(@sourcegraph/scip-python) that installs a `scip-python` binary.
|
|
103
|
+
"""
|
|
104
|
+
if os.environ.get("DESCRY_NO_SCIP"):
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
if "scip-python" not in _indexer_cache:
|
|
108
|
+
_indexer_cache["scip-python"] = _check_indexer(
|
|
109
|
+
"scip-python", "scip-python", "--version"
|
|
110
|
+
)
|
|
111
|
+
return _indexer_cache["scip-python"]["available"]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def scip_available() -> bool:
|
|
115
|
+
"""Check if any SCIP indexer is available (for backwards compatibility).
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
True if at least one SCIP indexer is available.
|
|
119
|
+
"""
|
|
120
|
+
if os.environ.get("DESCRY_NO_SCIP"):
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
rust_analyzer_available()
|
|
125
|
+
or scip_typescript_available()
|
|
126
|
+
or scip_python_available()
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_scip_status() -> dict:
|
|
131
|
+
"""Return SCIP status for diagnostics.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Dictionary with indexer availability information.
|
|
135
|
+
"""
|
|
136
|
+
# Force availability checks
|
|
137
|
+
_ = rust_analyzer_available()
|
|
138
|
+
_ = scip_typescript_available()
|
|
139
|
+
_ = scip_python_available()
|
|
140
|
+
|
|
141
|
+
rust_info = _indexer_cache.get("rust-analyzer", {})
|
|
142
|
+
ts_info = _indexer_cache.get("scip-typescript", {})
|
|
143
|
+
py_info = _indexer_cache.get("scip-python", {})
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
"available": scip_available(),
|
|
147
|
+
"disabled_by_env": bool(os.environ.get("DESCRY_NO_SCIP")),
|
|
148
|
+
"indexers": {
|
|
149
|
+
"rust-analyzer": {
|
|
150
|
+
"available": rust_info.get("available", False),
|
|
151
|
+
"path": rust_info.get("path"),
|
|
152
|
+
"version": rust_info.get("version"),
|
|
153
|
+
},
|
|
154
|
+
"scip-typescript": {
|
|
155
|
+
"available": ts_info.get("available", False),
|
|
156
|
+
"path": ts_info.get("path"),
|
|
157
|
+
"version": ts_info.get("version"),
|
|
158
|
+
},
|
|
159
|
+
"scip-python": {
|
|
160
|
+
"available": py_info.get("available", False),
|
|
161
|
+
"path": py_info.get("path"),
|
|
162
|
+
"version": py_info.get("version"),
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
# Backwards compatibility
|
|
166
|
+
"rust_analyzer_path": rust_info.get("path"),
|
|
167
|
+
"rust_analyzer_version": rust_info.get("version"),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def reset_scip_state():
|
|
172
|
+
"""Reset cached SCIP state. Useful for testing."""
|
|
173
|
+
global _indexer_cache
|
|
174
|
+
_indexer_cache = {}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""Optional tree-sitter-backed parser for TypeScript / TSX / JavaScript.
|
|
2
|
+
|
|
3
|
+
Activated when the ``descry-codegraph[ast]`` extra is installed (providing
|
|
4
|
+
``tree-sitter``, ``tree-sitter-typescript``, ``tree-sitter-javascript``) and
|
|
5
|
+
the user opts in via ``.descry.toml``::
|
|
6
|
+
|
|
7
|
+
[features]
|
|
8
|
+
use_tree_sitter_ts = true
|
|
9
|
+
|
|
10
|
+
When enabled, ``generate.TSParser`` routes file parsing through this module
|
|
11
|
+
and falls back to regex-only extraction for files the AST walker cannot
|
|
12
|
+
process. When disabled or when the extra is missing, behaviour is
|
|
13
|
+
indistinguishable from the regex-only baseline.
|
|
14
|
+
|
|
15
|
+
This parser extracts the symbol shapes needed for the call graph:
|
|
16
|
+
|
|
17
|
+
- ``class_declaration`` / ``interface_declaration`` / ``enum_declaration``
|
|
18
|
+
- ``function_declaration``
|
|
19
|
+
- ``method_definition`` (including ``get``/``set``) inside a class body
|
|
20
|
+
- Arrow / function-expression RHS of ``const foo = …``
|
|
21
|
+
- ``import_statement`` (ES) and ``call_expression`` (call edges)
|
|
22
|
+
|
|
23
|
+
Anything the grammar hits as ``ERROR`` is surfaced to the caller; the
|
|
24
|
+
BaseParser fallback can then re-run regex-only extraction on that file.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import TYPE_CHECKING, Literal
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
import tree_sitter
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# --- Optional dependency bootstrap -----------------------------------------
|
|
41
|
+
|
|
42
|
+
_TS_AVAILABLE: bool | None = None
|
|
43
|
+
_TS_LANGUAGE = None
|
|
44
|
+
_TSX_LANGUAGE = None
|
|
45
|
+
_JS_LANGUAGE = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def tree_sitter_available() -> bool:
|
|
49
|
+
"""True iff tree-sitter + the TS/JS grammars are importable.
|
|
50
|
+
|
|
51
|
+
Result is cached on first call.
|
|
52
|
+
"""
|
|
53
|
+
global _TS_AVAILABLE, _TS_LANGUAGE, _TSX_LANGUAGE, _JS_LANGUAGE
|
|
54
|
+
if _TS_AVAILABLE is not None:
|
|
55
|
+
return _TS_AVAILABLE
|
|
56
|
+
try:
|
|
57
|
+
import tree_sitter as ts
|
|
58
|
+
import tree_sitter_typescript as tsts
|
|
59
|
+
import tree_sitter_javascript as tsjs
|
|
60
|
+
|
|
61
|
+
_TS_LANGUAGE = ts.Language(tsts.language_typescript())
|
|
62
|
+
_TSX_LANGUAGE = ts.Language(tsts.language_tsx())
|
|
63
|
+
_JS_LANGUAGE = ts.Language(tsjs.language())
|
|
64
|
+
_TS_AVAILABLE = True
|
|
65
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
66
|
+
logger.debug("tree-sitter unavailable: %s", exc)
|
|
67
|
+
_TS_AVAILABLE = False
|
|
68
|
+
return _TS_AVAILABLE
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _language_for(file_path: str | Path):
|
|
72
|
+
"""Return the tree-sitter Language for a given TS/TSX/JS path, or None."""
|
|
73
|
+
if not tree_sitter_available():
|
|
74
|
+
return None
|
|
75
|
+
suffix = Path(file_path).suffix.lower()
|
|
76
|
+
if suffix == ".tsx":
|
|
77
|
+
return _TSX_LANGUAGE
|
|
78
|
+
if suffix in (".ts", ".d.ts"):
|
|
79
|
+
return _TS_LANGUAGE
|
|
80
|
+
if suffix in (".js", ".jsx", ".mjs", ".cjs"):
|
|
81
|
+
return _JS_LANGUAGE
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# --- Symbol data classes ---------------------------------------------------
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
SymbolKind = Literal[
|
|
89
|
+
"Class",
|
|
90
|
+
"Interface",
|
|
91
|
+
"Enum",
|
|
92
|
+
"Function",
|
|
93
|
+
"Method",
|
|
94
|
+
"Const",
|
|
95
|
+
"Import",
|
|
96
|
+
"Call",
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class ExtractedSymbol:
|
|
102
|
+
kind: SymbolKind
|
|
103
|
+
name: str
|
|
104
|
+
lineno: int
|
|
105
|
+
end_lineno: int
|
|
106
|
+
parent_name: str | None = None
|
|
107
|
+
signature: str | None = None
|
|
108
|
+
docstring: str | None = None
|
|
109
|
+
is_async: bool = False
|
|
110
|
+
is_static: bool = False
|
|
111
|
+
accessor: Literal["get", "set", None] | None = None
|
|
112
|
+
module: str | None = None # populated for Import
|
|
113
|
+
imported_names: list[str] = field(default_factory=list) # for Import
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class ParseResult:
|
|
118
|
+
"""Symbols + raw call sites extracted from a single file."""
|
|
119
|
+
|
|
120
|
+
symbols: list[ExtractedSymbol] = field(default_factory=list)
|
|
121
|
+
calls: list[tuple[int, str]] = field(default_factory=list) # (lineno, callee)
|
|
122
|
+
had_errors: bool = False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# --- Extractor -------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _node_text(node: "tree_sitter.Node") -> str:
|
|
129
|
+
return node.text.decode("utf-8", errors="replace") if node.text else ""
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _first_child_of_type(
|
|
133
|
+
node: "tree_sitter.Node", type_name: str
|
|
134
|
+
) -> "tree_sitter.Node | None":
|
|
135
|
+
for child in node.named_children:
|
|
136
|
+
if child.type == type_name:
|
|
137
|
+
return child
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _line(node: "tree_sitter.Node") -> int:
|
|
142
|
+
return node.start_point[0] + 1
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _end_line(node: "tree_sitter.Node") -> int:
|
|
146
|
+
return node.end_point[0] + 1
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _walk(
|
|
150
|
+
node: "tree_sitter.Node",
|
|
151
|
+
on_enter,
|
|
152
|
+
parent_name: str | None = None,
|
|
153
|
+
) -> None:
|
|
154
|
+
"""Depth-first traversal delegating per-node logic to on_enter(node, parent_name).
|
|
155
|
+
|
|
156
|
+
on_enter returns the new `parent_name` to use for children (or None to
|
|
157
|
+
propagate the incoming one).
|
|
158
|
+
"""
|
|
159
|
+
next_parent = on_enter(node, parent_name)
|
|
160
|
+
new_parent = next_parent if next_parent is not None else parent_name
|
|
161
|
+
for child in node.named_children:
|
|
162
|
+
_walk(child, on_enter, new_parent)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def parse_file(content: bytes, file_path: str | Path) -> ParseResult | None:
|
|
166
|
+
"""Parse a TS/TSX/JS source buffer and return extracted symbols + calls.
|
|
167
|
+
|
|
168
|
+
Returns None when tree-sitter isn't available or the extension is not
|
|
169
|
+
one we handle; the caller should fall back to regex in that case.
|
|
170
|
+
"""
|
|
171
|
+
language = _language_for(file_path)
|
|
172
|
+
if language is None:
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
import tree_sitter as ts
|
|
176
|
+
|
|
177
|
+
parser = ts.Parser(language)
|
|
178
|
+
tree = parser.parse(content)
|
|
179
|
+
root = tree.root_node
|
|
180
|
+
|
|
181
|
+
result = ParseResult()
|
|
182
|
+
result.had_errors = root.has_error
|
|
183
|
+
|
|
184
|
+
def on_enter(node, parent_name):
|
|
185
|
+
t = node.type
|
|
186
|
+
if t == "class_declaration":
|
|
187
|
+
# JS grammar uses `identifier` for class names; TS uses
|
|
188
|
+
# `type_identifier`. Try both so the parser works on both
|
|
189
|
+
# language variants without a grammar-specific branch.
|
|
190
|
+
name_node = _first_child_of_type(
|
|
191
|
+
node, "type_identifier"
|
|
192
|
+
) or _first_child_of_type(node, "identifier")
|
|
193
|
+
name = _node_text(name_node) if name_node else "<anon>"
|
|
194
|
+
result.symbols.append(
|
|
195
|
+
ExtractedSymbol(
|
|
196
|
+
kind="Class",
|
|
197
|
+
name=name,
|
|
198
|
+
lineno=_line(node),
|
|
199
|
+
end_lineno=_end_line(node),
|
|
200
|
+
parent_name=parent_name,
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
return name
|
|
204
|
+
|
|
205
|
+
if t == "interface_declaration":
|
|
206
|
+
name_node = _first_child_of_type(node, "type_identifier")
|
|
207
|
+
name = _node_text(name_node) if name_node else "<anon>"
|
|
208
|
+
result.symbols.append(
|
|
209
|
+
ExtractedSymbol(
|
|
210
|
+
kind="Interface",
|
|
211
|
+
name=name,
|
|
212
|
+
lineno=_line(node),
|
|
213
|
+
end_lineno=_end_line(node),
|
|
214
|
+
parent_name=parent_name,
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
return name
|
|
218
|
+
|
|
219
|
+
if t == "enum_declaration":
|
|
220
|
+
name_node = _first_child_of_type(node, "identifier")
|
|
221
|
+
name = _node_text(name_node) if name_node else "<anon>"
|
|
222
|
+
result.symbols.append(
|
|
223
|
+
ExtractedSymbol(
|
|
224
|
+
kind="Enum",
|
|
225
|
+
name=name,
|
|
226
|
+
lineno=_line(node),
|
|
227
|
+
end_lineno=_end_line(node),
|
|
228
|
+
parent_name=parent_name,
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
if t == "function_declaration":
|
|
234
|
+
name_node = _first_child_of_type(node, "identifier")
|
|
235
|
+
name = _node_text(name_node) if name_node else "<anon>"
|
|
236
|
+
is_async = any(c.type == "async" for c in node.children)
|
|
237
|
+
result.symbols.append(
|
|
238
|
+
ExtractedSymbol(
|
|
239
|
+
kind="Function",
|
|
240
|
+
name=name,
|
|
241
|
+
lineno=_line(node),
|
|
242
|
+
end_lineno=_end_line(node),
|
|
243
|
+
parent_name=parent_name,
|
|
244
|
+
is_async=is_async,
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
return name
|
|
248
|
+
|
|
249
|
+
if t == "method_definition":
|
|
250
|
+
name_node = _first_child_of_type(node, "property_identifier")
|
|
251
|
+
name = _node_text(name_node) if name_node else "<anon>"
|
|
252
|
+
# Detect get/set/static/async modifiers as leading sibling tokens.
|
|
253
|
+
accessor = None
|
|
254
|
+
is_async = False
|
|
255
|
+
is_static = False
|
|
256
|
+
for child in node.children:
|
|
257
|
+
tt = child.type
|
|
258
|
+
if tt == "get":
|
|
259
|
+
accessor = "get"
|
|
260
|
+
elif tt == "set":
|
|
261
|
+
accessor = "set"
|
|
262
|
+
elif tt == "async":
|
|
263
|
+
is_async = True
|
|
264
|
+
elif tt == "static":
|
|
265
|
+
is_static = True
|
|
266
|
+
result.symbols.append(
|
|
267
|
+
ExtractedSymbol(
|
|
268
|
+
kind="Method",
|
|
269
|
+
name=name,
|
|
270
|
+
lineno=_line(node),
|
|
271
|
+
end_lineno=_end_line(node),
|
|
272
|
+
parent_name=parent_name,
|
|
273
|
+
is_async=is_async,
|
|
274
|
+
is_static=is_static,
|
|
275
|
+
accessor=accessor,
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
return name
|
|
279
|
+
|
|
280
|
+
if t == "lexical_declaration":
|
|
281
|
+
# const foo = (...) => {...} | const foo = function(){}
|
|
282
|
+
for decl in node.named_children:
|
|
283
|
+
if decl.type != "variable_declarator":
|
|
284
|
+
continue
|
|
285
|
+
name_node = _first_child_of_type(decl, "identifier")
|
|
286
|
+
if not name_node:
|
|
287
|
+
continue
|
|
288
|
+
rhs = decl.named_children[-1] if decl.named_children else None
|
|
289
|
+
if rhs is None:
|
|
290
|
+
continue
|
|
291
|
+
if rhs.type in ("arrow_function", "function_expression"):
|
|
292
|
+
name = _node_text(name_node)
|
|
293
|
+
result.symbols.append(
|
|
294
|
+
ExtractedSymbol(
|
|
295
|
+
kind="Function",
|
|
296
|
+
name=name,
|
|
297
|
+
lineno=_line(decl),
|
|
298
|
+
end_lineno=_end_line(decl),
|
|
299
|
+
parent_name=parent_name,
|
|
300
|
+
is_async=any(c.type == "async" for c in rhs.children),
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
if t == "import_statement":
|
|
306
|
+
source = _first_child_of_type(node, "string")
|
|
307
|
+
module = None
|
|
308
|
+
if source is not None:
|
|
309
|
+
txt = _node_text(source)
|
|
310
|
+
if len(txt) >= 2 and txt[0] in ("'", '"') and txt[-1] in ("'", '"'):
|
|
311
|
+
module = txt[1:-1]
|
|
312
|
+
imported_names: list[str] = []
|
|
313
|
+
clause = _first_child_of_type(node, "import_clause")
|
|
314
|
+
if clause is not None:
|
|
315
|
+
# default + named + namespace
|
|
316
|
+
for child in clause.named_children:
|
|
317
|
+
if child.type == "identifier":
|
|
318
|
+
imported_names.append(_node_text(child))
|
|
319
|
+
elif child.type == "named_imports":
|
|
320
|
+
for spec in child.named_children:
|
|
321
|
+
if spec.type == "import_specifier":
|
|
322
|
+
ident = _first_child_of_type(spec, "identifier")
|
|
323
|
+
if ident is not None:
|
|
324
|
+
imported_names.append(_node_text(ident))
|
|
325
|
+
elif child.type == "namespace_import":
|
|
326
|
+
ident = _first_child_of_type(child, "identifier")
|
|
327
|
+
if ident is not None:
|
|
328
|
+
imported_names.append(_node_text(ident))
|
|
329
|
+
result.symbols.append(
|
|
330
|
+
ExtractedSymbol(
|
|
331
|
+
kind="Import",
|
|
332
|
+
name=module or "<unknown>",
|
|
333
|
+
lineno=_line(node),
|
|
334
|
+
end_lineno=_end_line(node),
|
|
335
|
+
parent_name=parent_name,
|
|
336
|
+
module=module,
|
|
337
|
+
imported_names=imported_names,
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
if t == "call_expression":
|
|
343
|
+
callee_node = node.named_children[0] if node.named_children else None
|
|
344
|
+
if callee_node is None:
|
|
345
|
+
return None
|
|
346
|
+
if callee_node.type == "identifier":
|
|
347
|
+
result.calls.append((_line(node), _node_text(callee_node)))
|
|
348
|
+
elif callee_node.type == "member_expression":
|
|
349
|
+
# a.b.c(...) -> "a.b.c"
|
|
350
|
+
result.calls.append((_line(node), _node_text(callee_node)))
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
_walk(root, on_enter)
|
|
356
|
+
return result
|
descry/web/__init__.py
ADDED
|
File without changes
|