@qa-gentic/stlc-agents 1.0.27 → 1.0.29
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/ARCHITECTURE-ADO.md +350 -0
- package/ARCHITECTURE-JIRA.md +203 -0
- package/QUICKSTART-ADO.md +400 -0
- package/QUICKSTART-JIRA.md +334 -0
- package/README.md +49 -0
- package/bin/postinstall.js +14 -4
- package/package.json +19 -7
- package/skills/migrate-framework/SKILL.md +207 -0
- package/src/stlc_agents/agent_migration/__init__.py +0 -0
- package/src/stlc_agents/agent_migration/_migrate.py +1398 -0
- package/src/stlc_agents/agent_migration/cli.py +217 -0
- package/src/stlc_agents/agent_migration/detector.py +81 -0
- package/src/stlc_agents/agent_migration/mapper.py +439 -0
- package/src/stlc_agents/agent_migration/reporter.py +86 -0
- package/src/stlc_agents/agent_migration/server.py +267 -0
- package/src/stlc_agents/agent_migration/transformer/__init__.py +0 -0
- package/src/stlc_agents/agent_migration/transformer/config_merger.py +513 -0
- package/src/stlc_agents/agent_migration/transformer/healer_injector.py +1143 -0
- package/src/stlc_agents/agent_migration/transformer/import_fixer.py +419 -0
- package/src/stlc_agents/agent_migration/transformer/js_to_ts.py +413 -0
- package/src/stlc_agents/agent_migration/transformer/local_var_hoister.py +378 -0
- package/src/stlc_agents/agent_migration/transformer/locator_moderniser.py +132 -0
- package/src/stlc_agents/agent_migration/transformer/locator_registrar.py +328 -0
- package/src/stlc_agents/agent_migration/transformer/spec_to_bdd.py +820 -0
- package/src/stlc_agents/agent_playwright_generator/server.py +926 -91
- package/src/stlc_agents/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/boilerplate.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/helix_write.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/auth.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/cost_tracker.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/pricing.cpython-314.pyc +0 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""Convert CommonJS JavaScript to TypeScript ES-module syntax."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def convert_js_to_ts(content: str) -> tuple[str, list[str]]:
|
|
7
|
+
"""
|
|
8
|
+
Convert a JS file to TS.
|
|
9
|
+
Returns (transformed_content, list_of_changes).
|
|
10
|
+
"""
|
|
11
|
+
lines = content.split("\n")
|
|
12
|
+
result = []
|
|
13
|
+
changes: list[str] = []
|
|
14
|
+
|
|
15
|
+
for line in lines:
|
|
16
|
+
line = _transform_line(line, changes)
|
|
17
|
+
result.append(line)
|
|
18
|
+
|
|
19
|
+
joined = "\n".join(result)
|
|
20
|
+
joined, rt_changes = _add_return_types(joined)
|
|
21
|
+
changes.extend(rt_changes)
|
|
22
|
+
joined, cls_changes = _add_class_field_decls(joined)
|
|
23
|
+
changes.extend(cls_changes)
|
|
24
|
+
joined, ctor_changes = _annotate_constructor_params(joined)
|
|
25
|
+
changes.extend(ctor_changes)
|
|
26
|
+
joined, dup_changes = _rename_duplicate_methods(joined)
|
|
27
|
+
changes.extend(dup_changes)
|
|
28
|
+
joined, intl_changes = _cast_intl_format_options(joined)
|
|
29
|
+
changes.extend(intl_changes)
|
|
30
|
+
joined, tobe_changes = _fix_tobe_typo(joined)
|
|
31
|
+
changes.extend(tobe_changes)
|
|
32
|
+
return joined, changes
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# `.tobe.<value>(<msg>)` → `.toBe(<value>)`
|
|
37
|
+
#
|
|
38
|
+
# JS doesn't catch this — `expect(x).tobe` evaluates to undefined and only
|
|
39
|
+
# throws at runtime when `.<value>(...)` is called. TS rejects it at compile
|
|
40
|
+
# time. The clear intent is `.toBe(<value>)`; any trailing args were going
|
|
41
|
+
# to fail anyway so we drop them with a MIGRATION TODO.
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
def _fix_tobe_typo(content: str) -> tuple[str, list[str]]:
|
|
45
|
+
changes: list[str] = []
|
|
46
|
+
|
|
47
|
+
def _repl(m: re.Match) -> str:
|
|
48
|
+
value = m.group(1)
|
|
49
|
+
args = m.group(2).strip()
|
|
50
|
+
changes.append(f".tobe.{value}({args}) → .toBe({value}) (dropped invalid args)")
|
|
51
|
+
if args:
|
|
52
|
+
return f".toBe({value}) /* MIGRATION TODO: original second arg was {args!r} */"
|
|
53
|
+
return f".toBe({value})"
|
|
54
|
+
|
|
55
|
+
content = re.sub(
|
|
56
|
+
r"\.tobe\.(\w+)\(([^)]*)\)",
|
|
57
|
+
_repl,
|
|
58
|
+
content,
|
|
59
|
+
)
|
|
60
|
+
return content, changes
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Duplicate-method renaming
|
|
65
|
+
#
|
|
66
|
+
# JS allows two methods with the same name in a class — the second silently
|
|
67
|
+
# overrides the first. TypeScript flags this with TS2393 "Duplicate function
|
|
68
|
+
# implementation". Rename the second (and later) occurrences with a numeric
|
|
69
|
+
# suffix so the user can see both and decide which to keep.
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
def _rename_duplicate_methods(content: str) -> tuple[str, list[str]]:
|
|
73
|
+
changes: list[str] = []
|
|
74
|
+
out: list[str] = []
|
|
75
|
+
cursor = 0
|
|
76
|
+
class_re = re.compile(
|
|
77
|
+
r"\bclass\s+\w+(?:\s+extends\s+[\w.<>\[\]\s,]+?)?\s*\{",
|
|
78
|
+
)
|
|
79
|
+
for m in class_re.finditer(content):
|
|
80
|
+
body_open = m.end() - 1
|
|
81
|
+
body_close = _find_matching_brace(content, body_open)
|
|
82
|
+
if body_close < 0:
|
|
83
|
+
continue
|
|
84
|
+
body = content[body_open + 1 : body_close]
|
|
85
|
+
|
|
86
|
+
# Find every method definition: `[async] name(args) {`
|
|
87
|
+
method_re = re.compile(
|
|
88
|
+
r"(^\s+)(?:async\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*[\w.<>\[\]\s|&,]+)?\s*\{",
|
|
89
|
+
re.MULTILINE,
|
|
90
|
+
)
|
|
91
|
+
# Track name → count of occurrences so far
|
|
92
|
+
seen: dict[str, int] = {}
|
|
93
|
+
# Build replacements as a list of (span_in_body, new_text)
|
|
94
|
+
replacements: list[tuple[int, int, str]] = []
|
|
95
|
+
for mm in method_re.finditer(body):
|
|
96
|
+
name = mm.group(2)
|
|
97
|
+
if name in {"constructor", "if", "for", "while", "switch", "return", "function", "async"}:
|
|
98
|
+
continue
|
|
99
|
+
seen[name] = seen.get(name, 0) + 1
|
|
100
|
+
if seen[name] == 1:
|
|
101
|
+
continue
|
|
102
|
+
new_name = f"{name}_{seen[name]}"
|
|
103
|
+
# Find the position of the method name within the match.
|
|
104
|
+
name_start = mm.start() + mm.group(0).find(name)
|
|
105
|
+
name_end = name_start + len(name)
|
|
106
|
+
replacements.append((name_start, name_end, new_name))
|
|
107
|
+
changes.append(
|
|
108
|
+
f"Renamed duplicate method {name} → {new_name} "
|
|
109
|
+
f"(JS allowed; TS does not)"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if not replacements:
|
|
113
|
+
continue
|
|
114
|
+
# Apply replacements right-to-left so indices stay valid.
|
|
115
|
+
new_body = body
|
|
116
|
+
for start, end, repl in sorted(replacements, key=lambda t: t[0], reverse=True):
|
|
117
|
+
new_body = new_body[:start] + repl + new_body[end:]
|
|
118
|
+
|
|
119
|
+
out.append(content[cursor : body_open + 1])
|
|
120
|
+
out.append(new_body)
|
|
121
|
+
cursor = body_close
|
|
122
|
+
out.append(content[cursor:])
|
|
123
|
+
return "".join(out), changes
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
# Intl.DateTimeFormatOptions cast
|
|
128
|
+
#
|
|
129
|
+
# `date.toLocaleDateString("en-US", options)` — JS accepts any object; TS
|
|
130
|
+
# requires `Intl.DateTimeFormatOptions` whose string fields are literal types.
|
|
131
|
+
# Casting the second arg with `as Intl.DateTimeFormatOptions` keeps the
|
|
132
|
+
# runtime identical while satisfying tsc.
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
def _cast_intl_format_options(content: str) -> tuple[str, list[str]]:
|
|
136
|
+
changes: list[str] = []
|
|
137
|
+
|
|
138
|
+
def _repl(m: re.Match) -> str:
|
|
139
|
+
full = m.group(0)
|
|
140
|
+
# Skip if already cast.
|
|
141
|
+
if "as Intl.DateTimeFormatOptions" in full:
|
|
142
|
+
return full
|
|
143
|
+
receiver, locale, opts = m.group(1), m.group(2), m.group(3).strip()
|
|
144
|
+
if not opts:
|
|
145
|
+
return full
|
|
146
|
+
changes.append(f"Cast toLocaleDateString options as Intl.DateTimeFormatOptions")
|
|
147
|
+
return f"{receiver}.toLocaleDateString({locale}, {opts} as Intl.DateTimeFormatOptions)"
|
|
148
|
+
|
|
149
|
+
content = re.sub(
|
|
150
|
+
r"([\w.\[\]'\"]+)\s*\.\s*toLocaleDateString\(\s*([^,)]+)\s*,\s*([^)]+)\)",
|
|
151
|
+
_repl,
|
|
152
|
+
content,
|
|
153
|
+
)
|
|
154
|
+
return content, changes
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# Class field declarations — surface `this.X = ...` constructor assignments
|
|
159
|
+
# as `X: any;` declarations so TypeScript doesn't complain that "Property X
|
|
160
|
+
# does not exist on type Foo". JS classes never need declarations; TS does.
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
def _add_class_field_decls(content: str) -> tuple[str, list[str]]:
|
|
164
|
+
changes: list[str] = []
|
|
165
|
+
out: list[str] = []
|
|
166
|
+
cursor = 0
|
|
167
|
+
# Find each `class Foo { ... }` opening. Handles `class Foo extends Bar` too.
|
|
168
|
+
class_re = re.compile(
|
|
169
|
+
r"\bclass\s+\w+(?:\s+extends\s+[\w.<>\[\]\s,]+?)?\s*\{",
|
|
170
|
+
)
|
|
171
|
+
for m in class_re.finditer(content):
|
|
172
|
+
body_open = m.end() - 1 # index of `{`
|
|
173
|
+
body_close = _find_matching_brace(content, body_open)
|
|
174
|
+
if body_close < 0:
|
|
175
|
+
continue
|
|
176
|
+
body = content[body_open + 1 : body_close]
|
|
177
|
+
# Find constructor and collect `this.X = ...` names.
|
|
178
|
+
names = _extract_constructor_this_assignments(body)
|
|
179
|
+
if not names:
|
|
180
|
+
continue
|
|
181
|
+
# Avoid re-declaring fields the class already has.
|
|
182
|
+
existing = set(re.findall(r"^\s*(?:private|public|protected|readonly)?\s*(\w+)[!:?=\s]", body, re.MULTILINE))
|
|
183
|
+
new_decls = [n for n in names if n not in existing]
|
|
184
|
+
if not new_decls:
|
|
185
|
+
continue
|
|
186
|
+
# Indent: match the existing body indentation if any (default 2 spaces).
|
|
187
|
+
indent_match = re.search(r"\n(\s+)", body)
|
|
188
|
+
indent = indent_match.group(1) if indent_match else " "
|
|
189
|
+
decls = "".join(f"\n{indent}{n}: any;" for n in new_decls)
|
|
190
|
+
# Splice the declarations just after the class opening `{`.
|
|
191
|
+
out.append(content[cursor : body_open + 1])
|
|
192
|
+
out.append(decls)
|
|
193
|
+
cursor = body_open + 1
|
|
194
|
+
changes.extend(f"Added class field declaration: {n}: any" for n in new_decls)
|
|
195
|
+
out.append(content[cursor:])
|
|
196
|
+
return "".join(out), changes
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _extract_constructor_this_assignments(class_body: str) -> list[str]:
|
|
200
|
+
"""Return ordered, deduplicated list of `X` names from `this.X = ...`."""
|
|
201
|
+
m = re.search(r"\bconstructor\s*\(", class_body)
|
|
202
|
+
if not m:
|
|
203
|
+
return []
|
|
204
|
+
paren_open = m.end() - 1
|
|
205
|
+
paren_close = _find_matching_paren(class_body, paren_open)
|
|
206
|
+
if paren_close < 0:
|
|
207
|
+
return []
|
|
208
|
+
# Find the `{` that opens the constructor body.
|
|
209
|
+
brace_open = class_body.find("{", paren_close)
|
|
210
|
+
if brace_open < 0:
|
|
211
|
+
return []
|
|
212
|
+
brace_close = _find_matching_brace(class_body, brace_open)
|
|
213
|
+
if brace_close < 0:
|
|
214
|
+
return []
|
|
215
|
+
ctor_body = class_body[brace_open + 1 : brace_close]
|
|
216
|
+
names: list[str] = []
|
|
217
|
+
seen: set[str] = set()
|
|
218
|
+
for am in re.finditer(r"\bthis\.(\w+)\s*=", ctor_body):
|
|
219
|
+
n = am.group(1)
|
|
220
|
+
if n in seen:
|
|
221
|
+
continue
|
|
222
|
+
# Skip already-declared private healer fields injected later — they
|
|
223
|
+
# follow a `_` prefix convention.
|
|
224
|
+
if n.startswith("_"):
|
|
225
|
+
continue
|
|
226
|
+
names.append(n)
|
|
227
|
+
seen.add(n)
|
|
228
|
+
return names
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _annotate_constructor_params(content: str) -> tuple[str, list[str]]:
|
|
232
|
+
"""Add `: any` to untyped constructor parameters."""
|
|
233
|
+
changes: list[str] = []
|
|
234
|
+
|
|
235
|
+
def _do(m: re.Match) -> str:
|
|
236
|
+
params = m.group(1)
|
|
237
|
+
if not params.strip():
|
|
238
|
+
return m.group(0)
|
|
239
|
+
annotated = []
|
|
240
|
+
for p in [p.strip() for p in params.split(",")]:
|
|
241
|
+
if not p:
|
|
242
|
+
continue
|
|
243
|
+
if ":" in p: # already typed
|
|
244
|
+
annotated.append(p)
|
|
245
|
+
else:
|
|
246
|
+
annotated.append(f"{p}: any")
|
|
247
|
+
if annotated == [p.strip() for p in params.split(",") if p.strip()]:
|
|
248
|
+
return m.group(0)
|
|
249
|
+
changes.append("Annotated constructor params as any")
|
|
250
|
+
return f"constructor({', '.join(annotated)})"
|
|
251
|
+
|
|
252
|
+
content = re.sub(r"\bconstructor\(([^)]*)\)", _do, content)
|
|
253
|
+
return content, changes
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _find_matching_brace(text: str, open_idx: int) -> int:
|
|
257
|
+
depth = 1
|
|
258
|
+
i = open_idx + 1
|
|
259
|
+
in_str: str | None = None
|
|
260
|
+
while i < len(text):
|
|
261
|
+
c = text[i]
|
|
262
|
+
if in_str:
|
|
263
|
+
if c == "\\":
|
|
264
|
+
i += 2
|
|
265
|
+
continue
|
|
266
|
+
if c == in_str:
|
|
267
|
+
in_str = None
|
|
268
|
+
i += 1
|
|
269
|
+
continue
|
|
270
|
+
if c in ("'", '"', "`"):
|
|
271
|
+
in_str = c
|
|
272
|
+
i += 1
|
|
273
|
+
continue
|
|
274
|
+
if c == "/" and i + 1 < len(text):
|
|
275
|
+
if text[i + 1] == "/":
|
|
276
|
+
nl = text.find("\n", i)
|
|
277
|
+
i = nl + 1 if nl >= 0 else len(text)
|
|
278
|
+
continue
|
|
279
|
+
if text[i + 1] == "*":
|
|
280
|
+
end = text.find("*/", i + 2)
|
|
281
|
+
i = end + 2 if end >= 0 else len(text)
|
|
282
|
+
continue
|
|
283
|
+
if c == "{":
|
|
284
|
+
depth += 1
|
|
285
|
+
elif c == "}":
|
|
286
|
+
depth -= 1
|
|
287
|
+
if depth == 0:
|
|
288
|
+
return i
|
|
289
|
+
i += 1
|
|
290
|
+
return -1
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _find_matching_paren(text: str, open_idx: int) -> int:
|
|
294
|
+
depth = 1
|
|
295
|
+
i = open_idx + 1
|
|
296
|
+
while i < len(text):
|
|
297
|
+
c = text[i]
|
|
298
|
+
if c == "(":
|
|
299
|
+
depth += 1
|
|
300
|
+
elif c == ")":
|
|
301
|
+
depth -= 1
|
|
302
|
+
if depth == 0:
|
|
303
|
+
return i
|
|
304
|
+
i += 1
|
|
305
|
+
return -1
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
# Line-level transformations
|
|
310
|
+
# ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
def _transform_line(line: str, changes: list[str]) -> str:
|
|
313
|
+
# const { X, Y } = require('Z') → import { X, Y } from 'Z'
|
|
314
|
+
m = re.match(
|
|
315
|
+
r"^(\s*)const\s*\{([^}]+)\}\s*=\s*require\(['\"]([^'\"]+)['\"]\)\s*;?\s*$",
|
|
316
|
+
line,
|
|
317
|
+
)
|
|
318
|
+
if m:
|
|
319
|
+
indent, names, module = m.groups()
|
|
320
|
+
module = _drop_js_ext(module)
|
|
321
|
+
changes.append(f"require('{module}') → import {{ ... }}")
|
|
322
|
+
return f"{indent}import {{ {names.strip()} }} from '{module}';"
|
|
323
|
+
|
|
324
|
+
# const X = require('Z') → import X from 'Z'
|
|
325
|
+
m = re.match(
|
|
326
|
+
r"^(\s*)const\s+(\w+)\s*=\s*require\(['\"]([^'\"]+)['\"]\)\s*;?\s*$",
|
|
327
|
+
line,
|
|
328
|
+
)
|
|
329
|
+
if m:
|
|
330
|
+
indent, name, module = m.groups()
|
|
331
|
+
module = _drop_js_ext(module)
|
|
332
|
+
changes.append(f"require('{module}') → import ... from ...")
|
|
333
|
+
return f"{indent}import {name} from '{module}';"
|
|
334
|
+
|
|
335
|
+
# module.exports.X = Y → export const X = Y
|
|
336
|
+
m = re.match(r"^(\s*)module\.exports\.(\w+)\s*=(.*)$", line)
|
|
337
|
+
if m:
|
|
338
|
+
indent, name, rest = m.groups()
|
|
339
|
+
changes.append(f"module.exports.{name} → export const {name}")
|
|
340
|
+
return f"{indent}export const {name} ={rest}"
|
|
341
|
+
|
|
342
|
+
# module.exports = X → export default X
|
|
343
|
+
m = re.match(r"^(\s*)module\.exports\s*=(.*)$", line)
|
|
344
|
+
if m:
|
|
345
|
+
indent, rest = m.groups()
|
|
346
|
+
changes.append("module.exports → export default")
|
|
347
|
+
return f"{indent}export default{rest}"
|
|
348
|
+
|
|
349
|
+
# exports.X = Y → export const X = Y
|
|
350
|
+
m = re.match(r"^(\s*)exports\.(\w+)\s*=(.*)$", line)
|
|
351
|
+
if m:
|
|
352
|
+
indent, name, rest = m.groups()
|
|
353
|
+
changes.append(f"exports.{name} → export const {name}")
|
|
354
|
+
return f"{indent}export const {name} ={rest}"
|
|
355
|
+
|
|
356
|
+
# Strip .js from import paths
|
|
357
|
+
if "from " in line:
|
|
358
|
+
new_line = re.sub(
|
|
359
|
+
r"(from\s+['\"])([^'\"]+)\.js(['\"])",
|
|
360
|
+
lambda m: f"{m.group(1)}{m.group(2)}{m.group(3)}",
|
|
361
|
+
line,
|
|
362
|
+
)
|
|
363
|
+
if new_line != line:
|
|
364
|
+
changes.append("Removed .js extension from import path")
|
|
365
|
+
return new_line
|
|
366
|
+
|
|
367
|
+
return line
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _drop_js_ext(module: str) -> str:
|
|
371
|
+
return module[:-3] if module.endswith(".js") else module
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# ---------------------------------------------------------------------------
|
|
375
|
+
# Return-type annotations
|
|
376
|
+
# ---------------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
def _add_return_types(content: str) -> tuple[str, list[str]]:
|
|
379
|
+
changes: list[str] = []
|
|
380
|
+
|
|
381
|
+
# async function foo(params) { → async function foo(params): Promise<any> {
|
|
382
|
+
def _annotate_fn(m: re.Match) -> str:
|
|
383
|
+
full = m.group(0)
|
|
384
|
+
if ": Promise" in full or "): void" in full:
|
|
385
|
+
return full
|
|
386
|
+
indent, name, params = m.group(1), m.group(2), m.group(3)
|
|
387
|
+
changes.append(f"Added Promise<any> to async function {name}")
|
|
388
|
+
return f"{indent}async function {name}({params}): Promise<any> {{"
|
|
389
|
+
|
|
390
|
+
content = re.sub(
|
|
391
|
+
r"^(\s*)async\s+function\s+(\w+)\s*\(([^)]*)\)\s*\{",
|
|
392
|
+
_annotate_fn,
|
|
393
|
+
content,
|
|
394
|
+
flags=re.MULTILINE,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# async methodName(params) { in class bodies
|
|
398
|
+
def _annotate_method(m: re.Match) -> str:
|
|
399
|
+
full = m.group(0)
|
|
400
|
+
if ": Promise" in full or "): void" in full:
|
|
401
|
+
return full
|
|
402
|
+
indent, name, params = m.group(1), m.group(2), m.group(3)
|
|
403
|
+
changes.append(f"Added Promise<any> to async method {name}")
|
|
404
|
+
return f"{indent}async {name}({params}): Promise<any> {{"
|
|
405
|
+
|
|
406
|
+
content = re.sub(
|
|
407
|
+
r"^(\s+)async\s+(\w+)\s*\(([^)]*)\)\s*\{",
|
|
408
|
+
_annotate_method,
|
|
409
|
+
content,
|
|
410
|
+
flags=re.MULTILINE,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
return content, changes
|