@openrewrite/rewrite 8.91.2 → 8.91.3
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/execution.d.ts +8 -0
- package/dist/execution.d.ts.map +1 -1
- package/dist/execution.js +31 -0
- package/dist/execution.js.map +1 -1
- package/dist/javascript/add-import.d.ts +64 -9
- package/dist/javascript/add-import.d.ts.map +1 -1
- package/dist/javascript/add-import.js +398 -71
- package/dist/javascript/add-import.js.map +1 -1
- package/dist/javascript/amd.d.ts +11 -1
- package/dist/javascript/amd.d.ts.map +1 -1
- package/dist/javascript/amd.js +43 -9
- package/dist/javascript/amd.js.map +1 -1
- package/dist/javascript/binding.d.ts +10 -2
- package/dist/javascript/binding.d.ts.map +1 -1
- package/dist/javascript/binding.js +44 -71
- package/dist/javascript/binding.js.map +1 -1
- package/dist/javascript/comparator.d.ts +2 -0
- package/dist/javascript/comparator.d.ts.map +1 -1
- package/dist/javascript/comparator.js +5 -1
- package/dist/javascript/comparator.js.map +1 -1
- package/dist/javascript/format/format.d.ts +5 -2
- package/dist/javascript/format/format.d.ts.map +1 -1
- package/dist/javascript/format/format.js +51 -11
- package/dist/javascript/format/format.js.map +1 -1
- package/dist/javascript/format/minimum-viable-spacing-visitor.d.ts +19 -0
- package/dist/javascript/format/minimum-viable-spacing-visitor.d.ts.map +1 -1
- package/dist/javascript/format/minimum-viable-spacing-visitor.js +180 -7
- package/dist/javascript/format/minimum-viable-spacing-visitor.js.map +1 -1
- package/dist/javascript/format/prettier-format.d.ts +2 -1
- package/dist/javascript/format/prettier-format.d.ts.map +1 -1
- package/dist/javascript/format/prettier-format.js +33 -23
- package/dist/javascript/format/prettier-format.js.map +1 -1
- package/dist/javascript/index.d.ts +3 -3
- package/dist/javascript/index.d.ts.map +1 -1
- package/dist/javascript/index.js +21 -1
- package/dist/javascript/index.js.map +1 -1
- package/dist/javascript/parser-utils.d.ts.map +1 -1
- package/dist/javascript/parser-utils.js +5 -9
- package/dist/javascript/parser-utils.js.map +1 -1
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +48 -3
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/print.d.ts +1 -0
- package/dist/javascript/print.d.ts.map +1 -1
- package/dist/javascript/print.js +9 -0
- package/dist/javascript/print.js.map +1 -1
- package/dist/javascript/recipes/change-import.d.ts +6 -3
- package/dist/javascript/recipes/change-import.d.ts.map +1 -1
- package/dist/javascript/recipes/change-import.js +17 -277
- package/dist/javascript/recipes/change-import.js.map +1 -1
- package/dist/javascript/rpc.js +2 -0
- package/dist/javascript/rpc.js.map +1 -1
- package/dist/javascript/scope.d.ts +40 -7
- package/dist/javascript/scope.d.ts.map +1 -1
- package/dist/javascript/scope.js +182 -30
- package/dist/javascript/scope.js.map +1 -1
- package/dist/javascript/templating/bindings.js +2 -20
- package/dist/javascript/templating/bindings.js.map +1 -1
- package/dist/javascript/templating/comparator.d.ts +2 -0
- package/dist/javascript/templating/comparator.d.ts.map +1 -1
- package/dist/javascript/templating/comparator.js +25 -0
- package/dist/javascript/templating/comparator.js.map +1 -1
- package/dist/javascript/templating/engine.d.ts +5 -0
- package/dist/javascript/templating/engine.d.ts.map +1 -1
- package/dist/javascript/templating/engine.js +30 -12
- package/dist/javascript/templating/engine.js.map +1 -1
- package/dist/javascript/templating/template.d.ts.map +1 -1
- package/dist/javascript/templating/template.js +6 -0
- package/dist/javascript/templating/template.js.map +1 -1
- package/dist/javascript/templating/types.d.ts +5 -0
- package/dist/javascript/templating/types.d.ts.map +1 -1
- package/dist/javascript/tree.d.ts +1 -0
- package/dist/javascript/tree.d.ts.map +1 -1
- package/dist/javascript/tree.js.map +1 -1
- package/dist/javascript/type-mapping.d.ts.map +1 -1
- package/dist/javascript/type-mapping.js +14 -0
- package/dist/javascript/type-mapping.js.map +1 -1
- package/dist/javascript/visitor.d.ts.map +1 -1
- package/dist/javascript/visitor.js +1 -0
- package/dist/javascript/visitor.js.map +1 -1
- package/dist/parser.d.ts +6 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +23 -1
- package/dist/parser.js.map +1 -1
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +5 -2
- package/dist/print.js.map +1 -1
- package/dist/rewrite-javascript-version.txt +1 -1
- package/dist/rpc/request/parse-project.d.ts +11 -1
- package/dist/rpc/request/parse-project.d.ts.map +1 -1
- package/dist/rpc/request/parse-project.js +7 -2
- package/dist/rpc/request/parse-project.js.map +1 -1
- package/dist/rpc/request/parse.d.ts +13 -1
- package/dist/rpc/request/parse.d.ts.map +1 -1
- package/dist/rpc/request/parse.js +8 -2
- package/dist/rpc/request/parse.js.map +1 -1
- package/package.json +1 -1
- package/src/execution.ts +23 -0
- package/src/javascript/add-import.ts +449 -78
- package/src/javascript/amd.ts +49 -10
- package/src/javascript/binding.ts +60 -74
- package/src/javascript/comparator.ts +6 -1
- package/src/javascript/format/format.ts +56 -12
- package/src/javascript/format/minimum-viable-spacing-visitor.ts +196 -7
- package/src/javascript/format/prettier-format.ts +35 -25
- package/src/javascript/index.ts +10 -3
- package/src/javascript/parser-utils.ts +5 -11
- package/src/javascript/parser.ts +65 -6
- package/src/javascript/print.ts +10 -0
- package/src/javascript/recipes/change-import.ts +19 -349
- package/src/javascript/rpc.ts +2 -0
- package/src/javascript/scope.ts +210 -31
- package/src/javascript/templating/bindings.ts +2 -25
- package/src/javascript/templating/comparator.ts +29 -2
- package/src/javascript/templating/engine.ts +41 -14
- package/src/javascript/templating/template.ts +6 -0
- package/src/javascript/templating/types.ts +6 -0
- package/src/javascript/tree.ts +1 -0
- package/src/javascript/type-mapping.ts +15 -0
- package/src/javascript/visitor.ts +1 -0
- package/src/parser.ts +24 -1
- package/src/print.ts +5 -2
- package/src/rpc/request/parse-project.ts +6 -2
- package/src/rpc/request/parse.ts +7 -2
package/src/javascript/scope.ts
CHANGED
|
@@ -19,37 +19,113 @@ import {JS} from "./tree";
|
|
|
19
19
|
// scope.ts sits below the visitor, so this stays type-only.
|
|
20
20
|
import type {JavaScriptVisitor} from "./visitor";
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
const noNames: ReadonlySet<string> = new Set();
|
|
23
|
+
|
|
24
|
+
/** One scope: the names it binds itself, the scopes around it, and what they answer together. */
|
|
23
25
|
export interface Scope {
|
|
26
|
+
/** The names this scope binds itself, which is not what it reaches: for that, ask `declares`. */
|
|
27
|
+
names(): ReadonlySet<string>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Visits this scope and then each one enclosing it, innermost first, stopping where `visit`
|
|
31
|
+
* returns false. Builds a scope per step, so `declares` is the cheaper way to ask about a
|
|
32
|
+
* name already in hand.
|
|
33
|
+
*/
|
|
34
|
+
walk(visit: (scope: Scope) => boolean): void;
|
|
35
|
+
|
|
24
36
|
/** Whether this scope or one enclosing it binds `name`. */
|
|
25
37
|
declares(name: string): boolean;
|
|
26
38
|
|
|
27
|
-
|
|
39
|
+
/**
|
|
40
|
+
* The node owning the innermost scope that binds `name` — the compilation unit for a
|
|
41
|
+
* module-scope binding, otherwise the function, block or loop holding it — or undefined where
|
|
42
|
+
* nothing in scope does. A caller holding a declaration asks whether this is the node it came
|
|
43
|
+
* from: anything nearer shadows it.
|
|
44
|
+
*/
|
|
45
|
+
declaringScope(name: string): J | undefined;
|
|
28
46
|
}
|
|
29
47
|
|
|
30
48
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* reach the cursor.
|
|
49
|
+
* The innermost scope holding `cursor`, which a visitor's own cursor rarely is: it stands on
|
|
50
|
+
* whatever node it is visiting. Where a declaration's shape leaves its reach unreadable the answer
|
|
51
|
+
* counts it rather than miss it, so a name reported here may not truly reach the cursor.
|
|
35
52
|
*/
|
|
36
53
|
export function scopeOf(cursor: Cursor): Scope {
|
|
37
|
-
|
|
38
|
-
const names = () => resolved ??= namesInScope(cursor);
|
|
39
|
-
return {declares: name => names().has(name), names};
|
|
54
|
+
return scopeAt(enclosingScopeCursor(cursor) ?? cursor);
|
|
40
55
|
}
|
|
41
56
|
|
|
57
|
+
function scopeAt(cursor: Cursor): Scope {
|
|
58
|
+
return {
|
|
59
|
+
names: () => frameBindings(cursor.value, cursor.parent?.value),
|
|
60
|
+
walk: visit => {
|
|
61
|
+
for (let c: Cursor | undefined = cursor; c; c = enclosingScopeCursor(c.parent)) {
|
|
62
|
+
if (!visit(scopeAt(c))) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
declares: name => declaringScopeOf(cursor, name) !== undefined,
|
|
68
|
+
declaringScope: name => declaringScopeOf(cursor, name)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function enclosingScopeCursor(from: Cursor | undefined): Cursor | undefined {
|
|
73
|
+
for (let c = from; c; c = c.parent) {
|
|
74
|
+
if (scopeKinds.has((c.value as { kind?: string } | undefined)?.kind!)) {
|
|
75
|
+
return c;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The nodes {@link readBindings} answers for; everything else binds nothing and is not a scope. */
|
|
82
|
+
const scopeKinds = new Set<string>([
|
|
83
|
+
JS.Kind.CompilationUnit, J.Kind.Block, J.Kind.MethodDeclaration, J.Kind.Lambda,
|
|
84
|
+
J.Kind.ClassDeclaration, J.Kind.TryCatch, J.Kind.ForLoop, J.Kind.ForEachLoop, JS.Kind.ForInLoop
|
|
85
|
+
]);
|
|
86
|
+
|
|
42
87
|
/**
|
|
43
88
|
* Every name the file declares, wherever it sits. A binding the whole file shares is referenced from
|
|
44
89
|
* sites that are not known when it is named, so it has to steer clear of every name that could
|
|
45
90
|
* shadow it at one of them.
|
|
46
91
|
*/
|
|
47
92
|
export function namesDeclaredIn(cu: JS.CompilationUnit): ReadonlySet<string> {
|
|
48
|
-
return namesDeclaredWithin(cu
|
|
93
|
+
return namesDeclaredWithin(cu);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Every name the file spells, declared or not — what a name has to be absent from to be free. An
|
|
98
|
+
* ambient global is spelled and declared nowhere, and binding over one would capture its uses.
|
|
99
|
+
*/
|
|
100
|
+
export function namesUsedIn(cu: JS.CompilationUnit): ReadonlySet<string> {
|
|
101
|
+
return namesUsedWithin(cu);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** As {@link namesUsedIn}, over one subtree, for a name that only has to be free across that much. */
|
|
105
|
+
export function namesUsedWithin(node: unknown, cacheKey: object = node as object): ReadonlySet<string> {
|
|
106
|
+
if (cacheKey === null || typeof cacheKey !== 'object') {
|
|
107
|
+
return noNames;
|
|
108
|
+
}
|
|
109
|
+
const cached = used.get(cacheKey);
|
|
110
|
+
if (cached) {
|
|
111
|
+
return cached;
|
|
112
|
+
}
|
|
113
|
+
const names = new Set<string>();
|
|
114
|
+
walk(node, node => {
|
|
115
|
+
if (node.kind === J.Kind.Identifier) {
|
|
116
|
+
names.add((node as J.Identifier).simpleName);
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
});
|
|
120
|
+
used.set(cacheKey, names);
|
|
121
|
+
return names;
|
|
49
122
|
}
|
|
50
123
|
|
|
51
124
|
/** As {@link namesDeclaredIn}, over one subtree, for a binding shared across only that much of a file. */
|
|
52
125
|
export function namesDeclaredWithin(node: unknown, cacheKey: object = node as object): ReadonlySet<string> {
|
|
126
|
+
if (cacheKey === null || typeof cacheKey !== 'object') {
|
|
127
|
+
return noNames;
|
|
128
|
+
}
|
|
53
129
|
const cached = declared.get(cacheKey);
|
|
54
130
|
if (cached) {
|
|
55
131
|
return cached;
|
|
@@ -61,6 +137,8 @@ export function namesDeclaredWithin(node: unknown, cacheKey: object = node as ob
|
|
|
61
137
|
if (node.kind !== J.Kind.ClassDeclaration) {
|
|
62
138
|
return true;
|
|
63
139
|
}
|
|
140
|
+
// The walk ends at this branch, so the class's own type parameters are read here.
|
|
141
|
+
typeParameterNames(node).forEach(name => names.add(name));
|
|
64
142
|
// A member's name is not one the file binds, though the code inside one still declares names.
|
|
65
143
|
for (const member of (node as J.ClassDeclaration).body?.statements ?? []) {
|
|
66
144
|
const element = unwrap(member);
|
|
@@ -114,36 +192,56 @@ function unnamedMembers(bound: { name: string }[]): { name: string }[] {
|
|
|
114
192
|
return bound.map(({name}) => ({name}));
|
|
115
193
|
}
|
|
116
194
|
|
|
117
|
-
function
|
|
118
|
-
const names = new Set<string>();
|
|
195
|
+
function declaringScopeOf(cursor: Cursor, name: string): J | undefined {
|
|
119
196
|
for (let c: Cursor | undefined = cursor; c; c = c.parent) {
|
|
120
|
-
|
|
121
|
-
|
|
197
|
+
if (frameBindings(c.value, c.parent?.value).has(name)) {
|
|
198
|
+
return c.value;
|
|
122
199
|
}
|
|
123
200
|
}
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* What one node on the cursor path binds, `parent` being the node it hangs from. Only these two
|
|
206
|
+
* answers turn on the parent; a node alone settles the rest, which is what makes them cacheable.
|
|
207
|
+
*/
|
|
208
|
+
function frameBindings(node: any, parent: any): ReadonlySet<string> {
|
|
209
|
+
// A class body holds members, which are reached through an instance rather than by name.
|
|
210
|
+
if (node?.kind === J.Kind.Block && parent?.kind === J.Kind.ClassDeclaration) {
|
|
211
|
+
return noNames;
|
|
212
|
+
}
|
|
213
|
+
// A function expression is the only function whose own name its body reaches: a declaration's
|
|
214
|
+
// name belongs to the enclosing block, a method's to an instance.
|
|
215
|
+
if (node?.kind === J.Kind.MethodDeclaration && parent?.kind === JS.Kind.StatementExpression) {
|
|
216
|
+
const self = bindingNames((node as J.MethodDeclaration).name).map(bound => bound.name);
|
|
217
|
+
return new Set([...self, ...ownBindings(node)]);
|
|
218
|
+
}
|
|
219
|
+
return ownBindings(node);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function ownBindings(node: any): ReadonlySet<string> {
|
|
223
|
+
if (typeof node !== 'object' || node === null) {
|
|
224
|
+
return noNames;
|
|
225
|
+
}
|
|
226
|
+
let names = frames.get(node);
|
|
227
|
+
if (!names) {
|
|
228
|
+
frames.set(node, names = new Set(readBindings(node)));
|
|
229
|
+
}
|
|
124
230
|
return names;
|
|
125
231
|
}
|
|
126
232
|
|
|
127
|
-
|
|
128
|
-
function
|
|
129
|
-
switch (node
|
|
233
|
+
|
|
234
|
+
function readBindings(node: any): string[] {
|
|
235
|
+
switch (node.kind) {
|
|
130
236
|
case JS.Kind.CompilationUnit: {
|
|
131
237
|
const statements = (node as JS.CompilationUnit).statements;
|
|
132
238
|
return [...declaredNames(statements), ...hoistedNames(statements)];
|
|
133
239
|
}
|
|
134
240
|
case J.Kind.Block:
|
|
135
|
-
|
|
136
|
-
return parent?.kind === J.Kind.ClassDeclaration ? [] : declaredNames((node as J.Block).statements);
|
|
241
|
+
return blockScopedNames((node as J.Block).statements);
|
|
137
242
|
case J.Kind.MethodDeclaration: {
|
|
138
243
|
const method = node as J.MethodDeclaration;
|
|
139
|
-
|
|
140
|
-
// declaration's name belongs to the enclosing block, a method's to an instance.
|
|
141
|
-
const self = parent?.kind === JS.Kind.StatementExpression ? bindingNames(method.name) : [];
|
|
142
|
-
return [
|
|
143
|
-
...self.map(bound => bound.name),
|
|
144
|
-
...declaredNames(method.parameters.elements),
|
|
145
|
-
...hoistedNames(method.body)
|
|
146
|
-
];
|
|
244
|
+
return [...declaredNames(method.parameters.elements), ...hoistedNames(method.body)];
|
|
147
245
|
}
|
|
148
246
|
case J.Kind.Lambda: {
|
|
149
247
|
const lambda = node as J.Lambda;
|
|
@@ -164,6 +262,31 @@ function frameBindings(node: any, parent: any): string[] {
|
|
|
164
262
|
}
|
|
165
263
|
}
|
|
166
264
|
|
|
265
|
+
/**
|
|
266
|
+
* The names statements bind in the block holding them — the complement of what
|
|
267
|
+
* {@link hoistedNames} claims, so `function (x) { var x = 1; }` reads as one binding, not two.
|
|
268
|
+
*/
|
|
269
|
+
function blockScopedNames(statements: any[]): string[] {
|
|
270
|
+
return statements.flatMap(statement => {
|
|
271
|
+
const element = unwrap(statement);
|
|
272
|
+
switch (element?.kind) {
|
|
273
|
+
case J.Kind.MethodDeclaration:
|
|
274
|
+
return [];
|
|
275
|
+
case J.Kind.VariableDeclarations:
|
|
276
|
+
case JS.Kind.ScopedVariableDeclarations:
|
|
277
|
+
return isBlockScoped(element) ? declarationNames(element) : [];
|
|
278
|
+
case J.Kind.Case:
|
|
279
|
+
return blockScopedNames((element as J.Case).statements.elements);
|
|
280
|
+
default:
|
|
281
|
+
return declarationNames(element);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function isBlockScoped(declaration: any): boolean {
|
|
287
|
+
return ((declaration.modifiers ?? []) as J.Modifier[]).some(m => blockScoped.has(m.keyword!));
|
|
288
|
+
}
|
|
289
|
+
|
|
167
290
|
/** The names statements declare directly in the scope holding them. */
|
|
168
291
|
function declaredNames(statements: any[]): string[] {
|
|
169
292
|
return statements.flatMap(statement => declarationNames(unwrap(statement)));
|
|
@@ -187,6 +310,10 @@ function declarationNames(statement: any): string[] {
|
|
|
187
310
|
return bindingNames(unwrap((statement as JS.NamespaceDeclaration).name)).map(bound => bound.name);
|
|
188
311
|
case JS.Kind.TypeDeclaration:
|
|
189
312
|
return bindingNames(unwrap((statement as JS.TypeDeclaration).name)).map(bound => bound.name);
|
|
313
|
+
case J.Kind.TypeParameter:
|
|
314
|
+
// Reached by namesDeclaredWithin's walk, never through a statement list: a type
|
|
315
|
+
// parameter binds across the declaration carrying it, not in the scope that one sits in.
|
|
316
|
+
return bindingNames((statement as J.TypeParameter).name).map(bound => bound.name);
|
|
190
317
|
case J.Kind.Case:
|
|
191
318
|
// The cases of a switch share the block it opens, so each one's declarations bind in all.
|
|
192
319
|
return declaredNames((statement as J.Case).statements.elements);
|
|
@@ -195,6 +322,18 @@ function declarationNames(statement: any): string[] {
|
|
|
195
322
|
}
|
|
196
323
|
}
|
|
197
324
|
|
|
325
|
+
/**
|
|
326
|
+
* The names a declaration's type parameters bind. A class keeps them in a container and everything
|
|
327
|
+
* else in a `J.TypeParameters`, and both hold the same right-padded list.
|
|
328
|
+
*/
|
|
329
|
+
function typeParameterNames(node: any): string[] {
|
|
330
|
+
const held = node?.typeParameters;
|
|
331
|
+
const parameters = held?.kind === J.Kind.TypeParameters
|
|
332
|
+
? (held as J.TypeParameters).typeParameters
|
|
333
|
+
: (held as J.Container<J.TypeParameter> | undefined)?.elements;
|
|
334
|
+
return (parameters ?? []).flatMap(parameter => declarationNames(unwrap(parameter)));
|
|
335
|
+
}
|
|
336
|
+
|
|
198
337
|
/** The names an import binds, which for an aliased or namespace specifier is the alias. */
|
|
199
338
|
function importNames(jsImport: JS.Import): string[] {
|
|
200
339
|
const importClause = jsImport.importClause;
|
|
@@ -224,10 +363,13 @@ function aliasedName(specifier: J | undefined): string[] {
|
|
|
224
363
|
const blockScoped = new Set(['let', 'const', 'using']);
|
|
225
364
|
|
|
226
365
|
// A walk of an immutable subtree has one answer, so it runs once. Keyed on the subtree itself, a
|
|
227
|
-
// replaced one is walked afresh: every call site in a function asks what that body hoists,
|
|
228
|
-
// import added to a file asks what that file declares
|
|
366
|
+
// replaced one is walked afresh: every call site in a function asks what that body hoists, every
|
|
367
|
+
// import added to a file asks what that file declares, and every scope is asked what it binds by
|
|
368
|
+
// each of the call sites it encloses.
|
|
229
369
|
const hoisted = new WeakMap<object, string[]>();
|
|
230
370
|
const declared = new WeakMap<object, ReadonlySet<string>>();
|
|
371
|
+
const used = new WeakMap<object, ReadonlySet<string>>();
|
|
372
|
+
const frames = new WeakMap<object, ReadonlySet<string>>();
|
|
231
373
|
|
|
232
374
|
/**
|
|
233
375
|
* The names blocks under `scope` hoist out to it. A `var` or function declaration reaches the whole
|
|
@@ -251,7 +393,7 @@ function hoistedNames(scope: any): string[] {
|
|
|
251
393
|
return false;
|
|
252
394
|
case J.Kind.VariableDeclarations:
|
|
253
395
|
case JS.Kind.ScopedVariableDeclarations:
|
|
254
|
-
if (!(
|
|
396
|
+
if (!isBlockScoped(node)) {
|
|
255
397
|
names.push(...declarationNames(node));
|
|
256
398
|
}
|
|
257
399
|
return false;
|
|
@@ -274,7 +416,12 @@ function hoistedNames(scope: any): string[] {
|
|
|
274
416
|
return names;
|
|
275
417
|
}
|
|
276
418
|
|
|
277
|
-
/**
|
|
419
|
+
/**
|
|
420
|
+
* Visits every LST node under `node`, leaving a subtree unvisited where `visit` returns false.
|
|
421
|
+
* `visit` sees nodes, never the padding holding them. Only what `isTree` accepts is descended
|
|
422
|
+
* into, which a `JavaType` is not, so the walk stays in the tree the source spells rather than
|
|
423
|
+
* entering the type graph — cyclic and shared, where it would not terminate.
|
|
424
|
+
*/
|
|
278
425
|
export function walk(node: unknown, visit: (node: any) => boolean): void {
|
|
279
426
|
if (Array.isArray(node)) {
|
|
280
427
|
node.forEach(child => walk(child, visit));
|
|
@@ -336,3 +483,35 @@ export function declarationsOf(statement: J | undefined): J.VariableDeclarations
|
|
|
336
483
|
}
|
|
337
484
|
return [];
|
|
338
485
|
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Whether the identifier references a binding rather than naming one, which is what tells a rename
|
|
489
|
+
* which occurrences to follow. A name its parent introduces does not — a property, a method, a
|
|
490
|
+
* variable, a type parameter — nor one drawn from a namespace of its own: a statement label,
|
|
491
|
+
* declaring (`x:`) or referencing (`break x`), and a JSX attribute's prop. Position is all this
|
|
492
|
+
* reads: an import specifier's own name answers true, and a type position reads alike to a value.
|
|
493
|
+
*/
|
|
494
|
+
export function isValueReference(cursor: Cursor, identifier: J.Identifier): boolean {
|
|
495
|
+
let c: Cursor | undefined = cursor.parent;
|
|
496
|
+
while (c && isPadding(c.value)) {
|
|
497
|
+
c = c.parent;
|
|
498
|
+
}
|
|
499
|
+
const parent = c?.value as
|
|
500
|
+
{ kind?: string; name?: unknown; key?: unknown; label?: unknown; select?: unknown } | undefined;
|
|
501
|
+
|
|
502
|
+
// A call names a member of whatever it selects from. With nothing selected there is no member,
|
|
503
|
+
// and its `name` is a reference to the function being called.
|
|
504
|
+
if (parent?.kind === J.Kind.MethodInvocation && !parent.select) {
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// A JSX attribute keeps its prop name on `key`; the three label-bearing nodes keep theirs on
|
|
509
|
+
// `label`. Every other kind that names rather than references keeps it on `name`.
|
|
510
|
+
const named = parent?.kind === JS.Kind.JsxAttribute ? parent.key : (parent?.name ?? parent?.label);
|
|
511
|
+
return named !== identifier && (named as { element?: unknown } | undefined)?.element !== identifier;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function isPadding(value: unknown): boolean {
|
|
515
|
+
const kind = (value as { kind?: string } | undefined)?.kind;
|
|
516
|
+
return kind === J.Kind.RightPadded || kind === J.Kind.LeftPadded || kind === J.Kind.Container;
|
|
517
|
+
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import {J, Type} from '../../java';
|
|
17
17
|
import {JavaScriptVisitor} from '../visitor';
|
|
18
|
-
import {
|
|
18
|
+
import {isValueReference} from '../scope';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Renames the identifiers a template uses for its declared bindings to the names the file
|
|
@@ -43,35 +43,12 @@ class RenameBindingsVisitor extends JavaScriptVisitor<undefined> {
|
|
|
43
43
|
const resolved = resolvedModule(identifier);
|
|
44
44
|
const refersToBinding = resolved !== undefined
|
|
45
45
|
? resolved === this.modules[identifier.simpleName]
|
|
46
|
-
:
|
|
46
|
+
: isValueReference(this.cursor, identifier);
|
|
47
47
|
|
|
48
48
|
return refersToBinding ? {...identifier, simpleName: renamed} as J.Identifier : identifier;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/** Whether the parent is naming this identifier — as a property, a method, a declaration — rather than referencing it. */
|
|
53
|
-
function namesItsParent(cursor: Cursor, identifier: J.Identifier): boolean {
|
|
54
|
-
let c: Cursor | undefined = cursor.parent;
|
|
55
|
-
while (c && isPadding(c.value)) {
|
|
56
|
-
c = c.parent;
|
|
57
|
-
}
|
|
58
|
-
const parent = c?.value as { kind?: string; name?: unknown; select?: unknown } | undefined;
|
|
59
|
-
|
|
60
|
-
// A call names a member of whatever it selects from. With nothing selected there is no member,
|
|
61
|
-
// and its `name` is a reference to the function being called.
|
|
62
|
-
if (parent?.kind === J.Kind.MethodInvocation && !parent.select) {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const name = parent?.name;
|
|
67
|
-
return name === identifier || (name as { element?: unknown } | undefined)?.element === identifier;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function isPadding(value: unknown): boolean {
|
|
71
|
-
const kind = (value as { kind?: string } | undefined)?.kind;
|
|
72
|
-
return kind === J.Kind.RightPadded || kind === J.Kind.LeftPadded || kind === J.Kind.Container;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
52
|
/** The module an identifier's attribution traces back to, following the owning-class chain to its root. */
|
|
76
53
|
function resolvedModule(identifier: J.Identifier): string | undefined {
|
|
77
54
|
const fieldType = identifier.fieldType;
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import {Cursor, Tree} from '../..';
|
|
17
|
-
import {J} from '../../java';
|
|
16
|
+
import {Cursor, Markers, Tree} from '../..';
|
|
17
|
+
import {J, Type} from '../../java';
|
|
18
18
|
import {JS} from '../index';
|
|
19
19
|
import {JavaScriptSemanticComparatorVisitor} from '../comparator';
|
|
20
20
|
import {CaptureMarker, CaptureStorageValue, PlaceholderUtils} from './utils';
|
|
@@ -194,6 +194,11 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
|
|
|
194
194
|
return await super.visit(j, p, parent);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
/** A capture in the receiver has to bind, so a matching `methodType` cannot stand in for it. */
|
|
198
|
+
protected override selectMustBeVisited(method: J.MethodInvocation): boolean {
|
|
199
|
+
return method.select !== undefined && containsCapture(method.select);
|
|
200
|
+
}
|
|
201
|
+
|
|
197
202
|
protected hasSameKind(j: J, other: J): boolean {
|
|
198
203
|
return super.hasSameKind(j, other) ||
|
|
199
204
|
(j.kind == J.Kind.Identifier && PlaceholderUtils.isCapture(j as J.Identifier));
|
|
@@ -1457,3 +1462,25 @@ export class DebugPatternMatchingComparator extends PatternMatchingComparator {
|
|
|
1457
1462
|
}
|
|
1458
1463
|
}
|
|
1459
1464
|
}
|
|
1465
|
+
|
|
1466
|
+
/** Whether anything under `node`, the padding wrappers a marker moves onto included, is a capture. */
|
|
1467
|
+
function containsCapture(node: unknown): boolean {
|
|
1468
|
+
if (node === null || typeof node !== 'object') {
|
|
1469
|
+
return false;
|
|
1470
|
+
}
|
|
1471
|
+
if (Array.isArray(node)) {
|
|
1472
|
+
return node.some(containsCapture);
|
|
1473
|
+
}
|
|
1474
|
+
const record = node as Record<string, unknown>;
|
|
1475
|
+
if (typeof record.kind !== 'string') {
|
|
1476
|
+
return false;
|
|
1477
|
+
}
|
|
1478
|
+
// Attribution is a cyclic graph, and a capture is never reachable through one
|
|
1479
|
+
if (Type.isType(node)) {
|
|
1480
|
+
return false;
|
|
1481
|
+
}
|
|
1482
|
+
if (record.markers && PlaceholderUtils.getCaptureMarker(node as { markers: Markers })) {
|
|
1483
|
+
return true;
|
|
1484
|
+
}
|
|
1485
|
+
return Object.entries(record).some(([key, value]) => key !== 'markers' && containsCapture(value));
|
|
1486
|
+
}
|
|
@@ -31,6 +31,7 @@ import {DependencyWorkspace} from "../dependency-workspace";
|
|
|
31
31
|
import {ModuleScopeBinding, moduleScopeBindings} from '../add-import';
|
|
32
32
|
import {walk} from '../scope';
|
|
33
33
|
import {isIdentifier} from '../../java';
|
|
34
|
+
import {findMarker, MarkersKind, ParseExceptionResult} from '../../markers';
|
|
34
35
|
|
|
35
36
|
/** A module a template's context binds, and whether the parse resolved it well enough to attribute. */
|
|
36
37
|
export interface ContextBinding extends ModuleScopeBinding {
|
|
@@ -205,6 +206,20 @@ export function clearTemplateCache(): void {
|
|
|
205
206
|
templateCache.clear();
|
|
206
207
|
}
|
|
207
208
|
|
|
209
|
+
/** Whether a name reads as a TypeScript type reference: a dotted chain of identifiers. */
|
|
210
|
+
function isTypeReference(name: string): boolean {
|
|
211
|
+
return /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/.test(name);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The compiler's diagnostic for code that did not parse, as one line: the marker states it ahead
|
|
216
|
+
* of a stack, and its positions index the whole parsed text, context statements included.
|
|
217
|
+
*/
|
|
218
|
+
function parseFailureReason(cu: JS.CompilationUnit): string {
|
|
219
|
+
const failure = findMarker<ParseExceptionResult>(cu, MarkersKind.ParseExceptionResult);
|
|
220
|
+
return failure ? failure.message.split('\n')[0].replace(/:$/, '') : 'no statements';
|
|
221
|
+
}
|
|
222
|
+
|
|
208
223
|
/**
|
|
209
224
|
* Internal template engine - handles the core templating logic.
|
|
210
225
|
* Not exported from index, so only visible within the templating module.
|
|
@@ -234,7 +249,26 @@ export class TemplateEngine {
|
|
|
234
249
|
const contextWithPreamble = preamble.length > 0
|
|
235
250
|
? [...contextStatements, ...preamble]
|
|
236
251
|
: contextStatements;
|
|
237
|
-
return
|
|
252
|
+
return TemplateEngine.parseOrThrow('template', templateString, [], contextWithPreamble, dependencies, types);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The parse behind every template and pattern. Code that fails to parse yields a compilation
|
|
257
|
+
* unit with no statements, which every caller goes on to read, so it fails here naming the code.
|
|
258
|
+
*/
|
|
259
|
+
private static async parseOrThrow(
|
|
260
|
+
kind: 'template' | 'pattern',
|
|
261
|
+
templateString: string,
|
|
262
|
+
captures: (Capture | Any)[],
|
|
263
|
+
contextStatements: string[],
|
|
264
|
+
dependencies: Record<string, string>,
|
|
265
|
+
types: string[] | undefined
|
|
266
|
+
): Promise<JS.CompilationUnit> {
|
|
267
|
+
const cu = await templateCache.getOrParse(templateString, captures, contextStatements, dependencies, types);
|
|
268
|
+
if (!cu.statements || cu.statements.length === 0) {
|
|
269
|
+
throw new Error(`Failed to parse ${kind} code (${parseFailureReason(cu)}):\n${templateString}`);
|
|
270
|
+
}
|
|
271
|
+
return cu;
|
|
238
272
|
}
|
|
239
273
|
|
|
240
274
|
/**
|
|
@@ -273,11 +307,6 @@ export class TemplateEngine {
|
|
|
273
307
|
): Promise<J> {
|
|
274
308
|
const cu = await TemplateEngine.parseWithContext(templateParts, parameters, contextStatements, dependencies, types);
|
|
275
309
|
|
|
276
|
-
// Check if there are any statements
|
|
277
|
-
if (!cu.statements || cu.statements.length === 0) {
|
|
278
|
-
throw new Error(`Failed to parse template code (no statements):\n${TemplateEngine.buildTemplateString(templateParts, parameters)}`);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
310
|
// The template code is always the last statement (after context + preamble)
|
|
282
311
|
const lastStatement = cu.statements[cu.statements.length - 1].element;
|
|
283
312
|
|
|
@@ -479,7 +508,10 @@ export class TemplateEngine {
|
|
|
479
508
|
// Handle Type.Class and Type.ShallowClass - return their fully qualified names
|
|
480
509
|
if (type.kind === Type.Kind.Class || type.kind === Type.Kind.ShallowClass) {
|
|
481
510
|
const classType = type as Type.Class;
|
|
482
|
-
|
|
511
|
+
// A module qualifies its types by where they come from — `src/a.Foo` — and a path that
|
|
512
|
+
// is no type reference cannot be declared for. Naming a module's type takes a `context`
|
|
513
|
+
// statement of the caller's own, which `type` then names.
|
|
514
|
+
return isTypeReference(classType.fullyQualifiedName) ? classType.fullyQualifiedName : 'any';
|
|
483
515
|
}
|
|
484
516
|
|
|
485
517
|
// Handle Type.Primitive - map to TypeScript primitive types
|
|
@@ -567,8 +599,8 @@ export class TemplateEngine {
|
|
|
567
599
|
!(c instanceof RawCode || (c && typeof c === 'object' && (c as any)[RAW_CODE_SYMBOL]))
|
|
568
600
|
) as (Capture | Any)[];
|
|
569
601
|
|
|
570
|
-
|
|
571
|
-
|
|
602
|
+
const cu = await TemplateEngine.parseOrThrow(
|
|
603
|
+
'pattern',
|
|
572
604
|
templateString,
|
|
573
605
|
actualCaptures,
|
|
574
606
|
contextWithPreamble,
|
|
@@ -576,11 +608,6 @@ export class TemplateEngine {
|
|
|
576
608
|
types
|
|
577
609
|
);
|
|
578
610
|
|
|
579
|
-
// Check if there are any statements
|
|
580
|
-
if (!cu.statements || cu.statements.length === 0) {
|
|
581
|
-
throw new Error(`Failed to parse pattern code (no statements):\n${templateString}`);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
611
|
// The pattern code is always the last statement (after context + preamble)
|
|
585
612
|
const lastStatement = cu.statements[cu.statements.length - 1].element;
|
|
586
613
|
|
|
@@ -222,6 +222,12 @@ export class Template {
|
|
|
222
222
|
* })
|
|
223
223
|
*/
|
|
224
224
|
configure(options: TemplateOptions): Template {
|
|
225
|
+
// The `never` applies only where a caller typechecks against it, so JavaScript and pre-#8685
|
|
226
|
+
// typings reach here, where a silently dropped option leaves the template's names unbound.
|
|
227
|
+
if ((options as { bindings?: unknown }).bindings !== undefined) {
|
|
228
|
+
throw new Error("Template option 'bindings' is now 'context': name each module as the " +
|
|
229
|
+
"import it stands for, e.g. context: [`import {vi} from 'vitest';`].");
|
|
230
|
+
}
|
|
225
231
|
this.options = {...this.options, ...options};
|
|
226
232
|
// Invalidate cache when configuration changes
|
|
227
233
|
this._cachedTemplate = undefined;
|
|
@@ -497,6 +497,12 @@ export interface TemplateOptions {
|
|
|
497
497
|
* @example `{dependencies: {'@sapui5/types': '^1.120.0'}, types: ['@sapui5/types']}`
|
|
498
498
|
*/
|
|
499
499
|
types?: string[];
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* @deprecated Name the module in {@link TemplateOptions.context} as the import it stands for:
|
|
503
|
+
* `bindings: {vi: {module: 'vitest', member: 'vi'}}` is `context: ["import {vi} from 'vitest';"]`.
|
|
504
|
+
*/
|
|
505
|
+
bindings?: never;
|
|
500
506
|
}
|
|
501
507
|
|
|
502
508
|
/**
|
package/src/javascript/tree.ts
CHANGED
|
@@ -417,6 +417,7 @@ export namespace JS {
|
|
|
417
417
|
*/
|
|
418
418
|
export interface PropertyAssignment extends JS, Statement, TypedTree {
|
|
419
419
|
readonly kind: typeof Kind.PropertyAssignment;
|
|
420
|
+
readonly modifiers: J.Modifier[];
|
|
420
421
|
readonly name: J.RightPadded<Expression>;
|
|
421
422
|
readonly assigmentToken: PropertyAssignment.Token;
|
|
422
423
|
readonly initializer?: Expression;
|
|
@@ -19,6 +19,20 @@ import {Type} from "../java";
|
|
|
19
19
|
import FUNCTION_TYPE_NAME = Type.FUNCTION_TYPE_NAME;
|
|
20
20
|
import OBJECT_TYPE_NAME = Type.OBJECT_TYPE_NAME;
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* TypeScript orders union constituents by internal type id, which varies with what the checker resolved
|
|
24
|
+
* earlier in the run; ordering by signature keeps a union's signature the same across parses. Intersections
|
|
25
|
+
* keep the order they were written in and are left alone. Call once `bounds` is attached to its owner, so
|
|
26
|
+
* a constituent that reaches back into it reads the whole constituent list rather than an empty one.
|
|
27
|
+
*/
|
|
28
|
+
function sortBySignature(bounds: Type[]): void {
|
|
29
|
+
const keys = new Map<Type, string>(bounds.map(b => [b, Type.signature(b)]));
|
|
30
|
+
bounds.sort((a, b) => {
|
|
31
|
+
const ka = keys.get(a)!, kb = keys.get(b)!;
|
|
32
|
+
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
export class JavaScriptTypeMapping {
|
|
23
37
|
// Primary cache: Use type signatures (preferring type.id) as cache keys
|
|
24
38
|
// TypeScript assigns stable IDs to all types, so we don't need secondary caches
|
|
@@ -1610,6 +1624,7 @@ export class JavaScriptTypeMapping {
|
|
|
1610
1624
|
|
|
1611
1625
|
// Update the bounds in the union we created
|
|
1612
1626
|
(union as any).bounds = bounds;
|
|
1627
|
+
sortBySignature(bounds);
|
|
1613
1628
|
|
|
1614
1629
|
return union;
|
|
1615
1630
|
}
|
|
@@ -551,6 +551,7 @@ export class JavaScriptVisitor<P> extends JavaVisitor<P> {
|
|
|
551
551
|
const updates: any = {
|
|
552
552
|
prefix: await this.visitSpace(propertyAssignment.prefix, p),
|
|
553
553
|
markers: await this.visitMarkers(propertyAssignment.markers, p),
|
|
554
|
+
modifiers: await mapAsync(propertyAssignment.modifiers, m => this.visitDefined<J.Modifier>(m, p)),
|
|
554
555
|
name: await this.visitRightPadded(propertyAssignment.name, p),
|
|
555
556
|
initializer: propertyAssignment.initializer && await this.visitDefined<Expression>(propertyAssignment.initializer, p)
|
|
556
557
|
};
|
package/src/parser.ts
CHANGED
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
import {createTwoFilesPatch} from "diff";
|
|
16
17
|
import {ExecutionContext} from "./execution";
|
|
17
18
|
import {SourceFile} from "./tree";
|
|
19
|
+
import {TreePrinters} from "./print";
|
|
18
20
|
import fs, {readFileSync} from "node:fs";
|
|
19
21
|
import {isAbsolute, relative} from "path";
|
|
20
22
|
import {ParseError, ParseErrorKind} from "./parse-error";
|
|
@@ -79,6 +81,27 @@ export abstract class Parser {
|
|
|
79
81
|
return isAbsolute(path) && this.relativeTo ? relative(this.relativeTo, path) : path;
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
/**
|
|
85
|
+
* A parse result that does not print back to its input has silently lost source, so it is reported as
|
|
86
|
+
* a `ParseError` carrying the tree that lost it. Mirrors `org.openrewrite.Parser`, down to the message.
|
|
87
|
+
* A parser whose LST cannot hold some byte of its input passes the form it can hold as `source`.
|
|
88
|
+
*/
|
|
89
|
+
protected async requirePrintEqualsInput(sourceFile: SourceFile, input: ParserInput,
|
|
90
|
+
source: string = parserInputRead(input)): Promise<SourceFile> {
|
|
91
|
+
if (!this.ctx.getBoolean(ExecutionContext.REQUIRE_PRINT_EQUALS_INPUT, true)) {
|
|
92
|
+
return sourceFile;
|
|
93
|
+
}
|
|
94
|
+
const printed = await TreePrinters.print(sourceFile);
|
|
95
|
+
if (printed === source) {
|
|
96
|
+
return sourceFile;
|
|
97
|
+
}
|
|
98
|
+
const path = this.relativePath(input);
|
|
99
|
+
const diff = createTwoFilesPatch(path, path, source, printed, "", "", {context: 3});
|
|
100
|
+
const error = this.error(input, new Error(`${sourceFile.sourcePath} is not print idempotent. \n${diff}`));
|
|
101
|
+
const withErroneous: ParseError = {...error, erroneous: sourceFile};
|
|
102
|
+
return withErroneous;
|
|
103
|
+
}
|
|
104
|
+
|
|
82
105
|
protected error(input: ParserInput, e: Error): ParseError {
|
|
83
106
|
return {
|
|
84
107
|
kind: ParseErrorKind,
|
|
@@ -88,7 +111,7 @@ export abstract class Parser {
|
|
|
88
111
|
id: randomId(),
|
|
89
112
|
parserType: this.constructor.name,
|
|
90
113
|
exceptionType: e.name,
|
|
91
|
-
message: e.
|
|
114
|
+
message: e.stack ?? e.message,
|
|
92
115
|
} satisfies ParseExceptionResult as ParseExceptionResult),
|
|
93
116
|
text: parserInputRead(input),
|
|
94
117
|
sourcePath: this.relativePath(input),
|