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.
Files changed (37) hide show
  1. codetruth/__init__.py +16 -0
  2. codetruth/api.py +102 -0
  3. codetruth/cli.py +183 -0
  4. codetruth/core/__init__.py +0 -0
  5. codetruth/core/cache.py +104 -0
  6. codetruth/core/config.py +74 -0
  7. codetruth/core/deletion.py +165 -0
  8. codetruth/core/evidence.py +306 -0
  9. codetruth/core/graph.py +44 -0
  10. codetruth/core/models.py +193 -0
  11. codetruth/core/plugin.py +88 -0
  12. codetruth/core/report.py +107 -0
  13. codetruth/core/scanner.py +228 -0
  14. codetruth/languages/__init__.py +0 -0
  15. codetruth/languages/go/__init__.py +1 -0
  16. codetruth/languages/javascript/__init__.py +8 -0
  17. codetruth/languages/javascript/edges.py +361 -0
  18. codetruth/languages/javascript/extractor.py +402 -0
  19. codetruth/languages/javascript/plugin.py +52 -0
  20. codetruth/languages/javascript/rules.py +201 -0
  21. codetruth/languages/python/__init__.py +0 -0
  22. codetruth/languages/python/edges.py +474 -0
  23. codetruth/languages/python/extractor.py +271 -0
  24. codetruth/languages/python/plugin.py +49 -0
  25. codetruth/languages/python/rules.py +332 -0
  26. codetruth/mcp_server.py +138 -0
  27. codetruth/rules/python/common_dynamic.yaml +70 -0
  28. codetruth/rules/python/django.yaml +57 -0
  29. codetruth/rules/python/fastapi.yaml +38 -0
  30. codetruth/rules/python/orm_web.yaml +50 -0
  31. codetruth/runtime/__init__.py +283 -0
  32. codetruth-0.2.0.dist-info/METADATA +215 -0
  33. codetruth-0.2.0.dist-info/RECORD +37 -0
  34. codetruth-0.2.0.dist-info/WHEEL +5 -0
  35. codetruth-0.2.0.dist-info/entry_points.txt +2 -0
  36. codetruth-0.2.0.dist-info/licenses/LICENSE +21 -0
  37. 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))