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.
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/cli.mjs +51 -0
- package/lib/codemod.mjs +77 -0
- package/lib/glob.mjs +51 -0
- package/lib/library.mjs +71 -0
- package/lib/load-typescript.mjs +33 -0
- package/lib/rename-map.mjs +292 -0
- package/lib/rewrite.mjs +946 -0
- package/lib/transforms/build-and-start.mjs +58 -0
- package/lib/transforms/collection-read.mjs +14 -0
- package/lib/transforms/collection-reference.mjs +10 -0
- package/lib/transforms/collection-token.mjs +20 -0
- package/lib/transforms/collection-tokens.mjs +98 -0
- package/lib/transforms/container-derivation.mjs +61 -0
- package/lib/transforms/index.mjs +19 -0
- package/lib/transforms/provider-facades.mjs +74 -0
- package/lib/transforms/provider-methods.mjs +100 -0
- package/lib/transforms/provider-sources.mjs +174 -0
- package/package.json +18 -0
- package/rename-map.json +160 -0
- package/rename-map.schema.json +208 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// tools/codemod/lib/rename-map.mjs
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {{ kind: 'bag', names: string[], alreadyBag?: true, trailing?: { mode: 'merge' | 'keep' | 'drop', keys?: Record<string, string> } } | { kind: 'array' }} ArgumentShape
|
|
6
|
+
* @typedef {{ owner: string, from: string, to: string, arity?: number[], arguments?: ArgumentShape, transform?: string, transformNames?: Record<string, string> }} MethodEntry
|
|
7
|
+
* @typedef {{ owner: string, method: string, argument: number, path?: string[], from: string, to: string }} OptionEntry
|
|
8
|
+
* @typedef {{ owner: string, method: string, argument: number, path?: string[], from: string, to: string } | { owner: string, property: string, from: string, to: string }} ValueEntry
|
|
9
|
+
* @typedef {{ owner: string, from: string, to: string } | { owner: string, from: string, manual: string }} PropertyEntry
|
|
10
|
+
* @typedef {{ from: string, to: string, genericArguments?: { index: number, values: Record<string, string> }[], literalValues?: Record<string, string> }} TypeEntry
|
|
11
|
+
* @typedef {{ from: string, to: string } | { from: string, manual: string }} CodeEntry
|
|
12
|
+
* @typedef {{ from: string, to: string } | { fromSuffix: string, toSuffix: string }} ImportEntry
|
|
13
|
+
* @typedef {{ version: 1, methods?: MethodEntry[], options?: OptionEntry[], values?: ValueEntry[], properties?: PropertyEntry[], types?: TypeEntry[], codes?: CodeEntry[], imports?: ImportEntry[] }} RenameMap
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const SECTIONS = ['methods', 'options', 'values', 'properties', 'types', 'codes', 'imports'];
|
|
17
|
+
const isObject = value => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
18
|
+
const isString = value => typeof value === 'string' && value.length > 0;
|
|
19
|
+
const isStrings = value => Array.isArray(value) && value.every(isString);
|
|
20
|
+
const BARE_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
21
|
+
const TYPE_NAME_FORBIDDEN = new Set([
|
|
22
|
+
'abstract', 'accessor', 'any', 'as', 'asserts', 'async', 'await', 'bigint', 'boolean', 'break', 'case', 'catch', 'class', 'const', 'constructor',
|
|
23
|
+
'continue', 'debugger', 'declare', 'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally',
|
|
24
|
+
'for', 'from', 'function', 'get', 'global', 'if', 'implements', 'import', 'in', 'infer', 'instanceof', 'interface',
|
|
25
|
+
'intrinsic', 'is', 'keyof', 'let', 'module', 'namespace', 'never', 'new', 'null', 'number', 'object', 'of', 'out', 'override', 'package',
|
|
26
|
+
'private', 'protected', 'public', 'readonly', 'require', 'return', 'satisfies', 'set', 'static', 'string', 'super',
|
|
27
|
+
'switch', 'symbol', 'this', 'throw', 'true', 'try', 'type', 'typeof', 'undefined', 'unique', 'unknown', 'using',
|
|
28
|
+
'var', 'void', 'while', 'with', 'yield',
|
|
29
|
+
]);
|
|
30
|
+
const isBareIdentifier = value => isString(value) && BARE_IDENTIFIER.test(value);
|
|
31
|
+
const isTypeIdentifier = value => isBareIdentifier(value) && !TYPE_NAME_FORBIDDEN.has(value);
|
|
32
|
+
const has = (value, key) => Object.hasOwn(value, key);
|
|
33
|
+
const pathsEqual = (left = [], right = []) => left.length === right.length && left.every((segment, index) => segment === right[index]);
|
|
34
|
+
const pathAtOrBelow = (path = [], prefix = []) => path.length >= prefix.length && prefix.every((segment, index) => segment === path[index]);
|
|
35
|
+
const stableValue = value => {
|
|
36
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
37
|
+
if (!isObject(value)) return value;
|
|
38
|
+
return Object.fromEntries(Object.keys(value).sort().map(key => [key, stableValue(value[key])]));
|
|
39
|
+
};
|
|
40
|
+
const sameEffectiveMethod = (left, right) => JSON.stringify(stableValue({ to: left.to, arguments: left.arguments, transform: left.transform, transformNames: left.transformNames })) === JSON.stringify(stableValue({ to: right.to, arguments: right.arguments, transform: right.transform, transformNames: right.transformNames }));
|
|
41
|
+
const aritiesOverlap = (left, right) => left === undefined || right === undefined || left.some(value => right.includes(value));
|
|
42
|
+
const importResult = (entry, specifier) => {
|
|
43
|
+
if (entry.from !== undefined) return specifier === entry.from ? entry.to : undefined;
|
|
44
|
+
return specifier.startsWith('.') && specifier.endsWith(entry.fromSuffix) ? specifier.slice(0, -entry.fromSuffix.length) + entry.toSuffix : undefined;
|
|
45
|
+
};
|
|
46
|
+
const importOverlapWitness = (left, right) => {
|
|
47
|
+
if (left.from !== undefined) return importResult(right, left.from) === undefined ? undefined : left.from;
|
|
48
|
+
if (right.from !== undefined) return importResult(left, right.from) === undefined ? undefined : right.from;
|
|
49
|
+
const suffix = left.fromSuffix.endsWith(right.fromSuffix) ? left.fromSuffix : right.fromSuffix.endsWith(left.fromSuffix) ? right.fromSuffix : undefined;
|
|
50
|
+
return suffix === undefined ? undefined : suffix.startsWith('.') ? suffix : `.${suffix}`;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Every problem in a map, as readable sentences. An empty array means the map is valid. */
|
|
54
|
+
export function validateRenameMap(map, transformIds = []) {
|
|
55
|
+
const problems = [];
|
|
56
|
+
const bad = (section, index, message) => problems.push(`${section}[${index}]: ${message}`);
|
|
57
|
+
const rejectUnknown = (section, index, value, allowed, label = '') => {
|
|
58
|
+
for (const key of Object.keys(value)) {
|
|
59
|
+
if (!allowed.includes(key)) bad(section, index, `${label ? `${label} has ` : ''}unknown field ${key}`);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
if (typeof map !== 'object' || map === null || Array.isArray(map)) return ['the rename map must be an object'];
|
|
63
|
+
if (map.version !== 1) problems.push('version must be 1');
|
|
64
|
+
if (has(map, '$schema') && !isString(map.$schema)) problems.push('$schema must be a non-empty string');
|
|
65
|
+
for (const key of Object.keys(map)) if (key !== 'version' && key !== '$schema' && !SECTIONS.includes(key)) problems.push(`unknown section ${key}`);
|
|
66
|
+
for (const section of SECTIONS) if (map[section] !== undefined && !Array.isArray(map[section])) problems.push(`${section} must be an array`);
|
|
67
|
+
const entries = section => Array.isArray(map[section]) ? map[section] : [];
|
|
68
|
+
entries('methods').forEach((entry, index) => {
|
|
69
|
+
if (!isObject(entry)) return bad('methods', index, 'entry must be an object');
|
|
70
|
+
rejectUnknown('methods', index, entry, ['owner', 'from', 'to', 'arity', 'arguments', 'transform', 'transformNames']);
|
|
71
|
+
if (!isString(entry.owner) || !isString(entry.from) || !isString(entry.to)) bad('methods', index, 'owner, from and to are required strings');
|
|
72
|
+
if (isString(entry.to) && !isBareIdentifier(entry.to)) bad('methods', index, 'to must be a safe bare identifier');
|
|
73
|
+
if (entry.arity !== undefined && !(Array.isArray(entry.arity) && entry.arity.every(value => Number.isInteger(value) && value >= 0))) bad('methods', index, 'arity must be an array of non-negative integers');
|
|
74
|
+
if (entry.transform !== undefined && !transformIds.includes(entry.transform)) bad('methods', index, `unknown transform ${entry.transform}`);
|
|
75
|
+
if (has(entry, 'transform') && has(entry, 'arguments')) bad('methods', index, 'use either transform or arguments');
|
|
76
|
+
if (entry.transformNames !== undefined) {
|
|
77
|
+
const names = entry.transformNames;
|
|
78
|
+
if (entry.transform === undefined) bad('methods', index, 'transformNames requires transform');
|
|
79
|
+
if (typeof names !== 'object' || names === null || Array.isArray(names)
|
|
80
|
+
|| Object.keys(names).length === 0 || !Object.values(names).every(isString)) {
|
|
81
|
+
bad('methods', index, 'transformNames must map at least one role to a non-empty string');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const shape = entry.arguments;
|
|
85
|
+
if (shape !== undefined) {
|
|
86
|
+
if (!isObject(shape)) bad('methods', index, 'arguments must be an object');
|
|
87
|
+
else if (shape.kind === 'bag') {
|
|
88
|
+
rejectUnknown('methods', index, shape, ['kind', 'names', 'alreadyBag', 'trailing'], 'arguments');
|
|
89
|
+
if (!isStrings(shape.names) || shape.names.length === 0) bad('methods', index, 'arguments.names must list at least one property name');
|
|
90
|
+
if (shape.alreadyBag !== undefined && shape.alreadyBag !== true) bad('methods', index, 'arguments.alreadyBag must be true when present');
|
|
91
|
+
if (shape.alreadyBag === true && (entry.from !== entry.to || shape.names.length !== 1)) bad('methods', index, 'arguments.alreadyBag requires a same-name method with exactly one argument name');
|
|
92
|
+
if (shape.trailing !== undefined) {
|
|
93
|
+
if (!isObject(shape.trailing)) bad('methods', index, 'arguments.trailing must be an object');
|
|
94
|
+
else {
|
|
95
|
+
rejectUnknown('methods', index, shape.trailing, ['mode', 'keys'], 'arguments.trailing');
|
|
96
|
+
if (!['merge', 'keep', 'drop'].includes(shape.trailing.mode)) bad('methods', index, 'arguments.trailing.mode must be merge, keep or drop');
|
|
97
|
+
if (shape.trailing.keys !== undefined && !(isObject(shape.trailing.keys) && Object.values(shape.trailing.keys).every(isString))) bad('methods', index, 'arguments.trailing.keys must map property names to property names');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} else if (shape.kind === 'array') rejectUnknown('methods', index, shape, ['kind'], 'arguments');
|
|
101
|
+
else {
|
|
102
|
+
rejectUnknown('methods', index, shape, ['kind', 'names', 'alreadyBag', 'trailing'], 'arguments');
|
|
103
|
+
bad('methods', index, 'arguments.kind must be bag or array');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
entries('options').forEach((entry, index) => {
|
|
108
|
+
if (!isObject(entry)) return bad('options', index, 'entry must be an object');
|
|
109
|
+
rejectUnknown('options', index, entry, ['owner', 'method', 'argument', 'path', 'from', 'to']);
|
|
110
|
+
if (!isString(entry.owner) || !isString(entry.method) || !isString(entry.from) || !isString(entry.to)) bad('options', index, 'owner, method, argument, from and to are required');
|
|
111
|
+
if (!Number.isInteger(entry.argument) || entry.argument < 0) bad('options', index, 'argument must be a non-negative integer');
|
|
112
|
+
if (entry.path !== undefined && !isStrings(entry.path)) bad('options', index, 'path must be an array of property names');
|
|
113
|
+
});
|
|
114
|
+
entries('values').forEach((entry, index) => {
|
|
115
|
+
if (!isObject(entry)) return bad('values', index, 'entry must be an object');
|
|
116
|
+
rejectUnknown('values', index, entry, ['owner', 'method', 'argument', 'path', 'property', 'from', 'to']);
|
|
117
|
+
const byArgument = has(entry, 'method') && has(entry, 'argument') && !has(entry, 'property');
|
|
118
|
+
const byProperty = has(entry, 'property') && !has(entry, 'method') && !has(entry, 'argument') && !has(entry, 'path');
|
|
119
|
+
if (!isString(entry.owner) || byArgument === byProperty || byArgument && !isString(entry.method) || byProperty && !isString(entry.property) || !isString(entry.from) || !isString(entry.to)) bad('values', index, 'owner, from, to and either method with argument or property are required');
|
|
120
|
+
if (byArgument && (!Number.isInteger(entry.argument) || entry.argument < 0)) bad('values', index, 'argument must be a non-negative integer');
|
|
121
|
+
if (byArgument && entry.path !== undefined && !isStrings(entry.path)) bad('values', index, 'path must be an array of property names');
|
|
122
|
+
});
|
|
123
|
+
entries('properties').forEach((entry, index) => {
|
|
124
|
+
if (!isObject(entry)) return bad('properties', index, 'entry must be an object');
|
|
125
|
+
rejectUnknown('properties', index, entry, ['owner', 'from', 'to', 'manual']);
|
|
126
|
+
const hasTo = has(entry, 'to');
|
|
127
|
+
const hasManual = has(entry, 'manual');
|
|
128
|
+
if (!isString(entry.owner) || !isString(entry.from) || hasTo === hasManual || hasTo && !isString(entry.to) || hasManual && !isString(entry.manual)) bad('properties', index, 'owner, from and exactly one of to or manual are required');
|
|
129
|
+
if (isString(entry.to) && !isBareIdentifier(entry.to)) bad('properties', index, 'to must be a safe bare identifier');
|
|
130
|
+
});
|
|
131
|
+
entries('types').forEach((entry, index) => {
|
|
132
|
+
if (!isObject(entry)) return bad('types', index, 'entry must be an object');
|
|
133
|
+
rejectUnknown('types', index, entry, ['from', 'to', 'genericArguments', 'literalValues']);
|
|
134
|
+
if (!isString(entry.from) || !isString(entry.to)) bad('types', index, 'from and to are required');
|
|
135
|
+
if (isString(entry.to) && !isTypeIdentifier(entry.to)) bad('types', index, 'to must be a safe type identifier');
|
|
136
|
+
const rules = entry.genericArguments;
|
|
137
|
+
const isRuleShape = rule =>
|
|
138
|
+
isObject(rule) && Number.isInteger(rule.index) && rule.index >= 0 && isObject(rule.values) &&
|
|
139
|
+
Object.keys(rule.values).length > 0 && Object.entries(rule.values).every(([from, to]) => isString(from) && isString(to));
|
|
140
|
+
const isValidRule = rule => isRuleShape(rule) && Object.keys(rule).every(key => ['index', 'values'].includes(key));
|
|
141
|
+
if (Array.isArray(rules)) rules.forEach((rule, ruleIndex) => {
|
|
142
|
+
if (isObject(rule)) rejectUnknown('types', index, rule, ['index', 'values'], `genericArguments[${ruleIndex}]`);
|
|
143
|
+
});
|
|
144
|
+
if (rules !== undefined && (!Array.isArray(rules) || rules.some(rule => !isRuleShape(rule)))) bad('types', index, 'genericArguments must map non-negative indices and string literal values');
|
|
145
|
+
const validRules = Array.isArray(rules) ? rules.filter(isValidRule) : [];
|
|
146
|
+
if (new Set(validRules.map(rule => rule.index)).size !== validRules.length) bad('types', index, 'genericArguments indices must be unique');
|
|
147
|
+
const literalValues = entry.literalValues;
|
|
148
|
+
if (literalValues !== undefined && (
|
|
149
|
+
typeof literalValues !== 'object' || literalValues === null || Array.isArray(literalValues) ||
|
|
150
|
+
Object.keys(literalValues).length === 0 || !Object.entries(literalValues).every(([from, to]) => isString(from) && isString(to))
|
|
151
|
+
)) bad('types', index, 'literalValues must map non-empty string literals');
|
|
152
|
+
});
|
|
153
|
+
entries('codes').forEach((entry, index) => {
|
|
154
|
+
if (!isObject(entry)) return bad('codes', index, 'entry must be an object');
|
|
155
|
+
rejectUnknown('codes', index, entry, ['from', 'to', 'manual']);
|
|
156
|
+
const hasTo = has(entry, 'to');
|
|
157
|
+
const hasManual = has(entry, 'manual');
|
|
158
|
+
if (!isString(entry.from) || !/^DI_BAG_[A-Z_]+$/.test(entry.from) || hasTo === hasManual || hasTo && !isString(entry.to) || hasManual && !isString(entry.manual)) bad('codes', index, 'from must be a DI_BAG_ code with exactly one of to or manual');
|
|
159
|
+
});
|
|
160
|
+
entries('imports').forEach((entry, index) => {
|
|
161
|
+
if (!isObject(entry)) return bad('imports', index, 'entry must be an object');
|
|
162
|
+
rejectUnknown('imports', index, entry, ['from', 'to', 'fromSuffix', 'toSuffix']);
|
|
163
|
+
const exact = has(entry, 'from') && has(entry, 'to') && !has(entry, 'fromSuffix') && !has(entry, 'toSuffix') && isString(entry.from) && isString(entry.to);
|
|
164
|
+
const suffix = !has(entry, 'from') && !has(entry, 'to') && has(entry, 'fromSuffix') && has(entry, 'toSuffix') && isString(entry.fromSuffix) && isString(entry.toSuffix);
|
|
165
|
+
if (exact === suffix) bad('imports', index, 'use either from with to, or fromSuffix with toSuffix');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const validMethods = entries('methods').map((entry, index) => ({ entry, index })).filter(({ entry }) => isObject(entry) && isString(entry.owner) && isString(entry.from) && isString(entry.to) && (entry.arity === undefined || Array.isArray(entry.arity) && entry.arity.every(value => Number.isInteger(value) && value >= 0)));
|
|
169
|
+
validMethods.forEach(({ entry, index }, position) => {
|
|
170
|
+
for (const prior of validMethods.slice(0, position)) {
|
|
171
|
+
if (entry.owner === prior.entry.owner && entry.from === prior.entry.from && aritiesOverlap(entry.arity, prior.entry.arity) && !sameEffectiveMethod(entry, prior.entry)) bad('methods', index, `conflicts with methods[${prior.index}] for ${entry.owner}.${entry.from}`);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
const conflictsBySelector = (section, selector, effective, describe) => {
|
|
175
|
+
const valid = entries(section).map((entry, index) => ({ entry, index })).filter(({ entry }) => isObject(entry) && selector(entry) !== undefined && effective(entry) !== undefined);
|
|
176
|
+
valid.forEach(({ entry, index }, position) => {
|
|
177
|
+
for (const prior of valid.slice(0, position)) {
|
|
178
|
+
if (selector(entry) === selector(prior.entry) && effective(entry) !== effective(prior.entry)) bad(section, index, `conflicts with ${section}[${prior.index}] for ${describe(entry)}`);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
const argumentSelector = entry => isString(entry.owner) && isString(entry.method) && Number.isInteger(entry.argument) && entry.argument >= 0 && (entry.path === undefined || isStrings(entry.path)) && isString(entry.from)
|
|
183
|
+
? JSON.stringify([entry.owner, entry.method, entry.argument, entry.path ?? [], entry.from]) : undefined;
|
|
184
|
+
conflictsBySelector('options', argumentSelector, entry => isString(entry.to) ? entry.to : undefined, entry => `${entry.owner}.${entry.method} argument ${entry.argument + 1} path ${[...(entry.path ?? []), `key ${entry.from}`].join(' ')}`);
|
|
185
|
+
conflictsBySelector('values', entry => {
|
|
186
|
+
if (isString(entry.owner) && isString(entry.from) && isString(entry.property) && !has(entry, 'method') && !has(entry, 'argument') && !has(entry, 'path')) return JSON.stringify(['property', entry.owner, entry.property, entry.from]);
|
|
187
|
+
if (has(entry, 'property')) return undefined;
|
|
188
|
+
const selector = argumentSelector(entry);
|
|
189
|
+
return selector === undefined ? undefined : `argument:${selector}`;
|
|
190
|
+
}, entry => isString(entry.to) ? entry.to : undefined, entry => entry.property !== undefined ? `${entry.owner}.${entry.property} value ${entry.from}` : `${entry.owner}.${entry.method} argument ${entry.argument + 1} path ${[...(entry.path ?? []), `value ${entry.from}`].join(' ')}`);
|
|
191
|
+
conflictsBySelector('properties', entry => isString(entry.owner) && isString(entry.from) ? `${entry.owner}\0${entry.from}` : undefined, entry => isString(entry.to) ? `to:${entry.to}` : isString(entry.manual) ? `manual:${entry.manual}` : undefined, entry => `${entry.owner}.${entry.from}`);
|
|
192
|
+
conflictsBySelector('types', entry => isString(entry.from) ? entry.from : undefined, entry => isString(entry.to) ? entry.to : undefined, entry => entry.from);
|
|
193
|
+
conflictsBySelector('codes', entry => isString(entry.from) && /^DI_BAG_[A-Z_]+$/.test(entry.from) ? entry.from : undefined, entry => isString(entry.to) ? `to:${entry.to}` : isString(entry.manual) ? `manual:${entry.manual}` : undefined, entry => entry.from);
|
|
194
|
+
const validImports = entries('imports').map((entry, index) => ({ entry, index })).filter(({ entry }) => isObject(entry) && (isString(entry.from) && isString(entry.to) && !has(entry, 'fromSuffix') && !has(entry, 'toSuffix') || isString(entry.fromSuffix) && isString(entry.toSuffix) && !has(entry, 'from') && !has(entry, 'to')));
|
|
195
|
+
validImports.forEach(({ entry, index }, position) => {
|
|
196
|
+
for (const prior of validImports.slice(0, position)) {
|
|
197
|
+
const witness = importOverlapWitness(entry, prior.entry);
|
|
198
|
+
if (witness !== undefined && importResult(entry, witness) !== importResult(prior.entry, witness)) bad('imports', index, `conflicts with imports[${prior.index}] for overlapping import selectors`);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
return problems;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Read and validate a map file. Throws one error that lists every problem. */
|
|
205
|
+
export function loadRenameMap(file, transformIds = []) {
|
|
206
|
+
let map;
|
|
207
|
+
try {
|
|
208
|
+
map = JSON.parse(readFileSync(file, 'utf8'));
|
|
209
|
+
} catch (error) {
|
|
210
|
+
throw new Error(`invalid rename map ${file}:\n${error.message}`, { cause: error });
|
|
211
|
+
}
|
|
212
|
+
const problems = validateRenameMap(map, transformIds);
|
|
213
|
+
if (problems.length) throw new Error(`invalid rename map ${file}:\n${problems.join('\n')}`);
|
|
214
|
+
return map;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Lookup tables over a valid map. Every lookup answers from the map alone; nothing is hardcoded. */
|
|
218
|
+
export function indexRenameMap(map) {
|
|
219
|
+
const methods = new Map();
|
|
220
|
+
for (const entry of map.methods ?? []) {
|
|
221
|
+
const key = `${entry.owner}.${entry.from}`;
|
|
222
|
+
methods.set(key, [...(methods.get(key) ?? []), entry]);
|
|
223
|
+
}
|
|
224
|
+
const properties = new Map((map.properties ?? []).map(entry => [`${entry.owner}.${entry.from}`, entry]));
|
|
225
|
+
const options = new Map();
|
|
226
|
+
for (const entry of map.options ?? []) {
|
|
227
|
+
const key = `${entry.owner}.${entry.method}`;
|
|
228
|
+
options.set(key, [...(options.get(key) ?? []), entry]);
|
|
229
|
+
}
|
|
230
|
+
const argumentValues = new Map();
|
|
231
|
+
const propertyValues = new Map();
|
|
232
|
+
for (const entry of map.values ?? []) {
|
|
233
|
+
if (entry.property !== undefined) propertyValues.set(`${entry.owner}.${entry.property}=${entry.from}`, entry.to);
|
|
234
|
+
else {
|
|
235
|
+
const key = `${entry.owner}.${entry.method}`;
|
|
236
|
+
argumentValues.set(key, [...(argumentValues.get(key) ?? []), entry]);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const codes = new Map((map.codes ?? []).map(entry => [entry.from, entry.to === 'manual' ? { from: entry.from, manual: 'this code was split; pick the new code by reading the errors page' } : entry]));
|
|
240
|
+
return {
|
|
241
|
+
methods, properties, options, argumentValues, propertyValues, codes,
|
|
242
|
+
types: new Map((map.types ?? []).map(entry => [entry.from, entry])),
|
|
243
|
+
imports: map.imports ?? [],
|
|
244
|
+
/** Names worth asking the checker about; everything else is skipped without a type query. */
|
|
245
|
+
callNames: new Set([...(map.methods ?? []).map(entry => entry.from), ...(map.options ?? []).map(entry => entry.method), ...(map.values ?? []).filter(entry => entry.method !== undefined).map(entry => entry.method)]),
|
|
246
|
+
methodNames: new Set((map.methods ?? []).map(entry => entry.from)),
|
|
247
|
+
memberNames: new Set([...(map.methods ?? []).map(entry => entry.from), ...(map.properties ?? []).map(entry => entry.from)]),
|
|
248
|
+
propertyNames: new Set([...(map.properties ?? []).map(entry => entry.from), ...(map.values ?? []).filter(entry => entry.property !== undefined).map(entry => entry.property)]),
|
|
249
|
+
valuePropertyNames: new Set((map.values ?? []).filter(entry => entry.property !== undefined).map(entry => entry.property)),
|
|
250
|
+
propertyValueTexts: new Set((map.values ?? []).filter(entry => entry.property !== undefined).map(entry => entry.from)),
|
|
251
|
+
/** The method entry for a call with `count` arguments, or undefined. */
|
|
252
|
+
methodFor(owner, name, count) {
|
|
253
|
+
const entries = methods.get(`${owner}.${name}`) ?? [];
|
|
254
|
+
if (count !== undefined) return entries.find(entry => entry.arity === undefined || entry.arity.includes(count));
|
|
255
|
+
return entries.length > 0 && entries.every(entry => sameEffectiveMethod(entry, entries[0])) ? entries[0] : undefined;
|
|
256
|
+
},
|
|
257
|
+
hasMethodEntries(owner, name) {
|
|
258
|
+
return methods.has(`${owner}.${name}`);
|
|
259
|
+
},
|
|
260
|
+
optionsFor(owner, method, argument, path) {
|
|
261
|
+
return (options.get(`${owner}.${method}`) ?? []).filter(entry => entry.argument === argument && pathsEqual(entry.path, path));
|
|
262
|
+
},
|
|
263
|
+
valuesFor(owner, method, argument, path) {
|
|
264
|
+
return (argumentValues.get(`${owner}.${method}`) ?? []).filter(entry => entry.argument === argument && pathsEqual(entry.path, path));
|
|
265
|
+
},
|
|
266
|
+
/** The renames that apply to one argument, as a sentence for a manual item. */
|
|
267
|
+
describeArgumentEntries(owner, method, argument) {
|
|
268
|
+
const label = entry => `${[...(entry.path ?? []), ''].join('.')}`;
|
|
269
|
+
return [
|
|
270
|
+
...(options.get(`${owner}.${method}`) ?? []).filter(entry => entry.argument === argument).map(entry => `key ${label(entry)}${entry.from} to ${entry.to}`),
|
|
271
|
+
...(argumentValues.get(`${owner}.${method}`) ?? []).filter(entry => entry.argument === argument).map(entry => `value '${entry.from}' to '${entry.to}'${entry.path?.length ? ` at ${entry.path.join('.')}` : ''}`),
|
|
272
|
+
].join('; ');
|
|
273
|
+
},
|
|
274
|
+
/** True when some option or value entry rewrites an argument of this method. */
|
|
275
|
+
hasArgumentEntries(owner, method) {
|
|
276
|
+
return options.has(`${owner}.${method}`) || argumentValues.has(`${owner}.${method}`);
|
|
277
|
+
},
|
|
278
|
+
/** True when some option or value entry sits below `path` of this argument. */
|
|
279
|
+
hasEntriesBelow(owner, method, argument, path) {
|
|
280
|
+
const below = entry => entry.argument === argument && pathAtOrBelow(entry.path, path);
|
|
281
|
+
return (options.get(`${owner}.${method}`) ?? []).some(below) || (argumentValues.get(`${owner}.${method}`) ?? []).some(below);
|
|
282
|
+
},
|
|
283
|
+
/** The current name of a library member: the map's target, or the old name when the map does not rename it. */
|
|
284
|
+
nameOf(owner, oldName) {
|
|
285
|
+
const entries = methods.get(`${owner}.${oldName}`) ?? [];
|
|
286
|
+
const method = entries.length > 0 && entries.every(entry => entry.to === entries[0].to) ? entries[0] : undefined;
|
|
287
|
+
if (method) return method.to;
|
|
288
|
+
const property = properties.get(`${owner}.${oldName}`);
|
|
289
|
+
return property?.to ?? oldName;
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|