forgedock 1.26.0 → 1.28.0
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.
- package/README.md +4 -1
- package/bin/bootstrap.mjs +55 -0
- package/bin/engine/outcome-codes.mjs +13 -0
- package/bin/engine/phases.mjs +38 -2
- package/bin/engine/telemetry-seam.mjs +41 -6
- package/bin/engine/telemetry-seam.test.mjs +35 -0
- package/bin/engine-cli.mjs +616 -54
- package/bin/engine.mjs +13 -5
- package/bin/forgedock.mjs +169 -39
- package/bin/gc.mjs +580 -0
- package/bin/init-detect.mjs +68 -3
- package/bin/init-enrich.mjs +3 -0
- package/bin/init-repo.mjs +213 -0
- package/bin/journey.mjs +278 -43
- package/bin/lean/admission.mjs +33 -3
- package/bin/lean/batch.mjs +24 -5
- package/bin/lean/batch.test.mjs +54 -4
- package/bin/lean/checks.mjs +199 -0
- package/bin/lean/checks.test.mjs +183 -0
- package/bin/lean/conformance.mjs +235 -37
- package/bin/lean/conformance.test.mjs +143 -5
- package/bin/lean/credentials.mjs +29 -0
- package/bin/lean/depcache.mjs +312 -0
- package/bin/lean/depcache.test.mjs +298 -0
- package/bin/lean/drift.mjs +198 -0
- package/bin/lean/drift.test.mjs +223 -0
- package/bin/lean/effects.mjs +178 -16
- package/bin/lean/execution.e2e.test.mjs +81 -0
- package/bin/lean/execution.mjs +431 -0
- package/bin/lean/execution.test.mjs +583 -0
- package/bin/lean/heavy.mjs +289 -0
- package/bin/lean/heavy.test.mjs +207 -0
- package/bin/lean/lean.mjs +877 -151
- package/bin/lean/lean.test.mjs +1798 -28
- package/bin/lean/ledger.mjs +92 -0
- package/bin/lean/ledger.test.mjs +100 -0
- package/bin/lean/limits.mjs +58 -0
- package/bin/lean/limits.test.mjs +150 -0
- package/bin/lean/orchestrate.mjs +321 -19
- package/bin/lean/prompts.mjs +125 -17
- package/bin/lean/refactor.mjs +276 -0
- package/bin/lean/refactor.test.mjs +359 -0
- package/bin/lean/render.mjs +25 -6
- package/bin/lean/review-trigger.mjs +134 -0
- package/bin/lean/review-trigger.test.mjs +104 -0
- package/bin/lean/sandbox-provider.contract.test.mjs +257 -0
- package/bin/lean/sandbox-remote.mjs +142 -0
- package/bin/lean/sched.mjs +221 -0
- package/bin/lean/sched.test.mjs +310 -0
- package/bin/lean/schedd.mjs +247 -0
- package/bin/lean/schedd.test.mjs +180 -0
- package/bin/lean/toolchain.mjs +228 -0
- package/bin/lean/toolchain.test.mjs +154 -0
- package/bin/lean/verify.mjs +124 -0
- package/bin/lean/verify.test.mjs +118 -0
- package/bin/lean/worktree.mjs +69 -0
- package/bin/lean/worktree.test.mjs +141 -0
- package/bin/runner.mjs +53 -10
- package/bin/stats/github-source.mjs +1 -1
- package/bin/stats/index.mjs +14 -2
- package/bin/stats/query.mjs +72 -0
- package/bin/stats/stats.test.mjs +54 -0
- package/commands/adopt.md +3 -0
- package/commands/audit.md +5 -8
- package/commands/autopilot.md +2 -0
- package/commands/bootstrap.md +35 -0
- package/commands/cleanup.md +7 -22
- package/commands/forge-stats.md +2 -1
- package/commands/forgedock-init.md +125 -6
- package/commands/issue.md +89 -830
- package/commands/milestone.md +55 -6
- package/commands/orchestrate.md +10 -2
- package/commands/review-pr.md +2 -2
- package/commands/upgrade-deps.md +3 -3
- package/commands/work-on/build/validate.md +4 -0
- package/commands/work-on/close.md +2 -0
- package/commands/work-on/decompose.md +24 -6
- package/docs/CI.md +3 -3
- package/docs/CONFIG.md +227 -3
- package/docs/OUTCOME-CODES.md +11 -0
- package/docs/TELEMETRY.md +1 -1
- package/docs/site/command-reference.md +22 -0
- package/docs/site/getting-started.md +2 -0
- package/docs/site/start-a-new-project.md +327 -0
- package/package.json +1 -1
- package/scripts/affected-files-check.sh +91 -0
- package/scripts/affected-files-check.test.sh +80 -0
- package/scripts/bootstrap-create.mjs +393 -0
- package/scripts/bootstrap-validate.mjs +321 -0
- package/scripts/calibration.mjs +116 -0
- package/scripts/check-devdocs-freshness.sh +125 -0
- package/scripts/check-devdocs-freshness.test.sh +130 -0
- package/scripts/check-spec-markers.sh +3 -0
- package/scripts/check-spec-size.sh +29 -11
- package/scripts/check-spec-size.test.sh +27 -4
- package/scripts/ci-local/lib.mjs +8 -1
- package/scripts/ci-local/parallelism.test.mjs +27 -0
- package/scripts/ci-local/run.mjs +16 -5
- package/scripts/ci-local/scoping.mjs +82 -0
- package/scripts/ci-local.test.sh +14 -2
- package/scripts/fixtures/symbol-index/other/sample.rb +3 -0
- package/scripts/fixtures/symbol-index/py/pkg/__init__.py +1 -0
- package/scripts/fixtures/symbol-index/py/pkg/a.py +5 -0
- package/scripts/fixtures/symbol-index/py/pkg/b.py +14 -0
- package/scripts/fixtures/symbol-index/py/pkg/c.py +1 -0
- package/scripts/fixtures/symbol-index/py/sample.py +41 -0
- package/scripts/fixtures/symbol-index/ts/sample.ts +13 -0
- package/scripts/fixtures/symbol-move/gen_big.py +201 -0
- package/scripts/fixtures/symbol-move-ts/gen_big.mjs +56 -0
- package/scripts/forge-root.test.sh +1 -1
- package/scripts/hotspots.sh +100 -0
- package/scripts/hotspots.test.sh +101 -0
- package/scripts/issue-create.sh +197 -0
- package/scripts/issue-create.test.sh +166 -0
- package/scripts/maintenance-scan.sh +185 -0
- package/scripts/maintenance-scan.test.sh +89 -0
- package/scripts/milestone-drift.sh +206 -0
- package/scripts/milestone-drift.test.sh +120 -0
- package/scripts/northstar-docs.test.sh +68 -0
- package/scripts/refactor-verify.sh +368 -0
- package/scripts/refactor-verify.test.sh +202 -0
- package/scripts/review-prepare.sh +11 -9
- package/scripts/review-prepare.test.sh +14 -0
- package/scripts/structure-delta.sh +155 -0
- package/scripts/structure-delta.test.sh +136 -0
- package/scripts/symbol-index.mjs +253 -0
- package/scripts/symbol-index.py +354 -0
- package/scripts/symbol-index.test.sh +119 -0
- package/scripts/symbol-move-ts.test.sh +161 -0
- package/scripts/symbol-move.mjs +379 -0
- package/scripts/symbol-move.py +383 -0
- package/scripts/symbol-move.test.sh +168 -0
- package/templates/bootstrap/interview.md +34 -0
- package/templates/bootstrap/issue-body.md +42 -0
- package/templates/bootstrap/needs-human-issue.md +45 -0
- package/templates/bootstrap/stack-profiles.md +34 -0
- package/templates/bootstrap/technical-spike.md +75 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# symbol-index.py: deterministic Python symbol index + module import-cycle detector for refactor:split (forge#4208).
|
|
6
|
+
# stdlib only (ast). No LLM, no network. Never emits source text, docstrings or literal values.
|
|
7
|
+
#
|
|
8
|
+
# Usage: python3 scripts/symbol-index.py <file.py | dir>...
|
|
9
|
+
# Output (stdout, one JSON object):
|
|
10
|
+
# {"symbols":[{name,kind,start,end,lines,public[,file]}], "edges":[{from,to}],
|
|
11
|
+
# "imports":[{module,names,line[,file]}], "cycles":[[module,...],...]}
|
|
12
|
+
# kind: function|async_function|class|method|const. Methods (and nested classes) are named Class.method.
|
|
13
|
+
# start includes decorators; lines = end - start + 1. public = no leading underscore (method dunders count as
|
|
14
|
+
# public), and listed in a static __all__ when the module defines one.
|
|
15
|
+
# One .py file: names are bare. Otherwise (a directory or several files) each entry carries `file` and edge ends
|
|
16
|
+
# are "<module>:<symbol>"; module names are dotted paths relative to each root (a root that has __init__.py is
|
|
17
|
+
# itself the top package, so root .../pkg yields pkg.a). edges = references from one indexed symbol to another.
|
|
18
|
+
# cycles = strongly connected components (size > 1, or a self-import) of the module-level import graph between
|
|
19
|
+
# indexed modules; function-local and `if TYPE_CHECKING:` imports are ignored. Everything is sorted, so a
|
|
20
|
+
# before/after pair of runs can be diffed for new cycles.
|
|
21
|
+
# Unsupported input (non-.py file, no .py files, a file that does not parse) prints only
|
|
22
|
+
# {"unsupported":"<lang>","reason":"..."} and exits 0: never partial output.
|
|
23
|
+
# Exit: 0 ok or unsupported | 2 usage or missing path.
|
|
24
|
+
|
|
25
|
+
import ast
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import sys
|
|
29
|
+
|
|
30
|
+
LANGS = {
|
|
31
|
+
".ts": "ts", ".tsx": "ts", ".mts": "ts", ".cts": "ts",
|
|
32
|
+
".js": "js", ".jsx": "js", ".mjs": "js", ".cjs": "js",
|
|
33
|
+
".go": "go", ".rs": "rust", ".rb": "ruby", ".java": "java", ".kt": "kotlin",
|
|
34
|
+
".c": "c", ".h": "c", ".cc": "cpp", ".cpp": "cpp", ".hpp": "cpp", ".cs": "csharp",
|
|
35
|
+
".php": "php", ".swift": "swift", ".sh": "shell", ".bash": "shell",
|
|
36
|
+
}
|
|
37
|
+
SKIP_DIRS = {"__pycache__", "node_modules", "site-packages", "venv", "env"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Unsupported(Exception):
|
|
41
|
+
def __init__(self, lang, reason):
|
|
42
|
+
super().__init__(reason)
|
|
43
|
+
self.lang, self.reason = lang, reason
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def lang_of(path):
|
|
47
|
+
ext = os.path.splitext(path)[1].lower()
|
|
48
|
+
return LANGS.get(ext, ext[1:] if ext else "unknown")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_private(name):
|
|
52
|
+
return name.startswith("_")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def is_dunder(name):
|
|
56
|
+
return len(name) > 4 and name.startswith("__") and name.endswith("__")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def static_all(tree):
|
|
60
|
+
"""Names in a statically readable module-level __all__, or None when absent or not static."""
|
|
61
|
+
names, seen = [], False
|
|
62
|
+
for node in tree.body:
|
|
63
|
+
if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id == "__all__" for t in node.targets):
|
|
64
|
+
value, seen, names = node.value, True, []
|
|
65
|
+
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and node.target.id == "__all__":
|
|
66
|
+
value, seen, names = node.value, True, []
|
|
67
|
+
elif isinstance(node, ast.AugAssign) and isinstance(node.target, ast.Name) and node.target.id == "__all__":
|
|
68
|
+
value = node.value
|
|
69
|
+
else:
|
|
70
|
+
continue
|
|
71
|
+
if not isinstance(value, (ast.List, ast.Tuple)):
|
|
72
|
+
return None
|
|
73
|
+
for elt in value.elts:
|
|
74
|
+
if not (isinstance(elt, ast.Constant) and isinstance(elt.value, str)):
|
|
75
|
+
return None
|
|
76
|
+
names.append(elt.value)
|
|
77
|
+
return set(names) if seen else None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def def_start(node):
|
|
81
|
+
decos = getattr(node, "decorator_list", None) or []
|
|
82
|
+
return min([node.lineno] + [d.lineno for d in decos])
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def is_type_checking(test):
|
|
86
|
+
return (isinstance(test, ast.Name) and test.id == "TYPE_CHECKING") or (
|
|
87
|
+
isinstance(test, ast.Attribute) and test.attr == "TYPE_CHECKING")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def module_imports(body):
|
|
91
|
+
"""Import statements at module level (descending into if/try/with, never into def/class or TYPE_CHECKING)."""
|
|
92
|
+
out = []
|
|
93
|
+
for node in body:
|
|
94
|
+
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
95
|
+
out.append(node)
|
|
96
|
+
elif isinstance(node, ast.If):
|
|
97
|
+
if not is_type_checking(node.test):
|
|
98
|
+
out.extend(module_imports(node.body))
|
|
99
|
+
out.extend(module_imports(node.orelse))
|
|
100
|
+
elif isinstance(node, (ast.Try, getattr(ast, "TryStar", ast.Try))):
|
|
101
|
+
out.extend(module_imports(node.body))
|
|
102
|
+
for h in node.handlers:
|
|
103
|
+
out.extend(module_imports(h.body))
|
|
104
|
+
out.extend(module_imports(node.orelse))
|
|
105
|
+
out.extend(module_imports(node.finalbody))
|
|
106
|
+
elif isinstance(node, (ast.With, ast.AsyncWith)):
|
|
107
|
+
out.extend(module_imports(node.body))
|
|
108
|
+
return out
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class Module:
|
|
112
|
+
def __init__(self, path, name, is_pkg):
|
|
113
|
+
self.path, self.name, self.is_pkg = path, name, is_pkg
|
|
114
|
+
self.symbols = [] # dicts without file
|
|
115
|
+
self.nodes = {} # symbol name -> (ast node, owning class name or None)
|
|
116
|
+
self.imports = [] # (ast import node)
|
|
117
|
+
self.tree = None
|
|
118
|
+
|
|
119
|
+
def parse(self):
|
|
120
|
+
try:
|
|
121
|
+
with open(self.path, "rb") as fh:
|
|
122
|
+
self.tree = ast.parse(fh.read(), filename=self.path)
|
|
123
|
+
except SyntaxError as e:
|
|
124
|
+
raise Unsupported("python", "parse-error: %s:%s" % (self.path, e.lineno or 0))
|
|
125
|
+
except (ValueError, UnicodeDecodeError, OSError):
|
|
126
|
+
raise Unsupported("python", "parse-error: %s:0" % self.path)
|
|
127
|
+
exported = static_all(self.tree)
|
|
128
|
+
|
|
129
|
+
def top_public(name):
|
|
130
|
+
return not is_private(name) and (exported is None or name in exported)
|
|
131
|
+
|
|
132
|
+
def add(name, kind, node, public, owner=None):
|
|
133
|
+
start, end = def_start(node), node.end_lineno or node.lineno
|
|
134
|
+
self.symbols.append({"name": name, "kind": kind, "start": start, "end": end,
|
|
135
|
+
"lines": end - start + 1, "public": public})
|
|
136
|
+
self.nodes[name] = (node, owner)
|
|
137
|
+
|
|
138
|
+
def add_class(node, prefix, public):
|
|
139
|
+
name = prefix + node.name
|
|
140
|
+
add(name, "class", node, public)
|
|
141
|
+
for item in node.body:
|
|
142
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
143
|
+
mpub = public and (not is_private(item.name) or is_dunder(item.name))
|
|
144
|
+
add(name + "." + item.name, "method", item, mpub, owner=name)
|
|
145
|
+
elif isinstance(item, ast.ClassDef):
|
|
146
|
+
add_class(item, name + ".", public and not is_private(item.name))
|
|
147
|
+
|
|
148
|
+
for node in self.tree.body:
|
|
149
|
+
if isinstance(node, ast.FunctionDef):
|
|
150
|
+
add(node.name, "function", node, top_public(node.name))
|
|
151
|
+
elif isinstance(node, ast.AsyncFunctionDef):
|
|
152
|
+
add(node.name, "async_function", node, top_public(node.name))
|
|
153
|
+
elif isinstance(node, ast.ClassDef):
|
|
154
|
+
add_class(node, "", top_public(node.name))
|
|
155
|
+
elif isinstance(node, (ast.Assign, ast.AnnAssign)):
|
|
156
|
+
targets = node.targets if isinstance(node, ast.Assign) else [node.target]
|
|
157
|
+
for t in targets:
|
|
158
|
+
for n in ([t] if isinstance(t, ast.Name) else getattr(t, "elts", [])):
|
|
159
|
+
if isinstance(n, ast.Name) and n.id not in self.nodes:
|
|
160
|
+
add(n.id, "const", node, top_public(n.id))
|
|
161
|
+
self.imports = module_imports(self.tree.body)
|
|
162
|
+
|
|
163
|
+
def package(self):
|
|
164
|
+
return self.name if self.is_pkg else self.name.rpartition(".")[0]
|
|
165
|
+
|
|
166
|
+
def resolve(self, node):
|
|
167
|
+
"""Absolute module named by an ImportFrom (relative levels resolved), or None when it climbs past the root."""
|
|
168
|
+
if node.level == 0:
|
|
169
|
+
return node.module or ""
|
|
170
|
+
parts = self.package().split(".") if self.package() else []
|
|
171
|
+
if node.level - 1 > len(parts):
|
|
172
|
+
return None
|
|
173
|
+
base = parts[:len(parts) - (node.level - 1)]
|
|
174
|
+
if node.module:
|
|
175
|
+
base = base + node.module.split(".")
|
|
176
|
+
return ".".join(base)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def walk_roots(paths):
|
|
180
|
+
"""[(path, module name, is_pkg)] for every .py file under the given roots, sorted."""
|
|
181
|
+
found = []
|
|
182
|
+
for root in paths:
|
|
183
|
+
root = os.path.normpath(root)
|
|
184
|
+
if os.path.isfile(root):
|
|
185
|
+
if not root.endswith(".py"):
|
|
186
|
+
raise Unsupported(lang_of(root), "unsupported-language")
|
|
187
|
+
stem = os.path.splitext(os.path.basename(root))[0]
|
|
188
|
+
found.append((root, stem, False))
|
|
189
|
+
continue
|
|
190
|
+
prefix = os.path.basename(os.path.abspath(root)) if os.path.isfile(os.path.join(root, "__init__.py")) else ""
|
|
191
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
192
|
+
dirnames[:] = sorted(d for d in dirnames if not d.startswith(".") and d not in SKIP_DIRS)
|
|
193
|
+
for fn in sorted(filenames):
|
|
194
|
+
if not fn.endswith(".py"):
|
|
195
|
+
continue
|
|
196
|
+
path = os.path.join(dirpath, fn)
|
|
197
|
+
rel = os.path.relpath(path, root)[:-3].split(os.sep)
|
|
198
|
+
is_pkg = rel[-1] == "__init__"
|
|
199
|
+
if is_pkg:
|
|
200
|
+
rel = rel[:-1]
|
|
201
|
+
name = ".".join(([prefix] if prefix else []) + rel)
|
|
202
|
+
found.append((path, name, is_pkg))
|
|
203
|
+
if not found:
|
|
204
|
+
raise Unsupported("unknown", "no-python-files")
|
|
205
|
+
return sorted(found)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def tarjan(graph):
|
|
209
|
+
"""Strongly connected components, iterative (no recursion limit on large packages)."""
|
|
210
|
+
index, low, on, stack, comps, counter = {}, {}, set(), [], [], [0]
|
|
211
|
+
for start in sorted(graph):
|
|
212
|
+
if start in index:
|
|
213
|
+
continue
|
|
214
|
+
work = [(start, iter(sorted(graph[start])))]
|
|
215
|
+
index[start] = low[start] = counter[0]; counter[0] += 1
|
|
216
|
+
stack.append(start); on.add(start)
|
|
217
|
+
while work:
|
|
218
|
+
v, it = work[-1]
|
|
219
|
+
advanced = False
|
|
220
|
+
for w in it:
|
|
221
|
+
if w not in index:
|
|
222
|
+
index[w] = low[w] = counter[0]; counter[0] += 1
|
|
223
|
+
stack.append(w); on.add(w)
|
|
224
|
+
work.append((w, iter(sorted(graph.get(w, ())))))
|
|
225
|
+
advanced = True
|
|
226
|
+
break
|
|
227
|
+
if w in on:
|
|
228
|
+
low[v] = min(low[v], index[w])
|
|
229
|
+
if advanced:
|
|
230
|
+
continue
|
|
231
|
+
work.pop()
|
|
232
|
+
if work:
|
|
233
|
+
low[work[-1][0]] = min(low[work[-1][0]], low[v])
|
|
234
|
+
if low[v] == index[v]:
|
|
235
|
+
comp = []
|
|
236
|
+
while True:
|
|
237
|
+
w = stack.pop(); on.discard(w); comp.append(w)
|
|
238
|
+
if w == v:
|
|
239
|
+
break
|
|
240
|
+
comps.append(sorted(comp))
|
|
241
|
+
return comps
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def build(paths):
|
|
245
|
+
entries = walk_roots(paths)
|
|
246
|
+
multi = not (len(entries) == 1 and os.path.isfile(paths[0]) and len(paths) == 1)
|
|
247
|
+
modules, by_name = [], {}
|
|
248
|
+
for path, name, is_pkg in entries:
|
|
249
|
+
m = Module(path, name, is_pkg)
|
|
250
|
+
m.parse()
|
|
251
|
+
modules.append(m)
|
|
252
|
+
by_name.setdefault(name, m)
|
|
253
|
+
|
|
254
|
+
def ref(m, sym):
|
|
255
|
+
return "%s:%s" % (m.name, sym) if multi else sym
|
|
256
|
+
|
|
257
|
+
symbols, imports, edges, graph = [], [], set(), {m.name: set() for m in modules}
|
|
258
|
+
for m in modules:
|
|
259
|
+
rel = os.path.relpath(m.path)
|
|
260
|
+
for s in m.symbols:
|
|
261
|
+
symbols.append(dict(s, file=rel) if multi else s)
|
|
262
|
+
# bindings: local name -> (module, symbol or None for a module object)
|
|
263
|
+
bindings, first_import = {}, len(imports)
|
|
264
|
+
for node in m.imports:
|
|
265
|
+
if isinstance(node, ast.Import):
|
|
266
|
+
for a in node.names:
|
|
267
|
+
imports.append({"module": a.name, "names": [], "line": node.lineno})
|
|
268
|
+
if a.name in by_name:
|
|
269
|
+
graph[m.name].add(a.name)
|
|
270
|
+
if a.asname:
|
|
271
|
+
bindings[a.asname] = (a.name, None)
|
|
272
|
+
else:
|
|
273
|
+
bindings[a.name.split(".")[0]] = (a.name.split(".")[0], None)
|
|
274
|
+
else:
|
|
275
|
+
target = m.resolve(node)
|
|
276
|
+
shown = target if target is not None else "." * node.level + (node.module or "")
|
|
277
|
+
imports.append({"module": shown, "names": [a.name for a in node.names], "line": node.lineno})
|
|
278
|
+
if target is None:
|
|
279
|
+
continue
|
|
280
|
+
for a in node.names:
|
|
281
|
+
sub = (target + "." + a.name) if target else a.name
|
|
282
|
+
if sub in by_name:
|
|
283
|
+
graph[m.name].add(sub)
|
|
284
|
+
bindings[a.asname or a.name] = (sub, None)
|
|
285
|
+
elif target in by_name:
|
|
286
|
+
graph[m.name].add(target)
|
|
287
|
+
bindings[a.asname or a.name] = (target, a.name)
|
|
288
|
+
local = set(m.nodes)
|
|
289
|
+
for name, (node, owner) in m.nodes.items():
|
|
290
|
+
src = ref(m, name)
|
|
291
|
+
first_arg = None
|
|
292
|
+
if owner and isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.args.args:
|
|
293
|
+
first_arg = node.args.args[0].arg
|
|
294
|
+
body = [node]
|
|
295
|
+
if isinstance(node, ast.ClassDef): # methods and nested classes are their own symbols
|
|
296
|
+
body = node.decorator_list + node.bases + [k.value for k in node.keywords] + [
|
|
297
|
+
b for b in node.body if not isinstance(b, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))]
|
|
298
|
+
elif isinstance(node, (ast.Assign, ast.AnnAssign)):
|
|
299
|
+
body = [node.value] if node.value is not None else []
|
|
300
|
+
for root in body:
|
|
301
|
+
for n in ast.walk(root):
|
|
302
|
+
to = None
|
|
303
|
+
if isinstance(n, ast.Name) and isinstance(n.ctx, ast.Load):
|
|
304
|
+
if n.id in local:
|
|
305
|
+
to = ref(m, n.id)
|
|
306
|
+
elif n.id in bindings:
|
|
307
|
+
mod, sym = bindings[n.id]
|
|
308
|
+
if sym is not None and sym in by_name[mod].nodes:
|
|
309
|
+
to = "%s:%s" % (mod, sym)
|
|
310
|
+
elif isinstance(n, ast.Attribute) and isinstance(n.value, ast.Name):
|
|
311
|
+
base = n.value.id
|
|
312
|
+
if owner and base == first_arg and (owner + "." + n.attr) in local:
|
|
313
|
+
to = ref(m, owner + "." + n.attr)
|
|
314
|
+
elif base in local and (base + "." + n.attr) in local:
|
|
315
|
+
to = ref(m, base + "." + n.attr)
|
|
316
|
+
elif base in bindings and bindings[base][1] is None:
|
|
317
|
+
mod = bindings[base][0]
|
|
318
|
+
if mod in by_name and n.attr in by_name[mod].nodes:
|
|
319
|
+
to = "%s:%s" % (mod, n.attr)
|
|
320
|
+
if to is not None and to != src:
|
|
321
|
+
edges.add((src, to))
|
|
322
|
+
if multi:
|
|
323
|
+
for imp in imports[first_import:]:
|
|
324
|
+
imp["file"] = rel
|
|
325
|
+
|
|
326
|
+
cycles = sorted(c for c in tarjan(graph) if len(c) > 1 or c[0] in graph[c[0]])
|
|
327
|
+
if multi:
|
|
328
|
+
symbols.sort(key=lambda s: (s["file"], s["start"], s["name"]))
|
|
329
|
+
imports.sort(key=lambda i: (i["file"], i["line"], i["module"]))
|
|
330
|
+
else:
|
|
331
|
+
symbols.sort(key=lambda s: (s["start"], s["name"]))
|
|
332
|
+
imports.sort(key=lambda i: (i["line"], i["module"]))
|
|
333
|
+
return {"symbols": symbols, "edges": [{"from": f, "to": t} for f, t in sorted(edges)],
|
|
334
|
+
"imports": imports, "cycles": cycles}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def main(argv):
|
|
338
|
+
if not argv or argv[0] in ("-h", "--help"):
|
|
339
|
+
sys.stderr.write("usage: symbol-index.py <file.py | dir>...\n")
|
|
340
|
+
return 2
|
|
341
|
+
for p in argv:
|
|
342
|
+
if not os.path.exists(p):
|
|
343
|
+
sys.stderr.write("symbol-index: no such path: %s\n" % p)
|
|
344
|
+
return 2
|
|
345
|
+
try:
|
|
346
|
+
out = build(argv)
|
|
347
|
+
except Unsupported as u:
|
|
348
|
+
out = {"unsupported": u.lang, "reason": u.reason}
|
|
349
|
+
sys.stdout.write(json.dumps(out, sort_keys=True, separators=(",", ":")) + "\n")
|
|
350
|
+
return 0
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
if __name__ == "__main__":
|
|
354
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# symbol-index.test.sh — cases for scripts/symbol-index.{py,mjs} (forge#4208).
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
# Optional: SYMBOL_INDEX_TS_NODE_MODULES=<node_modules holding typescript@5> also runs the TS-parser cases.
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
PY="$HERE/symbol-index.py"; MJS="$HERE/symbol-index.mjs"; FX="$HERE/fixtures/symbol-index"
|
|
9
|
+
PASS=0; FAILN=0
|
|
10
|
+
TMP="$(mktemp -d "${TMPDIR:-/tmp}/symbol-index-test.XXXXXX")"; trap 'rm -rf "$TMP"' EXIT
|
|
11
|
+
ok() { PASS=$((PASS+1)); }
|
|
12
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
13
|
+
eq() { if [ "$2" = "$3" ]; then ok; else bad "$1 (got: $2 want: $3)"; fi }
|
|
14
|
+
q() { printf '%s' "$OUT" | jq -c "$1"; }
|
|
15
|
+
run() { OUT="$("$@" 2>"$TMP/err")"; RC=$?; }
|
|
16
|
+
|
|
17
|
+
# ---- 1. Python file: schema, symbols, spans, public flags ----
|
|
18
|
+
run python3 "$PY" "$FX/py/sample.py"
|
|
19
|
+
eq "py file rc" "$RC" 0
|
|
20
|
+
eq "py top-level keys" "$(q 'keys')" '["cycles","edges","imports","symbols"]'
|
|
21
|
+
eq "symbol keys" "$(q '[.symbols[]|keys]|unique')" '[["end","kind","lines","name","public","start"]]'
|
|
22
|
+
eq "symbol names" "$(q '[.symbols[].name]')" \
|
|
23
|
+
'["__all__","LIMIT","_cache","_normalise","greet","fetch","helper","Greeter","Greeter.__init__","Greeter.text","Greeter._shout","Greeter.shout"]'
|
|
24
|
+
eq "kinds" "$(q '[.symbols[]|select(.name=="LIMIT" or .name=="greet" or .name=="fetch" or .name=="Greeter" or .name=="Greeter.shout")|.kind]')" \
|
|
25
|
+
'["const","function","async_function","class","method"]'
|
|
26
|
+
eq "greet span" "$(q '.symbols[]|select(.name=="greet")|[.start,.end,.lines]')" '[15,16,2]'
|
|
27
|
+
eq "class span" "$(q '.symbols[]|select(.name=="Greeter")|[.start,.end,.lines]')" '[27,41,15]'
|
|
28
|
+
eq "decorator in span" "$(q '.symbols[]|select(.name=="Greeter.text")|[.start,.end,.lines]')" '[33,35,3]'
|
|
29
|
+
eq "public set" "$(q '[.symbols[]|select(.public)|.name]')" \
|
|
30
|
+
'["LIMIT","greet","fetch","Greeter","Greeter.__init__","Greeter.text","Greeter.shout"]'
|
|
31
|
+
eq "helper not in __all__" "$(q '.symbols[]|select(.name=="helper")|.public')" 'false'
|
|
32
|
+
|
|
33
|
+
# ---- 2. edges and imports ----
|
|
34
|
+
eq "edges" "$(q '[.edges[]|"\(.from)>\(.to)"]')" \
|
|
35
|
+
'["Greeter._shout>Greeter.text","Greeter.shout>Greeter._shout","Greeter.text>greet","fetch>LIMIT","fetch>_cache","greet>_normalise"]'
|
|
36
|
+
eq "imports" "$(q '.imports')" \
|
|
37
|
+
'[{"line":2,"module":"os","names":[]},{"line":3,"module":"collections","names":["OrderedDict"]}]'
|
|
38
|
+
eq "single file has no cycles" "$(q '.cycles')" '[]'
|
|
39
|
+
|
|
40
|
+
# ---- 3. never includes file bodies ----
|
|
41
|
+
for needle in SECRET_DOCSTRING_MARKER SECRET_CLASS_DOC_MARKER 'hi ' 'strip()' 'getcwd' 'return'; do
|
|
42
|
+
case "$OUT" in *"$needle"*) bad "output leaks source text: $needle" ;; *) ok ;; esac
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
# ---- 4. import cycles across modules ----
|
|
46
|
+
run python3 "$PY" "$FX/py/pkg"
|
|
47
|
+
eq "pkg rc" "$RC" 0
|
|
48
|
+
eq "pkg cycles (TYPE_CHECKING + function-local imports ignored)" "$(q '.cycles')" '[["pkg.a","pkg.b"]]'
|
|
49
|
+
eq "pkg relative import resolved" "$(q '[.imports[]|select(.file|endswith("b.py"))|.module]')" '["typing","pkg","pkg.a"]'
|
|
50
|
+
eq "pkg cross-module edges" "$(q '[.edges[]|"\(.from)>\(.to)"]')" '["pkg.a:run>pkg.b:step","pkg.b:step>pkg.c:value"]'
|
|
51
|
+
eq "pkg symbols carry file" "$(q '[.symbols[]|has("file")]|unique')" '[true]'
|
|
52
|
+
BEFORE="$(q '.cycles')"
|
|
53
|
+
cp -R "$FX/py/pkg" "$TMP/pkg"
|
|
54
|
+
printf 'from . import a\n' > "$TMP/pkg/d.py"
|
|
55
|
+
printf 'from . import d\n' >> "$TMP/pkg/c.py"
|
|
56
|
+
run python3 "$PY" "$TMP/pkg"
|
|
57
|
+
AFTER="$(q '.cycles')"
|
|
58
|
+
eq "after: cycle set grows" "$AFTER" '[["pkg.a","pkg.b","pkg.c","pkg.d"]]'
|
|
59
|
+
eq "before/after diff finds the new cycle" \
|
|
60
|
+
"$(jq -nc --argjson b "$BEFORE" --argjson a "$AFTER" '$a - $b')" '[["pkg.a","pkg.b","pkg.c","pkg.d"]]'
|
|
61
|
+
printf 'import pkg.e\n' > "$TMP/pkg/e.py"
|
|
62
|
+
run python3 "$PY" "$TMP/pkg"
|
|
63
|
+
eq "self-import is a cycle" "$(q '.cycles|map(select(.==["pkg.e"]))|length')" '1'
|
|
64
|
+
|
|
65
|
+
# ---- 5. deterministic ----
|
|
66
|
+
A1="$(python3 "$PY" "$FX/py/pkg")"; A2="$(python3 "$PY" "$FX/py/pkg")"
|
|
67
|
+
eq "byte-identical reruns" "$([ "$A1" = "$A2" ] && echo same)" same
|
|
68
|
+
|
|
69
|
+
# ---- 6. unsupported: exit 0, only {unsupported, reason} ----
|
|
70
|
+
for t in "$FX/other/sample.rb" "$FX/ts/sample.ts"; do
|
|
71
|
+
run python3 "$PY" "$t"
|
|
72
|
+
eq "py unsupported rc $(basename "$t")" "$RC" 0
|
|
73
|
+
eq "py unsupported keys $(basename "$t")" "$(q 'keys')" '["reason","unsupported"]'
|
|
74
|
+
done
|
|
75
|
+
eq "py unsupported lang" "$(python3 "$PY" "$FX/other/sample.rb" | jq -r .unsupported)" ruby
|
|
76
|
+
run python3 "$PY" "$FX/py/sample.py" "$FX/other/sample.rb"
|
|
77
|
+
eq "mixed input: no partial output" "$(q 'keys')" '["reason","unsupported"]'
|
|
78
|
+
mkdir -p "$TMP/empty"; run python3 "$PY" "$TMP/empty"
|
|
79
|
+
eq "dir without .py" "$(q '.')" '{"reason":"no-python-files","unsupported":"unknown"}'
|
|
80
|
+
mkdir -p "$TMP/broken"; printf 'def ok():\n return 1\n' > "$TMP/broken/good.py"; printf 'def broken(:\n' > "$TMP/broken/bad.py"
|
|
81
|
+
run python3 "$PY" "$TMP/broken"
|
|
82
|
+
eq "parse error rc" "$RC" 0
|
|
83
|
+
eq "parse error: no partial symbols" "$(q 'keys')" '["reason","unsupported"]'
|
|
84
|
+
eq "parse error reason" "$(q '.reason|startswith("parse-error: ")')" 'true'
|
|
85
|
+
|
|
86
|
+
run node "$MJS" "$FX/other/sample.rb"
|
|
87
|
+
eq "mjs ruby rc" "$RC" 0
|
|
88
|
+
eq "mjs ruby" "$(q '.')" '{"reason":"unknown-language","unsupported":"ruby"}'
|
|
89
|
+
mkdir -p "$TMP/ts"; cp "$FX/ts/sample.ts" "$TMP/ts/"
|
|
90
|
+
run node "$MJS" "$TMP/ts/sample.ts"
|
|
91
|
+
eq "mjs ts without parser rc" "$RC" 0
|
|
92
|
+
eq "mjs ts without parser" "$(q '.')" '{"reason":"no-parser","unsupported":"ts"}'
|
|
93
|
+
run node "$MJS" "$FX/py/sample.py"
|
|
94
|
+
eq "mjs delegates .py" "$(q '.symbols|length')" '12'
|
|
95
|
+
|
|
96
|
+
# ---- 7. usage ----
|
|
97
|
+
python3 "$PY" >/dev/null 2>&1; eq "py no args" "$?" 2
|
|
98
|
+
python3 "$PY" "$TMP/missing.py" >/dev/null 2>&1; eq "py missing path" "$?" 2
|
|
99
|
+
node "$MJS" >/dev/null 2>&1; eq "mjs no args" "$?" 2
|
|
100
|
+
|
|
101
|
+
# ---- 8. optional: TS parser path ----
|
|
102
|
+
if [ -n "${SYMBOL_INDEX_TS_NODE_MODULES:-}" ] && [ -d "$SYMBOL_INDEX_TS_NODE_MODULES/typescript" ]; then
|
|
103
|
+
ln -s "$SYMBOL_INDEX_TS_NODE_MODULES" "$TMP/ts/node_modules"
|
|
104
|
+
run node "$MJS" "$TMP/ts/sample.ts"
|
|
105
|
+
eq "ts symbols" "$(q '[.symbols[]|"\(.kind):\(.name):\(.start)-\(.end):\(.public)"]')" \
|
|
106
|
+
'["const:LIMIT:3-3:true","function:load:5-7:true","class:Loader:9-13:true","method:Loader.read:10-12:true"]'
|
|
107
|
+
eq "ts edges" "$(q '[.edges[]|"\(.from)>\(.to)"]')" '["Loader.read>load","load>LIMIT"]'
|
|
108
|
+
mkdir -p "$TMP/ts/lib"
|
|
109
|
+
printf 'import { b } from "./b";\nimport type { T } from "./c";\nexport function a(): number { return b(); }\n' > "$TMP/ts/lib/a.ts"
|
|
110
|
+
printf 'import * as A from "./a.js";\nexport const b = () => 2;\nexport function useA() { return A.a(); }\n' > "$TMP/ts/lib/b.ts"
|
|
111
|
+
printf 'import { a } from "./a";\nexport type T = number;\n' > "$TMP/ts/lib/c.ts"
|
|
112
|
+
run node "$MJS" "$TMP/ts/lib"
|
|
113
|
+
eq "ts cycles (import type ignored)" "$(q '.cycles')" '[["a","b"]]'
|
|
114
|
+
else
|
|
115
|
+
echo "symbol-index.test.sh: TS-parser cases skipped (set SYMBOL_INDEX_TS_NODE_MODULES)"
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
echo "symbol-index.test.sh: $PASS passed, $FAILN failed"
|
|
119
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# symbol-move-ts.test.sh — cases for scripts/symbol-move.mjs, the ts-morph mover (forge#4212).
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
# Builds a throwaway git repo from scripts/fixtures/symbol-move-ts/gen_big.mjs (~700-line src/big.ts + node:test
|
|
6
|
+
# suite), moves four symbol groups into src/big_parts/ and proves the move with scripts/refactor-verify.sh; then
|
|
7
|
+
# checks the refusals, the --extend chain step and the no-ts-morph refusal (no partial edits).
|
|
8
|
+
# ts-morph: SYMBOL_MOVE_TS_NODE_MODULES=<node_modules holding ts-morph> (put on NODE_PATH for the mover, the symbol
|
|
9
|
+
# index and refactor-verify). Without it the ts-morph cases are skipped; SYMBOL_MOVE_REQUIRE_TS_MORPH=1 turns that
|
|
10
|
+
# skip into a failure.
|
|
11
|
+
set -uo pipefail
|
|
12
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
SM="$HERE/symbol-move.mjs"; RV="$HERE/refactor-verify.sh"; GEN="$HERE/fixtures/symbol-move-ts/gen_big.mjs"
|
|
14
|
+
PASS=0; FAILN=0
|
|
15
|
+
TMP="$(mktemp -d "${TMPDIR:-/tmp}/symbol-move-ts-test.XXXXXX")"; trap 'rm -rf "$TMP"' EXIT
|
|
16
|
+
ok() { PASS=$((PASS+1)); }
|
|
17
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
18
|
+
eq() { if [ "$2" = "$3" ]; then ok; else bad "$1 (got: $2 want: $3)"; fi }
|
|
19
|
+
has() { if grep -qF -- "$2" "$3"; then ok; else bad "$1 (no '$2' in $3)"; fi }
|
|
20
|
+
q() { printf '%s' "$OUT" | jq -c "$1"; }
|
|
21
|
+
TSM="${SYMBOL_MOVE_TS_NODE_MODULES:-}"
|
|
22
|
+
[ -n "$TSM" ] && [ -d "$TSM/ts-morph" ] || TSM=""
|
|
23
|
+
# without: no NODE_PATH and a HOME holding no node_modules, so ts-morph cannot resolve from anywhere
|
|
24
|
+
without() { OUT="$(cd "$R" && env -u NODE_PATH HOME="$TMP/home" node "$SM" "$@" 2>"$TMP/err")"; RC=$?; }
|
|
25
|
+
mover() { OUT="$(cd "$R" && NODE_PATH="$TSM" node "$SM" "$@" 2>"$TMP/err")"; RC=$?; }
|
|
26
|
+
mkdir -p "$TMP/home"
|
|
27
|
+
|
|
28
|
+
R="$TMP/repo"; B="$TMP/baseline.json"; P="$TMP/plan.json"
|
|
29
|
+
TEST_CMD='node --test test/big.test.ts'
|
|
30
|
+
g() { git -C "$R" -c user.name=t -c user.email=t@example.invalid "$@"; }
|
|
31
|
+
mkdir -p "$R"; git init -q "$R"
|
|
32
|
+
node "$GEN" "$R" || bad "generator failed"
|
|
33
|
+
g add -A && g commit -qm base && g tag base
|
|
34
|
+
eq "fixture size >= 600 lines" "$([ "$(wc -l < "$R/src/big.ts")" -ge 600 ] && echo yes || echo no)" yes
|
|
35
|
+
SHA0="$(git -C "$R" hash-object src/big.ts)"
|
|
36
|
+
reset() { g reset -q --hard base; g clean -fdq; }
|
|
37
|
+
|
|
38
|
+
seqp() { local f="$1" s=""; for i in $(seq -w 0 19); do s="$s\"$(printf "$f" "$i")\","; done; printf '%s' "${s%,}"; }
|
|
39
|
+
plan() { printf '%s' "$1" | jq -c . > "$2" || bad "bad plan json"; }
|
|
40
|
+
CONSTS="[$(seqp 'LIMIT_%s')]"
|
|
41
|
+
HELPERS="[$(for i in $(seq -w 0 19); do printf '"_scale_%s","compute_%s",' "$i" "$i"; done | sed 's/,$//')]"
|
|
42
|
+
MODELS="[$(seqp 'Item_%s'),$(seqp 'Store_%s')]"
|
|
43
|
+
AIO="[$(seqp 'fetch_%s')]"
|
|
44
|
+
FULL="{\"consts\":$CONSTS,\"helpers\":$HELPERS,\"models\":$MODELS,\"aio\":$AIO}"
|
|
45
|
+
plan "$FULL" "$P"
|
|
46
|
+
|
|
47
|
+
# refused <label>: rc 3, status refused, a reason, and nothing changed in the repo
|
|
48
|
+
refused() {
|
|
49
|
+
eq "$1: rc" "$RC" 3
|
|
50
|
+
eq "$1: status" "$(q '.status')" '"refused"'
|
|
51
|
+
eq "$1: has reason" "$(q '.reason|length>0')" true
|
|
52
|
+
eq "$1: tree untouched" "$(git -C "$R" status --porcelain)" ""
|
|
53
|
+
eq "$1: barrel untouched" "$(git -C "$R" hash-object src/big.ts)" "$SHA0"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# ---- 1. no codemod tool: ts-morph unresolvable -> clean refusal before anything is read or written ----
|
|
57
|
+
without --src src/big.ts --plan "$P"
|
|
58
|
+
refused "no ts-morph"
|
|
59
|
+
eq "no ts-morph: reason" "$(q '.reason|startswith("no codemod tool for ts: ts-morph is not resolvable")')" true
|
|
60
|
+
without --probe --src src/big.ts
|
|
61
|
+
eq "no ts-morph probe: rc" "$RC" 3
|
|
62
|
+
eq "no ts-morph probe: reason" "$(q '.reason|startswith("no codemod tool for ts:")')" true
|
|
63
|
+
eq "no ts-morph: no package dir" "$([ -e "$R/src/big_parts" ] && echo yes || echo no)" no
|
|
64
|
+
|
|
65
|
+
# ---- 2. usage ----
|
|
66
|
+
OUT="$(node "$SM" --plan "$P" 2>/dev/null)"; eq "usage: rc" "$?" 2
|
|
67
|
+
OUT="$(node "$SM" --src "$R/src/nope.ts" --plan "$P" 2>/dev/null)"; eq "missing src: rc" "$?" 2
|
|
68
|
+
|
|
69
|
+
if [ -z "$TSM" ]; then
|
|
70
|
+
if [ "${SYMBOL_MOVE_REQUIRE_TS_MORPH:-0}" = "1" ]; then bad "ts-morph required (SYMBOL_MOVE_REQUIRE_TS_MORPH=1) but SYMBOL_MOVE_TS_NODE_MODULES has no ts-morph"; fi
|
|
71
|
+
echo "symbol-move-ts.test.sh: ts-morph cases skipped (set SYMBOL_MOVE_TS_NODE_MODULES)"
|
|
72
|
+
echo "symbol-move-ts.test.sh: $PASS passed, $FAILN failed"
|
|
73
|
+
[ "$FAILN" -eq 0 ]; exit
|
|
74
|
+
fi
|
|
75
|
+
export NODE_PATH="$TSM"
|
|
76
|
+
|
|
77
|
+
# ---- 3. probe and dry run ----
|
|
78
|
+
mover --probe --src src/big.ts
|
|
79
|
+
eq "probe: rc" "$RC" 0
|
|
80
|
+
eq "probe: tool" "$(q '.tool')" '"ts-morph"'
|
|
81
|
+
mover --src src/big.ts --plan "$P" --dry-run
|
|
82
|
+
eq "dry-run: rc" "$RC" 0
|
|
83
|
+
eq "dry-run: tree untouched" "$(git -C "$R" status --porcelain)" ""
|
|
84
|
+
|
|
85
|
+
# ---- 4. the move: four groups into src/big_parts/, barrel at the old path, refactor-verify ok ----
|
|
86
|
+
bash "$RV" baseline --root "$R" --target src/big.ts --test-cmd "$TEST_CMD" --out "$B" >/dev/null 2>"$TMP/err" || { cat "$TMP/err"; bad "baseline failed"; }
|
|
87
|
+
eq "baseline counts" "$(jq -c .test "$B")" '{"exit":0,"passed":5,"skipped":1}'
|
|
88
|
+
eq "baseline flags erased types" "$(jq -c '[.symbols[]|select(.type)]|length' "$B")" 20
|
|
89
|
+
mover --src src/big.ts --plan "$P"
|
|
90
|
+
eq "move: rc" "$RC" 0
|
|
91
|
+
eq "move: status" "$(q '.status')" '"ok"'
|
|
92
|
+
eq "move: package" "$(q '.package')" '"src/big_parts"'
|
|
93
|
+
eq "move: modules" "$(q '.modules|keys')" '["aio","consts","helpers","models"]'
|
|
94
|
+
for m in consts helpers models aio; do
|
|
95
|
+
eq "module $m exists" "$([ -f "$R/src/big_parts/$m.ts" ] && echo yes || echo no)" yes
|
|
96
|
+
has "barrel re-exports $m" "} from \"./big_parts/$m.ts\";" "$R/src/big.ts"
|
|
97
|
+
done
|
|
98
|
+
has "barrel re-exports types with type" 'export { type Item_00, Store_00,' "$R/src/big.ts"
|
|
99
|
+
has "barrel imports names kept code uses" 'import { compute_00 } from "./big_parts/helpers.ts";' "$R/src/big.ts"
|
|
100
|
+
has "barrel keeps unmoved code" 'function _shared(x: number): number {' "$R/src/big.ts"
|
|
101
|
+
has "barrel keeps the header" '// SPDX-License-Identifier: MIT' "$R/src/big.ts"
|
|
102
|
+
has "relative import re-based" 'import { clamp } from "../util.ts";' "$R/src/big_parts/helpers.ts"
|
|
103
|
+
has "type import pruned and re-based" 'import { type Unit } from "../util.ts";' "$R/src/big_parts/models.ts"
|
|
104
|
+
has "sibling import" 'import { compute_00, compute_01,' "$R/src/big_parts/models.ts"
|
|
105
|
+
has "private helper moved verbatim" 'function _scale_07(x: number): number {' "$R/src/big_parts/helpers.ts"
|
|
106
|
+
has "doc comment moved" '/** compute 7: scaled and clamped. */' "$R/src/big_parts/helpers.ts"
|
|
107
|
+
eq "no moved declaration left" "$(grep -c '^export function compute_\|^function _scale_\|^export class Store_\|^export interface Item_' "$R/src/big.ts")" 0
|
|
108
|
+
g add -A && g commit -qm move
|
|
109
|
+
OUT="$(bash "$RV" verify --baseline "$B" --base base 2>"$TMP/err")"; RC=$?
|
|
110
|
+
eq "verify: rc" "$RC" 0
|
|
111
|
+
eq "verify: ok" "$(q '.ok')" true
|
|
112
|
+
# a dropped type re-export is caught statically
|
|
113
|
+
sed -i.bak 's/export { type Item_00, Store_00, /export { Store_00, /' "$R/src/big.ts"; rm -f "$R/src/big.ts.bak"
|
|
114
|
+
g commit -qam drop-type
|
|
115
|
+
OUT="$(bash "$RV" verify --baseline "$B" --base base 2>"$TMP/err")"; RC=$?
|
|
116
|
+
eq "dropped type: rc" "$RC" 1
|
|
117
|
+
eq "dropped type: missing" "$(q '[.failures[]|select(.kind=="missing-symbol")|.name]')" '["Item_00"]'
|
|
118
|
+
reset
|
|
119
|
+
|
|
120
|
+
# ---- 5. refusals: nothing touched ----
|
|
121
|
+
plan '{"a":["nope"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "unknown symbol"
|
|
122
|
+
plan '{"a":["LIMIT_00"],"b":["LIMIT_00"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "listed twice"
|
|
123
|
+
plan '{"a":["Store_00.add"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "method"
|
|
124
|
+
plan '{"index":["LIMIT_00"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "bad module name"
|
|
125
|
+
plan '{"a":["compute_00"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "needs a kept name"
|
|
126
|
+
eq "needs a kept name: reason" "$(q '.reason|test("needs \"_scale_00\", which stays")')" true
|
|
127
|
+
plan '{"a":["_shared"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "private used by kept code"
|
|
128
|
+
eq "private used by kept code: reason" "$(q '.reason|test("not exported but kept code uses it")')" true
|
|
129
|
+
plan '{"a":["_scale_00","LIMIT_00"],"b":["compute_00"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "private used by a sibling"
|
|
130
|
+
eq "private used by a sibling: reason" "$(q '.reason|test("not exported")')" true
|
|
131
|
+
plan '{"a":["ping"],"b":["pong"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "cycle"
|
|
132
|
+
eq "cycle: reason" "$(q '.reason|test("import cycle")')" true
|
|
133
|
+
mkdir -p "$R/src/big_parts"; plan "{\"consts\":$CONSTS}" "$P"; mover --src src/big.ts --plan "$P"
|
|
134
|
+
eq "existing package: rc" "$RC" 3
|
|
135
|
+
eq "existing package: reason" "$(q '.reason|test("already exists")')" true
|
|
136
|
+
rmdir "$R/src/big_parts"
|
|
137
|
+
printf 'export default function main(): number {\n return 1;\n}\n' >> "$R/src/big.ts"; g commit -qam default; SHA0="$(git -C "$R" hash-object src/big.ts)"
|
|
138
|
+
plan '{"a":["main"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "export default"
|
|
139
|
+
printf 'export const HERE = import.meta.url;\n' >> "$R/src/big.ts"; g commit -qam meta; SHA0="$(git -C "$R" hash-object src/big.ts)"
|
|
140
|
+
plan '{"a":["HERE"]}' "$P"; mover --src src/big.ts --plan "$P"; refused "import.meta"
|
|
141
|
+
reset; SHA0="$(git -C "$R" hash-object src/big.ts)"
|
|
142
|
+
|
|
143
|
+
# ---- 6. chain: a second step adds modules to the existing package with --extend ----
|
|
144
|
+
plan "{\"consts\":$CONSTS,\"helpers\":$HELPERS}" "$P"; mover --src src/big.ts --plan "$P"
|
|
145
|
+
eq "chain step 1: rc" "$RC" 0
|
|
146
|
+
g add -A && g commit -qm step1
|
|
147
|
+
plan "{\"models\":$MODELS}" "$P"; mover --src src/big.ts --plan "$P" --package big_parts
|
|
148
|
+
eq "chain without --extend: rc" "$RC" 3
|
|
149
|
+
plan "{\"models\":$MODELS}" "$P"; mover --src src/big.ts --plan "$P" --package big_parts --extend
|
|
150
|
+
eq "chain step 2: rc" "$RC" 0
|
|
151
|
+
has "chain step 2: barrel" '} from "./big_parts/models.ts";' "$R/src/big.ts"
|
|
152
|
+
has "chain step 2: earlier module imported in-package" '} from "./helpers.ts";' "$R/src/big_parts/models.ts"
|
|
153
|
+
g add -A && g commit -qm step2
|
|
154
|
+
OUT="$(bash "$RV" verify --baseline "$B" --base base 2>"$TMP/err")"; RC=$?
|
|
155
|
+
eq "chain verify: ok" "$(q '.ok')" true
|
|
156
|
+
plan "{\"models\":[\"fetch_00\"]}" "$P"; mover --src src/big.ts --plan "$P" --package big_parts --extend
|
|
157
|
+
eq "chain existing module: rc" "$RC" 3
|
|
158
|
+
eq "chain existing module: reason" "$(q '.reason|test("module already exists")')" true
|
|
159
|
+
|
|
160
|
+
echo "symbol-move-ts.test.sh: $PASS passed, $FAILN failed"
|
|
161
|
+
[ "$FAILN" -eq 0 ]
|