codegraph-ir 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.
Files changed (63) hide show
  1. cgir/__init__.py +6 -0
  2. cgir/analyses/__init__.py +7 -0
  3. cgir/analyses/call_graph.py +114 -0
  4. cgir/analyses/cfg.py +320 -0
  5. cgir/analyses/effects.py +95 -0
  6. cgir/analyses/entrypoints.py +55 -0
  7. cgir/analyses/param_flow.py +75 -0
  8. cgir/analyses/pdg.py +75 -0
  9. cgir/analyses/purity.py +37 -0
  10. cgir/analyses/reaching_defs.py +115 -0
  11. cgir/analyses/symbols.py +106 -0
  12. cgir/api/__init__.py +1 -0
  13. cgir/api/mcp_server.py +172 -0
  14. cgir/api/server.py +107 -0
  15. cgir/cli.py +688 -0
  16. cgir/config.py +22 -0
  17. cgir/export/__init__.py +5 -0
  18. cgir/export/graphml.py +52 -0
  19. cgir/export/html_viz.py +1087 -0
  20. cgir/export/json_export.py +37 -0
  21. cgir/export/mermaid.py +57 -0
  22. cgir/export/neo4j.py +11 -0
  23. cgir/hooks.py +189 -0
  24. cgir/ir/__init__.py +16 -0
  25. cgir/ir/component_spec.py +100 -0
  26. cgir/ir/edges.py +31 -0
  27. cgir/ir/graph.py +132 -0
  28. cgir/ir/nodes.py +38 -0
  29. cgir/languages/__init__.py +25 -0
  30. cgir/languages/base.py +221 -0
  31. cgir/languages/cache.py +73 -0
  32. cgir/languages/python.py +1145 -0
  33. cgir/languages/registry.py +24 -0
  34. cgir/languages/typescript.py +853 -0
  35. cgir/manifest.py +77 -0
  36. cgir/pipeline.py +54 -0
  37. cgir/py.typed +0 -0
  38. cgir/regenerate/__init__.py +6 -0
  39. cgir/regenerate/prompt_pack.py +16 -0
  40. cgir/regenerate/regenerator.py +108 -0
  41. cgir/report/__init__.py +5 -0
  42. cgir/report/diff.py +226 -0
  43. cgir/report/flow.py +84 -0
  44. cgir/report/impact.py +234 -0
  45. cgir/report/lint.py +84 -0
  46. cgir/report/pack.py +271 -0
  47. cgir/report/stats.py +121 -0
  48. cgir/slicing/__init__.py +5 -0
  49. cgir/slicing/slicer.py +174 -0
  50. cgir/sources/__init__.py +6 -0
  51. cgir/sources/base.py +14 -0
  52. cgir/sources/codeql_source.py +13 -0
  53. cgir/sources/joern_source.py +13 -0
  54. cgir/sources/tree_sitter_source.py +270 -0
  55. cgir/trace/__init__.py +5 -0
  56. cgir/trace/trace_map.py +83 -0
  57. cgir/verify.py +173 -0
  58. cgir/watch.py +171 -0
  59. codegraph_ir-0.1.0.dist-info/METADATA +143 -0
  60. codegraph_ir-0.1.0.dist-info/RECORD +63 -0
  61. codegraph_ir-0.1.0.dist-info/WHEEL +4 -0
  62. codegraph_ir-0.1.0.dist-info/entry_points.txt +2 -0
  63. codegraph_ir-0.1.0.dist-info/licenses/LICENSE +21 -0
