codetruth 0.2.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.
- codetruth/__init__.py +16 -0
- codetruth/api.py +102 -0
- codetruth/cli.py +183 -0
- codetruth/core/__init__.py +0 -0
- codetruth/core/cache.py +104 -0
- codetruth/core/config.py +74 -0
- codetruth/core/deletion.py +165 -0
- codetruth/core/evidence.py +306 -0
- codetruth/core/graph.py +44 -0
- codetruth/core/models.py +193 -0
- codetruth/core/plugin.py +88 -0
- codetruth/core/report.py +107 -0
- codetruth/core/scanner.py +228 -0
- codetruth/languages/__init__.py +0 -0
- codetruth/languages/go/__init__.py +1 -0
- codetruth/languages/javascript/__init__.py +8 -0
- codetruth/languages/javascript/edges.py +361 -0
- codetruth/languages/javascript/extractor.py +402 -0
- codetruth/languages/javascript/plugin.py +52 -0
- codetruth/languages/javascript/rules.py +201 -0
- codetruth/languages/python/__init__.py +0 -0
- codetruth/languages/python/edges.py +474 -0
- codetruth/languages/python/extractor.py +271 -0
- codetruth/languages/python/plugin.py +49 -0
- codetruth/languages/python/rules.py +332 -0
- codetruth/mcp_server.py +138 -0
- codetruth/rules/python/common_dynamic.yaml +70 -0
- codetruth/rules/python/django.yaml +57 -0
- codetruth/rules/python/fastapi.yaml +38 -0
- codetruth/rules/python/orm_web.yaml +50 -0
- codetruth/runtime/__init__.py +283 -0
- codetruth-0.2.0.dist-info/METADATA +215 -0
- codetruth-0.2.0.dist-info/RECORD +37 -0
- codetruth-0.2.0.dist-info/WHEEL +5 -0
- codetruth-0.2.0.dist-info/entry_points.txt +2 -0
- codetruth-0.2.0.dist-info/licenses/LICENSE +21 -0
- codetruth-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""Layer 2 — relationship graph construction for JavaScript/TypeScript.
|
|
2
|
+
|
|
3
|
+
Same contract as the Python edge builder: resolve what we can determinately
|
|
4
|
+
(imports, lexical references, namespace member access, `this.method`) into
|
|
5
|
+
STRONG edges; fall back to conservative name-matching WEAK edges for property
|
|
6
|
+
access on unresolved objects — JS is dynamic enough that the weak tier does a
|
|
7
|
+
lot of the safety work here.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import posixpath
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from ...core.graph import CodeGraph
|
|
16
|
+
from ...core.models import (Edge, EdgeKind, EdgeStrength, Marker, MarkerKind,
|
|
17
|
+
Symbol, SymbolType)
|
|
18
|
+
from .extractor import ModuleInfo, _text
|
|
19
|
+
|
|
20
|
+
MAX_NAME_MATCHES = 60
|
|
21
|
+
|
|
22
|
+
# Base classes that don't imply framework-invoked methods on subclasses.
|
|
23
|
+
BENIGN_BASES = {"Object", "Error", "TypeError", "RangeError", "Array", "Map",
|
|
24
|
+
"Set", "Promise", "EventTarget"}
|
|
25
|
+
|
|
26
|
+
_INDEX_CANDIDATES = ("/index", "/index.js", "/index.ts", "/index.tsx",
|
|
27
|
+
"/index.jsx")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SymbolIndex:
|
|
31
|
+
def __init__(self, modules: list[ModuleInfo]):
|
|
32
|
+
self.modules: dict[str, ModuleInfo] = {m.name: m for m in modules}
|
|
33
|
+
self.by_id: dict[str, Symbol] = {}
|
|
34
|
+
self.toplevel: dict[str, dict[str, str]] = defaultdict(dict)
|
|
35
|
+
self.by_name: dict[str, list[str]] = defaultdict(list)
|
|
36
|
+
self.children: dict[str, dict[str, str]] = defaultdict(dict)
|
|
37
|
+
for m in modules:
|
|
38
|
+
for s in m.symbols:
|
|
39
|
+
self.by_id[s.id] = s
|
|
40
|
+
if s.type is SymbolType.MODULE:
|
|
41
|
+
continue
|
|
42
|
+
self.by_name[s.name].append(s.id)
|
|
43
|
+
if s.parent == m.name:
|
|
44
|
+
self.toplevel[m.name][s.name] = s.id
|
|
45
|
+
if s.parent:
|
|
46
|
+
self.children[s.parent][s.name] = s.id
|
|
47
|
+
|
|
48
|
+
def all_symbols(self) -> list[Symbol]:
|
|
49
|
+
return list(self.by_id.values())
|
|
50
|
+
|
|
51
|
+
def resolve_source(self, importer: str, source: str) -> Optional[str]:
|
|
52
|
+
"""'./utils' relative to 'src/app' -> 'src/utils' (or None if
|
|
53
|
+
external / not found)."""
|
|
54
|
+
if not source.startswith("."):
|
|
55
|
+
return None
|
|
56
|
+
base_dir = posixpath.dirname(importer)
|
|
57
|
+
target = posixpath.normpath(posixpath.join(base_dir, source))
|
|
58
|
+
# strip a literal extension if the import included one
|
|
59
|
+
for ext in (".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"):
|
|
60
|
+
if target.endswith(ext):
|
|
61
|
+
target = target[: -len(ext)]
|
|
62
|
+
break
|
|
63
|
+
if target in self.modules:
|
|
64
|
+
return target
|
|
65
|
+
for suffix in _INDEX_CANDIDATES:
|
|
66
|
+
cand = posixpath.normpath(target + suffix)
|
|
67
|
+
cand = cand.rsplit(".", 1)[0] if "." in cand.rsplit("/", 1)[-1] else cand
|
|
68
|
+
if cand in self.modules:
|
|
69
|
+
return cand
|
|
70
|
+
if f"{target}/index" in self.modules:
|
|
71
|
+
return f"{target}/index"
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def build_scope(mi: ModuleInfo, idx: SymbolIndex, graph: CodeGraph) -> dict:
|
|
76
|
+
scope: dict[str, tuple[str, Optional[str]]] = {}
|
|
77
|
+
for name, sid in idx.toplevel.get(mi.name, {}).items():
|
|
78
|
+
scope[name] = ("symbol", sid)
|
|
79
|
+
|
|
80
|
+
for imp in mi.imports:
|
|
81
|
+
target_mod = idx.resolve_source(mi.name, imp.source)
|
|
82
|
+
if target_mod is None:
|
|
83
|
+
if imp.alias:
|
|
84
|
+
scope[imp.alias] = ("ext", None)
|
|
85
|
+
continue
|
|
86
|
+
graph.add_edge(Edge(mi.name, target_mod, EdgeKind.IMPORT,
|
|
87
|
+
EdgeStrength.STRONG, mi.rel_path, imp.line,
|
|
88
|
+
f"import from '{imp.source}'"))
|
|
89
|
+
if imp.kind == "named" or imp.kind == "reexport":
|
|
90
|
+
sid = idx.toplevel.get(target_mod, {}).get(imp.name)
|
|
91
|
+
if sid:
|
|
92
|
+
graph.add_edge(Edge(mi.name, sid, EdgeKind.IMPORT,
|
|
93
|
+
EdgeStrength.STRONG, mi.rel_path, imp.line,
|
|
94
|
+
f"import {{ {imp.name} }} from "
|
|
95
|
+
f"'{imp.source}'"))
|
|
96
|
+
if imp.alias:
|
|
97
|
+
scope[imp.alias] = ("symbol", sid)
|
|
98
|
+
elif imp.alias:
|
|
99
|
+
scope[imp.alias] = ("module", target_mod)
|
|
100
|
+
elif imp.kind in ("default", "namespace"):
|
|
101
|
+
if imp.alias:
|
|
102
|
+
scope[imp.alias] = ("module", target_mod)
|
|
103
|
+
# default import also credits an exported `default`-named symbol
|
|
104
|
+
sid = idx.toplevel.get(target_mod, {}).get("default")
|
|
105
|
+
if sid:
|
|
106
|
+
graph.add_edge(Edge(mi.name, sid, EdgeKind.IMPORT,
|
|
107
|
+
EdgeStrength.STRONG, mi.rel_path,
|
|
108
|
+
imp.line, "default import"))
|
|
109
|
+
elif imp.kind == "reexport_all":
|
|
110
|
+
for sid in idx.toplevel.get(target_mod, {}).values():
|
|
111
|
+
sym = idx.by_id[sid]
|
|
112
|
+
if sym.exported:
|
|
113
|
+
graph.add_edge(Edge(mi.name, sid, EdgeKind.IMPORT,
|
|
114
|
+
EdgeStrength.STRONG, mi.rel_path,
|
|
115
|
+
imp.line,
|
|
116
|
+
f"export * from '{imp.source}'"))
|
|
117
|
+
return scope
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class EdgeWalker:
|
|
121
|
+
"""Recursive tree walk emitting reference edges. Skips identifiers in
|
|
122
|
+
declaration positions (names, params, import clauses, object keys)."""
|
|
123
|
+
|
|
124
|
+
SKIP_SUBTREES = {"import_statement", "formal_parameters",
|
|
125
|
+
"type_parameters", "comment"}
|
|
126
|
+
_DECL_NAME_PARENTS = {
|
|
127
|
+
"function_declaration", "generator_function_declaration",
|
|
128
|
+
"class_declaration", "abstract_class_declaration",
|
|
129
|
+
"variable_declarator", "method_definition", "field_definition",
|
|
130
|
+
"public_field_definition", "interface_declaration",
|
|
131
|
+
"enum_declaration", "type_alias_declaration",
|
|
132
|
+
"required_parameter", "optional_parameter",
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
def __init__(self, mi: ModuleInfo, scope: dict, idx: SymbolIndex,
|
|
136
|
+
graph: CodeGraph):
|
|
137
|
+
self.mi = mi
|
|
138
|
+
self.idx = idx
|
|
139
|
+
self.graph = graph
|
|
140
|
+
self.scopes: list[dict] = [scope]
|
|
141
|
+
self.src_stack: list[str] = [mi.name]
|
|
142
|
+
self.qual_stack: list[str] = []
|
|
143
|
+
self.class_stack: list[str] = []
|
|
144
|
+
|
|
145
|
+
# -- helpers ---------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
def _lookup(self, name: str):
|
|
148
|
+
for scope in reversed(self.scopes):
|
|
149
|
+
if name in scope:
|
|
150
|
+
return scope[name]
|
|
151
|
+
return None, None
|
|
152
|
+
|
|
153
|
+
def _emit(self, dst: str, kind: EdgeKind, strength: EdgeStrength, node,
|
|
154
|
+
detail: str = "") -> None:
|
|
155
|
+
src = self.src_stack[-1]
|
|
156
|
+
self.graph.add_edge(Edge(src, dst, kind, strength, self.mi.rel_path,
|
|
157
|
+
node.start_point[0] + 1, detail))
|
|
158
|
+
|
|
159
|
+
def _weak_name_edges(self, name: str, node) -> None:
|
|
160
|
+
if len(name) < 2:
|
|
161
|
+
return
|
|
162
|
+
matches = self.idx.by_name.get(name, [])
|
|
163
|
+
if not matches or len(matches) > MAX_NAME_MATCHES:
|
|
164
|
+
return
|
|
165
|
+
for sid in matches:
|
|
166
|
+
if sid != self.src_stack[-1]:
|
|
167
|
+
self._emit(sid, EdgeKind.ATTRIBUTE, EdgeStrength.WEAK, node,
|
|
168
|
+
f"property name match '.{name}'")
|
|
169
|
+
|
|
170
|
+
def _local_defs(self, sym_id: str) -> dict:
|
|
171
|
+
return {name: ("symbol", cid)
|
|
172
|
+
for name, cid in self.idx.children.get(sym_id, {}).items()}
|
|
173
|
+
|
|
174
|
+
# -- walk --------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
def walk(self, node) -> None:
|
|
177
|
+
t = node.type
|
|
178
|
+
if t in self.SKIP_SUBTREES:
|
|
179
|
+
return
|
|
180
|
+
if t == "identifier" or t == "type_identifier":
|
|
181
|
+
# handled by parent dispatch; identifiers reached here are loads
|
|
182
|
+
self._ref(node)
|
|
183
|
+
return
|
|
184
|
+
if t == "member_expression":
|
|
185
|
+
self._member(node)
|
|
186
|
+
return
|
|
187
|
+
if t == "subscript_expression":
|
|
188
|
+
self._subscript(node)
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
entering = None
|
|
192
|
+
if t in ("function_declaration", "generator_function_declaration",
|
|
193
|
+
"class_declaration", "abstract_class_declaration",
|
|
194
|
+
"method_definition"):
|
|
195
|
+
entering = self._enter_named(node)
|
|
196
|
+
elif t == "variable_declarator":
|
|
197
|
+
entering = self._enter_declarator(node)
|
|
198
|
+
|
|
199
|
+
for i, child in enumerate(node.children):
|
|
200
|
+
if not child.is_named:
|
|
201
|
+
continue
|
|
202
|
+
field = node.field_name_for_child(i)
|
|
203
|
+
if field == "name" and t in self._DECL_NAME_PARENTS:
|
|
204
|
+
continue
|
|
205
|
+
if field == "property" and t in ("field_definition",
|
|
206
|
+
"public_field_definition"):
|
|
207
|
+
continue
|
|
208
|
+
if field == "key" and t == "pair":
|
|
209
|
+
continue
|
|
210
|
+
if t == "export_statement" and field is None \
|
|
211
|
+
and child.type == "export_clause":
|
|
212
|
+
continue
|
|
213
|
+
self.walk(child)
|
|
214
|
+
|
|
215
|
+
if entering:
|
|
216
|
+
self._exit(*entering)
|
|
217
|
+
|
|
218
|
+
# -- scoped declarations ------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
def _enter_named(self, node):
|
|
221
|
+
name_node = node.child_by_field_name("name")
|
|
222
|
+
if name_node is None:
|
|
223
|
+
return None
|
|
224
|
+
name = _text(name_node, self.mi.source)
|
|
225
|
+
self.qual_stack.append(name)
|
|
226
|
+
sym_id = f"{self.mi.name}:{'.'.join(self.qual_stack)}"
|
|
227
|
+
known = sym_id in self.idx.by_id
|
|
228
|
+
self.src_stack.append(sym_id if known else self.src_stack[-1])
|
|
229
|
+
self.scopes.append(self._local_defs(sym_id) if known else {})
|
|
230
|
+
is_class = node.type in ("class_declaration",
|
|
231
|
+
"abstract_class_declaration")
|
|
232
|
+
if is_class and known:
|
|
233
|
+
self.class_stack.append(sym_id)
|
|
234
|
+
return (is_class and known,)
|
|
235
|
+
|
|
236
|
+
def _enter_declarator(self, node):
|
|
237
|
+
name_node = node.child_by_field_name("name")
|
|
238
|
+
value = node.child_by_field_name("value")
|
|
239
|
+
if name_node is None or name_node.type != "identifier" \
|
|
240
|
+
or value is None or value.type not in (
|
|
241
|
+
"arrow_function", "function_expression", "function",
|
|
242
|
+
"generator_function"):
|
|
243
|
+
return None
|
|
244
|
+
name = _text(name_node, self.mi.source)
|
|
245
|
+
self.qual_stack.append(name)
|
|
246
|
+
sym_id = f"{self.mi.name}:{'.'.join(self.qual_stack)}"
|
|
247
|
+
known = sym_id in self.idx.by_id
|
|
248
|
+
self.src_stack.append(sym_id if known else self.src_stack[-1])
|
|
249
|
+
self.scopes.append(self._local_defs(sym_id) if known else {})
|
|
250
|
+
return (False,)
|
|
251
|
+
|
|
252
|
+
def _exit(self, was_class: bool) -> None:
|
|
253
|
+
if was_class:
|
|
254
|
+
self.class_stack.pop()
|
|
255
|
+
self.scopes.pop()
|
|
256
|
+
self.src_stack.pop()
|
|
257
|
+
self.qual_stack.pop()
|
|
258
|
+
|
|
259
|
+
# -- reference emission ---------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
def _ref(self, node) -> None:
|
|
262
|
+
name = _text(node, self.mi.source)
|
|
263
|
+
kind, target = self._lookup(name)
|
|
264
|
+
if kind == "symbol" and target != self.src_stack[-1]:
|
|
265
|
+
self._emit(target, EdgeKind.REFERENCE, EdgeStrength.STRONG, node,
|
|
266
|
+
name)
|
|
267
|
+
elif kind == "module" and target in self.idx.by_id:
|
|
268
|
+
self._emit(target, EdgeKind.REFERENCE, EdgeStrength.STRONG, node,
|
|
269
|
+
name)
|
|
270
|
+
|
|
271
|
+
def _member(self, node) -> None:
|
|
272
|
+
obj = node.child_by_field_name("object")
|
|
273
|
+
prop = node.child_by_field_name("property")
|
|
274
|
+
prop_name = _text(prop, self.mi.source) if prop is not None else ""
|
|
275
|
+
if obj is not None and obj.type == "identifier":
|
|
276
|
+
oname = _text(obj, self.mi.source)
|
|
277
|
+
kind, target = self._lookup(oname)
|
|
278
|
+
if kind == "module":
|
|
279
|
+
sid = self.idx.toplevel.get(target, {}).get(prop_name)
|
|
280
|
+
if sid:
|
|
281
|
+
self._emit(sid, EdgeKind.REFERENCE, EdgeStrength.STRONG,
|
|
282
|
+
node, f"{oname}.{prop_name}")
|
|
283
|
+
else:
|
|
284
|
+
self._emit(target, EdgeKind.REFERENCE, EdgeStrength.STRONG,
|
|
285
|
+
node, oname)
|
|
286
|
+
return
|
|
287
|
+
if kind == "symbol":
|
|
288
|
+
sym = self.idx.by_id.get(target)
|
|
289
|
+
if sym is not None and sym.type is SymbolType.CLASS:
|
|
290
|
+
member = self.idx.children.get(target, {}).get(prop_name)
|
|
291
|
+
self._emit(target, EdgeKind.REFERENCE, EdgeStrength.STRONG,
|
|
292
|
+
node, oname)
|
|
293
|
+
if member:
|
|
294
|
+
self._emit(member, EdgeKind.ATTRIBUTE,
|
|
295
|
+
EdgeStrength.STRONG, node,
|
|
296
|
+
f"{oname}.{prop_name}")
|
|
297
|
+
return
|
|
298
|
+
self._emit(target, EdgeKind.REFERENCE, EdgeStrength.STRONG,
|
|
299
|
+
node, oname)
|
|
300
|
+
if prop_name:
|
|
301
|
+
self._weak_name_edges(prop_name, node)
|
|
302
|
+
return
|
|
303
|
+
if obj is not None and obj.type == "this" and self.class_stack:
|
|
304
|
+
member = self.idx.children.get(self.class_stack[-1], {}) \
|
|
305
|
+
.get(prop_name)
|
|
306
|
+
if member:
|
|
307
|
+
self._emit(member, EdgeKind.ATTRIBUTE, EdgeStrength.STRONG,
|
|
308
|
+
node, f"this.{prop_name}")
|
|
309
|
+
return
|
|
310
|
+
if obj is not None:
|
|
311
|
+
self.walk(obj)
|
|
312
|
+
if prop_name:
|
|
313
|
+
self._weak_name_edges(prop_name, node)
|
|
314
|
+
|
|
315
|
+
def _subscript(self, node) -> None:
|
|
316
|
+
obj = node.child_by_field_name("object")
|
|
317
|
+
index = node.child_by_field_name("index")
|
|
318
|
+
if obj is not None:
|
|
319
|
+
self.walk(obj)
|
|
320
|
+
if index is not None:
|
|
321
|
+
if index.type == "string":
|
|
322
|
+
literal = _text(index, self.mi.source).strip("'\"`")
|
|
323
|
+
for sid in self.idx.by_name.get(literal, []):
|
|
324
|
+
self._emit(sid, EdgeKind.DYNAMIC, EdgeStrength.WEAK, node,
|
|
325
|
+
f"computed access ['{literal}']")
|
|
326
|
+
else:
|
|
327
|
+
self.walk(index)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def build_edges(modules: list[ModuleInfo], idx: SymbolIndex, graph: CodeGraph,
|
|
331
|
+
markers: list[Marker]) -> None:
|
|
332
|
+
for sym in idx.all_symbols():
|
|
333
|
+
graph.add_symbol(sym)
|
|
334
|
+
for mi in modules:
|
|
335
|
+
if mi.tree is None:
|
|
336
|
+
continue
|
|
337
|
+
scope = build_scope(mi, idx, graph)
|
|
338
|
+
EdgeWalker(mi, scope, idx, graph).walk(mi.tree.root_node)
|
|
339
|
+
|
|
340
|
+
# Methods on classes extending something unresolvable may implement a
|
|
341
|
+
# framework interface (React lifecycle, web components, ORM hooks).
|
|
342
|
+
for mi in modules:
|
|
343
|
+
for sym in mi.symbols:
|
|
344
|
+
if sym.type is not SymbolType.CLASS or not sym.bases:
|
|
345
|
+
continue
|
|
346
|
+
base = sym.bases[0]
|
|
347
|
+
root = base.split(".")[0]
|
|
348
|
+
local = idx.toplevel.get(mi.name, {})
|
|
349
|
+
imported = any(imp.alias == root or imp.name == root
|
|
350
|
+
for imp in mi.imports
|
|
351
|
+
if idx.resolve_source(mi.name, imp.source))
|
|
352
|
+
if base in BENIGN_BASES or root in local or imported:
|
|
353
|
+
continue
|
|
354
|
+
for member_id in idx.children.get(sym.id, {}).values():
|
|
355
|
+
member = idx.by_id[member_id]
|
|
356
|
+
if member.type is SymbolType.METHOD:
|
|
357
|
+
markers.append(Marker(
|
|
358
|
+
member_id, MarkerKind.CAUTION,
|
|
359
|
+
f"class {sym.qualname} extends external base "
|
|
360
|
+
f"{base!r}; this method may implement its interface",
|
|
361
|
+
rule="external-base", file=sym.file, line=sym.line))
|