diffgenome 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.
- diffgenome/__init__.py +7 -0
- diffgenome/__main__.py +240 -0
- diffgenome/_collectors/go/dg/dg.go +623 -0
- diffgenome/_collectors/go/go.mod +3 -0
- diffgenome/_collectors/go/instrument/facts.go +346 -0
- diffgenome/_collectors/go/instrument/main.go +484 -0
- diffgenome/_collectors/node/instrument.js +289 -0
- diffgenome/_collectors/node/jest-setup.js +40 -0
- diffgenome/_collectors/node/package-lock.json +35 -0
- diffgenome/_collectors/node/package.json +11 -0
- diffgenome/_collectors/node/runtime.js +426 -0
- diffgenome/ambiguity.py +122 -0
- diffgenome/api.py +67 -0
- diffgenome/change.py +86 -0
- diffgenome/change_artifact.py +310 -0
- diffgenome/collect/__init__.py +2 -0
- diffgenome/collect/go_test.py +271 -0
- diffgenome/collect/node_jest.py +319 -0
- diffgenome/collect/py_monitoring.py +985 -0
- diffgenome/collect/py_runtime.py +116 -0
- diffgenome/collect/py_symbols.py +238 -0
- diffgenome/collect/pytest_plugin.py +130 -0
- diffgenome/compose.py +469 -0
- diffgenome/dependence.py +264 -0
- diffgenome/evaluate.py +669 -0
- diffgenome/frontends/__init__.py +0 -0
- diffgenome/frontends/python_ir.py +335 -0
- diffgenome/genome.py +1016 -0
- diffgenome/genome_pipeline.py +674 -0
- diffgenome/genome_prompt.py +33 -0
- diffgenome/genome_state.py +2118 -0
- diffgenome/graph.py +426 -0
- diffgenome/llm.py +189 -0
- diffgenome/model.py +364 -0
- diffgenome/mvp.py +398 -0
- diffgenome/probe.py +509 -0
- diffgenome/projection.py +308 -0
- diffgenome/py.typed +0 -0
- diffgenome/render.py +118 -0
- diffgenome/report.py +363 -0
- diffgenome/resolve.py +37 -0
- diffgenome/runtime.py +74 -0
- diffgenome/runtime_evidence.py +261 -0
- diffgenome/sandbox.py +166 -0
- diffgenome/serialize.py +96 -0
- diffgenome/sites.py +19 -0
- diffgenome/static_types.py +69 -0
- diffgenome/structure.py +462 -0
- diffgenome-0.1.0.dist-info/METADATA +139 -0
- diffgenome-0.1.0.dist-info/RECORD +53 -0
- diffgenome-0.1.0.dist-info/WHEEL +4 -0
- diffgenome-0.1.0.dist-info/entry_points.txt +2 -0
- diffgenome-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""Node/TypeScript runtime adapter: source instrumentation + Jest.
|
|
2
|
+
|
|
3
|
+
Everything Node-specific lives here and in diffgenome/_collectors/node. The workspace copy is
|
|
4
|
+
instrumented in place by `instrument.js` (our tool, our pinned TypeScript; no target code
|
|
5
|
+
runs), a workspace-only Jest config loads `jest-setup.js`, and the instrumented tests run
|
|
6
|
+
inside the sandbox with loopback allowed (test servers bind localhost; that is not egress).
|
|
7
|
+
The symbol index comes from the instrumenter's definition list, so diff lines map to the
|
|
8
|
+
same identities the collector emits.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from diffgenome.model import Execution, Stimulus, SymbolId
|
|
21
|
+
from diffgenome.probe import copy_probe_traces
|
|
22
|
+
from diffgenome.runtime import SymbolIndex
|
|
23
|
+
from diffgenome.sandbox import Workspace
|
|
24
|
+
from diffgenome.serialize import execution_from_json
|
|
25
|
+
|
|
26
|
+
TOOLS = Path(__file__).resolve().parent.parent / "_collectors" / "node"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _node_path(ws_repo: Path) -> str:
|
|
30
|
+
"""Where the instrumenter finds `typescript`: an explicit DIFFGENOME_NODE_PATH, the
|
|
31
|
+
target's own node_modules (TypeScript projects carry it), then a node_modules next to the
|
|
32
|
+
collector (a development checkout). No dependency is bundled with the Python package."""
|
|
33
|
+
parts = [
|
|
34
|
+
os.environ.get("DIFFGENOME_NODE_PATH", ""),
|
|
35
|
+
str(ws_repo / "node_modules"),
|
|
36
|
+
str(TOOLS / "node_modules"),
|
|
37
|
+
]
|
|
38
|
+
return os.pathsep.join(p for p in parts if p)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class Definition:
|
|
43
|
+
symbol: SymbolId
|
|
44
|
+
path: str
|
|
45
|
+
start: int
|
|
46
|
+
end: int
|
|
47
|
+
kind: str
|
|
48
|
+
is_async: bool = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class NodeSymbolIndex:
|
|
52
|
+
"""Definitions as the instrumenter indexed them, against the ORIGINAL sources."""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self, repo_root: Path, source_roots: list[Path], test_roots: list[Path], index_file: Path
|
|
56
|
+
) -> None:
|
|
57
|
+
self.repo_root = repo_root.resolve()
|
|
58
|
+
self.source_roots = [p.resolve() for p in source_roots]
|
|
59
|
+
self.test_roots = [p.resolve() for p in test_roots]
|
|
60
|
+
doc = json.loads(index_file.read_text())
|
|
61
|
+
self._defs: list[Definition] = [
|
|
62
|
+
Definition(
|
|
63
|
+
d["symbol"], d["path"], d["start"], d["end"], d["kind"], d.get("is_async", False)
|
|
64
|
+
)
|
|
65
|
+
for d in doc["definitions"]
|
|
66
|
+
]
|
|
67
|
+
self._by_symbol = {d.symbol: d for d in self._defs}
|
|
68
|
+
self._by_path: dict[str, list[Definition]] = {}
|
|
69
|
+
for d in self._defs:
|
|
70
|
+
self._by_path.setdefault(d.path, []).append(d)
|
|
71
|
+
|
|
72
|
+
def definitions(self, rel_path: str) -> list[Definition]:
|
|
73
|
+
return self._by_path.get(rel_path, [])
|
|
74
|
+
|
|
75
|
+
def symbols_at(self, rel_path: str, lines: set[int]) -> list[Definition]:
|
|
76
|
+
hits: dict[SymbolId, Definition] = {}
|
|
77
|
+
for line in lines:
|
|
78
|
+
covering = [d for d in self.definitions(rel_path) if d.start <= line <= d.end]
|
|
79
|
+
funcs = [d for d in covering if d.kind == "function"]
|
|
80
|
+
pick = (
|
|
81
|
+
max(funcs, key=lambda d: d.start)
|
|
82
|
+
if funcs
|
|
83
|
+
else (max(covering, key=lambda d: d.start) if covering else None)
|
|
84
|
+
)
|
|
85
|
+
if pick:
|
|
86
|
+
hits[pick.symbol] = pick
|
|
87
|
+
return sorted(hits.values(), key=lambda d: (d.path, d.start))
|
|
88
|
+
|
|
89
|
+
def find(self, symbol: SymbolId) -> Definition | None:
|
|
90
|
+
return self._by_symbol.get(symbol)
|
|
91
|
+
|
|
92
|
+
def source(self, symbol: SymbolId, context: int = 0) -> str | None:
|
|
93
|
+
d = self.find(symbol)
|
|
94
|
+
if d is None:
|
|
95
|
+
return None
|
|
96
|
+
lines = (self.repo_root / d.path).read_text(encoding="utf-8").splitlines()
|
|
97
|
+
lo, hi = max(0, d.start - 1 - context), min(len(lines), d.end + context)
|
|
98
|
+
return "\n".join(f"{i + 1:5d} {lines[i]}" for i in range(lo, hi))
|
|
99
|
+
|
|
100
|
+
def enclosing_class(self, symbol: SymbolId) -> Definition | None:
|
|
101
|
+
d = self.find(symbol)
|
|
102
|
+
if d is None:
|
|
103
|
+
return None
|
|
104
|
+
classes = [
|
|
105
|
+
c for c in self.definitions(d.path)
|
|
106
|
+
if c.kind == "class" and c.start <= d.start and c.end >= d.end and c.symbol != d.symbol
|
|
107
|
+
] # fmt: skip
|
|
108
|
+
return max(classes, key=lambda c: c.start) if classes else None
|
|
109
|
+
|
|
110
|
+
def is_test(self, symbol: SymbolId) -> bool:
|
|
111
|
+
d = self.find(symbol)
|
|
112
|
+
if d is None:
|
|
113
|
+
return False
|
|
114
|
+
path = self.repo_root / d.path
|
|
115
|
+
return any(path.is_relative_to(r) for r in self.test_roots)
|
|
116
|
+
|
|
117
|
+
def return_type_of(self, symbol: SymbolId) -> SymbolId | None:
|
|
118
|
+
return None # not extracted for this runtime yet
|
|
119
|
+
|
|
120
|
+
def module_of(self, symbol: SymbolId) -> str | None:
|
|
121
|
+
d = self.find(symbol)
|
|
122
|
+
if d is None:
|
|
123
|
+
return None
|
|
124
|
+
return d.path.rsplit(".", 1)[0] # e.g. src/api/services/PetService
|
|
125
|
+
|
|
126
|
+
def tests_importing(self, module: str, limit: int = 3) -> list[str]:
|
|
127
|
+
needle = module.split("/")[-1] # PetService
|
|
128
|
+
hits: list[str] = []
|
|
129
|
+
for root in self.test_roots:
|
|
130
|
+
for file in sorted(root.rglob("*.test.ts")) + sorted(root.rglob("*.spec.ts")):
|
|
131
|
+
try:
|
|
132
|
+
text = file.read_text(encoding="utf-8")
|
|
133
|
+
except (OSError, UnicodeDecodeError):
|
|
134
|
+
continue
|
|
135
|
+
if f"/{needle}'" in text or f'/{needle}"' in text:
|
|
136
|
+
hits.append(str(file.resolve().relative_to(self.repo_root)))
|
|
137
|
+
if len(hits) >= limit:
|
|
138
|
+
return hits
|
|
139
|
+
return hits
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class NodeJestRuntime:
|
|
143
|
+
name = "node/jest"
|
|
144
|
+
|
|
145
|
+
def __init__(
|
|
146
|
+
self, repo: Path, source_root: str, test_root: str, tests: str, node: Path | None = None
|
|
147
|
+
) -> None:
|
|
148
|
+
self.repo = repo.resolve()
|
|
149
|
+
self.source_root = source_root
|
|
150
|
+
self.test_root = test_root
|
|
151
|
+
self.tests = tests
|
|
152
|
+
which = shutil.which("node")
|
|
153
|
+
found = node or (Path(which) if which else None)
|
|
154
|
+
if found is None:
|
|
155
|
+
raise RuntimeError("node not found on PATH")
|
|
156
|
+
self.node = found.resolve()
|
|
157
|
+
self.index_file: Path | None = None
|
|
158
|
+
|
|
159
|
+
# ------------------------------------------------------------------ preparation
|
|
160
|
+
|
|
161
|
+
def prepare(self, ws: Workspace) -> SymbolIndex:
|
|
162
|
+
# Dependencies are the target's environment (like a venv): linked read-only, never copied.
|
|
163
|
+
nm = self.repo / "node_modules"
|
|
164
|
+
if nm.is_dir() and not (ws.repo / "node_modules").exists():
|
|
165
|
+
os.symlink(nm, ws.repo / "node_modules")
|
|
166
|
+
index_file = ws.root / "symbol-index.json"
|
|
167
|
+
result = subprocess.run(
|
|
168
|
+
[
|
|
169
|
+
str(self.node),
|
|
170
|
+
str(TOOLS / "instrument.js"),
|
|
171
|
+
"--root",
|
|
172
|
+
str(ws.repo),
|
|
173
|
+
"--src",
|
|
174
|
+
self.source_root,
|
|
175
|
+
"--tests",
|
|
176
|
+
self.test_root,
|
|
177
|
+
"--runtime",
|
|
178
|
+
str(TOOLS / "runtime.js"),
|
|
179
|
+
"--index",
|
|
180
|
+
str(index_file),
|
|
181
|
+
],
|
|
182
|
+
env={**os.environ, "NODE_PATH": _node_path(ws.repo)},
|
|
183
|
+
capture_output=True,
|
|
184
|
+
text=True,
|
|
185
|
+
check=True,
|
|
186
|
+
)
|
|
187
|
+
(ws.root / "instrument.log").write_text(result.stdout + result.stderr)
|
|
188
|
+
self.index_file = index_file
|
|
189
|
+
self._write_jest_config(ws)
|
|
190
|
+
# the analyzed revision's copy, not the checkout (which may be at another revision)
|
|
191
|
+
return NodeSymbolIndex(
|
|
192
|
+
ws.repo, [ws.repo / self.source_root], [ws.repo / self.test_root], index_file
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def _jest_major(self) -> int:
|
|
196
|
+
try:
|
|
197
|
+
pkg = json.loads((self.repo / "node_modules" / "jest" / "package.json").read_text())
|
|
198
|
+
return int(str(pkg["version"]).split(".")[0])
|
|
199
|
+
except (OSError, KeyError, ValueError):
|
|
200
|
+
return 29
|
|
201
|
+
|
|
202
|
+
def _write_jest_config(self, ws: Workspace) -> None:
|
|
203
|
+
setup = str(TOOLS / "jest-setup.js")
|
|
204
|
+
major = self._jest_major()
|
|
205
|
+
base_expr = (
|
|
206
|
+
'require("./package.json").jest || {}'
|
|
207
|
+
if (ws.repo / "package.json").is_file()
|
|
208
|
+
and "jest" in json.loads((ws.repo / "package.json").read_text())
|
|
209
|
+
else ('require("./jest.config.js")' if (ws.repo / "jest.config.js").is_file() else "{}")
|
|
210
|
+
)
|
|
211
|
+
if major < 24:
|
|
212
|
+
# The original setup file is chained by jest-setup.js via DIFFGENOME_CHAIN_SETUP.
|
|
213
|
+
hook = f" setupTestFrameworkScriptFile: {json.dumps(setup)},\n"
|
|
214
|
+
else:
|
|
215
|
+
hook = (
|
|
216
|
+
f" setupFilesAfterEnv: [{json.dumps(setup)}]"
|
|
217
|
+
".concat(base.setupFilesAfterEnv || []),\n"
|
|
218
|
+
)
|
|
219
|
+
# optional: a pure-JS bcrypt when the target's native binding cannot build here
|
|
220
|
+
shim = next(
|
|
221
|
+
(
|
|
222
|
+
p
|
|
223
|
+
for p in (
|
|
224
|
+
ws.repo / "node_modules" / "bcryptjs",
|
|
225
|
+
TOOLS / "node_modules" / "bcryptjs",
|
|
226
|
+
)
|
|
227
|
+
if p.is_dir()
|
|
228
|
+
),
|
|
229
|
+
TOOLS / "node_modules" / "bcryptjs",
|
|
230
|
+
)
|
|
231
|
+
mapper = ""
|
|
232
|
+
if shim.is_dir():
|
|
233
|
+
# bcrypt's native binding does not build on this Node; the pure-JS port is an
|
|
234
|
+
# external substitution and is recorded as such in the run notes.
|
|
235
|
+
mapper = (
|
|
236
|
+
" moduleNameMapper: Object.assign({}, base.moduleNameMapper || {}, "
|
|
237
|
+
f'{{ "^bcrypt$": {json.dumps(str(shim))} }}),\n'
|
|
238
|
+
)
|
|
239
|
+
(ws.repo / "diffgenome.jest.config.js").write_text(
|
|
240
|
+
f"const base = {base_expr};\n"
|
|
241
|
+
"module.exports = Object.assign({}, base, {\n"
|
|
242
|
+
" rootDir: __dirname,\n"
|
|
243
|
+
f"{hook}{mapper}"
|
|
244
|
+
"});\n"
|
|
245
|
+
)
|
|
246
|
+
self._chain_setup = None
|
|
247
|
+
if major < 24:
|
|
248
|
+
base = (
|
|
249
|
+
json.loads((ws.repo / "package.json").read_text()).get("jest", {})
|
|
250
|
+
if (ws.repo / "package.json").is_file()
|
|
251
|
+
else {}
|
|
252
|
+
)
|
|
253
|
+
original = base.get("setupTestFrameworkScriptFile")
|
|
254
|
+
if original:
|
|
255
|
+
self._chain_setup = str(ws.repo / original.replace("<rootDir>/", ""))
|
|
256
|
+
|
|
257
|
+
# ------------------------------------------------------------------ execution
|
|
258
|
+
|
|
259
|
+
def trace(
|
|
260
|
+
self, ws: Workspace, out_dir: Path, stimulus: Stimulus, only: list[str] | None
|
|
261
|
+
) -> tuple[list[Execution], str, str]:
|
|
262
|
+
tag = "existing" if stimulus is Stimulus.EXISTING_TEST else "probe"
|
|
263
|
+
traces_ws = ws.root / f"traces-{tag}-{len(list(ws.root.glob('traces-*')))}"
|
|
264
|
+
traces_ws.mkdir(parents=True, exist_ok=True)
|
|
265
|
+
env = {
|
|
266
|
+
"DIFFGENOME_OUT": str(traces_ws),
|
|
267
|
+
"DIFFGENOME_RUNTIME": str(TOOLS / "runtime.js"),
|
|
268
|
+
"DIFFGENOME_STIMULUS": stimulus.value,
|
|
269
|
+
"NODE_OPTIONS": "--max-old-space-size=2048",
|
|
270
|
+
"CI": "1",
|
|
271
|
+
}
|
|
272
|
+
if getattr(self, "_chain_setup", None):
|
|
273
|
+
env["DIFFGENOME_CHAIN_SETUP"] = self._chain_setup or ""
|
|
274
|
+
argv = [
|
|
275
|
+
str(self.node), str(ws.repo / "node_modules" / "jest" / "bin" / "jest.js"),
|
|
276
|
+
"--config", "diffgenome.jest.config.js", "--runInBand", "--forceExit",
|
|
277
|
+
*(only if only else [self.tests]),
|
|
278
|
+
] # fmt: skip
|
|
279
|
+
result = ws.run(argv, env=env, timeout=1800, allow_loopback=True)
|
|
280
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
281
|
+
copy_probe_traces(traces_ws, out_dir)
|
|
282
|
+
executions = [execution_from_json(f.read_text()) for f in sorted(out_dir.glob("*.json"))]
|
|
283
|
+
return executions, result.stdout, result.stderr
|
|
284
|
+
|
|
285
|
+
def probe_relpath(self, tag: str) -> str:
|
|
286
|
+
return f"{self.test_root}/diffgenome_probe_{tag}.test.ts"
|
|
287
|
+
|
|
288
|
+
def conventions(self, index: SymbolIndex) -> str:
|
|
289
|
+
major = self._jest_major()
|
|
290
|
+
rel = os.path.relpath(self.repo / self.source_root, self.repo / self.test_root)
|
|
291
|
+
parts = [
|
|
292
|
+
f"Runtime: Node.js {self._node_version()}, TypeScript, Jest {major} (TypeScript is "
|
|
293
|
+
f"compiled by the repository's own preprocessor, no type checking). The probe is a "
|
|
294
|
+
f"Jest test file named *.test.ts placed directly in {self.test_root}/, so imports of "
|
|
295
|
+
f"the source use '{rel}/...' (for example '{rel}/lib/logger/Logger'). Use ES imports "
|
|
296
|
+
f"at the top of the file like the existing tests do."
|
|
297
|
+
]
|
|
298
|
+
if major < 24:
|
|
299
|
+
parts.append(
|
|
300
|
+
f"Jest {major} API caveats: jest.isolateModules, setupFilesAfterEnv and "
|
|
301
|
+
"expect().resolves/rejects-with-toMatchObject on older matchers may not exist; "
|
|
302
|
+
"use jest.mock(path, factory) at top level, jest.fn(), jest.resetModules() + "
|
|
303
|
+
"require(), and async test bodies."
|
|
304
|
+
)
|
|
305
|
+
pkg = self.repo / "package.json"
|
|
306
|
+
if pkg.is_file():
|
|
307
|
+
doc = json.loads(pkg.read_text())
|
|
308
|
+
jest_cfg = doc.get("jest")
|
|
309
|
+
if jest_cfg:
|
|
310
|
+
parts.append("jest config (package.json):\n" + json.dumps(jest_cfg, indent=1)[:800])
|
|
311
|
+
return "\n\n".join(parts)
|
|
312
|
+
|
|
313
|
+
def _node_version(self) -> str:
|
|
314
|
+
try:
|
|
315
|
+
return subprocess.run(
|
|
316
|
+
[str(self.node), "--version"], capture_output=True, text=True, timeout=10
|
|
317
|
+
).stdout.strip()
|
|
318
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
319
|
+
return "?"
|