cgir/languages/base.py ADDED
@@ -0,0 +1,221 @@
1
+ """LanguageAdapter — the per-language seam.
2
+
3
+ Every downstream analysis (symbols, cfg, effects, pdg, param-flow, purity,
4
+ slicing) is written *once* against the RepoGraph and a shared node-attr
5
+ contract. What differs between languages is only grammar-specific
6
+ extraction: what node type is a call, which builtins are effectful, how a
7
+ branch's condition is reached. A ``LanguageAdapter`` answers exactly those
8
+ questions, so a new language is one adapter, not a re-implemented pipeline.
9
+
10
+ Tree-sitter is the shared substrate (Python, TypeScript, Go, Rust all have
11
+ grammars), so adapters trade in :class:`tree_sitter.Node`. The adapter
12
+ abstracts the *grammar*, not the parser technology.
13
+
14
+ The surface grows by phase as passes are migrated behind it:
15
+ * phase 1 — parse / locate / effects / calls
16
+ * phase 2 — CFG statement classification + field extraction
17
+ * phase 3 — ingest structural dispatch + attr extraction
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from abc import ABC, abstractmethod
23
+ from dataclasses import dataclass, field
24
+
25
+ from tree_sitter import Node as TSNode
26
+
27
+ # (dotted_callee, arg_identifier_names, 1-based_line)
28
+ CallSite = tuple[str, list[str], int]
29
+
30
+
31
+ # --- normalized statement descriptors (phase 2: CFG) ---------------------------
32
+ #
33
+ # The CFG *topology* (how branches wire, loop back-edges, try/finally joins)
34
+ # is language-universal and lives in cgir/analyses/cfg.py. The adapter's job
35
+ # is to classify each statement and hand back its parts in one of these
36
+ # shapes; the builder never looks at grammar node types.
37
+
38
+
39
+ @dataclass(slots=True)
40
+ class SimpleDesc:
41
+ reads: list[str] = field(default_factory=list)
42
+ mutates: list[str] = field(default_factory=list)
43
+
44
+
45
+ @dataclass(slots=True)
46
+ class AssignDesc:
47
+ writes: list[str] = field(default_factory=list)
48
+ mutates: list[str] = field(default_factory=list)
49
+ reads: list[str] = field(default_factory=list)
50
+
51
+
52
+ @dataclass(slots=True)
53
+ class ReturnDesc:
54
+ reads: list[str] = field(default_factory=list)
55
+ mutates: list[str] = field(default_factory=list)
56
+
57
+
58
+ @dataclass(slots=True)
59
+ class BranchDesc:
60
+ """An if/elif (or `else if`) arm. Chains via ``next_branch``."""
61
+
62
+ reads: list[str] = field(default_factory=list)
63
+ consequence: TSNode | None = None
64
+ else_block: TSNode | None = None
65
+ next_branch: TSNode | None = None # elif_clause / nested `if` — described again
66
+
67
+
68
+ @dataclass(slots=True)
69
+ class LoopDesc:
70
+ reads: list[str] = field(default_factory=list)
71
+ writes: list[str] = field(default_factory=list) # for-targets
72
+ body: TSNode | None = None
73
+
74
+
75
+ @dataclass(slots=True)
76
+ class WithDesc:
77
+ """Resource-acquisition header (with / using / try-with-resources)."""
78
+
79
+ writes: list[str] = field(default_factory=list) # `as` aliases
80
+ reads: list[str] = field(default_factory=list)
81
+ body: TSNode | None = None
82
+
83
+
84
+ @dataclass(slots=True)
85
+ class HandlerDesc:
86
+ node: TSNode
87
+ writes: list[str] = field(default_factory=list) # `except ... as e`
88
+ block: TSNode | None = None
89
+
90
+
91
+ @dataclass(slots=True)
92
+ class TryDesc:
93
+ body: TSNode | None = None
94
+ handlers: list[HandlerDesc] = field(default_factory=list)
95
+ else_block: TSNode | None = None
96
+ finally_block: TSNode | None = None
97
+
98
+
99
+ @dataclass(slots=True)
100
+ class CaseDesc:
101
+ node: TSNode
102
+ reads: list[str] = field(default_factory=list) # subject + guard
103
+ consequence: TSNode | None = None
104
+
105
+
106
+ @dataclass(slots=True)
107
+ class MatchDesc:
108
+ cases: list[CaseDesc] = field(default_factory=list)
109
+
110
+
111
+ StatementDesc = (
112
+ SimpleDesc | AssignDesc | ReturnDesc | BranchDesc | LoopDesc | WithDesc | TryDesc | MatchDesc
113
+ )
114
+
115
+
116
+ # --- normalized module declarations (phase 3: ingest) ---------------------------
117
+ #
118
+ # The structural spine (Repository → File → Module → members + CONTAINS) and
119
+ # node/edge construction are language-universal and live in
120
+ # sources/tree_sitter_source.py. The adapter walks a module root and yields
121
+ # these; the ingester never looks at grammar node types.
122
+
123
+
124
+ @dataclass(slots=True)
125
+ class ParamDecl:
126
+ name: str
127
+ node: TSNode
128
+
129
+
130
+ @dataclass(slots=True)
131
+ class FunctionDecl:
132
+ node: TSNode
133
+ name: str
134
+ params: list[ParamDecl] = field(default_factory=list)
135
+ signature: str = ""
136
+ returns: str | None = None
137
+ doc: str = ""
138
+ raises: list[str] = field(default_factory=list)
139
+ decorators: list[str] = field(default_factory=list)
140
+ free_names: list[str] = field(default_factory=list)
141
+
142
+
143
+ @dataclass(slots=True)
144
+ class ClassDecl:
145
+ node: TSNode
146
+ name: str
147
+ methods: list[FunctionDecl] = field(default_factory=list)
148
+ # field name → declared type name, for DI/receiver call resolution
149
+ # (``this.svc.method()`` where ``svc: ChaptersService``).
150
+ fields: dict[str, str] = field(default_factory=dict)
151
+
152
+
153
+ @dataclass(slots=True)
154
+ class ImportDecl:
155
+ node: TSNode
156
+ target: str
157
+ alias: str | None = None
158
+
159
+
160
+ @dataclass(slots=True)
161
+ class VariableDecl:
162
+ node: TSNode
163
+ name: str
164
+
165
+
166
+ Declaration = FunctionDecl | ClassDecl | ImportDecl | VariableDecl
167
+
168
+
169
+ class LanguageAdapter(ABC):
170
+ name: str = "base"
171
+ file_extensions: tuple[str, ...] = ()
172
+
173
+ @abstractmethod
174
+ def parse(self, source: bytes) -> TSNode:
175
+ """Parse source bytes; return the tree's root node."""
176
+
177
+ @abstractmethod
178
+ def locate_function(self, root: TSNode, name: str, start_row: int) -> TSNode | None:
179
+ """The function/method node named ``name`` starting on 0-based ``start_row``."""
180
+
181
+ @abstractmethod
182
+ def direct_effects(self, func_node: TSNode, source: bytes, aliases: dict[str, str]) -> set[str]:
183
+ """Direct effect tags (io/net/fs/db/nondeterm/raise) in the body.
184
+
185
+ ``aliases`` maps local import names to their absolute dotted target
186
+ (built language-neutrally from ``Import`` nodes), so an adapter can
187
+ resolve ``r.get`` → ``requests.get`` before matching its tables.
188
+ """
189
+
190
+ @abstractmethod
191
+ def call_sites(self, func_node: TSNode, source: bytes) -> list[CallSite]:
192
+ """Call sites in the body: dotted callee, arg identifier names, line."""
193
+
194
+ # --- phase 2: CFG extraction --------------------------------------------
195
+
196
+ @abstractmethod
197
+ def function_body(self, func_node: TSNode) -> TSNode | None:
198
+ """The function's body block node."""
199
+
200
+ @abstractmethod
201
+ def block_statements(self, block: TSNode) -> list[TSNode]:
202
+ """Statement nodes of a block, comments filtered."""
203
+
204
+ @abstractmethod
205
+ def describe_statement(self, node: TSNode, source: bytes) -> StatementDesc:
206
+ """Classify one statement and extract its parts (see descriptors)."""
207
+
208
+ # --- phase 3: ingest extraction ------------------------------------------
209
+
210
+ @abstractmethod
211
+ def module_declarations(
212
+ self, root: TSNode, source: bytes, module_name: str, rel_path: str
213
+ ) -> list[Declaration]:
214
+ """Top-level declarations of a module, fully extracted.
215
+
216
+ ``module_name`` is the dotted module path (for languages with dotted
217
+ relative imports, e.g. Python ``from ..a import x``); ``rel_path`` is
218
+ the repo-relative file path (for path-specifier imports, e.g. TS
219
+ ``from './util'``). Method params exclude implicit receivers
220
+ (``self``/``this``).
221
+ """
@@ -0,0 +1,73 @@
1
+ """Parse-once source cache.
2
+
3
+ Two layers of caching, both keyed so they are always correct:
4
+
5
+ * :func:`parse_cached` — a process-wide store keyed by ``(adapter, content
6
+ hash)``. A parse tree is a pure function of the bytes and the grammar, so
7
+ this is safe across analysis passes *and* across repeated scans (watch
8
+ mode): an unchanged file is parsed once, ever. Bounded LRU so a long-lived
9
+ watch process doesn't grow without limit.
10
+ * :class:`SourceCache` — a per-run, path-keyed view used by the analysis
11
+ passes, which walk *per function* and would otherwise re-read/re-parse a
12
+ file once per function. It now resolves parse trees through
13
+ :func:`parse_cached`, so the ingest pass and every analysis share one
14
+ parse per content.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import hashlib
20
+ from collections import OrderedDict
21
+ from pathlib import Path
22
+
23
+ from tree_sitter import Node as TSNode
24
+
25
+ from cgir.languages.base import LanguageAdapter
26
+ from cgir.languages.registry import adapter_for_extension
27
+
28
+ # (adapter name, sha256(content)) -> parsed root. Bounded LRU.
29
+ _PARSE_STORE: OrderedDict[tuple[str, bytes], TSNode] = OrderedDict()
30
+ _PARSE_STORE_MAX = 8192
31
+
32
+
33
+ def parse_cached(adapter: LanguageAdapter, source: bytes) -> TSNode:
34
+ """Parse ``source`` with ``adapter``, reusing a cached tree for identical
35
+ content. Correct across passes and scans — the tree only depends on the
36
+ grammar and the bytes."""
37
+ key = (adapter.name, hashlib.sha256(source).digest())
38
+ cached = _PARSE_STORE.get(key)
39
+ if cached is not None:
40
+ _PARSE_STORE.move_to_end(key)
41
+ return cached
42
+ root = adapter.parse(source)
43
+ _PARSE_STORE[key] = root
44
+ if len(_PARSE_STORE) > _PARSE_STORE_MAX:
45
+ _PARSE_STORE.popitem(last=False)
46
+ return root
47
+
48
+
49
+ def clear_parse_cache() -> None:
50
+ """Drop the process-wide parse store (tests / memory reclamation)."""
51
+ _PARSE_STORE.clear()
52
+
53
+
54
+ class SourceCache:
55
+ def __init__(self, repo_path: Path, adapter: LanguageAdapter | None = None) -> None:
56
+ self._repo_path = repo_path
57
+ self._forced = adapter
58
+ self._entries: dict[str, tuple[bytes, TSNode, LanguageAdapter] | None] = {}
59
+
60
+ def get(self, rel_path: str) -> tuple[bytes, TSNode, LanguageAdapter] | None:
61
+ """Return ``(source_bytes, root_node, adapter)`` for a path, or None."""
62
+ if rel_path not in self._entries:
63
+ adapter = self._forced or adapter_for_extension(Path(rel_path).suffix)
64
+ if adapter is None:
65
+ self._entries[rel_path] = None
66
+ else:
67
+ try:
68
+ source = (self._repo_path / rel_path).read_bytes()
69
+ except OSError:
70
+ self._entries[rel_path] = None
71
+ else:
72
+ self._entries[rel_path] = (source, parse_cached(adapter, source), adapter)
73
+ return self._entries[rel_path]