@openrewrite/rewrite 8.91.0 → 8.91.2
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/dist/java/formatting-utils.d.ts +8 -5
- package/dist/java/formatting-utils.d.ts.map +1 -1
- package/dist/java/formatting-utils.js +14 -10
- package/dist/java/formatting-utils.js.map +1 -1
- package/dist/java/type.d.ts +2 -0
- package/dist/java/type.d.ts.map +1 -1
- package/dist/java/type.js +5 -0
- package/dist/java/type.js.map +1 -1
- package/dist/javascript/add-import.d.ts +96 -43
- package/dist/javascript/add-import.d.ts.map +1 -1
- package/dist/javascript/add-import.js +668 -236
- package/dist/javascript/add-import.js.map +1 -1
- package/dist/javascript/amd.d.ts +138 -0
- package/dist/javascript/amd.d.ts.map +1 -0
- package/dist/javascript/amd.js +768 -0
- package/dist/javascript/amd.js.map +1 -0
- package/dist/javascript/assertions.d.ts +7 -0
- package/dist/javascript/assertions.d.ts.map +1 -1
- package/dist/javascript/assertions.js +31 -5
- package/dist/javascript/assertions.js.map +1 -1
- package/dist/javascript/binding.d.ts +94 -0
- package/dist/javascript/binding.d.ts.map +1 -0
- package/dist/javascript/binding.js +335 -0
- package/dist/javascript/binding.js.map +1 -0
- package/dist/javascript/comparator.d.ts +5 -0
- package/dist/javascript/comparator.d.ts.map +1 -1
- package/dist/javascript/comparator.js +34 -9
- package/dist/javascript/comparator.js.map +1 -1
- package/dist/javascript/format/format.d.ts.map +1 -1
- package/dist/javascript/format/format.js +8 -9
- package/dist/javascript/format/format.js.map +1 -1
- package/dist/javascript/format/tabs-and-indents-visitor.d.ts +2 -0
- package/dist/javascript/format/tabs-and-indents-visitor.d.ts.map +1 -1
- package/dist/javascript/format/tabs-and-indents-visitor.js +17 -11
- package/dist/javascript/format/tabs-and-indents-visitor.js.map +1 -1
- package/dist/javascript/index.d.ts +8 -1
- package/dist/javascript/index.d.ts.map +1 -1
- package/dist/javascript/index.js +26 -1
- package/dist/javascript/index.js.map +1 -1
- package/dist/javascript/node-resolution-result.d.ts +19 -1
- package/dist/javascript/node-resolution-result.d.ts.map +1 -1
- package/dist/javascript/node-resolution-result.js +37 -2
- package/dist/javascript/node-resolution-result.js.map +1 -1
- package/dist/javascript/package-exported-types.d.ts.map +1 -1
- package/dist/javascript/package-exported-types.js +56 -72
- package/dist/javascript/package-exported-types.js.map +1 -1
- package/dist/javascript/parser-utils.d.ts.map +1 -1
- package/dist/javascript/parser-utils.js +2 -0
- package/dist/javascript/parser-utils.js.map +1 -1
- package/dist/javascript/parser.d.ts +7 -1
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +17 -4
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/recipes/change-import.d.ts +2 -4
- package/dist/javascript/recipes/change-import.d.ts.map +1 -1
- package/dist/javascript/recipes/change-import.js +8 -244
- package/dist/javascript/recipes/change-import.js.map +1 -1
- package/dist/javascript/remove-import.d.ts +2 -28
- package/dist/javascript/remove-import.d.ts.map +1 -1
- package/dist/javascript/remove-import.js +19 -57
- package/dist/javascript/remove-import.js.map +1 -1
- package/dist/javascript/scope.d.ts +49 -0
- package/dist/javascript/scope.d.ts.map +1 -0
- package/dist/javascript/scope.js +324 -0
- package/dist/javascript/scope.js.map +1 -0
- package/dist/javascript/style.d.ts.map +1 -1
- package/dist/javascript/style.js +3 -10
- package/dist/javascript/style.js.map +1 -1
- package/dist/javascript/templating/bindings.d.ts +8 -0
- package/dist/javascript/templating/bindings.d.ts.map +1 -0
- package/dist/javascript/templating/bindings.js +92 -0
- package/dist/javascript/templating/bindings.js.map +1 -0
- package/dist/javascript/templating/comparator.d.ts +10 -0
- package/dist/javascript/templating/comparator.d.ts.map +1 -1
- package/dist/javascript/templating/comparator.js +48 -20
- package/dist/javascript/templating/comparator.js.map +1 -1
- package/dist/javascript/templating/engine.d.ts +34 -10
- package/dist/javascript/templating/engine.d.ts.map +1 -1
- package/dist/javascript/templating/engine.js +109 -55
- package/dist/javascript/templating/engine.js.map +1 -1
- package/dist/javascript/templating/index.d.ts +1 -1
- package/dist/javascript/templating/index.d.ts.map +1 -1
- package/dist/javascript/templating/index.js.map +1 -1
- package/dist/javascript/templating/pattern.d.ts.map +1 -1
- package/dist/javascript/templating/pattern.js +13 -10
- package/dist/javascript/templating/pattern.js.map +1 -1
- package/dist/javascript/templating/placeholder-replacement.d.ts +4 -0
- package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
- package/dist/javascript/templating/placeholder-replacement.js +24 -10
- package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
- package/dist/javascript/templating/precedence.d.ts +12 -4
- package/dist/javascript/templating/precedence.d.ts.map +1 -1
- package/dist/javascript/templating/precedence.js +18 -11
- package/dist/javascript/templating/precedence.js.map +1 -1
- package/dist/javascript/templating/rewrite.d.ts.map +1 -1
- package/dist/javascript/templating/rewrite.js +19 -17
- package/dist/javascript/templating/rewrite.js.map +1 -1
- package/dist/javascript/templating/template.d.ts +14 -0
- package/dist/javascript/templating/template.d.ts.map +1 -1
- package/dist/javascript/templating/template.js +63 -3
- package/dist/javascript/templating/template.js.map +1 -1
- package/dist/javascript/templating/types.d.ts +39 -1
- package/dist/javascript/templating/types.d.ts.map +1 -1
- package/dist/javascript/templating/utils.d.ts +6 -1
- package/dist/javascript/templating/utils.d.ts.map +1 -1
- package/dist/javascript/templating/utils.js +20 -2
- package/dist/javascript/templating/utils.js.map +1 -1
- package/dist/javascript/type-mapping.d.ts +1 -0
- package/dist/javascript/type-mapping.d.ts.map +1 -1
- package/dist/javascript/type-mapping.js +52 -2
- package/dist/javascript/type-mapping.js.map +1 -1
- package/dist/rewrite-javascript-version.txt +1 -1
- package/dist/style.d.ts +2 -0
- package/dist/style.d.ts.map +1 -1
- package/dist/style.js +5 -0
- package/dist/style.js.map +1 -1
- package/dist/test/rewrite-test.d.ts +12 -0
- package/dist/test/rewrite-test.d.ts.map +1 -1
- package/dist/test/rewrite-test.js +40 -3
- package/dist/test/rewrite-test.js.map +1 -1
- package/package.json +2 -2
- package/src/java/formatting-utils.ts +14 -10
- package/src/java/type.ts +5 -0
- package/src/javascript/add-import.ts +751 -247
- package/src/javascript/amd.ts +915 -0
- package/src/javascript/assertions.ts +33 -5
- package/src/javascript/binding.ts +449 -0
- package/src/javascript/comparator.ts +39 -10
- package/src/javascript/format/format.ts +9 -10
- package/src/javascript/format/tabs-and-indents-visitor.ts +18 -12
- package/src/javascript/index.ts +12 -1
- package/src/javascript/node-resolution-result.ts +59 -4
- package/src/javascript/package-exported-types.ts +60 -74
- package/src/javascript/parser-utils.ts +2 -0
- package/src/javascript/parser.ts +23 -3
- package/src/javascript/recipes/change-import.ts +10 -263
- package/src/javascript/remove-import.ts +20 -56
- package/src/javascript/scope.ts +338 -0
- package/src/javascript/style.ts +3 -11
- package/src/javascript/templating/bindings.ts +99 -0
- package/src/javascript/templating/comparator.ts +57 -20
- package/src/javascript/templating/engine.ts +148 -68
- package/src/javascript/templating/index.ts +1 -0
- package/src/javascript/templating/pattern.ts +19 -16
- package/src/javascript/templating/placeholder-replacement.ts +32 -11
- package/src/javascript/templating/precedence.ts +21 -13
- package/src/javascript/templating/rewrite.ts +23 -17
- package/src/javascript/templating/template.ts +84 -9
- package/src/javascript/templating/types.ts +46 -1
- package/src/javascript/templating/utils.ts +21 -2
- package/src/javascript/type-mapping.ts +54 -2
- package/src/style.ts +5 -0
- package/src/test/rewrite-test.ts +61 -3
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 the original author or authors.
|
|
3
|
+
* <p>
|
|
4
|
+
* Licensed under the Moderne Source Available License (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
* <p>
|
|
8
|
+
* https://docs.moderne.io/licensing/moderne-source-available-license
|
|
9
|
+
* <p>
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import {emptyMarkers, findMarker} from "../markers";
|
|
17
|
+
import {randomId, UUID} from "../uuid";
|
|
18
|
+
import {
|
|
19
|
+
emptyContainer,
|
|
20
|
+
emptySpace,
|
|
21
|
+
Expression,
|
|
22
|
+
isIdentifier,
|
|
23
|
+
isLiteral,
|
|
24
|
+
J,
|
|
25
|
+
rightPadded,
|
|
26
|
+
space,
|
|
27
|
+
spaceContainsNewline,
|
|
28
|
+
Statement,
|
|
29
|
+
TrailingComma
|
|
30
|
+
} from "../java";
|
|
31
|
+
import {JS} from "./tree";
|
|
32
|
+
import {cursorOf, deconflict, namesDeclaredWithin} from "./scope";
|
|
33
|
+
import {JavaScriptVisitor} from "./visitor";
|
|
34
|
+
import {ExecutionContext} from "../execution";
|
|
35
|
+
|
|
36
|
+
/** RequireJS and Dojo write `define`; UI5 namespaces it as `sap.ui.define`. */
|
|
37
|
+
export const DEFAULT_AMD_CALLEES: readonly string[] = ["define", "require"];
|
|
38
|
+
|
|
39
|
+
export interface AmdBlock {
|
|
40
|
+
/** -1 where the block is written without one, as `define(factory)`. */
|
|
41
|
+
readonly dependenciesIndex: number;
|
|
42
|
+
readonly dependencies: J.NewArray;
|
|
43
|
+
readonly factoryIndex: number;
|
|
44
|
+
readonly factory: J.MethodDeclaration | J.Lambda;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function amdBlockOf(
|
|
48
|
+
call: J.MethodInvocation,
|
|
49
|
+
callees: readonly string[] = DEFAULT_AMD_CALLEES
|
|
50
|
+
): AmdBlock | undefined {
|
|
51
|
+
if (!isAmdCallee(call, callees)) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const args = call.arguments.elements;
|
|
55
|
+
const dependenciesIndex = args.findIndex(arg => arg.element.kind === J.Kind.NewArray);
|
|
56
|
+
const factoryIndex = dependenciesIndex >= 0 ?
|
|
57
|
+
dependenciesIndex + 1 :
|
|
58
|
+
args.findIndex(arg => asFactory(arg.element) !== undefined);
|
|
59
|
+
const factory = factoryIndex >= 0 && factoryIndex < args.length ?
|
|
60
|
+
asFactory(args[factoryIndex].element) :
|
|
61
|
+
undefined;
|
|
62
|
+
if (factory === undefined) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const dependencies = dependenciesIndex >= 0 ?
|
|
66
|
+
args[dependenciesIndex].element as J.NewArray :
|
|
67
|
+
noDependencies();
|
|
68
|
+
return {dependenciesIndex, dependencies, factoryIndex, factory};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Stands in for the array a block written without one would have had. */
|
|
72
|
+
function noDependencies(): J.NewArray {
|
|
73
|
+
return {
|
|
74
|
+
kind: J.Kind.NewArray,
|
|
75
|
+
id: randomId(),
|
|
76
|
+
prefix: emptySpace,
|
|
77
|
+
markers: emptyMarkers,
|
|
78
|
+
dimensions: [],
|
|
79
|
+
initializer: emptyContainer<Expression>()
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A callee written without a dot (`"define"`) matches by simple name on any receiver, so it
|
|
85
|
+
* also matches `foo.define(...)`; a dotted callee (`"sap.ui.define"`) requires the whole path.
|
|
86
|
+
*/
|
|
87
|
+
function isAmdCallee(call: J.MethodInvocation, callees: readonly string[]): boolean {
|
|
88
|
+
// The name check first: this runs for every call in a file, and almost none are these.
|
|
89
|
+
const simpleName = call.name.simpleName;
|
|
90
|
+
return callees.some(callee => {
|
|
91
|
+
const last = callee.substring(callee.lastIndexOf(".") + 1);
|
|
92
|
+
return simpleName === last && (callee === last || namespaceOf(call) === callee);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The dotted path a call is written under, `sap.ui.define` for `sap.ui.define(…)`. */
|
|
97
|
+
function namespaceOf(call: J.MethodInvocation): string | undefined {
|
|
98
|
+
const segments: string[] = [call.name.simpleName];
|
|
99
|
+
let node: J | undefined = call.select?.element;
|
|
100
|
+
while (node !== undefined) {
|
|
101
|
+
if (node.kind === J.Kind.FieldAccess) {
|
|
102
|
+
const access = node as J.FieldAccess;
|
|
103
|
+
segments.unshift(access.name.element.simpleName);
|
|
104
|
+
node = access.target;
|
|
105
|
+
} else if (isIdentifier(node)) {
|
|
106
|
+
segments.unshift(node.simpleName);
|
|
107
|
+
return segments.join(".");
|
|
108
|
+
} else {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return segments.join(".");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** A factory is written either as a function expression or as an arrow function. */
|
|
116
|
+
function asFactory(argument: J): J.MethodDeclaration | J.Lambda | undefined {
|
|
117
|
+
if (argument.kind === JS.Kind.StatementExpression) {
|
|
118
|
+
const statement = (argument as JS.StatementExpression).statement;
|
|
119
|
+
return statement.kind === J.Kind.MethodDeclaration ? statement as J.MethodDeclaration : undefined;
|
|
120
|
+
}
|
|
121
|
+
return argument.kind === JS.Kind.ArrowFunction ? (argument as JS.ArrowFunction).lambda : undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The elements of a `[]` or `()`, which the parser fills with a single `J.Empty` when
|
|
126
|
+
* there are none.
|
|
127
|
+
*/
|
|
128
|
+
export function present<T extends J>(elements: readonly J.RightPadded<T>[]): J.RightPadded<T>[] {
|
|
129
|
+
return elements.length === 1 && elements[0].element.kind === J.Kind.Empty ? [] : [...elements];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function elementsOf(block: AmdBlock): J.RightPadded<Expression>[] {
|
|
133
|
+
return present(block.dependencies.initializer?.elements ?? []);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function parametersOf(block: AmdBlock): J.RightPadded<J>[] {
|
|
137
|
+
return present(block.factory.kind === J.Kind.MethodDeclaration ?
|
|
138
|
+
(block.factory as J.MethodDeclaration).parameters.elements :
|
|
139
|
+
normalizeArrowParameters((block.factory as J.Lambda).parameters.parameters));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* An arrow function's parameter list isn't built the way every other comma-separated list in
|
|
144
|
+
* the tree is: a trailing comma is an extra `J.Empty` entry rather than a `TrailingComma`
|
|
145
|
+
* marker, and a parameter's own trailing whitespace sits on the identifier's declaration
|
|
146
|
+
* rather than on the list entry that holds it. Folding both into the shape the rest of this
|
|
147
|
+
* module already handles lets append/remove treat every kind of factory alike.
|
|
148
|
+
*/
|
|
149
|
+
function normalizeArrowParameters(raw: readonly J.RightPadded<J>[]): J.RightPadded<J>[] {
|
|
150
|
+
if (raw.length === 1 && raw[0].element.kind === J.Kind.Empty) {
|
|
151
|
+
return [...raw];
|
|
152
|
+
}
|
|
153
|
+
const trailingEmpty = raw.length > 1 && raw[raw.length - 1].element.kind === J.Kind.Empty ?
|
|
154
|
+
raw[raw.length - 1] : undefined;
|
|
155
|
+
const real = (trailingEmpty === undefined ? raw : raw.slice(0, -1)).map(foldDeclarationTrailingSpace);
|
|
156
|
+
if (trailingEmpty === undefined) {
|
|
157
|
+
return real;
|
|
158
|
+
}
|
|
159
|
+
const last = real[real.length - 1];
|
|
160
|
+
const marker: TrailingComma = {kind: J.Markers.TrailingComma, id: randomId(), suffix: trailingEmpty.after};
|
|
161
|
+
return [...real.slice(0, -1), {...last, markers: {...last.markers, markers: [...last.markers.markers, marker]}}];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Moves a VariableDeclarations parameter's trailing whitespace from the identifier's own declaration up to the list entry, when the entry doesn't already carry any. */
|
|
165
|
+
function foldDeclarationTrailingSpace(entry: J.RightPadded<J>): J.RightPadded<J> {
|
|
166
|
+
if (entry.element.kind !== J.Kind.VariableDeclarations ||
|
|
167
|
+
entry.after.whitespace !== "" || entry.after.comments.length > 0) {
|
|
168
|
+
return entry;
|
|
169
|
+
}
|
|
170
|
+
const declaration = entry.element as J.VariableDeclarations;
|
|
171
|
+
const variable = declaration.variables[0];
|
|
172
|
+
if (variable === undefined) {
|
|
173
|
+
return entry;
|
|
174
|
+
}
|
|
175
|
+
const folded: J.VariableDeclarations = {
|
|
176
|
+
...declaration,
|
|
177
|
+
variables: [{...variable, after: emptySpace}, ...declaration.variables.slice(1)]
|
|
178
|
+
};
|
|
179
|
+
return {...entry, element: folded, after: variable.after};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function dependencyNames(block: AmdBlock): string[] {
|
|
183
|
+
return elementsOf(block).map(padded => {
|
|
184
|
+
const element = padded.element;
|
|
185
|
+
return isLiteral(element) && typeof element.value === "string" ? element.value : "";
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function parameterNames(block: AmdBlock): (string | undefined)[] {
|
|
190
|
+
return parametersOf(block).map(padded => identifierOf(padded.element)?.simpleName);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function identifierOf(parameter: J): J.Identifier | undefined {
|
|
194
|
+
if (parameter.kind === J.Kind.VariableDeclarations) {
|
|
195
|
+
const name = (parameter as J.VariableDeclarations).variables[0]?.element.name;
|
|
196
|
+
return name !== undefined && isIdentifier(name) ? name : undefined;
|
|
197
|
+
}
|
|
198
|
+
return isIdentifier(parameter) ? parameter : undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Where an entry's leading whitespace sits. A dependency string carries it directly, while
|
|
203
|
+
* a factory parameter carries it on the identifier inside the declaration that wraps it.
|
|
204
|
+
*/
|
|
205
|
+
interface Slot<T extends J> {
|
|
206
|
+
prefixOf(element: T): J.Space;
|
|
207
|
+
|
|
208
|
+
withPrefix(element: T, prefix: J.Space): T;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const dependencySlot: Slot<Expression> = {
|
|
212
|
+
prefixOf: element => element.prefix,
|
|
213
|
+
withPrefix: (element, prefix) => ({...element, prefix})
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const parameterSlot: Slot<J> = {
|
|
217
|
+
prefixOf: element => identifierOf(element)?.prefix ?? element.prefix,
|
|
218
|
+
withPrefix: (element, prefix) => {
|
|
219
|
+
if (element.kind !== J.Kind.VariableDeclarations) {
|
|
220
|
+
return {...element, prefix};
|
|
221
|
+
}
|
|
222
|
+
const declaration = element as J.VariableDeclarations;
|
|
223
|
+
const variable = declaration.variables[0];
|
|
224
|
+
if (variable === undefined) {
|
|
225
|
+
return element;
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
...declaration,
|
|
229
|
+
variables: [
|
|
230
|
+
{...variable, element: {...variable.element, name: {...variable.element.name, prefix}}},
|
|
231
|
+
...declaration.variables.slice(1)
|
|
232
|
+
]
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* A trailing comma is a marker on the entry it follows, so it has to travel to whichever
|
|
239
|
+
* entry ends up last. Left where it was, it prints as a second comma and the array gains a
|
|
240
|
+
* hole, which shifts every dependency after it out of step with its parameter.
|
|
241
|
+
*/
|
|
242
|
+
function moveTrailingComma<T extends J>(
|
|
243
|
+
from: J.RightPadded<T>,
|
|
244
|
+
to: J.RightPadded<T>
|
|
245
|
+
): {from: J.RightPadded<T>, to: J.RightPadded<T>} {
|
|
246
|
+
const trailing = findMarker<TrailingComma>(from, J.Markers.TrailingComma);
|
|
247
|
+
if (trailing === undefined) {
|
|
248
|
+
return {from, to};
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
from: {...from, markers: {...from.markers, markers: from.markers.markers.filter(m => m !== trailing)}},
|
|
252
|
+
to: {...to, markers: {...to.markers, markers: [...to.markers.markers, trailing]}}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The whitespace that separates one entry from the next, taken from the entries already
|
|
258
|
+
* there so that a block listing its dependencies one per line keeps doing so.
|
|
259
|
+
*/
|
|
260
|
+
function separator<T extends J>(entries: readonly J.RightPadded<T>[], slot: Slot<T>): J.Space {
|
|
261
|
+
if (entries.length >= 2) {
|
|
262
|
+
return slot.prefixOf(entries[1].element);
|
|
263
|
+
}
|
|
264
|
+
const first = slot.prefixOf(entries[0].element);
|
|
265
|
+
return spaceContainsNewline(first) ? first : space(" ");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Appends `element`. The trailing entry's padding holds the whitespace before the closing
|
|
270
|
+
* bracket, so it has to move along with the position.
|
|
271
|
+
*/
|
|
272
|
+
function appendEntry<T extends J>(
|
|
273
|
+
entries: readonly J.RightPadded<T>[],
|
|
274
|
+
element: T,
|
|
275
|
+
slot: Slot<T>
|
|
276
|
+
): J.RightPadded<T>[] {
|
|
277
|
+
const last = entries[entries.length - 1];
|
|
278
|
+
if (last === undefined) {
|
|
279
|
+
return [rightPadded<T>(element, emptySpace)];
|
|
280
|
+
}
|
|
281
|
+
const positioned = slot.withPrefix(element, separator(entries, slot));
|
|
282
|
+
const trailing = splitTrailingComments(last.after);
|
|
283
|
+
const moved = moveTrailingComma({...last, after: trailing.comments}, rightPadded<T>(positioned, trailing.whitespace));
|
|
284
|
+
return [...entries.slice(0, -1), moved.from, moved.to];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Splits an entry's trailing padding when it stops being last. Plain whitespace only ever
|
|
289
|
+
* positioned the closing delimiter, so it travels to whichever entry becomes the new last. A
|
|
290
|
+
* comment documents the entry it follows and keeps its own trailing newline, so it stays put
|
|
291
|
+
* whole — a line comment with nothing left after it would otherwise swallow the comma the
|
|
292
|
+
* printer inserts right after this padding.
|
|
293
|
+
*/
|
|
294
|
+
function splitTrailingComments(after: J.Space): {comments: J.Space, whitespace: J.Space} {
|
|
295
|
+
return after.comments.length === 0 ?
|
|
296
|
+
{comments: {...after, whitespace: ""}, whitespace: space(after.whitespace)} :
|
|
297
|
+
{comments: after, whitespace: emptySpace};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* The plain-whitespace part of a leading prefix whose entry is about to be removed. A comment in
|
|
302
|
+
* that prefix documents the entry it precedes and is dropped along with it; the whitespace only
|
|
303
|
+
* ever positioned that entry after the opening bracket or a comma, so it's what travels to
|
|
304
|
+
* whichever entry takes its place.
|
|
305
|
+
*/
|
|
306
|
+
function leadingWhitespaceOf(prefix: J.Space): string {
|
|
307
|
+
return prefix.comments.length === 0 ? prefix.whitespace : "";
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Removes the entry at `index`. The first entry carries no separating whitespace and the
|
|
312
|
+
* last carries the whitespace before the closing bracket, so removing either hands its
|
|
313
|
+
* padding to whichever entry takes its place.
|
|
314
|
+
*/
|
|
315
|
+
function removeEntry<T extends J>(
|
|
316
|
+
entries: readonly J.RightPadded<T>[],
|
|
317
|
+
index: number,
|
|
318
|
+
slot: Slot<T>
|
|
319
|
+
): J.RightPadded<T>[] {
|
|
320
|
+
if (index < 0 || index >= entries.length) {
|
|
321
|
+
return [...entries];
|
|
322
|
+
}
|
|
323
|
+
const remaining = [...entries];
|
|
324
|
+
const [removed] = remaining.splice(index, 1);
|
|
325
|
+
if (remaining.length === 0) {
|
|
326
|
+
return remaining;
|
|
327
|
+
}
|
|
328
|
+
if (index === 0) {
|
|
329
|
+
// Spread the survivor's own prefix rather than replacing it outright, so a comment it
|
|
330
|
+
// already carries survives; only its plain whitespace is replaced.
|
|
331
|
+
const whitespace = leadingWhitespaceOf(slot.prefixOf(removed.element));
|
|
332
|
+
const survivor = remaining[0];
|
|
333
|
+
remaining[0] = {
|
|
334
|
+
...survivor,
|
|
335
|
+
element: slot.withPrefix(survivor.element, {...slot.prefixOf(survivor.element), whitespace})
|
|
336
|
+
};
|
|
337
|
+
} else if (index === remaining.length) {
|
|
338
|
+
const last = {...remaining[index - 1], after: removed.after};
|
|
339
|
+
remaining[index - 1] = moveTrailingComma(removed, last).to;
|
|
340
|
+
}
|
|
341
|
+
return remaining;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function emptyExpression(): J.Empty {
|
|
345
|
+
return {kind: J.Kind.Empty, id: randomId(), prefix: emptySpace, markers: emptyMarkers};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function withElements(dependencies: J.NewArray, elements: J.RightPadded<Expression>[]): J.NewArray {
|
|
349
|
+
const initializer = dependencies.initializer!;
|
|
350
|
+
return {
|
|
351
|
+
...dependencies,
|
|
352
|
+
initializer: {
|
|
353
|
+
...initializer,
|
|
354
|
+
elements: elements.length === 0 ? [rightPadded<Expression>(emptyExpression(), emptySpace)] : elements
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function withParameters(
|
|
360
|
+
factory: J.MethodDeclaration | J.Lambda,
|
|
361
|
+
parameters: J.RightPadded<J>[]
|
|
362
|
+
): J.MethodDeclaration | J.Lambda {
|
|
363
|
+
const elements = parameters.length === 0 ? [rightPadded<J>(emptyExpression(), emptySpace)] : parameters;
|
|
364
|
+
if (factory.kind === J.Kind.MethodDeclaration) {
|
|
365
|
+
const method = factory as J.MethodDeclaration;
|
|
366
|
+
return {...method, parameters: {...method.parameters, elements: elements as J.RightPadded<Statement>[]}};
|
|
367
|
+
}
|
|
368
|
+
const lambda = factory as J.Lambda;
|
|
369
|
+
// Unparenthesized arrow parameters are only valid JavaScript for exactly one parameter.
|
|
370
|
+
if (lambda.parameters.parenthesized || parameters.length === 1) {
|
|
371
|
+
return {...lambda, parameters: {...lambda.parameters, parameters: elements}};
|
|
372
|
+
}
|
|
373
|
+
return parenthesize(lambda, elements);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* A bare arrow parameter has no closing delimiter of its own — the space before `=>` sits on
|
|
378
|
+
* the parameter itself. Wrapping it in parens turns that into the space before the new `)`,
|
|
379
|
+
* so it has to move to `arrow` instead, where it will actually print before `=>` again.
|
|
380
|
+
*/
|
|
381
|
+
function parenthesize(lambda: J.Lambda, elements: J.RightPadded<J>[]): J.Lambda {
|
|
382
|
+
const last = elements[elements.length - 1];
|
|
383
|
+
const arrowSpace = last.after.whitespace === "" ? lambda.arrow : last.after;
|
|
384
|
+
return {
|
|
385
|
+
...lambda,
|
|
386
|
+
parameters: {...lambda.parameters, parenthesized: true, parameters: [...elements.slice(0, -1), {...last, after: emptySpace}]},
|
|
387
|
+
arrow: arrowSpace
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function identifier(name: string): J.Identifier {
|
|
392
|
+
return {
|
|
393
|
+
kind: J.Kind.Identifier,
|
|
394
|
+
id: randomId(),
|
|
395
|
+
prefix: emptySpace,
|
|
396
|
+
markers: emptyMarkers,
|
|
397
|
+
annotations: [],
|
|
398
|
+
simpleName: name,
|
|
399
|
+
type: undefined,
|
|
400
|
+
fieldType: undefined
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function dependencyLiteral(module: string, quote: string): J.Literal {
|
|
405
|
+
return {
|
|
406
|
+
kind: J.Kind.Literal,
|
|
407
|
+
id: randomId(),
|
|
408
|
+
prefix: emptySpace,
|
|
409
|
+
markers: emptyMarkers,
|
|
410
|
+
value: module,
|
|
411
|
+
valueSource: `${quote}${module}${quote}`
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** The quote the block's own dependencies use, so a new one matches them. */
|
|
416
|
+
function quoteOf(block: AmdBlock): string {
|
|
417
|
+
for (const padded of elementsOf(block)) {
|
|
418
|
+
const source = isLiteral(padded.element) ? (padded.element as J.Literal).valueSource : undefined;
|
|
419
|
+
const quote = source?.charAt(0);
|
|
420
|
+
if (quote === '"' || quote === "'") {
|
|
421
|
+
return quote;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return '"';
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function parameterDeclaration(name: string): J.VariableDeclarations {
|
|
428
|
+
return {
|
|
429
|
+
kind: J.Kind.VariableDeclarations,
|
|
430
|
+
id: randomId(),
|
|
431
|
+
prefix: emptySpace,
|
|
432
|
+
markers: emptyMarkers,
|
|
433
|
+
leadingAnnotations: [],
|
|
434
|
+
modifiers: [],
|
|
435
|
+
variables: [rightPadded({
|
|
436
|
+
kind: J.Kind.NamedVariable,
|
|
437
|
+
id: randomId(),
|
|
438
|
+
prefix: emptySpace,
|
|
439
|
+
markers: emptyMarkers,
|
|
440
|
+
name: identifier(name),
|
|
441
|
+
dimensionsAfterName: []
|
|
442
|
+
} as J.VariableDeclarations.NamedVariable, emptySpace)]
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Adds a dependency and its binding, keeping the array and the parameter list index-aligned.
|
|
448
|
+
* Refuses (returns `undefined`) unless the two are already the same length: fewer parameters
|
|
449
|
+
* than dependencies would invent a binding for one an author left unbound on purpose, and more
|
|
450
|
+
* would pair the new dependency with an existing surplus parameter instead of a fresh one —
|
|
451
|
+
* either way corrupting the pairing this module exists to protect.
|
|
452
|
+
*/
|
|
453
|
+
export function withDependency(
|
|
454
|
+
call: J.MethodInvocation,
|
|
455
|
+
block: AmdBlock,
|
|
456
|
+
module: string,
|
|
457
|
+
binding: string
|
|
458
|
+
): J.MethodInvocation | undefined {
|
|
459
|
+
if (parametersOf(block).length !== elementsOf(block).length) {
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
const dependencies = withElements(
|
|
463
|
+
block.dependencies,
|
|
464
|
+
appendEntry(elementsOf(block), dependencyLiteral(module, quoteOf(block)), dependencySlot));
|
|
465
|
+
const factory = withParameters(
|
|
466
|
+
block.factory,
|
|
467
|
+
appendEntry(parametersOf(block), parameterDeclaration(binding), parameterSlot));
|
|
468
|
+
return withParts(call, block, dependencies, factory);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function withoutDependencyAt(
|
|
472
|
+
call: J.MethodInvocation,
|
|
473
|
+
block: AmdBlock,
|
|
474
|
+
index: number
|
|
475
|
+
): J.MethodInvocation {
|
|
476
|
+
const dependencies = withElements(block.dependencies, removeEntry(elementsOf(block), index, dependencySlot));
|
|
477
|
+
const parameters = parametersOf(block);
|
|
478
|
+
const factory = index >= parameters.length ?
|
|
479
|
+
block.factory :
|
|
480
|
+
withParameters(block.factory, removeEntry(parameters, index, parameterSlot));
|
|
481
|
+
return withParts(call, block, dependencies, factory);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Swaps the module the dependency at `index` names, leaving its parameter and every other
|
|
486
|
+
* entry's position untouched — simpler than a removal and a fresh append, which would shift
|
|
487
|
+
* every dependency after it and re-litigate the parameter pairing this module exists to protect.
|
|
488
|
+
*/
|
|
489
|
+
export function withDependencyModuleAt(
|
|
490
|
+
call: J.MethodInvocation,
|
|
491
|
+
block: AmdBlock,
|
|
492
|
+
index: number,
|
|
493
|
+
module: string
|
|
494
|
+
): J.MethodInvocation {
|
|
495
|
+
const quote = quoteOf(block);
|
|
496
|
+
const elements = [...elementsOf(block)];
|
|
497
|
+
const entry = elements[index];
|
|
498
|
+
const literal = entry.element as J.Literal;
|
|
499
|
+
const updated: J.Literal = {...literal, value: module, valueSource: `${quote}${module}${quote}`};
|
|
500
|
+
elements[index] = {...entry, element: updated};
|
|
501
|
+
const dependencies = withElements(block.dependencies, elements);
|
|
502
|
+
return withParts(call, block, dependencies, block.factory);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function withParts(
|
|
506
|
+
call: J.MethodInvocation,
|
|
507
|
+
block: AmdBlock,
|
|
508
|
+
dependencies: J.NewArray,
|
|
509
|
+
factory: J.MethodDeclaration | J.Lambda
|
|
510
|
+
): J.MethodInvocation {
|
|
511
|
+
const args = [...call.arguments.elements];
|
|
512
|
+
let factoryIndex = block.factoryIndex;
|
|
513
|
+
if (block.dependenciesIndex >= 0) {
|
|
514
|
+
args[block.dependenciesIndex] = {...args[block.dependenciesIndex], element: dependencies};
|
|
515
|
+
} else if (present(dependencies.initializer?.elements ?? []).length > 0) {
|
|
516
|
+
// The array takes the position the factory held, so it inherits what led up to it.
|
|
517
|
+
const leading = args[factoryIndex].element;
|
|
518
|
+
args[factoryIndex] = {...args[factoryIndex], element: {...leading, prefix: space(" ")}};
|
|
519
|
+
args.splice(factoryIndex, 0, rightPadded<Expression>({...dependencies, prefix: leading.prefix}, emptySpace));
|
|
520
|
+
factoryIndex += 1;
|
|
521
|
+
}
|
|
522
|
+
args[factoryIndex] = {...args[factoryIndex], element: restoreFactory(args[factoryIndex].element, factory)};
|
|
523
|
+
return {...call, arguments: {...call.arguments, elements: args}};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Puts the rewritten factory back into whichever wrapper the original was written as. */
|
|
527
|
+
function restoreFactory(original: Expression, factory: J.MethodDeclaration | J.Lambda): Expression {
|
|
528
|
+
if (original.kind === JS.Kind.StatementExpression) {
|
|
529
|
+
const statementExpression: JS.StatementExpression = {
|
|
530
|
+
...(original as JS.StatementExpression),
|
|
531
|
+
statement: factory as J.MethodDeclaration
|
|
532
|
+
};
|
|
533
|
+
return statementExpression;
|
|
534
|
+
}
|
|
535
|
+
const arrowFunction: JS.ArrowFunction = {...(original as JS.ArrowFunction), lambda: factory as J.Lambda};
|
|
536
|
+
return arrowFunction;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** The subset of `MaybeBindOptions` that selects which callees introduce an AMD block. */
|
|
540
|
+
export interface AmdCalleeOptions {
|
|
541
|
+
/** Callees that introduce an AMD block. */
|
|
542
|
+
amdCallee?: string | readonly string[];
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export function calleesOf(options?: AmdCalleeOptions): readonly string[] {
|
|
546
|
+
const callee = options?.amdCallee;
|
|
547
|
+
return callee === undefined ? DEFAULT_AMD_CALLEES : typeof callee === "string" ? [callee] : callee;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** The nearest AMD block the cursor sits inside, which is the one a binding belongs to. */
|
|
551
|
+
export function enclosingAmdBlock(
|
|
552
|
+
visitor: JavaScriptVisitor<any>,
|
|
553
|
+
options?: AmdCalleeOptions
|
|
554
|
+
): {call: J.MethodInvocation, block: AmdBlock} | undefined {
|
|
555
|
+
const callees = calleesOf(options);
|
|
556
|
+
let cursor = cursorOf(visitor);
|
|
557
|
+
while (cursor !== undefined) {
|
|
558
|
+
const value = cursor.value as J | undefined;
|
|
559
|
+
if (value?.kind === J.Kind.MethodInvocation) {
|
|
560
|
+
const block = amdBlockOf(value as J.MethodInvocation, callees);
|
|
561
|
+
if (block !== undefined) {
|
|
562
|
+
return {call: value as J.MethodInvocation, block};
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
cursor = cursor.parent;
|
|
566
|
+
}
|
|
567
|
+
return undefined;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** The conventional local name for a module: its last path segment. */
|
|
571
|
+
export function lastSegment(module: string): string {
|
|
572
|
+
return module.substring(module.lastIndexOf("/") + 1);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Always-reserved words, plus the strict-mode-only ones (module code is always strict) and
|
|
576
|
+
// `await`/`yield`, contextual elsewhere but a trap to bind regardless of where they're legal.
|
|
577
|
+
const RESERVED_WORDS = new Set([
|
|
578
|
+
"break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do",
|
|
579
|
+
"else", "enum", "export", "extends", "false", "finally", "for", "function", "if", "import",
|
|
580
|
+
"in", "instanceof", "new", "null", "return", "super", "switch", "this", "throw", "true", "try",
|
|
581
|
+
"typeof", "var", "void", "while", "with",
|
|
582
|
+
"await", "implements", "interface", "let", "package", "private", "protected", "public",
|
|
583
|
+
"static", "yield"
|
|
584
|
+
]);
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* `lastSegment(module)`, or `undefined` where that string cannot bind a name — a scoped
|
|
588
|
+
* package's `@scope/my-lib`, a subpath's `lodash.merge`, a `-`/`.`-bearing name, or a reserved
|
|
589
|
+
* word are all real module strings that are not legal identifiers. ASCII identifiers only —
|
|
590
|
+
* stricter than JavaScript itself, which allows Unicode, but refusing there is safe.
|
|
591
|
+
*/
|
|
592
|
+
export function derivedBindingName(module: string): string | undefined {
|
|
593
|
+
const segment = lastSegment(module);
|
|
594
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment) && !RESERVED_WORDS.has(segment) ? segment : undefined;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Queued reservations already targeting this block: the shared source for both dedup and deconfliction. */
|
|
598
|
+
function queuedFor(visitor: JavaScriptVisitor<any>, blockId: UUID): AddAmdDependency<any>[] {
|
|
599
|
+
return visitor.afterVisit.filter((v): v is AddAmdDependency<any> => v instanceof AddAmdDependency && v.blockId === blockId);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* A local binding for `module` on the AMD lane, creating one where the block does not already
|
|
604
|
+
* have it, or `undefined` where no safe binding exists.
|
|
605
|
+
*
|
|
606
|
+
* The name is decided from the cursor and returned at once; the edit that creates the binding
|
|
607
|
+
* is deferred onto `visitor.afterVisit`, as `bindImport`'s is.
|
|
608
|
+
*/
|
|
609
|
+
export function bindAmd(
|
|
610
|
+
visitor: JavaScriptVisitor<any>,
|
|
611
|
+
amd: {call: J.MethodInvocation, block: AmdBlock},
|
|
612
|
+
module: string,
|
|
613
|
+
preferredName: string | undefined,
|
|
614
|
+
callees: readonly string[],
|
|
615
|
+
pinned: boolean = false
|
|
616
|
+
): string | undefined {
|
|
617
|
+
const modules = dependencyNames(amd.block);
|
|
618
|
+
const bindings = parameterNames(amd.block);
|
|
619
|
+
|
|
620
|
+
const declared = module === "" ? -1 : modules.indexOf(module);
|
|
621
|
+
if (declared >= 0) {
|
|
622
|
+
return bindings[declared];
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Two calls naming the same module at the same block are the same request (ADR 0013),
|
|
626
|
+
// answered from whichever reservation is already queued rather than doubling the dependency.
|
|
627
|
+
const queued = queuedFor(visitor, amd.call.id);
|
|
628
|
+
const reserved = queued.find(v => v.module === module)?.binding;
|
|
629
|
+
if (reserved !== undefined) {
|
|
630
|
+
return reserved;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// A parameter can only be appended at the end, so a block whose dependency and parameter
|
|
634
|
+
// counts already disagree would bind the new module against the wrong parameter either way.
|
|
635
|
+
if (bindings.length !== elementsOf(amd.block).length) {
|
|
636
|
+
return undefined;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (preferredName === undefined && derivedBindingName(module) === undefined) {
|
|
640
|
+
return undefined;
|
|
641
|
+
}
|
|
642
|
+
// A parameter is in scope across the whole factory, and the queue hands this name to later
|
|
643
|
+
// requests at cursors inside it, so every name the factory declares is one it has to clear.
|
|
644
|
+
const taken = [...bindings, ...namesDeclaredWithin(amd.block.factory), ...queued.map(v => v.binding)];
|
|
645
|
+
const requested = preferredName ?? derivedBindingName(module)!;
|
|
646
|
+
const binding = deconflict(requested, candidate => taken.includes(candidate));
|
|
647
|
+
if (pinned && binding !== requested) {
|
|
648
|
+
// An alias is bound verbatim or not at all, since the caller may already have emitted
|
|
649
|
+
// code naming it, and a deconflicted spelling would leave that unbound.
|
|
650
|
+
return undefined;
|
|
651
|
+
}
|
|
652
|
+
visitor.afterVisit.push(new AddAmdDependency(amd.call.id, module, binding, callees));
|
|
653
|
+
return binding;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** The factory's body: a `J.Block` for `function(){}` and most arrows, or the bare expression for `() => expr`. */
|
|
657
|
+
export function bodyOf(block: AmdBlock): J | undefined {
|
|
658
|
+
return block.factory.kind === J.Kind.MethodDeclaration ?
|
|
659
|
+
(block.factory as J.MethodDeclaration).body :
|
|
660
|
+
(block.factory as J.Lambda).body;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Whether the factory body references `name`. This counts any identifier of that name,
|
|
665
|
+
* including a member in a field access, since a stray dependency is fine where a shadowed one
|
|
666
|
+
* is not. `missingBodyAnswer` covers a body-less factory: `false` for an addition (nothing to
|
|
667
|
+
* conflict with), `true` for a removal (nothing to prove the binding unused).
|
|
668
|
+
*/
|
|
669
|
+
export async function references(block: AmdBlock, name: string, missingBodyAnswer: boolean): Promise<boolean> {
|
|
670
|
+
const body = bodyOf(block);
|
|
671
|
+
if (body === undefined) {
|
|
672
|
+
return missingBodyAnswer;
|
|
673
|
+
}
|
|
674
|
+
let found = false;
|
|
675
|
+
const finder = new class extends JavaScriptVisitor<ExecutionContext> {
|
|
676
|
+
override async visitIdentifier(id: J.Identifier, c: ExecutionContext) {
|
|
677
|
+
if (id.simpleName === name) {
|
|
678
|
+
found = true;
|
|
679
|
+
}
|
|
680
|
+
return super.visitIdentifier(id, c);
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
await finder.visit(body, new ExecutionContext());
|
|
684
|
+
return found;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Every name the factory body references, one walk for the whole block rather than one per
|
|
689
|
+
* name. `missingBodyAnswer` means what it means on `references`: the answer `.has` gives, for
|
|
690
|
+
* every name, when the factory has no body to walk.
|
|
691
|
+
*/
|
|
692
|
+
export async function namesUsed(block: AmdBlock, missingBodyAnswer: boolean): Promise<{has(name: string): boolean}> {
|
|
693
|
+
const body = bodyOf(block);
|
|
694
|
+
if (body === undefined) {
|
|
695
|
+
return {has: () => missingBodyAnswer};
|
|
696
|
+
}
|
|
697
|
+
const names = new Set<string>();
|
|
698
|
+
const collector = new class extends JavaScriptVisitor<ExecutionContext> {
|
|
699
|
+
override async visitIdentifier(id: J.Identifier, c: ExecutionContext) {
|
|
700
|
+
names.add(id.simpleName);
|
|
701
|
+
return super.visitIdentifier(id, c);
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
await collector.visit(body, new ExecutionContext());
|
|
705
|
+
return names;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Applies the dependency a `maybeBind` call settled on. The block is found by id: its caller
|
|
710
|
+
* has already emitted a reference to the binding, so a block that cannot be found is an error
|
|
711
|
+
* rather than a skipped edit.
|
|
712
|
+
*/
|
|
713
|
+
export class AddAmdDependency<P> extends JavaScriptVisitor<P> {
|
|
714
|
+
constructor(
|
|
715
|
+
readonly blockId: UUID,
|
|
716
|
+
readonly module: string,
|
|
717
|
+
readonly binding: string,
|
|
718
|
+
readonly callees: readonly string[]
|
|
719
|
+
) {
|
|
720
|
+
super();
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
private applied = false;
|
|
724
|
+
|
|
725
|
+
override async visitJsCompilationUnit(cu: JS.CompilationUnit, p: P): Promise<J | undefined> {
|
|
726
|
+
const visited = await super.visitJsCompilationUnit(cu, p) as JS.CompilationUnit;
|
|
727
|
+
if (!this.applied) {
|
|
728
|
+
throw new Error(
|
|
729
|
+
`No AMD block ${this.blockId} to declare '${this.module}' on, but '${this.binding}' was reported bound`);
|
|
730
|
+
}
|
|
731
|
+
return visited;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
override async visitMethodInvocation(m: J.MethodInvocation, p: P): Promise<J | undefined> {
|
|
735
|
+
const visited = await super.visitMethodInvocation(m, p) as J.MethodInvocation;
|
|
736
|
+
if (visited.id !== this.blockId) {
|
|
737
|
+
return visited;
|
|
738
|
+
}
|
|
739
|
+
const block = amdBlockOf(visited, this.callees);
|
|
740
|
+
if (block === undefined) {
|
|
741
|
+
return visited;
|
|
742
|
+
}
|
|
743
|
+
this.applied = true;
|
|
744
|
+
if (!(await references(block, this.binding, false))) {
|
|
745
|
+
// Nothing referenced the binding, so the caller asked and then did not use the answer.
|
|
746
|
+
return visited;
|
|
747
|
+
}
|
|
748
|
+
// Another edit in the same visit can drop or add a factory parameter, reopening a count
|
|
749
|
+
// mismatch bindAmd's own check already cleared — an error here for the same reason a missing
|
|
750
|
+
// block is one.
|
|
751
|
+
const dependency = withDependency(visited, block, this.module, this.binding);
|
|
752
|
+
if (dependency === undefined) {
|
|
753
|
+
throw new Error(
|
|
754
|
+
`AMD block ${this.blockId} no longer has matching dependency and parameter counts, ` +
|
|
755
|
+
`so '${this.module}' could not be declared for '${this.binding}', which was reported bound`);
|
|
756
|
+
}
|
|
757
|
+
return dependency;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Applies a `maybeRebind` call's decision on the AMD lane: swaps the dependency's module in
|
|
763
|
+
* place, found by the block's tree id and the module it named when the caller asked.
|
|
764
|
+
*/
|
|
765
|
+
export class RebindAmdDependency<P> extends JavaScriptVisitor<P> {
|
|
766
|
+
constructor(
|
|
767
|
+
readonly blockId: UUID,
|
|
768
|
+
readonly fromModule: string,
|
|
769
|
+
readonly toModule: string,
|
|
770
|
+
readonly callees: readonly string[]
|
|
771
|
+
) {
|
|
772
|
+
super();
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
private applied = false;
|
|
776
|
+
|
|
777
|
+
override async visitJsCompilationUnit(cu: JS.CompilationUnit, p: P): Promise<J | undefined> {
|
|
778
|
+
const visited = await super.visitJsCompilationUnit(cu, p) as JS.CompilationUnit;
|
|
779
|
+
if (!this.applied) {
|
|
780
|
+
throw new Error(`No AMD block ${this.blockId} to rebind '${this.fromModule}' to '${this.toModule}' on`);
|
|
781
|
+
}
|
|
782
|
+
return visited;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
override async visitMethodInvocation(m: J.MethodInvocation, p: P): Promise<J | undefined> {
|
|
786
|
+
const visited = await super.visitMethodInvocation(m, p) as J.MethodInvocation;
|
|
787
|
+
if (visited.id !== this.blockId) {
|
|
788
|
+
return visited;
|
|
789
|
+
}
|
|
790
|
+
const block = amdBlockOf(visited, this.callees);
|
|
791
|
+
if (block === undefined) {
|
|
792
|
+
return visited;
|
|
793
|
+
}
|
|
794
|
+
this.applied = true;
|
|
795
|
+
const index = this.fromModule === "" ? -1 : dependencyNames(block).indexOf(this.fromModule);
|
|
796
|
+
if (index < 0) {
|
|
797
|
+
throw new Error(
|
|
798
|
+
`AMD block ${this.blockId} no longer has dependency '${this.fromModule}' to rebind to '${this.toModule}'`);
|
|
799
|
+
}
|
|
800
|
+
return withDependencyModuleAt(visited, block, index, this.toModule);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* The AMD counterpart to `RemoveImport`: drops a dependency the block's body no longer names,
|
|
806
|
+
* along with the parameter bound to it. A dependency the factory takes no parameter for is
|
|
807
|
+
* loaded for its side effects and stays, matching `bindAmd`'s own read of such a block. A
|
|
808
|
+
* `member`-scoped request is a no-op here — see `maybeUnbind`.
|
|
809
|
+
*/
|
|
810
|
+
export class RemoveAmdDependency<P> extends JavaScriptVisitor<P> {
|
|
811
|
+
constructor(
|
|
812
|
+
readonly module: string,
|
|
813
|
+
readonly member?: string,
|
|
814
|
+
readonly callees: readonly string[] = DEFAULT_AMD_CALLEES
|
|
815
|
+
) {
|
|
816
|
+
super();
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
override async visitMethodInvocation(m: J.MethodInvocation, p: P): Promise<J | undefined> {
|
|
820
|
+
const visited = await super.visitMethodInvocation(m, p) as J.MethodInvocation;
|
|
821
|
+
if (this.member !== undefined) {
|
|
822
|
+
return visited;
|
|
823
|
+
}
|
|
824
|
+
const block = amdBlockOf(visited, this.callees);
|
|
825
|
+
if (block === undefined) {
|
|
826
|
+
return visited;
|
|
827
|
+
}
|
|
828
|
+
const index = dependencyNames(block).indexOf(this.module);
|
|
829
|
+
if (index < 0) {
|
|
830
|
+
return visited;
|
|
831
|
+
}
|
|
832
|
+
const binding = parameterNames(block)[index];
|
|
833
|
+
if (binding === undefined || await references(block, binding, true)) {
|
|
834
|
+
return visited;
|
|
835
|
+
}
|
|
836
|
+
return withoutDependencyAt(visited, block, index);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Drops AMD bindings a rewrite left unreferenced. One already unused beforehand stays: it is
|
|
842
|
+
* loaded for its side effects, so dropping it changes what the module loads. Needing the tree as
|
|
843
|
+
* it stood before the rewrite is why this takes both and does not defer. Blocks are matched by
|
|
844
|
+
* the call's id, so one a rewrite rebuilds rather than edits is left alone.
|
|
845
|
+
*/
|
|
846
|
+
export async function removeNewlyUnusedAmdBindings(
|
|
847
|
+
before: JS.CompilationUnit,
|
|
848
|
+
after: JS.CompilationUnit,
|
|
849
|
+
ctx: ExecutionContext,
|
|
850
|
+
options?: AmdCalleeOptions
|
|
851
|
+
): Promise<JS.CompilationUnit> {
|
|
852
|
+
const callees = calleesOf(options);
|
|
853
|
+
const usedBeforeByBlock = new Map<UUID, Set<string>>();
|
|
854
|
+
const collector = new class extends JavaScriptVisitor<ExecutionContext> {
|
|
855
|
+
override async visitMethodInvocation(m: J.MethodInvocation, c: ExecutionContext) {
|
|
856
|
+
const block = amdBlockOf(m, callees);
|
|
857
|
+
if (block !== undefined) {
|
|
858
|
+
usedBeforeByBlock.set(m.id, await usedBindings(block));
|
|
859
|
+
}
|
|
860
|
+
return super.visitMethodInvocation(m, c);
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
await collector.visit(before, ctx);
|
|
864
|
+
if (usedBeforeByBlock.size === 0) {
|
|
865
|
+
return after;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const sweeper = new class extends JavaScriptVisitor<ExecutionContext> {
|
|
869
|
+
override async visitMethodInvocation(m: J.MethodInvocation, c: ExecutionContext): Promise<J | undefined> {
|
|
870
|
+
let call = await super.visitMethodInvocation(m, c) as J.MethodInvocation;
|
|
871
|
+
const usedBefore = usedBeforeByBlock.get(call.id);
|
|
872
|
+
let block = usedBefore === undefined ? undefined : amdBlockOf(call, callees);
|
|
873
|
+
if (usedBefore === undefined || block === undefined) {
|
|
874
|
+
return call;
|
|
875
|
+
}
|
|
876
|
+
// A block written `define(factory)` takes `require`, `exports` and `module` from the
|
|
877
|
+
// loader, so a parameter there is positional against arguments no dependency list names.
|
|
878
|
+
if (block.dependenciesIndex < 0 || parameterNames(block).length !== elementsOf(block).length) {
|
|
879
|
+
return call;
|
|
880
|
+
}
|
|
881
|
+
// withoutDependencyAt only edits the dependency array and parameter list, never the
|
|
882
|
+
// body, so which names it references can't change between removals.
|
|
883
|
+
const usedNow = await namesUsed(block, true);
|
|
884
|
+
for (; ;) {
|
|
885
|
+
const bindings = parameterNames(block);
|
|
886
|
+
const goneIndex = bindings.findIndex(binding =>
|
|
887
|
+
binding !== undefined && usedBefore.has(binding) && !usedNow.has(binding));
|
|
888
|
+
if (goneIndex < 0) {
|
|
889
|
+
return call;
|
|
890
|
+
}
|
|
891
|
+
call = withoutDependencyAt(call, block, goneIndex);
|
|
892
|
+
// A removal shifts the indices of the dependencies after it, so the block is
|
|
893
|
+
// re-derived from `call` rather than reused with stale offsets.
|
|
894
|
+
const next = amdBlockOf(call, callees);
|
|
895
|
+
if (next === undefined) {
|
|
896
|
+
return call;
|
|
897
|
+
}
|
|
898
|
+
block = next;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
return await sweeper.visit(after, ctx) as JS.CompilationUnit;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/** The block's parameter names that its body actually references, from a single walk of it. */
|
|
906
|
+
async function usedBindings(block: AmdBlock): Promise<Set<string>> {
|
|
907
|
+
const usedNames = await namesUsed(block, false);
|
|
908
|
+
const used = new Set<string>();
|
|
909
|
+
for (const binding of parameterNames(block)) {
|
|
910
|
+
if (binding !== undefined && usedNames.has(binding)) {
|
|
911
|
+
used.add(binding);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return used;
|
|
915
|
+
}
|