codegraph-brain 0.6.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.
- cgis/__init__.py +10 -0
- cgis/__main__.py +16 -0
- cgis/api/.gitkeep +0 -0
- cgis/api/__init__.py +1 -0
- cgis/api/mcp_server.py +550 -0
- cgis/cli.py +1516 -0
- cgis/core/.gitkeep +0 -0
- cgis/core/models.py +140 -0
- cgis/extractors/.gitkeep +0 -0
- cgis/extractors/_python_ast.py +194 -0
- cgis/extractors/_python_classes.py +126 -0
- cgis/extractors/_python_functions.py +312 -0
- cgis/extractors/_python_imports.py +188 -0
- cgis/extractors/_python_types.py +84 -0
- cgis/extractors/base.py +42 -0
- cgis/extractors/python_extractor.py +235 -0
- cgis/extractors/typescript_extractor.py +310 -0
- cgis/guardian/__init__.py +1 -0
- cgis/guardian/bench.py +199 -0
- cgis/guardian/chunked.py +240 -0
- cgis/guardian/chunker.py +148 -0
- cgis/guardian/collector.py +309 -0
- cgis/guardian/core.py +120 -0
- cgis/guardian/diff_index.py +151 -0
- cgis/guardian/findings.py +70 -0
- cgis/guardian/github_poster.py +133 -0
- cgis/guardian/metrics.py +108 -0
- cgis/guardian/prompts.py +217 -0
- cgis/guardian/providers/__init__.py +1 -0
- cgis/guardian/providers/base.py +112 -0
- cgis/guardian/providers/gemini.py +83 -0
- cgis/guardian/providers/mistral.py +83 -0
- cgis/guardian/providers/ollama.py +99 -0
- cgis/guardian/recording.py +82 -0
- cgis/guardian/render.py +107 -0
- cgis/guardian/runner.py +295 -0
- cgis/guardian/skeptic.py +208 -0
- cgis/pipeline.py +252 -0
- cgis/py.typed +0 -0
- cgis/query/analysis/__init__.py +0 -0
- cgis/query/analysis/analyzer.py +241 -0
- cgis/query/analysis/anomaly.py +34 -0
- cgis/query/analysis/cohesion.py +277 -0
- cgis/query/analysis/health.py +128 -0
- cgis/query/analysis/suggest_service.py +221 -0
- cgis/query/context/__init__.py +0 -0
- cgis/query/context/audit.py +134 -0
- cgis/query/context/context_service.py +129 -0
- cgis/query/context/prompt.py +184 -0
- cgis/query/context/snippet.py +96 -0
- cgis/query/drift/__init__.py +0 -0
- cgis/query/drift/_scc.py +90 -0
- cgis/query/drift/drift.py +867 -0
- cgis/query/drift/drift_service.py +217 -0
- cgis/query/drift/fingerprint.py +255 -0
- cgis/query/drift/fractal.py +292 -0
- cgis/query/drift/ontology_init.py +470 -0
- cgis/query/drift/quotient.py +75 -0
- cgis/query/drift/triads.py +234 -0
- cgis/query/engine.py +170 -0
- cgis/query/fqn.py +65 -0
- cgis/query/render/__init__.py +0 -0
- cgis/query/render/graph_json.py +42 -0
- cgis/query/render/mermaid.py +235 -0
- cgis/query/render/metrics.py +346 -0
- cgis/resolver/.gitkeep +0 -0
- cgis/resolver/__init__.py +1 -0
- cgis/resolver/engine.py +145 -0
- cgis/resolver/indices.py +184 -0
- cgis/resolver/symbols.py +179 -0
- cgis/resolver/uplift.py +263 -0
- cgis/storage/.gitkeep +0 -0
- cgis/storage/sqlite_store.py +589 -0
- codegraph_brain-0.6.0.dist-info/METADATA +240 -0
- codegraph_brain-0.6.0.dist-info/RECORD +78 -0
- codegraph_brain-0.6.0.dist-info/WHEEL +4 -0
- codegraph_brain-0.6.0.dist-info/entry_points.txt +3 -0
- codegraph_brain-0.6.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Implements Python Extractor."""
|
|
2
|
+
|
|
3
|
+
import tree_sitter_python as tspython
|
|
4
|
+
from tree_sitter import Language, Parser
|
|
5
|
+
from tree_sitter import Node as BaseNode
|
|
6
|
+
|
|
7
|
+
from cgis.core.models import Edge, Node, NodeType
|
|
8
|
+
from cgis.extractors._python_ast import extract_decorator_names, is_module_level_assignment
|
|
9
|
+
from cgis.extractors._python_ast import file_path_to_module_fqn as _file_path_to_module_fqn
|
|
10
|
+
from cgis.extractors._python_classes import ClassHandler
|
|
11
|
+
from cgis.extractors._python_functions import FunctionHandler
|
|
12
|
+
from cgis.extractors._python_imports import ImportHandler
|
|
13
|
+
from cgis.extractors._python_types import TypeResolver
|
|
14
|
+
from cgis.extractors.base import BaseExtractor
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def file_path_to_module_fqn(file_path: str, source_root: str | None = None) -> str:
|
|
18
|
+
"""Convert a file path to a dot-separated module namespace.
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
src/cgis/pipeline.py -> src.cgis.pipeline
|
|
22
|
+
src/cgis/__init__.py -> src.cgis
|
|
23
|
+
/abs/path/mod.py -> abs.path.mod
|
|
24
|
+
C:\\path\\to\\mod.py -> path.to.mod
|
|
25
|
+
|
|
26
|
+
With source_root="src":
|
|
27
|
+
src/cgis/pipeline.py -> cgis.pipeline
|
|
28
|
+
"""
|
|
29
|
+
return _file_path_to_module_fqn(file_path, source_root)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PythonExtractor(BaseExtractor):
|
|
33
|
+
"""
|
|
34
|
+
A concrete extractor for Python source code using tree-sitter.
|
|
35
|
+
|
|
36
|
+
Acts as a thin orchestrator: the recursive AST walk dispatches each node to
|
|
37
|
+
a focused collaborator (ImportHandler, FunctionHandler, ClassHandler,
|
|
38
|
+
TypeResolver) which owns the actual node/edge extraction.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
LANG: str = "python"
|
|
42
|
+
|
|
43
|
+
def __init__(self, source_roots: list[str] | None = None) -> None:
|
|
44
|
+
"""Initialise the tree-sitter Python parser and the extraction handlers."""
|
|
45
|
+
super().__init__(source_roots=source_roots)
|
|
46
|
+
self._parser = Parser(Language(tspython.language()))
|
|
47
|
+
self._imports = ImportHandler()
|
|
48
|
+
self._types = TypeResolver(self._pick_source_root)
|
|
49
|
+
self._functions = FunctionHandler(self._pick_source_root, self._types)
|
|
50
|
+
self._classes = ClassHandler(self._pick_source_root)
|
|
51
|
+
|
|
52
|
+
def parse(self, code: str, file_path: str) -> tuple[list[Node], list[Edge]]:
|
|
53
|
+
"""
|
|
54
|
+
Extracts structural nodes and edges (Functions, Classes, Imports).
|
|
55
|
+
"""
|
|
56
|
+
code_bytes = code.encode("utf8")
|
|
57
|
+
tree = self._parser.parse(code_bytes)
|
|
58
|
+
root_node: BaseNode = tree.root_node
|
|
59
|
+
|
|
60
|
+
nodes: list[Node] = []
|
|
61
|
+
edges: list[Edge] = []
|
|
62
|
+
import_map: dict[str, str] = {}
|
|
63
|
+
module_fqn = file_path_to_module_fqn(file_path, self._pick_source_root(file_path))
|
|
64
|
+
local_types_acc: dict[str, dict[str, str]] = {}
|
|
65
|
+
|
|
66
|
+
self._walk(
|
|
67
|
+
root_node,
|
|
68
|
+
code_bytes,
|
|
69
|
+
file_path,
|
|
70
|
+
nodes,
|
|
71
|
+
edges,
|
|
72
|
+
import_map=import_map,
|
|
73
|
+
module_fqn=module_fqn,
|
|
74
|
+
local_types_acc=local_types_acc,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Apply accumulated local types from assignments and param annotations
|
|
78
|
+
nodes_by_id = {n.id: i for i, n in enumerate(nodes)}
|
|
79
|
+
for func_id, lt in local_types_acc.items():
|
|
80
|
+
if func_id in nodes_by_id:
|
|
81
|
+
i = nodes_by_id[func_id]
|
|
82
|
+
nodes[i] = nodes[i].model_copy(
|
|
83
|
+
update={"metadata": {**nodes[i].metadata, "local_types": lt}}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
file_node = Node(
|
|
87
|
+
id=module_fqn,
|
|
88
|
+
type=NodeType.FILE,
|
|
89
|
+
name=file_path.rsplit("/", maxsplit=1)[-1].rsplit("\\", maxsplit=1)[-1],
|
|
90
|
+
file_path=file_path,
|
|
91
|
+
start_line=1,
|
|
92
|
+
end_line=root_node.end_point.row + 1,
|
|
93
|
+
metadata={"import_map": import_map},
|
|
94
|
+
)
|
|
95
|
+
nodes.insert(0, file_node)
|
|
96
|
+
|
|
97
|
+
return nodes, edges
|
|
98
|
+
|
|
99
|
+
def _walk(
|
|
100
|
+
self,
|
|
101
|
+
node: BaseNode,
|
|
102
|
+
code_bytes: bytes,
|
|
103
|
+
file_path: str,
|
|
104
|
+
nodes: list[Node],
|
|
105
|
+
edges: list[Edge],
|
|
106
|
+
current_func_node: Node | None = None,
|
|
107
|
+
import_map: dict[str, str] | None = None,
|
|
108
|
+
module_fqn: str | None = None,
|
|
109
|
+
local_types_acc: dict[str, dict[str, str]] | None = None,
|
|
110
|
+
) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Recursive AST walker that dispatches each node to its handler.
|
|
113
|
+
"""
|
|
114
|
+
if node.type in ("import_statement", "import_from_statement"):
|
|
115
|
+
self._imports.handle(node, code_bytes, file_path, import_map, module_fqn, edges)
|
|
116
|
+
return # never recurse into import nodes
|
|
117
|
+
|
|
118
|
+
if node.type == "decorated_definition":
|
|
119
|
+
self._handle_decorated_definition(
|
|
120
|
+
node,
|
|
121
|
+
code_bytes,
|
|
122
|
+
file_path,
|
|
123
|
+
nodes,
|
|
124
|
+
edges,
|
|
125
|
+
import_map,
|
|
126
|
+
module_fqn,
|
|
127
|
+
local_types_acc,
|
|
128
|
+
)
|
|
129
|
+
return # prevent double-processing the inner definition
|
|
130
|
+
|
|
131
|
+
next_func_node = current_func_node
|
|
132
|
+
if node.type in ("function_definition", "async_function_definition"):
|
|
133
|
+
next_func_node = self._functions.process_function_node(
|
|
134
|
+
node, code_bytes, file_path, nodes, edges, module_fqn or ""
|
|
135
|
+
)
|
|
136
|
+
elif node.type == "class_definition":
|
|
137
|
+
self._classes.process_class_node(
|
|
138
|
+
node, code_bytes, file_path, nodes, edges, module_fqn or ""
|
|
139
|
+
)
|
|
140
|
+
next_func_node = None
|
|
141
|
+
elif node.type == "call" and current_func_node:
|
|
142
|
+
self._functions.process_call_node(
|
|
143
|
+
node, code_bytes, file_path, current_func_node.id, edges
|
|
144
|
+
)
|
|
145
|
+
elif node.type == "assignment" and current_func_node and local_types_acc is not None:
|
|
146
|
+
self._functions.collect_assignment_type(
|
|
147
|
+
node, code_bytes, import_map, current_func_node, local_types_acc
|
|
148
|
+
)
|
|
149
|
+
elif is_module_level_assignment(node, code_bytes, current_func_node):
|
|
150
|
+
# True module level: not in a function (current_func_node) and not
|
|
151
|
+
# in a class body (get_fqn_prefix). Class-body DI aliases are out
|
|
152
|
+
# of scope (spec §6).
|
|
153
|
+
self._functions.process_module_assignment(
|
|
154
|
+
node, code_bytes, file_path, nodes, edges, module_fqn or ""
|
|
155
|
+
)
|
|
156
|
+
elif (
|
|
157
|
+
node.type in ("typed_parameter", "typed_default_parameter")
|
|
158
|
+
and current_func_node
|
|
159
|
+
and local_types_acc is not None
|
|
160
|
+
):
|
|
161
|
+
self._functions.collect_param_type(
|
|
162
|
+
node, code_bytes, import_map, current_func_node, local_types_acc, edges
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
for child in node.children:
|
|
166
|
+
self._walk(
|
|
167
|
+
child,
|
|
168
|
+
code_bytes,
|
|
169
|
+
file_path,
|
|
170
|
+
nodes,
|
|
171
|
+
edges,
|
|
172
|
+
next_func_node,
|
|
173
|
+
import_map=import_map,
|
|
174
|
+
module_fqn=module_fqn,
|
|
175
|
+
local_types_acc=local_types_acc,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def _handle_decorated_definition(
|
|
179
|
+
self,
|
|
180
|
+
node: BaseNode,
|
|
181
|
+
code_bytes: bytes,
|
|
182
|
+
file_path: str,
|
|
183
|
+
nodes: list[Node],
|
|
184
|
+
edges: list[Edge],
|
|
185
|
+
import_map: dict[str, str] | None,
|
|
186
|
+
module_fqn: str | None,
|
|
187
|
+
local_types_acc: dict[str, dict[str, str]] | None,
|
|
188
|
+
) -> None:
|
|
189
|
+
"""Process a decorated function or class definition, forwarding decorator names."""
|
|
190
|
+
raw_decorators = extract_decorator_names(node, code_bytes)
|
|
191
|
+
for child in node.children:
|
|
192
|
+
if child.type in ("function_definition", "async_function_definition"):
|
|
193
|
+
inner = self._functions.process_function_node(
|
|
194
|
+
child,
|
|
195
|
+
code_bytes,
|
|
196
|
+
file_path,
|
|
197
|
+
nodes,
|
|
198
|
+
edges,
|
|
199
|
+
module_fqn or "",
|
|
200
|
+
decorators=raw_decorators,
|
|
201
|
+
)
|
|
202
|
+
for grandchild in child.children:
|
|
203
|
+
self._walk(
|
|
204
|
+
grandchild,
|
|
205
|
+
code_bytes,
|
|
206
|
+
file_path,
|
|
207
|
+
nodes,
|
|
208
|
+
edges,
|
|
209
|
+
inner,
|
|
210
|
+
import_map=import_map,
|
|
211
|
+
module_fqn=module_fqn,
|
|
212
|
+
local_types_acc=local_types_acc,
|
|
213
|
+
)
|
|
214
|
+
elif child.type == "class_definition":
|
|
215
|
+
self._classes.process_class_node(
|
|
216
|
+
child,
|
|
217
|
+
code_bytes,
|
|
218
|
+
file_path,
|
|
219
|
+
nodes,
|
|
220
|
+
edges,
|
|
221
|
+
module_fqn or "",
|
|
222
|
+
decorators=raw_decorators,
|
|
223
|
+
)
|
|
224
|
+
for grandchild in child.children:
|
|
225
|
+
self._walk(
|
|
226
|
+
grandchild,
|
|
227
|
+
code_bytes,
|
|
228
|
+
file_path,
|
|
229
|
+
nodes,
|
|
230
|
+
edges,
|
|
231
|
+
None,
|
|
232
|
+
import_map=import_map,
|
|
233
|
+
module_fqn=module_fqn,
|
|
234
|
+
local_types_acc=local_types_acc,
|
|
235
|
+
)
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Implements TypeScript/TSX Extractor using tree-sitter."""
|
|
2
|
+
|
|
3
|
+
import tree_sitter_typescript as tsts
|
|
4
|
+
from tree_sitter import Language, Parser
|
|
5
|
+
from tree_sitter import Node as TSNode
|
|
6
|
+
|
|
7
|
+
from cgis.core.models import Edge, EdgeType, Node, NodeNamespace, NodeType
|
|
8
|
+
from cgis.extractors.base import BaseExtractor
|
|
9
|
+
|
|
10
|
+
_RAW_CALL_PREFIX = "raw_call:"
|
|
11
|
+
_EXPORT_UNWRAP_TYPES = frozenset(
|
|
12
|
+
{
|
|
13
|
+
"class_declaration",
|
|
14
|
+
"abstract_class_declaration",
|
|
15
|
+
"function_declaration",
|
|
16
|
+
"lexical_declaration",
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def file_path_to_module_fqn(file_path: str, source_root: str | None = None) -> str:
|
|
22
|
+
"""Convert a TS/TSX file path to a dot-separated module namespace.
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
src/api/handler.ts -> src.api.handler
|
|
26
|
+
src/components/index.tsx -> src.components
|
|
27
|
+
C:\\path\\to\\mod.ts -> path.to.mod
|
|
28
|
+
|
|
29
|
+
With source_root="src":
|
|
30
|
+
src/api/handler.ts -> api.handler
|
|
31
|
+
"""
|
|
32
|
+
clean = file_path
|
|
33
|
+
if len(clean) >= 2 and clean[1] == ":" and clean[0].isalpha():
|
|
34
|
+
clean = clean[2:]
|
|
35
|
+
clean = clean.replace("\\", "/").lstrip("/")
|
|
36
|
+
if source_root:
|
|
37
|
+
sr = source_root.replace("\\", "/").strip("/") + "/"
|
|
38
|
+
if clean.startswith(sr):
|
|
39
|
+
clean = clean[len(sr) :]
|
|
40
|
+
for ext in (".tsx", ".ts", ".jsx", ".js"):
|
|
41
|
+
if clean.endswith(ext):
|
|
42
|
+
clean = clean[: -len(ext)]
|
|
43
|
+
break
|
|
44
|
+
if clean.endswith("/index"):
|
|
45
|
+
clean = clean[:-6]
|
|
46
|
+
return clean.replace("/", ".")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _node_text(node: TSNode | None) -> str:
|
|
50
|
+
"""Return decoded text of a tree-sitter node, or empty string."""
|
|
51
|
+
if node is None or node.text is None:
|
|
52
|
+
return ""
|
|
53
|
+
return node.text.decode("utf-8")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _get_name(node: TSNode) -> str:
|
|
57
|
+
"""Extract identifier/property_identifier/type_identifier text from a node."""
|
|
58
|
+
child = node.child_by_field_name("name")
|
|
59
|
+
if child is not None:
|
|
60
|
+
return _node_text(child)
|
|
61
|
+
for child in node.children:
|
|
62
|
+
if child.type in ("identifier", "property_identifier", "type_identifier"):
|
|
63
|
+
return _node_text(child)
|
|
64
|
+
return ""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _make_node(fqn: str, name: str, node_type: NodeType, file_path: str, ts_node: TSNode) -> Node:
|
|
68
|
+
"""Construct an internal Node from a tree-sitter node's position."""
|
|
69
|
+
return Node(
|
|
70
|
+
id=fqn,
|
|
71
|
+
name=name,
|
|
72
|
+
type=node_type,
|
|
73
|
+
file_path=file_path,
|
|
74
|
+
start_line=ts_node.start_point[0] + 1,
|
|
75
|
+
end_line=ts_node.end_point[0] + 1,
|
|
76
|
+
namespace=NodeNamespace.INTERNAL,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _resolve_relative_import(namespace: str, raw_source: str) -> str:
|
|
81
|
+
"""Resolve a relative import path to a dot-separated module FQN.
|
|
82
|
+
|
|
83
|
+
Walks segments so that ``./utils``, ``../api``, and ``../../lib`` all
|
|
84
|
+
produce the correct absolute FQN relative to *namespace*.
|
|
85
|
+
"""
|
|
86
|
+
dir_parts = namespace.split(".")[:-1]
|
|
87
|
+
for segment in raw_source.replace("\\", "/").split("/"):
|
|
88
|
+
if segment == ".." and dir_parts:
|
|
89
|
+
dir_parts.pop()
|
|
90
|
+
elif segment and segment not in (".", ".."):
|
|
91
|
+
dir_parts.append(segment)
|
|
92
|
+
if dir_parts and dir_parts[-1] == "index":
|
|
93
|
+
dir_parts.pop()
|
|
94
|
+
return ".".join(dir_parts)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _make_edge(source: str, target: str, edge_type: EdgeType) -> Edge:
|
|
98
|
+
"""Construct an Edge with a deterministic id."""
|
|
99
|
+
return Edge(id=f"{source}->{target}", source=source, target=target, type=edge_type)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TypeScriptExtractor(BaseExtractor):
|
|
103
|
+
"""Extracts structural nodes and edges from TypeScript/TSX source files."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, tsx: bool = False, source_roots: list[str] | None = None) -> None:
|
|
106
|
+
"""Initialise the tree-sitter TypeScript (or TSX) parser."""
|
|
107
|
+
super().__init__(source_roots=source_roots)
|
|
108
|
+
lang = tsts.language_tsx() if tsx else tsts.language_typescript()
|
|
109
|
+
self._parser = Parser(Language(lang))
|
|
110
|
+
|
|
111
|
+
def parse(self, code: str, file_path: str) -> tuple[list[Node], list[Edge]]:
|
|
112
|
+
"""Extract nodes and edges from TypeScript source code."""
|
|
113
|
+
code_bytes = code.encode("utf-8")
|
|
114
|
+
tree = self._parser.parse(code_bytes)
|
|
115
|
+
module_fqn = file_path_to_module_fqn(file_path, self._pick_source_root(file_path))
|
|
116
|
+
|
|
117
|
+
file_node = Node(
|
|
118
|
+
id=module_fqn,
|
|
119
|
+
name=file_path.replace("\\", "/").split("/")[-1],
|
|
120
|
+
type=NodeType.FILE,
|
|
121
|
+
file_path=file_path,
|
|
122
|
+
start_line=1,
|
|
123
|
+
end_line=tree.root_node.end_point[0] + 1,
|
|
124
|
+
namespace=NodeNamespace.INTERNAL,
|
|
125
|
+
)
|
|
126
|
+
nodes: list[Node] = [file_node]
|
|
127
|
+
edges: list[Edge] = []
|
|
128
|
+
|
|
129
|
+
self._walk(
|
|
130
|
+
tree.root_node,
|
|
131
|
+
namespace=module_fqn,
|
|
132
|
+
file_path=file_path,
|
|
133
|
+
file_id=module_fqn,
|
|
134
|
+
active_class_fqn=None,
|
|
135
|
+
nodes=nodes,
|
|
136
|
+
edges=edges,
|
|
137
|
+
)
|
|
138
|
+
return nodes, edges
|
|
139
|
+
|
|
140
|
+
def _walk(
|
|
141
|
+
self,
|
|
142
|
+
node: TSNode,
|
|
143
|
+
namespace: str,
|
|
144
|
+
file_path: str,
|
|
145
|
+
file_id: str,
|
|
146
|
+
active_class_fqn: str | None,
|
|
147
|
+
nodes: list[Node],
|
|
148
|
+
edges: list[Edge],
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Recursively walk the AST, emitting nodes and edges."""
|
|
151
|
+
inner = node
|
|
152
|
+
if node.type == "export_statement":
|
|
153
|
+
for child in node.children:
|
|
154
|
+
if child.type in _EXPORT_UNWRAP_TYPES:
|
|
155
|
+
inner = child
|
|
156
|
+
break
|
|
157
|
+
|
|
158
|
+
if inner.type in ("class_declaration", "abstract_class_declaration"):
|
|
159
|
+
self._handle_class(inner, namespace, file_path, file_id, nodes, edges)
|
|
160
|
+
elif inner.type == "function_declaration":
|
|
161
|
+
self._handle_function(
|
|
162
|
+
inner, namespace, file_path, file_id, active_class_fqn, nodes, edges
|
|
163
|
+
)
|
|
164
|
+
elif inner.type == "lexical_declaration":
|
|
165
|
+
self._handle_lexical(
|
|
166
|
+
inner, namespace, file_path, file_id, active_class_fqn, nodes, edges
|
|
167
|
+
)
|
|
168
|
+
elif inner.type == "import_statement":
|
|
169
|
+
self._handle_import(inner, namespace, file_id, edges)
|
|
170
|
+
else:
|
|
171
|
+
for child in node.children:
|
|
172
|
+
self._walk(child, namespace, file_path, file_id, active_class_fqn, nodes, edges)
|
|
173
|
+
|
|
174
|
+
def _handle_class(
|
|
175
|
+
self,
|
|
176
|
+
node: TSNode,
|
|
177
|
+
namespace: str,
|
|
178
|
+
file_path: str,
|
|
179
|
+
file_id: str,
|
|
180
|
+
nodes: list[Node],
|
|
181
|
+
edges: list[Edge],
|
|
182
|
+
) -> None:
|
|
183
|
+
"""Emit a CLASS node + CONTAINS edge, then walk its body for methods."""
|
|
184
|
+
name = _get_name(node)
|
|
185
|
+
if not name:
|
|
186
|
+
return
|
|
187
|
+
class_fqn = f"{namespace}.{name}"
|
|
188
|
+
nodes.append(_make_node(class_fqn, name, NodeType.CLASS, file_path, node))
|
|
189
|
+
edges.append(_make_edge(file_id, class_fqn, EdgeType.CONTAINS))
|
|
190
|
+
body = node.child_by_field_name("body")
|
|
191
|
+
if body is not None:
|
|
192
|
+
for child in body.children:
|
|
193
|
+
if child.type in ("method_definition", "abstract_method_signature"):
|
|
194
|
+
self._handle_method(child, class_fqn, file_path, nodes, edges)
|
|
195
|
+
|
|
196
|
+
def _handle_method(
|
|
197
|
+
self,
|
|
198
|
+
node: TSNode,
|
|
199
|
+
class_fqn: str,
|
|
200
|
+
file_path: str,
|
|
201
|
+
nodes: list[Node],
|
|
202
|
+
edges: list[Edge],
|
|
203
|
+
) -> None:
|
|
204
|
+
"""Emit a METHOD node + DECLARES edge, then find calls in the body."""
|
|
205
|
+
name = _get_name(node)
|
|
206
|
+
if not name:
|
|
207
|
+
return
|
|
208
|
+
method_fqn = f"{class_fqn}.{name}"
|
|
209
|
+
nodes.append(_make_node(method_fqn, name, NodeType.METHOD, file_path, node))
|
|
210
|
+
edges.append(_make_edge(class_fqn, method_fqn, EdgeType.DECLARES))
|
|
211
|
+
self._find_calls(node, method_fqn, edges)
|
|
212
|
+
|
|
213
|
+
def _handle_function(
|
|
214
|
+
self,
|
|
215
|
+
node: TSNode,
|
|
216
|
+
namespace: str,
|
|
217
|
+
file_path: str,
|
|
218
|
+
file_id: str,
|
|
219
|
+
active_class_fqn: str | None,
|
|
220
|
+
nodes: list[Node],
|
|
221
|
+
edges: list[Edge],
|
|
222
|
+
) -> None:
|
|
223
|
+
"""Emit a FUNCTION node + CONTAINS edge, then find calls in the body."""
|
|
224
|
+
name = _get_name(node)
|
|
225
|
+
if not name:
|
|
226
|
+
return
|
|
227
|
+
func_fqn = f"{namespace}.{name}"
|
|
228
|
+
nodes.append(_make_node(func_fqn, name, NodeType.FUNCTION, file_path, node))
|
|
229
|
+
edges.append(_make_edge(active_class_fqn or file_id, func_fqn, EdgeType.CONTAINS))
|
|
230
|
+
self._find_calls(node, func_fqn, edges)
|
|
231
|
+
|
|
232
|
+
def _handle_lexical(
|
|
233
|
+
self,
|
|
234
|
+
node: TSNode,
|
|
235
|
+
namespace: str,
|
|
236
|
+
file_path: str,
|
|
237
|
+
file_id: str,
|
|
238
|
+
active_class_fqn: str | None,
|
|
239
|
+
nodes: list[Node],
|
|
240
|
+
edges: list[Edge],
|
|
241
|
+
) -> None:
|
|
242
|
+
"""Handle const/let arrow-function declarations."""
|
|
243
|
+
for child in node.children:
|
|
244
|
+
if child.type != "variable_declarator":
|
|
245
|
+
continue
|
|
246
|
+
value = child.child_by_field_name("value")
|
|
247
|
+
if value is None or value.type != "arrow_function":
|
|
248
|
+
continue
|
|
249
|
+
name = _node_text(child.child_by_field_name("name"))
|
|
250
|
+
if not name:
|
|
251
|
+
continue
|
|
252
|
+
func_fqn = f"{namespace}.{name}"
|
|
253
|
+
nodes.append(_make_node(func_fqn, name, NodeType.FUNCTION, file_path, node))
|
|
254
|
+
edges.append(_make_edge(active_class_fqn or file_id, func_fqn, EdgeType.CONTAINS))
|
|
255
|
+
self._find_calls(value, func_fqn, edges)
|
|
256
|
+
|
|
257
|
+
def _handle_import(
|
|
258
|
+
self,
|
|
259
|
+
node: TSNode,
|
|
260
|
+
namespace: str,
|
|
261
|
+
file_id: str,
|
|
262
|
+
edges: list[Edge],
|
|
263
|
+
) -> None:
|
|
264
|
+
"""Emit an IMPORTS edge for an import statement."""
|
|
265
|
+
raw_source = _node_text(node.child_by_field_name("source")).strip("'\"")
|
|
266
|
+
if not raw_source:
|
|
267
|
+
return
|
|
268
|
+
if raw_source.startswith("."):
|
|
269
|
+
target_fqn = _resolve_relative_import(namespace, raw_source)
|
|
270
|
+
else:
|
|
271
|
+
target_fqn = raw_source.replace("/", ".")
|
|
272
|
+
edges.append(
|
|
273
|
+
Edge(
|
|
274
|
+
id=f"{file_id}->import:{target_fqn}",
|
|
275
|
+
source=file_id,
|
|
276
|
+
target=target_fqn,
|
|
277
|
+
type=EdgeType.IMPORTS,
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def _find_calls(self, node: TSNode, source_id: str, edges: list[Edge]) -> None:
|
|
282
|
+
"""Recursively find call_expression nodes and emit CALLS edges."""
|
|
283
|
+
if node.type == "call_expression":
|
|
284
|
+
func_node = node.child_by_field_name("function")
|
|
285
|
+
if func_node is not None:
|
|
286
|
+
call_name = self._get_call_name(func_node)
|
|
287
|
+
if call_name:
|
|
288
|
+
target = f"{_RAW_CALL_PREFIX}{call_name}"
|
|
289
|
+
edges.append(
|
|
290
|
+
Edge(
|
|
291
|
+
id=f"{source_id}->{target}@{node.start_point[0]}:{node.start_point[1]}",
|
|
292
|
+
source=source_id,
|
|
293
|
+
target=target,
|
|
294
|
+
type=EdgeType.CALLS,
|
|
295
|
+
confidence=0.1,
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
for child in node.children:
|
|
299
|
+
self._find_calls(child, source_id, edges)
|
|
300
|
+
|
|
301
|
+
def _get_call_name(self, node: TSNode) -> str:
|
|
302
|
+
"""Extract a call target name from identifier or member_expression."""
|
|
303
|
+
if node.type == "identifier":
|
|
304
|
+
return _node_text(node)
|
|
305
|
+
if node.type == "member_expression":
|
|
306
|
+
obj_text = _node_text(node.child_by_field_name("object"))
|
|
307
|
+
prop_text = _node_text(node.child_by_field_name("property"))
|
|
308
|
+
if obj_text and prop_text:
|
|
309
|
+
return f"self.{prop_text}" if obj_text == "this" else f"{obj_text}.{prop_text}"
|
|
310
|
+
return ""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CGIS Guardian — AI-powered code review."""
|