di-bag-codemod 0.1.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.
@@ -0,0 +1,946 @@
1
+ // tools/codemod/lib/rewrite.mjs
2
+
3
+ // Members of built-in objects. A call such as `text.replace(...)` on an `any` receiver is not worth a report.
4
+ const BUILTIN_MEMBERS = new Set([String.prototype, Array.prototype, Promise.prototype, Promise, Object, Object.prototype, Map.prototype, Set.prototype]
5
+ .flatMap(target => Object.getOwnPropertyNames(target)));
6
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
7
+ const RESHAPE_BLOCKED = Symbol('reshape blocked');
8
+
9
+ /**
10
+ * Rewrite one source file. Every decision reads the original program; the new text is
11
+ * assembled bottom-up, so a rewritten call may contain other rewritten calls.
12
+ * @returns {{ text: string, rewrites: number }}
13
+ */
14
+ export function rewriteSourceFile({ ts, checker, program, sourceFile, library, index, transforms, writableSourceFiles, manualItems, fileLabel }) {
15
+ const source = sourceFile.text;
16
+ const start = node => node.getStart(sourceFile);
17
+ const slice = (from, to) => source.slice(from, to);
18
+ const skip = new Set();
19
+ const blockedReshapes = new WeakSet();
20
+ let rewrites = 0;
21
+
22
+ function manual(node, reason) {
23
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(start(node));
24
+ manualItems.push({ file: fileLabel, line: line + 1, column: character + 1, reason, text: slice(start(node), node.end).split('\n')[0].slice(0, 120) });
25
+ }
26
+
27
+ /** The transformed text of a node without its leading trivia. */
28
+ function text(node) {
29
+ if (skip.has(node)) return slice(start(node), node.end);
30
+ const replaced = rewriteNode(node);
31
+ if (replaced !== undefined) { rewrites++; return replaced; }
32
+ if (skip.has(node)) return slice(start(node), node.end);
33
+ return assemble(node, []);
34
+ }
35
+
36
+ /**
37
+ * The text of `node` with `replacements` applied and every other child transformed.
38
+ * Replacements are `{ start, end, text }` in file positions, inside the node, sorted and disjoint.
39
+ * A child that contains replacements is assembled with them and is not offered to the handlers again.
40
+ */
41
+ function assemble(node, replacements) {
42
+ const pending = [...replacements].sort((left, right) => left.start - right.start || left.end - right.end);
43
+ const children = [];
44
+ ts.forEachChild(node, child => { children.push(child); });
45
+ let cursor = start(node);
46
+ let out = '';
47
+ const emit = replacement => { out += slice(cursor, replacement.start) + replacement.text; cursor = replacement.end; };
48
+ for (const child of children) {
49
+ const childStart = start(child);
50
+ while (pending.length && pending[0].end <= childStart) emit(pending.shift());
51
+ if (child.end <= cursor) continue;
52
+ if (pending.length && pending[0].start <= childStart && pending[0].end >= child.end) continue;
53
+ const inner = [];
54
+ while (pending.length && pending[0].start >= childStart && pending[0].end <= child.end) inner.push(pending.shift());
55
+ if (pending.length && pending[0].start < child.end) throw new Error(`overlapping rewrite in ${fileLabel} at offset ${pending[0].start}`);
56
+ if (childStart < cursor) throw new Error(`children out of order in ${fileLabel} at offset ${childStart}`);
57
+ out += slice(cursor, childStart) + (inner.length ? assemble(child, inner) : text(child));
58
+ cursor = child.end;
59
+ }
60
+ while (pending.length) emit(pending.shift());
61
+ return out + slice(cursor, node.end);
62
+ }
63
+
64
+ const escapeLiteral = (value, delimiter) => {
65
+ let escaped = '';
66
+ for (let index = 0; index < value.length; index++) {
67
+ const character = value[index];
68
+ const code = value.charCodeAt(index);
69
+ if (character === '\\') escaped += '\\\\';
70
+ else if (character === delimiter) escaped += `\\${character}`;
71
+ else if (delimiter === '`' && character === '$' && value[index + 1] === '{') escaped += '\\$';
72
+ else if (character === '\b') escaped += '\\b';
73
+ else if (character === '\t') escaped += '\\t';
74
+ else if (character === '\n') escaped += '\\n';
75
+ else if (character === '\v') escaped += '\\v';
76
+ else if (character === '\f') escaped += '\\f';
77
+ else if (character === '\r') escaped += '\\r';
78
+ else if (code < 0x20 || code === 0x7f) escaped += `\\x${code.toString(16).padStart(2, '0')}`;
79
+ else if (code === 0x2028 || code === 0x2029 || code >= 0xd800 && code <= 0xdfff) escaped += `\\u${code.toString(16).padStart(4, '0')}`;
80
+ else escaped += character;
81
+ }
82
+ return escaped;
83
+ };
84
+ const quoted = (delimiter, value) => `${delimiter}${escapeLiteral(value, delimiter)}${delimiter}`;
85
+ const quote = (literal, value) => quoted(slice(start(literal), start(literal) + 1), value);
86
+ const safeKeyText = key => IDENTIFIER.test(key) ? key : quoted("'", key);
87
+ const keyText = (nameNode, key) => ts.isStringLiteral(nameNode) ? quote(nameNode, key) : safeKeyText(key);
88
+ const isStringValue = node => ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node);
89
+ const literalKey = property => property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ? property.name.text : undefined;
90
+
91
+ const isLibraryMemberCall = (node, owner, name) => {
92
+ if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) || node.expression.name.text !== name) return false;
93
+ const coverage = library.memberCoverage(library.symbolAt(node.expression.name));
94
+ return coverage.complete && coverage.members.length > 0 && coverage.members.every(member => member.owner === owner && member.name === name);
95
+ };
96
+
97
+ const childLifetimeAdjustments = new Map();
98
+ const childLifetimeManual = new Map();
99
+ const childManualReason = 'this child replacement may change a root-lifetime service; pin lifetimes and scoped captures by hand';
100
+
101
+ const localConst = expression => {
102
+ let node = expression;
103
+ while (ts.isParenthesizedExpression(node)) node = node.expression;
104
+ if (!ts.isIdentifier(node)) return undefined;
105
+ const symbol = ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node
106
+ ? checker.getShorthandAssignmentValueSymbol(node.parent)
107
+ : library.symbolAt(node);
108
+ const declarations = symbol?.declarations ?? [];
109
+ if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0])) return undefined;
110
+ const declaration = declarations[0];
111
+ return declaration.getSourceFile() === sourceFile && declaration.parent.flags & ts.NodeFlags.Const && declaration.initializer !== undefined ? declaration : undefined;
112
+ };
113
+
114
+ const immutableOrigin = (expression, seen = new Set()) => {
115
+ let node = expression;
116
+ while (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node)) node = node.expression;
117
+ const declaration = localConst(node);
118
+ if (declaration === undefined || seen.has(declaration)) return node;
119
+ seen.add(declaration);
120
+ return immutableOrigin(declaration.initializer, seen);
121
+ };
122
+
123
+ const containerIdentity = (expression, seen = new Set()) => {
124
+ const declaration = localConst(expression);
125
+ if (declaration === undefined || seen.has(declaration)) return undefined;
126
+ seen.add(declaration);
127
+ let initializer = declaration.initializer;
128
+ while (ts.isParenthesizedExpression(initializer)) initializer = initializer.expression;
129
+ const alias = localConst(initializer);
130
+ if (alias !== undefined) return containerIdentity(initializer, seen);
131
+ return declaration;
132
+ };
133
+
134
+ const literalKeys = node => {
135
+ if (!node) return undefined;
136
+ node = immutableOrigin(node);
137
+ if (!ts.isArrayLiteralExpression(node)) return undefined;
138
+ const keys = [];
139
+ for (const element of node.elements) {
140
+ if (!isStringValue(element)) return undefined;
141
+ keys.push(element.text);
142
+ }
143
+ return keys;
144
+ };
145
+
146
+ const literalSlots = node => {
147
+ if (!node) return undefined;
148
+ node = immutableOrigin(node);
149
+ if (!ts.isObjectLiteralExpression(node)) return undefined;
150
+ const slots = new Map();
151
+ for (const property of node.properties) {
152
+ if (!ts.isPropertyAssignment(property) || ts.isComputedPropertyName(property.name)) return undefined;
153
+ const key = literalKey(property);
154
+ if (key === undefined || slots.has(key)) return undefined;
155
+ slots.set(key, property.initializer);
156
+ }
157
+ return slots;
158
+ };
159
+
160
+ const shareKeys = call => {
161
+ if (call.arguments.length < 3) return [];
162
+ const options = immutableOrigin(call.arguments[2]);
163
+ if (!ts.isObjectLiteralExpression(options) || options.properties.length !== 1) return undefined;
164
+ const property = options.properties[0];
165
+ if (!ts.isPropertyAssignment(property) || literalKey(property) !== 'share') return undefined;
166
+ return literalKeys(property.initializer);
167
+ };
168
+
169
+ const factoryDependencies = node => {
170
+ if (!ts.isArrowFunction(node) && !ts.isFunctionExpression(node)) return undefined;
171
+ if (node.parameters.length === 0) return [];
172
+ if (node.parameters.length !== 1) return undefined;
173
+ const type = checker.getTypeAtLocation(node.parameters[0]);
174
+ if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.TypeParameter)) return undefined;
175
+ return checker.getPropertiesOfType(type).map(property => property.name);
176
+ };
177
+
178
+ function providerInfo(node) {
179
+ const direct = factoryDependencies(node);
180
+ if (direct !== undefined) return { dependencies: direct, lifetime: 'scoped' };
181
+ if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return undefined;
182
+ const name = node.expression.name.text;
183
+ if (isLibraryMemberCall(node, 'DiBagApi', 'withLifetime')) {
184
+ const inner = providerInfo(node.arguments[0]);
185
+ const lifetime = node.arguments[1];
186
+ if (inner === undefined || !isStringValue(lifetime) || !['root', 'scoped', 'transient'].includes(lifetime.text)) return undefined;
187
+ let allows;
188
+ if (node.arguments[2] !== undefined) {
189
+ const options = node.arguments[2];
190
+ if (!ts.isObjectLiteralExpression(options) || options.properties.length !== 1) return undefined;
191
+ const property = options.properties[0];
192
+ if (!ts.isPropertyAssignment(property) || literalKey(property) !== 'allowScopedDependencies'
193
+ || (property.initializer.kind !== ts.SyntaxKind.TrueKeyword && property.initializer.kind !== ts.SyntaxKind.FalseKeyword)) return undefined;
194
+ allows = property.initializer.kind === ts.SyntaxKind.TrueKeyword;
195
+ }
196
+ return { ...inner, lifetime: lifetime.text, lifetimeCall: node, allows };
197
+ }
198
+ if (['withDisposal', 'withMetadata', 'transformService'].includes(name) && isLibraryMemberCall(node, 'DiBagApi', name)) return providerInfo(node.arguments[0]);
199
+ if (['fromFactory', 'fromSyncFactory', 'fromAsyncFactory'].includes(name) && isLibraryMemberCall(node, 'DiBagApi', name)) return providerInfo(node.arguments[0]);
200
+ return undefined;
201
+ }
202
+
203
+ const builderSlots = declaration => {
204
+ let initializer = declaration.initializer;
205
+ while (ts.isParenthesizedExpression(initializer) || ts.isAwaitExpression(initializer)) initializer = initializer.expression;
206
+ if (!ts.isCallExpression(initializer) || !ts.isPropertyAccessExpression(initializer.expression)
207
+ || !['build', 'buildAndStart'].some(name => isLibraryMemberCall(initializer, 'Builder', name))) return undefined;
208
+ let expression = initializer.expression.expression;
209
+ let slots;
210
+ while (ts.isCallExpression(expression) && ts.isPropertyAccessExpression(expression.expression)) {
211
+ const name = expression.expression.name.text;
212
+ if (isLibraryMemberCall(expression, 'Builder', 'register')) {
213
+ if (slots !== undefined || expression.arguments.length !== 1) return undefined;
214
+ slots = literalSlots(expression.arguments[0]);
215
+ if (slots === undefined) return undefined;
216
+ } else if (!isLibraryMemberCall(expression, 'DiBagApi', 'createBuilder')) return undefined;
217
+ expression = expression.expression.expression;
218
+ }
219
+ return slots;
220
+ };
221
+
222
+ const containerSlots = declaration => {
223
+ const built = builderSlots(declaration);
224
+ if (built !== undefined) return built;
225
+ let initializer = declaration.initializer;
226
+ while (ts.isParenthesizedExpression(initializer) || ts.isAwaitExpression(initializer)) initializer = initializer.expression;
227
+ return isLibraryMemberCall(initializer, 'Bag', 'createScope') && (initializer.arguments.length === 2 || initializer.arguments.length === 3)
228
+ ? literalSlots(initializer.arguments[1])
229
+ : undefined;
230
+ };
231
+
232
+ const hasContainerEscape = identity => {
233
+ let escaped = false;
234
+ const visit = node => {
235
+ if (escaped) return;
236
+ if (ts.isIdentifier(node)) {
237
+ const declaration = localConst(node);
238
+ if (declaration !== undefined && containerIdentity(node) === identity) {
239
+ const statement = declaration.parent?.parent;
240
+ const exported = statement && ts.isVariableStatement(statement)
241
+ && statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword);
242
+ if (exported) escaped = true;
243
+ else if (node === declaration.name) { /* private declaration */ }
244
+ else if (ts.isVariableDeclaration(node.parent) && node.parent.initializer === node
245
+ && node.parent.parent.flags & ts.NodeFlags.Const && ts.isIdentifier(node.parent.name)) { /* private immutable alias */ }
246
+ else if (ts.isPropertyAccessExpression(node.parent) && node.parent.expression === node
247
+ && ts.isCallExpression(node.parent.parent) && node.parent.parent.expression === node.parent) {
248
+ const coverage = library.memberCoverage(library.symbolAt(node.parent.name));
249
+ if (!coverage.complete || coverage.members.length === 0 || coverage.members.some(member => member.owner !== 'Bag')) escaped = true;
250
+ } else escaped = true;
251
+ }
252
+ }
253
+ ts.forEachChild(node, visit);
254
+ };
255
+ visit(sourceFile);
256
+ return escaped;
257
+ };
258
+
259
+ const childCalls = [];
260
+ const collectChildCalls = node => {
261
+ if (isLibraryMemberCall(node, 'Bag', 'createScope')) childCalls.push(node);
262
+ ts.forEachChild(node, collectChildCalls);
263
+ };
264
+ collectChildCalls(sourceFile);
265
+
266
+ for (const call of childCalls) {
267
+ if (call.arguments.length !== 2 && call.arguments.length !== 3) continue;
268
+ const selected = literalKeys(call.arguments[0]);
269
+ const replacements = literalSlots(call.arguments[1]);
270
+ const identity = containerIdentity(call.expression.expression);
271
+ if (selected === undefined || replacements === undefined || selected.some(key => !replacements.has(key)) || identity === undefined) {
272
+ childLifetimeManual.set(call, childManualReason);
273
+ continue;
274
+ }
275
+ const slots = containerSlots(identity);
276
+ if (slots === undefined) { childLifetimeManual.set(call, childManualReason); continue; }
277
+ if (selected.some(key => !slots.has(key))) { childLifetimeManual.set(call, childManualReason); continue; }
278
+ const infos = new Map();
279
+ let failed = false;
280
+ for (const [key, provider] of slots) {
281
+ const info = providerInfo(provider);
282
+ if (info === undefined) { failed = true; break; }
283
+ infos.set(key, info);
284
+ }
285
+ const downgraded = selected.filter(key => infos.get(key)?.lifetime === 'root');
286
+ if (failed || downgraded.some(key => infos.get(key)?.lifetimeCall === undefined)) { childLifetimeManual.set(call, childManualReason); continue; }
287
+ if (downgraded.length === 0) continue;
288
+ if (hasContainerEscape(identity)) { childLifetimeManual.set(call, childManualReason); continue; }
289
+ const siblings = childCalls.filter(candidate => candidate !== call && containerIdentity(candidate.expression.expression) === identity);
290
+ for (const sibling of siblings) {
291
+ const siblingSelected = literalKeys(sibling.arguments[0]);
292
+ const siblingShared = shareKeys(sibling);
293
+ if (siblingSelected === undefined || siblingShared === undefined
294
+ || downgraded.some(key => !siblingSelected.includes(key) && !siblingShared.includes(key))) failed = true;
295
+ }
296
+ const reaches = (from, targets, visiting = new Set()) => {
297
+ if (targets.has(from)) return true;
298
+ if (visiting.has(from)) return false;
299
+ visiting.add(from);
300
+ const info = infos.get(from);
301
+ return info !== undefined && info.dependencies.some(dependency => reaches(dependency, targets, visiting));
302
+ };
303
+ const consumers = [];
304
+ for (const [key, info] of infos) {
305
+ if (info.lifetime === 'root' && !downgraded.includes(key) && reaches(key, new Set(downgraded))) {
306
+ if (info.lifetimeCall === undefined || info.allows === false) failed = true;
307
+ else consumers.push(info.lifetimeCall);
308
+ }
309
+ }
310
+ if (failed) { childLifetimeManual.set(call, childManualReason); continue; }
311
+ for (const key of downgraded) childLifetimeAdjustments.set(infos.get(key).lifetimeCall, { lifetime: 'scoped:one-per-container' });
312
+ for (const lifetimeCall of consumers) childLifetimeAdjustments.set(lifetimeCall, { lifetime: 'singleton:one-per-container-tree', addAllows: true });
313
+ }
314
+
315
+ /**
316
+ * Rewrite the properties of an object literal.
317
+ * `rename(key)` gives a new key; `value(key, current)` a new string value; `nested(key, literal)` the whole
318
+ * text of a nested literal; `replace[key](property)` the whole property, `null` to drop it, `undefined` to give up.
319
+ * @returns {{ text: string, changed: boolean, remaining: number } | undefined} undefined when a `replace` callback gave up.
320
+ */
321
+ function objectLiteral(literal, { rename = () => undefined, value = () => undefined, renamedValues = () => [], nested = () => undefined, replace = {}, spreadReason } = {}) {
322
+ const replacements = [];
323
+ const properties = literal.properties;
324
+ let removed = 0;
325
+ let previousRemoved = false;
326
+ for (let position = 0; position < properties.length; position++) {
327
+ const property = properties[position];
328
+ const wasPreviousRemoved = previousRemoved;
329
+ previousRemoved = false;
330
+ if (ts.isSpreadAssignment(property)) { if (spreadReason) manual(property, spreadReason); continue; }
331
+ const key = literalKey(property);
332
+ if (key === undefined) continue;
333
+ if (Object.hasOwn(replace, key)) {
334
+ const result = replace[key](property);
335
+ if (result === undefined) return undefined;
336
+ if (result === null) {
337
+ removed++;
338
+ previousRemoved = true;
339
+ if (position + 1 < properties.length) replacements.push({ start: start(property), end: start(properties[position + 1]), text: '' });
340
+ else replacements.push({ start: position > 0 && !wasPreviousRemoved ? properties[position - 1].end : start(property), end: property.end, text: '' });
341
+ } else replacements.push({ start: start(property), end: property.end, text: result });
342
+ continue;
343
+ }
344
+ const newKey = rename(key, property);
345
+ if (ts.isShorthandPropertyAssignment(property)) {
346
+ if (newKey !== undefined && newKey !== key) replacements.push({ start: start(property), end: property.end, text: `${keyText(property.name, newKey)}: ${key}` });
347
+ if (renamedValues(key).length) manual(property, `the value of ${key} is not a string literal; where it is produced, rename ${renamedValues(key).join(', ')}`);
348
+ continue;
349
+ }
350
+ if (newKey !== undefined && newKey !== key) replacements.push({ start: start(property.name), end: property.name.end, text: keyText(property.name, newKey) });
351
+ if (!ts.isPropertyAssignment(property)) continue;
352
+ const initializer = property.initializer;
353
+ if (isStringValue(initializer)) {
354
+ const newValue = value(key, initializer.text);
355
+ if (newValue !== undefined) replacements.push({ start: start(initializer), end: initializer.end, text: quote(initializer, newValue) });
356
+ } else if (renamedValues(key).length && !ts.isObjectLiteralExpression(initializer)) {
357
+ manual(initializer, `the value of ${key} is not a string literal; where it is produced, rename ${renamedValues(key).join(', ')}`);
358
+ } else if (ts.isObjectLiteralExpression(initializer)) {
359
+ const newText = nested(key, initializer);
360
+ if (newText !== undefined) replacements.push({ start: start(initializer), end: initializer.end, text: newText });
361
+ }
362
+ }
363
+ return { text: assemble(literal, replacements), changed: replacements.length > 0, remaining: properties.length - removed };
364
+ }
365
+
366
+ /** An object-literal argument of a library call, rewritten by the map's `options` and `values` entries for that path. */
367
+ function argumentObject(literal, member, argument, path) {
368
+ const { owner, name } = member;
369
+ const renames = new Map(index.optionsFor(owner, name, argument, path).map(entry => [entry.from, entry.to]));
370
+ return objectLiteral(literal, {
371
+ rename: key => renames.get(key),
372
+ value: (key, current) => index.valuesFor(owner, name, argument, [...path, key]).find(entry => entry.from === current)?.to,
373
+ renamedValues: key => index.valuesFor(owner, name, argument, [...path, key]).map(entry => `'${entry.from}' to '${entry.to}'`),
374
+ nested: (key, inner) => index.hasEntriesBelow(owner, name, argument, [...path, key]) ? argumentObject(inner, member, argument, [...path, key]).text : undefined,
375
+ spreadReason: renames.size ? 'an options object is spread here; rename its keys where that object is built' : undefined,
376
+ });
377
+ }
378
+
379
+ /** The transformed text of one call argument, with the map's entries for its position applied. */
380
+ function argumentText(argumentNode, member, argument) {
381
+ if (isStringValue(argumentNode)) {
382
+ const entry = index.valuesFor(member.owner, member.name, argument, []).find(candidate => candidate.from === argumentNode.text);
383
+ if (entry) return quote(argumentNode, entry.to);
384
+ }
385
+ if (ts.isObjectLiteralExpression(argumentNode) && index.hasEntriesBelow(member.owner, member.name, argument, [])) return argumentObject(argumentNode, member, argument, []).text;
386
+ return text(argumentNode);
387
+ }
388
+
389
+ const lineIndent = position => /^[ \t]*/.exec(slice(source.lastIndexOf('\n', position - 1) + 1, position))[0];
390
+
391
+ /** The one exported DI Bag type of an expression, ignoring only null and undefined union members. */
392
+ function libraryTypeName(node) {
393
+ const names = new Set();
394
+ let unknown = false;
395
+ const visit = type => {
396
+ if (type.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) return;
397
+ if (type.isUnion()) { for (const member of type.types) visit(member); return; }
398
+ const symbol = type.aliasSymbol ?? type.symbol;
399
+ const name = symbol === undefined ? undefined : library.exportNameOf(symbol);
400
+ if (name === undefined) unknown = true;
401
+ else names.add(name);
402
+ };
403
+ visit(checker.getTypeAtLocation(node));
404
+ return !unknown && names.size === 1 ? [...names][0] : undefined;
405
+ }
406
+
407
+ const resolvedSymbol = node => {
408
+ let symbol = checker.getSymbolAtLocation(node);
409
+ if (symbol && symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
410
+ return symbol;
411
+ };
412
+ const isWritableDeclaration = declaration => !declaration.getSourceFile().isDeclarationFile
413
+ && writableSourceFiles.has(declaration.getSourceFile().fileName);
414
+ const unwrapExpression = node => {
415
+ let current = node;
416
+ while (ts.isParenthesizedExpression(current)) current = current.expression;
417
+ return current;
418
+ };
419
+
420
+ function supportedOptionLiteral(node, owner) {
421
+ if (!ts.isObjectLiteralExpression(node) || node.properties.some(property => ts.isSpreadAssignment(property))) return false;
422
+ const contextual = checker.getContextualType(node);
423
+ if (contextual === undefined || libraryTypeNameFromType(checker.getNonNullableType(contextual)) !== owner) return false;
424
+ return node.properties.every(property => {
425
+ const key = literalKey(property);
426
+ const entry = key === undefined ? undefined : index.properties.get(`${owner}.${key}`);
427
+ return entry?.to !== undefined;
428
+ });
429
+ }
430
+
431
+ function libraryTypeNameFromType(type) {
432
+ const names = new Set();
433
+ let unknown = false;
434
+ const visit = candidate => {
435
+ if (candidate.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) return;
436
+ if (candidate.isUnion()) { for (const member of candidate.types) visit(member); return; }
437
+ const symbol = candidate.aliasSymbol ?? candidate.symbol;
438
+ const name = symbol === undefined ? undefined : library.exportNameOf(symbol);
439
+ if (name === undefined) unknown = true;
440
+ else names.add(name);
441
+ };
442
+ visit(type);
443
+ return !unknown && names.size === 1 ? [...names][0] : undefined;
444
+ }
445
+
446
+ const enclosingFunction = node => {
447
+ for (let current = node.parent; current; current = current.parent) if (ts.isFunctionLike(current)) return current;
448
+ return undefined;
449
+ };
450
+ const exportedBoundary = fn => {
451
+ if (ts.isFunctionDeclaration(fn) && fn.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) return true;
452
+ for (let current = fn.parent; current; current = current.parent) {
453
+ if (ts.isFunctionDeclaration(current)) return Boolean(current.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword));
454
+ }
455
+ return false;
456
+ };
457
+ const functionSymbol = fn => {
458
+ if (fn.name && ts.isIdentifier(fn.name)) return resolvedSymbol(fn.name);
459
+ if (ts.isVariableDeclaration(fn.parent) && ts.isIdentifier(fn.parent.name)) return resolvedSymbol(fn.parent.name);
460
+ return undefined;
461
+ };
462
+ const parameterProof = new Map();
463
+ function provenParameter(parameter, owner, seen) {
464
+ const cacheKey = `${parameter.getSourceFile().fileName}:${parameter.pos}:${owner}`;
465
+ if (parameterProof.has(cacheKey)) return parameterProof.get(cacheKey);
466
+ const fn = enclosingFunction(parameter);
467
+ if (fn === undefined || !isWritableDeclaration(parameter)) return false;
468
+ const position = fn.parameters.indexOf(parameter);
469
+ const symbol = functionSymbol(fn);
470
+ let calls = 0;
471
+ let valid = true;
472
+ if (symbol !== undefined) {
473
+ for (const candidateFile of program.getSourceFiles()) {
474
+ if (candidateFile.isDeclarationFile) continue;
475
+ const visit = node => {
476
+ if (!valid) return;
477
+ if (ts.isCallExpression(node) && resolvedSymbol(ts.isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression) === symbol) {
478
+ calls++;
479
+ if (!writableSourceFiles.has(candidateFile.fileName)) { valid = false; return; }
480
+ const argument = node.arguments[position];
481
+ if (argument !== undefined && !provenOptionOrigin(argument, owner, seen)) valid = false;
482
+ }
483
+ ts.forEachChild(node, visit);
484
+ };
485
+ visit(candidateFile);
486
+ }
487
+ }
488
+ const result = valid && (calls > 0 || exportedBoundary(fn));
489
+ parameterProof.set(cacheKey, result);
490
+ return result;
491
+ }
492
+
493
+ function provenOptionOrigin(node, owner, seen = new Set()) {
494
+ const expression = unwrapExpression(node);
495
+ if (seen.has(expression)) return false;
496
+ seen.add(expression);
497
+ if (supportedOptionLiteral(expression, owner)) return true;
498
+ if (libraryTypeName(expression) !== owner) return false;
499
+ if (!ts.isIdentifier(expression)) return false;
500
+ const symbol = resolvedSymbol(expression);
501
+ const declarations = symbol?.declarations ?? [];
502
+ if (declarations.length !== 1) return false;
503
+ const declaration = declarations[0];
504
+ if (ts.isParameter(declaration)) return provenParameter(declaration, owner, seen);
505
+ if (!ts.isVariableDeclaration(declaration) || !isWritableDeclaration(declaration)
506
+ || !(declaration.parent.flags & ts.NodeFlags.Const) || declaration.initializer === undefined) return false;
507
+ return provenOptionOrigin(declaration.initializer, owner, seen);
508
+ }
509
+
510
+ /** A typed nonliteral is safe when its library type carries the same property migration as this call argument. */
511
+ function typedArgumentCarriesMappedShape(node, member, position) {
512
+ const owner = libraryTypeName(node);
513
+ if (owner === undefined) return false;
514
+ const entries = index.optionsFor(member.owner, member.name, position, []);
515
+ return entries.length > 0 && entries.every(entry => index.properties.get(`${owner}.${entry.from}`)?.to === entry.to)
516
+ && provenOptionOrigin(node, owner);
517
+ }
518
+
519
+ /** Positional arguments as one options bag, or undefined after reporting why it cannot be done. */
520
+ function bagArguments(call, entry, member) {
521
+ const { names, trailing } = entry.arguments;
522
+ const argumentNodes = [...call.arguments];
523
+ const extra = argumentNodes.slice(names.length);
524
+ if (extra.length > 1 || (extra.length === 1 && !trailing)) { manual(call, `${member.name} has more arguments than the rename map describes; rewrite it to ${entry.to} by hand`); return undefined; }
525
+ if (extra.length === 1 && trailing.mode === 'merge' && !ts.isObjectLiteralExpression(extra[0])) {
526
+ manual(call, `the last argument of ${member.name} is not an object literal; merge it into the ${entry.to} bag by hand`);
527
+ return undefined;
528
+ }
529
+ const rewriteCheckpoint = rewrites;
530
+ const parts = argumentNodes.slice(0, names.length).map((argumentNode, position) => {
531
+ const value = argumentText(argumentNode, member, position);
532
+ return value === names[position] && IDENTIFIER.test(names[position]) ? value : `${safeKeyText(names[position])}: ${value}`;
533
+ });
534
+ if (blockedReshapes.has(call)) { rewrites = rewriteCheckpoint; return RESHAPE_BLOCKED; }
535
+ let after = '';
536
+ if (extra.length === 1 && trailing.mode === 'keep') after = `, ${argumentText(extra[0], member, names.length)}`;
537
+ if (extra.length === 1 && trailing.mode === 'merge') {
538
+ const keys = trailing.keys ?? {};
539
+ const merged = objectLiteral(extra[0], { rename: key => keys[key], spreadReason: 'an options object is spread here; rename its keys where that object is built' });
540
+ const inner = merged.text.slice(1, -1).trim().replace(/,$/, '');
541
+ if (inner) parts.push(inner);
542
+ }
543
+ if (parts.length === 0) return after.replace(/^, /, '');
544
+ const multiline = parts.some(part => part.includes('\n')) || slice(call.arguments.pos, call.arguments.end).includes('\n');
545
+ if (!multiline) return `{ ${parts.join(', ')} }${after}`;
546
+ const indent = lineIndent(start(call.expression.name));
547
+ return `{\n${parts.map(part => `${indent} ${part},`).join('\n')}\n${indent}}${after}`;
548
+ }
549
+
550
+ /** Whether one argument is proven to be a legacy positional array or an existing options bag. */
551
+ function alreadyBagShape(node, key) {
552
+ const classify = type => {
553
+ if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) return 'ambiguous';
554
+ if (type.isUnion()) {
555
+ const choices = new Set(type.types.map(classify));
556
+ return choices.size === 1 ? [...choices][0] : 'ambiguous';
557
+ }
558
+ const signals = candidate => {
559
+ if (candidate.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) return { array: false, bag: false, uncertain: true };
560
+ if (candidate.isIntersection()) {
561
+ return candidate.types.map(signals).reduce((left, right) => ({
562
+ array: left.array || right.array,
563
+ bag: left.bag || right.bag,
564
+ uncertain: left.uncertain || right.uncertain,
565
+ }), { array: false, bag: false, uncertain: false });
566
+ }
567
+ const property = checker.getPropertyOfType(candidate, key);
568
+ const optional = property !== undefined && Boolean(property.flags & ts.SymbolFlags.Optional);
569
+ return {
570
+ array: checker.isArrayType(candidate) || checker.isTupleType(candidate),
571
+ bag: property !== undefined && !optional,
572
+ uncertain: optional,
573
+ };
574
+ };
575
+ const shape = signals(type);
576
+ if (shape.uncertain) return 'ambiguous';
577
+ if (shape.array && !shape.bag) return 'positional';
578
+ if (shape.bag && !shape.array) return 'bag';
579
+ return 'ambiguous';
580
+ };
581
+ return classify(checker.getTypeAtLocation(node));
582
+ }
583
+
584
+ function reportAnyReceiver(callee) {
585
+ const name = callee.name.text;
586
+ if (BUILTIN_MEMBERS.has(name) || !index.methodNames.has(name)) return;
587
+ const receiver = checker.getTypeAtLocation(callee.expression);
588
+ if (receiver.flags & ts.TypeFlags.Any) manual(callee, `the receiver of ${name} has type any, so this call cannot be checked; migrate it by hand if it is a DI Bag call`);
589
+ }
590
+
591
+ function transformApi(member, entry) {
592
+ return {
593
+ ts, checker, program, library, sourceFile, member,
594
+ text, slice, start, assemble, objectLiteral, quote, manual,
595
+ provenOptionOrigin,
596
+ childLifetimeAdjustment: node => childLifetimeAdjustments.get(node),
597
+ childLifetimeManualReason: node => childLifetimeManual.get(node),
598
+ nameOf: index.nameOf,
599
+ nameForRole(role) {
600
+ const value = entry?.transformNames?.[role];
601
+ if (value === undefined) throw new Error(`transform ${entry?.transform ?? '<unknown>'} has no name for role ${role}`);
602
+ return value;
603
+ },
604
+ abortParentReshape(node, expected) {
605
+ const parent = node.parent;
606
+ if (!ts.isCallExpression(parent) || parent.arguments[expected.argument] !== node) return;
607
+ const callee = parent.expression;
608
+ if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== expected.name) return;
609
+ const coverage = library.memberCoverage(library.symbolAt(callee.name));
610
+ if (coverage.complete && coverage.members.length > 0
611
+ && coverage.members.every(candidate => candidate.owner === expected.owner && candidate.name === expected.name)) {
612
+ blockedReshapes.add(parent);
613
+ }
614
+ },
615
+ };
616
+ }
617
+
618
+ const partialReason = name => `${name} resolves to both DI Bag and non-library declarations; migrate this use by hand`;
619
+ const conflictReason = name => `${name} resolves to declarations with incompatible rename-map entries; migrate this use by hand`;
620
+ const withoutOwner = entry => {
621
+ const { owner: _owner, ...rest } = entry;
622
+ return rest;
623
+ };
624
+
625
+ /** The map behavior relevant to this call for one possible library owner. */
626
+ function callPlan(member, name, count) {
627
+ const entry = index.methodFor(member.owner, name, count);
628
+ const touched = Array.from({ length: count }, (_, position) => position)
629
+ .filter(position => index.hasEntriesBelow(member.owner, name, position, []));
630
+ if (!entry && touched.length === 0) return undefined;
631
+ const key = `${member.owner}.${name}`;
632
+ const method = entry === undefined ? undefined : withoutOwner(entry);
633
+ if (method) delete method.arity;
634
+ const options = (index.options.get(key) ?? []).filter(candidate => candidate.argument < count).map(withoutOwner);
635
+ const values = (index.argumentValues.get(key) ?? []).filter(candidate => candidate.argument < count).map(withoutOwner);
636
+ return { member, entry, touched, signature: JSON.stringify({ method, options, values }) };
637
+ }
638
+
639
+ function rewriteCall(call) {
640
+ const callee = call.expression;
641
+ if (!ts.isPropertyAccessExpression(callee) || !index.callNames.has(callee.name.text)) return undefined;
642
+ const name = callee.name.text;
643
+ const coverage = library.memberCoverage(library.symbolAt(callee.name));
644
+ const { members } = coverage;
645
+ if (members.length === 0) { reportAnyReceiver(callee); return undefined; }
646
+ const plans = members.map(member => callPlan(member, name, call.arguments.length));
647
+ const relevant = plans.filter(Boolean);
648
+ if (relevant.length === 0) return undefined;
649
+ if (!coverage.complete) { manual(call, partialReason(name)); skip.add(callee); return undefined; }
650
+ if (relevant.length !== members.length || new Set(relevant.map(plan => plan.signature)).size !== 1) {
651
+ manual(call, conflictReason(name));
652
+ skip.add(callee);
653
+ return undefined;
654
+ }
655
+ const { member, entry, touched } = relevant[0];
656
+ if ((entry?.arguments || entry?.transform) && call.arguments.some(ts.isSpreadElement)) {
657
+ manual(call, `${name} is called with a spread argument; rewrite it to ${entry.to} by hand`);
658
+ if (entry.transform) skip.add(call);
659
+ return undefined;
660
+ }
661
+ if (entry?.transform) {
662
+ const result = transforms[entry.transform](call, transformApi(member, entry));
663
+ if (result === undefined) skip.add(call);
664
+ return result;
665
+ }
666
+ const replacements = [];
667
+ if (entry && entry.to !== name) replacements.push({ start: start(callee.name), end: callee.name.end, text: entry.to });
668
+ if (entry?.arguments?.kind === 'bag') {
669
+ if (entry.arguments.alreadyBag && call.arguments.length === 1) {
670
+ const shape = alreadyBagShape(call.arguments[0], entry.arguments.names[0]);
671
+ if (shape === 'bag') return undefined;
672
+ if (shape === 'ambiguous') {
673
+ manual(call, `the argument of ${name} could be either its positional value or an existing options bag; migrate this call by hand`);
674
+ return undefined;
675
+ }
676
+ }
677
+ const bag = bagArguments(call, entry, member);
678
+ if (bag === RESHAPE_BLOCKED) { skip.add(call); return undefined; }
679
+ if (bag === undefined) return undefined;
680
+ // Up to the closing parenthesis, so a bag written over several lines does not leave `}` and `)` on separate lines.
681
+ const closing = source[call.end - 1] === ')' ? call.end - 1 : call.arguments.end;
682
+ if (call.arguments.pos !== closing || bag !== '') replacements.push({ start: call.arguments.pos, end: closing, text: bag });
683
+ } else if (entry?.arguments?.kind === 'array') {
684
+ if (call.arguments.length !== 1) { manual(call, `${name} is expected to take one argument; rewrite it to ${entry.to} by hand`); return undefined; }
685
+ replacements.push({ start: start(call.arguments[0]), end: call.arguments[0].end, text: `[${argumentText(call.arguments[0], member, 0)}]` });
686
+ } else {
687
+ for (const position of touched) {
688
+ const argumentNode = call.arguments[position];
689
+ if (isStringValue(argumentNode) || ts.isObjectLiteralExpression(argumentNode)) replacements.push({ start: start(argumentNode), end: argumentNode.end, text: argumentText(argumentNode, member, position) });
690
+ else if (!typedArgumentCarriesMappedShape(argumentNode, member, position)) manual(argumentNode, `argument ${position + 1} of ${name} is not a literal; where it is built, apply: ${index.describeArgumentEntries(member.owner, name, position)}`);
691
+ }
692
+ }
693
+ return replacements.length ? assemble(call, replacements) : undefined;
694
+ }
695
+
696
+ /** The single rename target of a member, `null` when it must stay, after reporting what cannot be decided. */
697
+ function memberRename(node, coverage, name, { called }) {
698
+ const { members } = coverage;
699
+ const propertyEntries = members.map(member => index.properties.get(`${member.owner}.${name}`));
700
+ if (propertyEntries.some(Boolean)) {
701
+ if (!coverage.complete) { manual(node, partialReason(name)); return null; }
702
+ if (propertyEntries.some(entry => !entry)) { manual(node, `${name} resolves to several declarations and the rename map covers only some of them`); return null; }
703
+ const guidance = new Set(propertyEntries.map(entry => entry.manual === undefined ? `to:${entry.to}` : `manual:${entry.manual}`));
704
+ if (guidance.size !== 1) { manual(node, conflictReason(name)); return null; }
705
+ const entry = propertyEntries[0];
706
+ if (entry.manual !== undefined) { manual(node, entry.manual); return null; }
707
+ return entry.to;
708
+ }
709
+ if (called) return null;
710
+ const entries = members.map(member => index.methodFor(member.owner, name, undefined));
711
+ const mapped = members.map(member => index.hasMethodEntries(member.owner, name));
712
+ if (!mapped.some(Boolean)) return null;
713
+ if (!coverage.complete) { manual(node, partialReason(name)); return null; }
714
+ if (mapped.some(value => !value)) { manual(node, `${name} resolves to several declarations and the rename map covers only some of them`); return null; }
715
+ if (entries.some(entry => !entry)) { manual(node, `${name} has incompatible arity-specific rename-map entries; migrate this reference by hand`); return null; }
716
+ const signatures = new Set(entries.map(entry => {
717
+ const mapped = withoutOwner(entry);
718
+ delete mapped.arity;
719
+ return JSON.stringify(mapped);
720
+ }));
721
+ if (signatures.size !== 1) { manual(node, conflictReason(name)); return null; }
722
+ const entry = entries[0];
723
+ if (entry.transform || entry.arguments) { manual(node, `${name} is referenced without being called; rewrite this reference to ${entry.to} by hand`); return null; }
724
+ return entry.to;
725
+ }
726
+
727
+ function rewritePropertyAccess(node) {
728
+ const name = node.name.text;
729
+ if (skip.has(node) || !index.memberNames.has(name)) return undefined;
730
+ const coverage = library.memberCoverage(library.symbolAt(node.name));
731
+ if (coverage.members.length === 0) return undefined;
732
+ const called = ts.isCallExpression(node.parent) && node.parent.expression === node;
733
+ const target = memberRename(node, coverage, name, { called });
734
+ return target === null || target === name ? undefined : assemble(node, [{ start: start(node.name), end: node.name.end, text: target }]);
735
+ }
736
+
737
+ function rewriteTypeQuery(node) {
738
+ const expression = node.exprName;
739
+ if (!ts.isQualifiedName(expression)) return undefined;
740
+ const nameNode = expression.right;
741
+ const name = nameNode.text;
742
+ if (!index.memberNames.has(name)) return undefined;
743
+ const coverage = library.memberCoverage(library.symbolAt(nameNode));
744
+ if (coverage.members.length === 0) return undefined;
745
+ const manualCheckpoint = manualItems.length;
746
+ const target = memberRename(expression, coverage, name, { called: false });
747
+ if (target === null) {
748
+ if (manualItems.length > manualCheckpoint) skip.add(node);
749
+ return undefined;
750
+ }
751
+ if (target === name) return undefined;
752
+ return assemble(node, [{ start: start(nameNode), end: nameNode.end, text: target }]);
753
+ }
754
+
755
+ function rewriteBindingElement(element) {
756
+ if (!ts.isObjectBindingPattern(element.parent)) return undefined;
757
+ const keyNode = element.propertyName ?? element.name;
758
+ if (!ts.isIdentifier(keyNode) || !index.memberNames.has(keyNode.text)) return undefined;
759
+ const coverage = library.memberCoverage(checker.getTypeAtLocation(element.parent).getProperty(keyNode.text));
760
+ const { members } = coverage;
761
+ if (members.length === 0) return undefined;
762
+ const entry = members.map(member => index.methodFor(member.owner, keyNode.text, undefined)).find(Boolean);
763
+ const reshaped = entry?.transform !== undefined || entry?.arguments !== undefined || members.some(member => index.hasArgumentEntries(member.owner, keyNode.text));
764
+ if (reshaped) manual(element, `${keyNode.text} is destructured; calls through the local name are not rewritten, migrate them by hand`);
765
+ const target = memberRename(element, coverage, keyNode.text, { called: false });
766
+ if (target === null || target === keyNode.text) return undefined;
767
+ const replacement = element.propertyName
768
+ ? { start: start(element.propertyName), end: element.propertyName.end, text: target }
769
+ : { start: start(element.name), end: element.name.end, text: `${target}: ${keyNode.text}` };
770
+ return assemble(element, [replacement]);
771
+ }
772
+
773
+ function propertyValueTarget(node, coverage, property, current) {
774
+ const candidates = coverage.members.map(member => index.propertyValues.get(`${member.owner}.${property}=${current}`));
775
+ if (!candidates.some(candidate => candidate !== undefined)) return undefined;
776
+ if (!coverage.complete) { manual(node, partialReason(property)); return undefined; }
777
+ if (candidates.some(candidate => candidate === undefined)) {
778
+ manual(node, `${property} resolves to several declarations and the rename map covers only some of them`);
779
+ return undefined;
780
+ }
781
+ const targets = new Set(candidates);
782
+ if (targets.size !== 1) { manual(node, conflictReason(property)); return undefined; }
783
+ return candidates[0];
784
+ }
785
+
786
+ function rewriteObjectLiteral(literal) {
787
+ if (!literal.properties.some(property => index.propertyNames.has(literalKey(property) ?? ''))) return undefined;
788
+ const contextual = checker.getContextualType(literal);
789
+ if (!contextual) return undefined;
790
+ const coverageFor = key => library.memberCoverage(checker.getPropertyOfType(checker.getNonNullableType(contextual), key));
791
+ const result = objectLiteral(literal, {
792
+ rename: (key, property) => {
793
+ if (!index.memberNames.has(key)) return undefined;
794
+ const coverage = coverageFor(key);
795
+ if (coverage.members.length === 0) return undefined;
796
+ const target = memberRename(property, coverage, key, { called: true });
797
+ return target === null ? undefined : target;
798
+ },
799
+ value: (key, current) => index.valuePropertyNames.has(key)
800
+ ? propertyValueTarget(literal, coverageFor(key), key, current)
801
+ : undefined,
802
+ });
803
+ return result.changed ? result.text : undefined;
804
+ }
805
+
806
+ function typeTarget(node) {
807
+ const name = ts.isTypeReferenceNode(node) ? node.typeName
808
+ : ts.isImportTypeNode(node) ? node.qualifier
809
+ : undefined;
810
+ if (name === undefined) return undefined;
811
+ const target = ts.isQualifiedName(name) ? name.right : name;
812
+ if (!ts.isIdentifier(target)) return undefined;
813
+ const canonical = library.exportNameOf(library.symbolAt(target));
814
+ const entry = canonical === undefined ? undefined : index.types.get(canonical);
815
+ return entry === undefined ? undefined : { name, target, canonical, entry };
816
+ }
817
+
818
+ function mapTypeArgument(argument, values, replacements) {
819
+ if (ts.isLiteralTypeNode(argument) && isStringValue(argument.literal)) {
820
+ const target = values[argument.literal.text];
821
+ if (target === undefined) return false;
822
+ replacements.push({ start: start(argument.literal), end: argument.literal.end, text: quote(argument.literal, target) });
823
+ return true;
824
+ }
825
+ if (ts.isUnionTypeNode(argument)) return argument.types.every(item => mapTypeArgument(item, values, replacements));
826
+ return false;
827
+ }
828
+
829
+ function rewriteMappedType(node) {
830
+ const resolved = typeTarget(node);
831
+ if (resolved === undefined || resolved.entry.genericArguments === undefined) return undefined;
832
+ const { name, target, canonical, entry } = resolved;
833
+ const replacements = [];
834
+ if (target.text === canonical || ts.isQualifiedName(name) || ts.isImportTypeNode(node)) {
835
+ replacements.push({ start: start(target), end: target.end, text: entry.to });
836
+ }
837
+ for (const rule of entry.genericArguments) {
838
+ const argument = node.typeArguments?.[rule.index];
839
+ if (argument === undefined || !mapTypeArgument(argument, rule.values, replacements)) {
840
+ manual(node, `${entry.from} has a nonliteral or unsupported generic argument ${rule.index}; rewrite it to ${entry.to} by hand`);
841
+ return replacements.length === 0 ? undefined : assemble(node, replacements);
842
+ }
843
+ }
844
+ return assemble(node, replacements);
845
+ }
846
+
847
+ function rewriteTypedLegacyLiteral(node) {
848
+ if (!isStringValue(node)) return undefined;
849
+ const contextual = checker.getContextualType(node);
850
+ const symbol = contextual?.aliasSymbol ?? contextual?.symbol;
851
+ const canonical = symbol && library.exportNameOf(symbol);
852
+ const entry = canonical === undefined ? undefined : index.types.get(canonical);
853
+ const target = entry?.literalValues?.[node.text];
854
+ return target === undefined ? undefined : quote(node, target);
855
+ }
856
+
857
+ function rewriteIdentifier(node) {
858
+ const entry = index.types.get(node.text);
859
+ if (entry === undefined) return undefined;
860
+ if ((ts.isTypeReferenceNode(node.parent) || ts.isImportTypeNode(node.parent)) && entry.genericArguments !== undefined) return undefined;
861
+ const target = entry.to;
862
+ const parent = node.parent;
863
+ if (ts.isPropertyAccessExpression(parent) && parent.name === node) return undefined;
864
+ const isKey = (ts.isPropertyAssignment(parent) || ts.isPropertySignature(parent) || ts.isPropertyDeclaration(parent) || ts.isMethodDeclaration(parent)
865
+ || ts.isMethodSignature(parent) || ts.isBindingElement(parent) || ts.isEnumMember(parent)) && parent.name === node;
866
+ if (isKey) return undefined;
867
+ if ((ts.isImportSpecifier(parent) || ts.isExportSpecifier(parent)) && parent.propertyName !== undefined && parent.propertyName !== node) return undefined;
868
+ if (library.exportNameOf(library.symbolAt(node)) !== node.text) return undefined;
869
+ if (ts.isShorthandPropertyAssignment(parent)) return `${node.text}: ${target}`;
870
+ if (ts.isExportSpecifier(parent) && parent.propertyName === undefined) {
871
+ manual(parent, `${node.text} is re-exported; the re-export keeps its old public name, rename it when your own consumers can follow`);
872
+ return `${target} as ${node.text}`;
873
+ }
874
+ return target;
875
+ }
876
+
877
+ const isModuleSpecifier = node => {
878
+ const parent = node.parent;
879
+ if ((ts.isImportDeclaration(parent) || ts.isExportDeclaration(parent)) && parent.moduleSpecifier === node) return true;
880
+ if (ts.isLiteralTypeNode(parent) && ts.isImportTypeNode(parent.parent)) return true;
881
+ if (ts.isExternalModuleReference(parent)) return true;
882
+ return ts.isCallExpression(parent) && parent.arguments[0] === node
883
+ && (parent.expression.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(parent.expression) && parent.expression.text === 'require'));
884
+ };
885
+
886
+ /** The expression a string is compared with: the other side of `===`, or the subject of its `switch`. */
887
+ function comparedWith(node) {
888
+ const parent = node.parent;
889
+ const equality = [ts.SyntaxKind.EqualsEqualsEqualsToken, ts.SyntaxKind.ExclamationEqualsEqualsToken, ts.SyntaxKind.EqualsEqualsToken, ts.SyntaxKind.ExclamationEqualsToken];
890
+ if (ts.isBinaryExpression(parent) && equality.includes(parent.operatorToken.kind)) return parent.left === node ? parent.right : parent.left;
891
+ if (ts.isCaseClause(parent) && parent.expression === node) return parent.parent.parent.expression;
892
+ return undefined;
893
+ }
894
+
895
+ function rewriteString(node) {
896
+ if (isModuleSpecifier(node)) {
897
+ for (const entry of index.imports) {
898
+ if (entry.from !== undefined && node.text === entry.from) return quote(node, entry.to);
899
+ if (entry.fromSuffix !== undefined && node.text.startsWith('.')) {
900
+ if (node.text.endsWith(entry.fromSuffix)) return quote(node, node.text.slice(0, -entry.fromSuffix.length) + entry.toSuffix);
901
+ if (/\.[cm]?[jt]sx?$/.test(node.text) && node.text.replace(/\.[cm]?[jt]sx?$/, '').endsWith(entry.fromSuffix)) manual(node, `this import names the file with an extension; point it at ${entry.toSuffix} by hand`);
902
+ }
903
+ }
904
+ return undefined;
905
+ }
906
+ const code = index.codes.get(node.text);
907
+ if (code?.to !== undefined) return quote(node, code.to);
908
+ if (index.propertyValueTexts.has(node.text)) {
909
+ const other = comparedWith(node);
910
+ if (other && ts.isPropertyAccessExpression(other) && index.valuePropertyNames.has(other.name.text)) {
911
+ const property = other.name.text;
912
+ const target = propertyValueTarget(node, library.memberCoverage(library.symbolAt(other.name)), property, node.text);
913
+ if (target !== undefined) return quote(node, target);
914
+ }
915
+ }
916
+ return undefined;
917
+ }
918
+
919
+ function rewriteNode(node) {
920
+ if (ts.isTypeQueryNode(node)) return rewriteTypeQuery(node);
921
+ if (ts.isTypeReferenceNode(node) || ts.isImportTypeNode(node)) {
922
+ const result = rewriteMappedType(node);
923
+ if (result !== undefined) return result;
924
+ }
925
+ if (ts.isCallExpression(node)) return rewriteCall(node);
926
+ if (ts.isPropertyAccessExpression(node)) return rewritePropertyAccess(node);
927
+ if (ts.isObjectLiteralExpression(node)) return rewriteObjectLiteral(node);
928
+ if (ts.isBindingElement(node)) return rewriteBindingElement(node);
929
+ if (ts.isIdentifier(node)) return rewriteIdentifier(node);
930
+ if (isStringValue(node)) return rewriteTypedLegacyLiteral(node) ?? rewriteString(node);
931
+ return undefined;
932
+ }
933
+
934
+ let result = assemble(sourceFile, []);
935
+ // `assemble` starts at the first token; keep the file's leading comments.
936
+ result = slice(0, start(sourceFile)) + result;
937
+ // Codes that survive sit in places no literal rewrite reaches: a split code, a regular expression, a template, a comment.
938
+ const lines = result.split('\n');
939
+ for (const [from, entry] of index.codes) {
940
+ const pattern = new RegExp(`\\b${from}\\b`);
941
+ lines.forEach((line, position) => {
942
+ if (pattern.test(line)) manualItems.push({ file: fileLabel, line: position + 1, column: line.search(pattern) + 1, reason: entry.manual ?? `${from} appears outside a plain string; replace it with ${entry.to} by hand`, text: line.trim().slice(0, 120) });
943
+ });
944
+ }
945
+ return { text: result, rewrites };
946
+ }