@openrewrite/rewrite 8.91.1 → 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/javascript/add-import.d.ts +68 -34
- package/dist/javascript/add-import.d.ts.map +1 -1
- package/dist/javascript/add-import.js +322 -160
- 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/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/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/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 +7 -1
- 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 -240
- 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/templating/bindings.d.ts +0 -11
- package/dist/javascript/templating/bindings.d.ts.map +1 -1
- package/dist/javascript/templating/bindings.js +0 -29
- package/dist/javascript/templating/bindings.js.map +1 -1
- 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 +43 -14
- package/dist/javascript/templating/comparator.js.map +1 -1
- package/dist/javascript/templating/engine.d.ts +17 -4
- package/dist/javascript/templating/engine.d.ts.map +1 -1
- package/dist/javascript/templating/engine.js +43 -17
- 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 +2 -2
- package/dist/javascript/templating/pattern.js.map +1 -1
- package/dist/javascript/templating/placeholder-replacement.d.ts +2 -0
- package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
- package/dist/javascript/templating/placeholder-replacement.js +10 -5
- 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 +7 -1
- package/dist/javascript/templating/rewrite.js.map +1 -1
- package/dist/javascript/templating/template.d.ts +7 -1
- package/dist/javascript/templating/template.d.ts.map +1 -1
- package/dist/javascript/templating/template.js +48 -28
- package/dist/javascript/templating/template.js.map +1 -1
- package/dist/javascript/templating/types.d.ts +10 -18
- package/dist/javascript/templating/types.d.ts.map +1 -1
- package/dist/javascript/templating/utils.d.ts +1 -1
- package/dist/javascript/templating/utils.d.ts.map +1 -1
- package/dist/javascript/templating/utils.js +3 -2
- package/dist/javascript/templating/utils.js.map +1 -1
- package/dist/rewrite-javascript-version.txt +1 -1
- package/package.json +2 -2
- package/src/javascript/add-import.ts +342 -198
- package/src/javascript/amd.ts +915 -0
- package/src/javascript/binding.ts +449 -0
- package/src/javascript/index.ts +12 -1
- package/src/javascript/parser-utils.ts +2 -0
- package/src/javascript/parser.ts +13 -0
- package/src/javascript/recipes/change-import.ts +10 -260
- package/src/javascript/remove-import.ts +20 -56
- package/src/javascript/scope.ts +338 -0
- package/src/javascript/templating/bindings.ts +0 -30
- package/src/javascript/templating/comparator.ts +52 -14
- package/src/javascript/templating/engine.ts +70 -28
- package/src/javascript/templating/index.ts +0 -1
- package/src/javascript/templating/pattern.ts +4 -2
- package/src/javascript/templating/placeholder-replacement.ts +16 -6
- package/src/javascript/templating/precedence.ts +21 -13
- package/src/javascript/templating/rewrite.ts +9 -1
- package/src/javascript/templating/template.ts +62 -29
- package/src/javascript/templating/types.ts +11 -18
- package/src/javascript/templating/utils.ts +4 -2
|
@@ -0,0 +1,338 @@
|
|
|
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 {Cursor, isTree} from "../tree";
|
|
17
|
+
import {J} from "../java";
|
|
18
|
+
import {JS} from "./tree";
|
|
19
|
+
// scope.ts sits below the visitor, so this stays type-only.
|
|
20
|
+
import type {JavaScriptVisitor} from "./visitor";
|
|
21
|
+
|
|
22
|
+
/** The names code at some position can reach unqualified. */
|
|
23
|
+
export interface Scope {
|
|
24
|
+
/** Whether this scope or one enclosing it binds `name`. */
|
|
25
|
+
declares(name: string): boolean;
|
|
26
|
+
|
|
27
|
+
names(): ReadonlySet<string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What the code at `cursor` can name: what every enclosing scope binds, plus the declarations that
|
|
32
|
+
* hoist into those scopes from blocks that do not enclose it. Where a declaration's shape leaves its
|
|
33
|
+
* reach unreadable the answer counts it rather than miss it, so a name reported here may not truly
|
|
34
|
+
* reach the cursor.
|
|
35
|
+
*/
|
|
36
|
+
export function scopeOf(cursor: Cursor): Scope {
|
|
37
|
+
let resolved: Set<string> | undefined;
|
|
38
|
+
const names = () => resolved ??= namesInScope(cursor);
|
|
39
|
+
return {declares: name => names().has(name), names};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Every name the file declares, wherever it sits. A binding the whole file shares is referenced from
|
|
44
|
+
* sites that are not known when it is named, so it has to steer clear of every name that could
|
|
45
|
+
* shadow it at one of them.
|
|
46
|
+
*/
|
|
47
|
+
export function namesDeclaredIn(cu: JS.CompilationUnit): ReadonlySet<string> {
|
|
48
|
+
return namesDeclaredWithin(cu.statements, cu);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** As {@link namesDeclaredIn}, over one subtree, for a binding shared across only that much of a file. */
|
|
52
|
+
export function namesDeclaredWithin(node: unknown, cacheKey: object = node as object): ReadonlySet<string> {
|
|
53
|
+
const cached = declared.get(cacheKey);
|
|
54
|
+
if (cached) {
|
|
55
|
+
return cached;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const names = new Set<string>();
|
|
59
|
+
const collect = (node: any): boolean => {
|
|
60
|
+
declarationNames(node).forEach(name => names.add(name));
|
|
61
|
+
if (node.kind !== J.Kind.ClassDeclaration) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
// A member's name is not one the file binds, though the code inside one still declares names.
|
|
65
|
+
for (const member of (node as J.ClassDeclaration).body?.statements ?? []) {
|
|
66
|
+
const element = unwrap(member);
|
|
67
|
+
walk(element, node => node === element || collect(node));
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
};
|
|
71
|
+
walk(node, collect);
|
|
72
|
+
|
|
73
|
+
declared.set(cacheKey, names);
|
|
74
|
+
return names;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Every name a binding pattern introduces. `member` is the property a name takes its value from,
|
|
79
|
+
* which only a name an object pattern binds directly has: anything deeper reads a property of a
|
|
80
|
+
* property, an array element is chosen by position, and a rest name gathers what nothing claimed.
|
|
81
|
+
*/
|
|
82
|
+
export function bindingNames(pattern: J | undefined): { name: string; member?: string }[] {
|
|
83
|
+
switch (pattern?.kind) {
|
|
84
|
+
case J.Kind.Identifier: {
|
|
85
|
+
const simpleName = (pattern as J.Identifier).simpleName;
|
|
86
|
+
return simpleName ? [{name: simpleName}] : [];
|
|
87
|
+
}
|
|
88
|
+
case JS.Kind.Spread:
|
|
89
|
+
return bindingNames((pattern as JS.Spread).expression);
|
|
90
|
+
case JS.Kind.ArrayBindingPattern:
|
|
91
|
+
return unnamedMembers((pattern as JS.ArrayBindingPattern).elements.elements
|
|
92
|
+
.flatMap(element => bindingNames(unwrap(element))));
|
|
93
|
+
case JS.Kind.ObjectBindingPattern:
|
|
94
|
+
return (pattern as JS.ObjectBindingPattern).bindings.elements
|
|
95
|
+
.flatMap(element => bindingNames(unwrap(element)));
|
|
96
|
+
case JS.Kind.BindingElement: {
|
|
97
|
+
const element = pattern as JS.BindingElement;
|
|
98
|
+
if (element.name?.kind !== J.Kind.Identifier) {
|
|
99
|
+
return unnamedMembers(bindingNames(element.name as J));
|
|
100
|
+
}
|
|
101
|
+
const name = (element.name as J.Identifier).simpleName;
|
|
102
|
+
const propertyName = unwrap(element.propertyName);
|
|
103
|
+
return [{
|
|
104
|
+
name,
|
|
105
|
+
member: propertyName?.kind === J.Kind.Identifier ? (propertyName as J.Identifier).simpleName : name
|
|
106
|
+
}];
|
|
107
|
+
}
|
|
108
|
+
default:
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function unnamedMembers(bound: { name: string }[]): { name: string }[] {
|
|
114
|
+
return bound.map(({name}) => ({name}));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function namesInScope(cursor: Cursor): Set<string> {
|
|
118
|
+
const names = new Set<string>();
|
|
119
|
+
for (let c: Cursor | undefined = cursor; c; c = c.parent) {
|
|
120
|
+
for (const name of frameBindings(c.value, c.parent?.value)) {
|
|
121
|
+
names.add(name);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return names;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** What one node on the cursor path binds, `parent` being the node it hangs from. */
|
|
128
|
+
function frameBindings(node: any, parent: any): string[] {
|
|
129
|
+
switch (node?.kind) {
|
|
130
|
+
case JS.Kind.CompilationUnit: {
|
|
131
|
+
const statements = (node as JS.CompilationUnit).statements;
|
|
132
|
+
return [...declaredNames(statements), ...hoistedNames(statements)];
|
|
133
|
+
}
|
|
134
|
+
case J.Kind.Block:
|
|
135
|
+
// A class body holds members, which are reached through an instance rather than by name.
|
|
136
|
+
return parent?.kind === J.Kind.ClassDeclaration ? [] : declaredNames((node as J.Block).statements);
|
|
137
|
+
case J.Kind.MethodDeclaration: {
|
|
138
|
+
const method = node as J.MethodDeclaration;
|
|
139
|
+
// A function expression is the only function whose own name its body reaches: a
|
|
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
|
+
];
|
|
147
|
+
}
|
|
148
|
+
case J.Kind.Lambda: {
|
|
149
|
+
const lambda = node as J.Lambda;
|
|
150
|
+
return [...declaredNames(lambda.parameters.parameters), ...hoistedNames(lambda.body)];
|
|
151
|
+
}
|
|
152
|
+
case J.Kind.ClassDeclaration:
|
|
153
|
+
return bindingNames((node as J.ClassDeclaration).name).map(bound => bound.name);
|
|
154
|
+
case J.Kind.TryCatch:
|
|
155
|
+
return declaredNames([(node as J.Try.Catch).parameter.tree]);
|
|
156
|
+
case J.Kind.ForLoop:
|
|
157
|
+
return declaredNames((node as J.ForLoop).control.init);
|
|
158
|
+
case J.Kind.ForEachLoop:
|
|
159
|
+
return declaredNames([(node as J.ForEachLoop).control.variable]);
|
|
160
|
+
case JS.Kind.ForInLoop:
|
|
161
|
+
return declaredNames([(node as JS.ForInLoop).control.variable]);
|
|
162
|
+
default:
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The names statements declare directly in the scope holding them. */
|
|
168
|
+
function declaredNames(statements: any[]): string[] {
|
|
169
|
+
return statements.flatMap(statement => declarationNames(unwrap(statement)));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function declarationNames(statement: any): string[] {
|
|
173
|
+
switch (statement?.kind) {
|
|
174
|
+
case JS.Kind.Import:
|
|
175
|
+
return importNames(statement as JS.Import);
|
|
176
|
+
case J.Kind.VariableDeclarations:
|
|
177
|
+
return (statement as J.VariableDeclarations).variables
|
|
178
|
+
.flatMap(variable => bindingNames(unwrap(variable)?.name))
|
|
179
|
+
.map(bound => bound.name);
|
|
180
|
+
case JS.Kind.ScopedVariableDeclarations:
|
|
181
|
+
return declaredNames((statement as JS.ScopedVariableDeclarations).variables);
|
|
182
|
+
case J.Kind.MethodDeclaration:
|
|
183
|
+
return bindingNames((statement as J.MethodDeclaration).name).map(bound => bound.name);
|
|
184
|
+
case J.Kind.ClassDeclaration:
|
|
185
|
+
return bindingNames((statement as J.ClassDeclaration).name).map(bound => bound.name);
|
|
186
|
+
case JS.Kind.NamespaceDeclaration:
|
|
187
|
+
return bindingNames(unwrap((statement as JS.NamespaceDeclaration).name)).map(bound => bound.name);
|
|
188
|
+
case JS.Kind.TypeDeclaration:
|
|
189
|
+
return bindingNames(unwrap((statement as JS.TypeDeclaration).name)).map(bound => bound.name);
|
|
190
|
+
case J.Kind.Case:
|
|
191
|
+
// The cases of a switch share the block it opens, so each one's declarations bind in all.
|
|
192
|
+
return declaredNames((statement as J.Case).statements.elements);
|
|
193
|
+
default:
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The names an import binds, which for an aliased or namespace specifier is the alias. */
|
|
199
|
+
function importNames(jsImport: JS.Import): string[] {
|
|
200
|
+
const importClause = jsImport.importClause;
|
|
201
|
+
if (!importClause) {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
const names = bindingNames(unwrap(importClause.name)).map(bound => bound.name);
|
|
205
|
+
const namedBindings = importClause.namedBindings;
|
|
206
|
+
if (namedBindings?.kind === JS.Kind.NamedImports) {
|
|
207
|
+
for (const element of (namedBindings as JS.NamedImports).elements.elements) {
|
|
208
|
+
const specifier = unwrap(element);
|
|
209
|
+
if (specifier?.kind === JS.Kind.ImportSpecifier) {
|
|
210
|
+
names.push(...aliasedName((specifier as JS.ImportSpecifier).specifier));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
names.push(...aliasedName(namedBindings));
|
|
215
|
+
}
|
|
216
|
+
return names;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function aliasedName(specifier: J | undefined): string[] {
|
|
220
|
+
const name = specifier?.kind === JS.Kind.Alias ? (specifier as JS.Alias).alias : specifier;
|
|
221
|
+
return bindingNames(name).map(bound => bound.name);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const blockScoped = new Set(['let', 'const', 'using']);
|
|
225
|
+
|
|
226
|
+
// 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, and every
|
|
228
|
+
// import added to a file asks what that file declares.
|
|
229
|
+
const hoisted = new WeakMap<object, string[]>();
|
|
230
|
+
const declared = new WeakMap<object, ReadonlySet<string>>();
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The names blocks under `scope` hoist out to it. A `var` or function declaration reaches the whole
|
|
234
|
+
* function it sits in, so one nested in a block is in scope outside that block; only a `let`, `const`
|
|
235
|
+
* or `using` keyword says otherwise.
|
|
236
|
+
*/
|
|
237
|
+
function hoistedNames(scope: any): string[] {
|
|
238
|
+
if (typeof scope !== 'object' || scope === null) {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
const cached = hoisted.get(scope);
|
|
242
|
+
if (cached) {
|
|
243
|
+
return cached;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const names: string[] = [];
|
|
247
|
+
const collect = (node: any): boolean => {
|
|
248
|
+
switch (node.kind) {
|
|
249
|
+
case J.Kind.MethodDeclaration:
|
|
250
|
+
names.push(...declarationNames(node));
|
|
251
|
+
return false;
|
|
252
|
+
case J.Kind.VariableDeclarations:
|
|
253
|
+
case JS.Kind.ScopedVariableDeclarations:
|
|
254
|
+
if (!((node.modifiers ?? []) as J.Modifier[]).some(m => blockScoped.has(m.keyword!))) {
|
|
255
|
+
names.push(...declarationNames(node));
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
case J.Kind.TryCatch:
|
|
259
|
+
// A catch parameter carries no keyword to read, and binds only in the catch block.
|
|
260
|
+
walk((node as J.Try.Catch).body, collect);
|
|
261
|
+
return false;
|
|
262
|
+
case JS.Kind.StatementExpression:
|
|
263
|
+
// A function or class used as an expression names itself for its own body alone.
|
|
264
|
+
return false;
|
|
265
|
+
case J.Kind.Lambda:
|
|
266
|
+
case J.Kind.ClassDeclaration:
|
|
267
|
+
return false;
|
|
268
|
+
default:
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
walk(scope, collect);
|
|
273
|
+
hoisted.set(scope, names);
|
|
274
|
+
return names;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Visits every LST node under `node`, leaving a subtree unvisited where `visit` returns false. */
|
|
278
|
+
export function walk(node: unknown, visit: (node: any) => boolean): void {
|
|
279
|
+
if (Array.isArray(node)) {
|
|
280
|
+
node.forEach(child => walk(child, visit));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const kind = (node as any)?.kind;
|
|
284
|
+
if (kind === J.Kind.RightPadded || kind === J.Kind.LeftPadded) {
|
|
285
|
+
walk((node as J.RightPadded<any>).element, visit);
|
|
286
|
+
} else if (kind === J.Kind.Container) {
|
|
287
|
+
walk((node as J.Container<any>).elements, visit);
|
|
288
|
+
} else if (isTree(node) && visit(node)) {
|
|
289
|
+
// Markers hang off a node rather than being part of the code it holds.
|
|
290
|
+
Object.entries(node).forEach(([key, value]) => key !== 'markers' && walk(value, visit));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** The element a padding wrapper holds, or the node itself. */
|
|
295
|
+
function unwrap(node: any): any {
|
|
296
|
+
return node?.kind === J.Kind.RightPadded || node?.kind === J.Kind.LeftPadded ? unwrap(node.element) : node;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** `cursor` is protected on `TreeVisitor` and these APIs are free functions, so reaching it takes a cast. */
|
|
300
|
+
export function cursorOf(visitor: JavaScriptVisitor<any>): Cursor | undefined {
|
|
301
|
+
return (visitor as unknown as {cursor?: Cursor}).cursor;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** The compilation unit a cursor sits in, or the one a visitor is currently positioned in. */
|
|
305
|
+
export function compilationUnitOf(from: Cursor | JavaScriptVisitor<any>): JS.CompilationUnit | undefined {
|
|
306
|
+
const cursor = from instanceof Cursor ? from : cursorOf(from);
|
|
307
|
+
return cursor?.firstEnclosing((v): v is JS.CompilationUnit => v?.kind === JS.Kind.CompilationUnit);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** `preferred`, or the first `preferred_N` that `isTaken` rejects, so a new name never shadows one in scope. */
|
|
311
|
+
export function deconflict(preferred: string, isTaken: (name: string) => boolean): string {
|
|
312
|
+
if (!isTaken(preferred)) {
|
|
313
|
+
return preferred;
|
|
314
|
+
}
|
|
315
|
+
for (let suffix = 1; ; suffix++) {
|
|
316
|
+
const candidate = `${preferred}_${suffix}`;
|
|
317
|
+
if (!isTaken(candidate)) {
|
|
318
|
+
return candidate;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* The `J.VariableDeclarations` a statement declares — itself for a bare `const x = …`, one per
|
|
325
|
+
* declarator for `const a = …, b = …`, which the parser wraps in a `JS.ScopedVariableDeclarations`
|
|
326
|
+
* instead.
|
|
327
|
+
*/
|
|
328
|
+
export function declarationsOf(statement: J | undefined): J.VariableDeclarations[] {
|
|
329
|
+
if (statement?.kind === J.Kind.VariableDeclarations) {
|
|
330
|
+
return [statement as J.VariableDeclarations];
|
|
331
|
+
}
|
|
332
|
+
if (statement?.kind === JS.Kind.ScopedVariableDeclarations) {
|
|
333
|
+
return (statement as JS.ScopedVariableDeclarations).variables
|
|
334
|
+
.map(v => v.element)
|
|
335
|
+
.filter((v): v is J.VariableDeclarations => v?.kind === J.Kind.VariableDeclarations);
|
|
336
|
+
}
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
@@ -16,36 +16,6 @@
|
|
|
16
16
|
import {J, Type} from '../../java';
|
|
17
17
|
import {JavaScriptVisitor} from '../visitor';
|
|
18
18
|
import {Cursor} from '../../tree';
|
|
19
|
-
import {ModuleBinding} from './types';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Whether the template's dependencies bring a workspace that could resolve `module`.
|
|
23
|
-
*/
|
|
24
|
-
export function isResolvable(module: string, dependencies: Record<string, string>): boolean {
|
|
25
|
-
const segments = module.split('/');
|
|
26
|
-
const pkg = module.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0];
|
|
27
|
-
return Object.prototype.hasOwnProperty.call(dependencies, pkg);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* What a declared binding's name is parsed against, ahead of the template and so out of its output.
|
|
32
|
-
* An import is what carries attribution, and costs a module resolution to get it; a declaration
|
|
33
|
-
* names the binding for a module no workspace could have resolved anyway.
|
|
34
|
-
*/
|
|
35
|
-
export function bindingContextStatement(name: string, binding: ModuleBinding, dependencies: Record<string, string>): string {
|
|
36
|
-
if (!isResolvable(binding.module, dependencies)) {
|
|
37
|
-
return binding.typeOnly ? `type ${name} = any;` : `declare const ${name}: any;`;
|
|
38
|
-
}
|
|
39
|
-
const type = binding.typeOnly ? 'type ' : '';
|
|
40
|
-
if (binding.member === '*') {
|
|
41
|
-
return `import ${type}* as ${name} from '${binding.module}';`;
|
|
42
|
-
}
|
|
43
|
-
if (binding.member === undefined || binding.member === 'default') {
|
|
44
|
-
return `import ${type}${name} from '${binding.module}';`;
|
|
45
|
-
}
|
|
46
|
-
const specifier = binding.member === name ? name : `${binding.member} as ${name}`;
|
|
47
|
-
return `import ${type}{${specifier}} from '${binding.module}';`;
|
|
48
|
-
}
|
|
49
19
|
|
|
50
20
|
/**
|
|
51
21
|
* Renames the identifiers a template uses for its declared bindings to the names the file
|
|
@@ -85,6 +85,17 @@ class CaptureMapImpl implements CaptureMap {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/** A capture found in a pattern, and the target node it binds to. */
|
|
89
|
+
interface CaptureAt {
|
|
90
|
+
marker: CaptureMarker;
|
|
91
|
+
target: J;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Parentheses that `JavaScriptSemanticComparatorVisitor.unwrap()` sees through. */
|
|
95
|
+
function isParenthesized(j: any): boolean {
|
|
96
|
+
return j?.kind === J.Kind.Parentheses || j?.kind === J.Kind.ControlParentheses;
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
/**
|
|
89
100
|
* A comparator for pattern matching that is lenient about optional properties.
|
|
90
101
|
* Allows patterns without type annotations to match actual code with type annotations.
|
|
@@ -112,33 +123,59 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
|
|
|
112
123
|
};
|
|
113
124
|
}
|
|
114
125
|
|
|
126
|
+
/** The capture this pattern node stands for, paired with the target node it should bind to. */
|
|
127
|
+
protected captureAt(j: Tree, p: J): CaptureAt | undefined {
|
|
128
|
+
const marker = PlaceholderUtils.getCaptureMarker(j as J);
|
|
129
|
+
return marker ? {marker, target: p} : this.captureBehindParentheses(j, p);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The marker `attachCaptureMarkers` moved onto a parenthesized capture's `J.RightPadded`, which `unwrap()` skips past. */
|
|
133
|
+
private captureBehindParentheses(j: Tree, p: J): CaptureAt | undefined {
|
|
134
|
+
let pattern: any = j;
|
|
135
|
+
let target: any = p;
|
|
136
|
+
while (isParenthesized(pattern)) {
|
|
137
|
+
const padded = pattern.tree as J.RightPadded<J> | undefined;
|
|
138
|
+
if (!padded) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
target = isParenthesized(target) ? target.tree.element : target;
|
|
142
|
+
const marker = PlaceholderUtils.getCaptureMarker(padded);
|
|
143
|
+
if (marker) {
|
|
144
|
+
return {marker, target: target as J};
|
|
145
|
+
}
|
|
146
|
+
pattern = padded.element;
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
115
151
|
override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
|
|
116
152
|
// Check if the pattern node is a capture - this handles unwrapped captures
|
|
117
153
|
// (Wrapped captures in J.RightPadded are handled by visitRightPadded override)
|
|
118
154
|
// Note: targetCursor will be pushed by parent's visit() method after this check
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
155
|
+
const capture = this.captureAt(j, p);
|
|
156
|
+
if (capture) {
|
|
157
|
+
const {marker: captureMarker, target} = capture;
|
|
121
158
|
|
|
122
159
|
// Push targetCursor to position it at the captured node for constraint evaluation
|
|
123
160
|
// Only create cursor if targetCursor was initialized (meaning user provided one)
|
|
124
161
|
const savedTargetCursor = this.targetCursor;
|
|
125
162
|
const cursorAtCapturedNode = this.targetCursor !== undefined
|
|
126
|
-
? new Cursor(
|
|
127
|
-
: new Cursor(
|
|
163
|
+
? new Cursor(target, this.targetCursor)
|
|
164
|
+
: new Cursor(target);
|
|
128
165
|
this.targetCursor = cursorAtCapturedNode;
|
|
129
166
|
try {
|
|
130
167
|
// Evaluate constraint with context (cursor + previous captures)
|
|
131
168
|
// Skip constraint for variadic captures - they're evaluated in matchSequence with the full array
|
|
132
169
|
if (captureMarker.constraint && !captureMarker.variadicOptions) {
|
|
133
170
|
const context = this.buildConstraintContext(cursorAtCapturedNode);
|
|
134
|
-
if (!captureMarker.constraint(
|
|
171
|
+
if (!captureMarker.constraint(target, context)) {
|
|
135
172
|
const captureName = captureMarker.captureName || 'unnamed';
|
|
136
|
-
const targetKind = (
|
|
173
|
+
const targetKind = (target as any).kind || 'unknown';
|
|
137
174
|
return this.constraintFailed(captureName, targetKind) as R;
|
|
138
175
|
}
|
|
139
176
|
}
|
|
140
177
|
|
|
141
|
-
const success = this.matcher.handleCapture(captureMarker,
|
|
178
|
+
const success = this.matcher.handleCapture(captureMarker, target, undefined);
|
|
142
179
|
if (!success) {
|
|
143
180
|
const captureName = captureMarker.captureName || 'unnamed';
|
|
144
181
|
return this.captureConflict(captureName) as R;
|
|
@@ -896,26 +933,27 @@ export class DebugPatternMatchingComparator extends PatternMatchingComparator {
|
|
|
896
933
|
}
|
|
897
934
|
|
|
898
935
|
override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
|
|
899
|
-
const
|
|
900
|
-
if (
|
|
936
|
+
const capture = this.captureAt(j, p);
|
|
937
|
+
if (capture) {
|
|
938
|
+
const {marker: captureMarker, target} = capture;
|
|
901
939
|
const savedTargetCursor = this.targetCursor;
|
|
902
940
|
const cursorAtCapturedNode = this.targetCursor !== undefined
|
|
903
|
-
? new Cursor(
|
|
904
|
-
: new Cursor(
|
|
941
|
+
? new Cursor(target, this.targetCursor)
|
|
942
|
+
: new Cursor(target);
|
|
905
943
|
this.targetCursor = cursorAtCapturedNode;
|
|
906
944
|
try {
|
|
907
945
|
if (captureMarker.constraint && !captureMarker.variadicOptions) {
|
|
908
946
|
this.debug.log('debug', 'constraint', `Evaluating constraint for capture: ${captureMarker.captureName}`);
|
|
909
|
-
const constraintResult = captureMarker.constraint(
|
|
947
|
+
const constraintResult = captureMarker.constraint(target, this.buildConstraintContext(cursorAtCapturedNode));
|
|
910
948
|
if (!constraintResult) {
|
|
911
949
|
this.debug.log('info', 'constraint', `Constraint failed for capture: ${captureMarker.captureName}`);
|
|
912
|
-
this.debug.setExplanation('constraint-failed', `Capture ${captureMarker.captureName} with valid constraint`, `Constraint failed for ${(
|
|
950
|
+
this.debug.setExplanation('constraint-failed', `Capture ${captureMarker.captureName} with valid constraint`, `Constraint failed for ${(target as any).kind}`, `Constraint evaluation returned false`);
|
|
913
951
|
return this.abort(j) as R;
|
|
914
952
|
}
|
|
915
953
|
this.debug.log('debug', 'constraint', `Constraint passed for capture: ${captureMarker.captureName}`);
|
|
916
954
|
}
|
|
917
955
|
|
|
918
|
-
const success = this.matcher.handleCapture(captureMarker,
|
|
956
|
+
const success = this.matcher.handleCapture(captureMarker, target, undefined);
|
|
919
957
|
if (!success) {
|
|
920
958
|
return this.abort(j) as R;
|
|
921
959
|
}
|
|
@@ -28,6 +28,14 @@ import {isExpression, isStatement} from '../parser-utils';
|
|
|
28
28
|
import {randomId} from '../../uuid';
|
|
29
29
|
import ts from "typescript";
|
|
30
30
|
import {DependencyWorkspace} from "../dependency-workspace";
|
|
31
|
+
import {ModuleScopeBinding, moduleScopeBindings} from '../add-import';
|
|
32
|
+
import {walk} from '../scope';
|
|
33
|
+
import {isIdentifier} from '../../java';
|
|
34
|
+
|
|
35
|
+
/** A module a template's context binds, and whether the parse resolved it well enough to attribute. */
|
|
36
|
+
export interface ContextBinding extends ModuleScopeBinding {
|
|
37
|
+
attributed: boolean;
|
|
38
|
+
}
|
|
31
39
|
import {Parameter} from "./types";
|
|
32
40
|
|
|
33
41
|
/**
|
|
@@ -90,11 +98,13 @@ export function setTemplateSourceFileCache(cache?: Map<string, ts.SourceFile>):
|
|
|
90
98
|
// from one compile to the next; see `JavaScriptParser.parse`.
|
|
91
99
|
const templateParsers: Map<string, JavaScriptParser> = new Map();
|
|
92
100
|
|
|
93
|
-
function templateParser(workspaceDir?: string): JavaScriptParser {
|
|
94
|
-
|
|
101
|
+
function templateParser(workspaceDir?: string, types?: string[]): JavaScriptParser {
|
|
102
|
+
// `types` changes what the compiler loads, so parsers cannot be shared across differing sets.
|
|
103
|
+
// An empty list loads nothing where absence loads the defaults, so the two encode apart.
|
|
104
|
+
const key = `${workspaceDir ?? ""}::${JSON.stringify(types ?? null)}`;
|
|
95
105
|
let parser = templateParsers.get(key);
|
|
96
106
|
if (!parser) {
|
|
97
|
-
parser = new JavaScriptParser({relativeTo: workspaceDir, sourceFileCache: templateSourceFileCache});
|
|
107
|
+
parser = new JavaScriptParser({relativeTo: workspaceDir, sourceFileCache: templateSourceFileCache, types});
|
|
98
108
|
templateParsers.set(key, parser);
|
|
99
109
|
}
|
|
100
110
|
return parser;
|
|
@@ -115,7 +125,8 @@ class TemplateCache {
|
|
|
115
125
|
templateString: string,
|
|
116
126
|
captures: (Capture | Any)[],
|
|
117
127
|
contextStatements: string[],
|
|
118
|
-
dependencies: Record<string, string
|
|
128
|
+
dependencies: Record<string, string>,
|
|
129
|
+
types: string[] | undefined
|
|
119
130
|
): string {
|
|
120
131
|
// Use the actual template string (with placeholders) as the primary key
|
|
121
132
|
const templateKey = templateString;
|
|
@@ -129,7 +140,7 @@ class TemplateCache {
|
|
|
129
140
|
// Dependencies
|
|
130
141
|
const depsKey = JSON.stringify(dependencies || {});
|
|
131
142
|
|
|
132
|
-
return `${templateKey}::${capturesKey}::${contextKey}::${depsKey}`;
|
|
143
|
+
return `${templateKey}::${capturesKey}::${contextKey}::${depsKey}::${JSON.stringify(types ?? null)}`;
|
|
133
144
|
}
|
|
134
145
|
|
|
135
146
|
/**
|
|
@@ -139,9 +150,10 @@ class TemplateCache {
|
|
|
139
150
|
templateString: string,
|
|
140
151
|
captures: (Capture | Any)[],
|
|
141
152
|
contextStatements: string[],
|
|
142
|
-
dependencies: Record<string, string
|
|
153
|
+
dependencies: Record<string, string>,
|
|
154
|
+
types?: string[]
|
|
143
155
|
): Promise<JS.CompilationUnit> {
|
|
144
|
-
const key = this.generateKey(templateString, captures, contextStatements, dependencies);
|
|
156
|
+
const key = this.generateKey(templateString, captures, contextStatements, dependencies, types);
|
|
145
157
|
|
|
146
158
|
let cu = this.cache.get(key);
|
|
147
159
|
if (cu) {
|
|
@@ -163,7 +175,7 @@ class TemplateCache {
|
|
|
163
175
|
|
|
164
176
|
// Parse and cache (workspace only needed during parsing)
|
|
165
177
|
// Use templateSourceFileCache if configured for ~3.2x speedup on dependency file parsing
|
|
166
|
-
const parser = templateParser(workspaceDir);
|
|
178
|
+
const parser = templateParser(workspaceDir, types);
|
|
167
179
|
const parseGenerator = parser.parse({text: fullTemplateString, sourcePath: 'template.tsx'});
|
|
168
180
|
cu = (await parseGenerator.next()).value as JS.CompilationUnit;
|
|
169
181
|
|
|
@@ -208,34 +220,62 @@ export class TemplateEngine {
|
|
|
208
220
|
* @param dependencies NPM dependencies for type attribution
|
|
209
221
|
* @returns A Promise resolving to the extracted template AST
|
|
210
222
|
*/
|
|
211
|
-
|
|
223
|
+
/** The template parsed with its context, which is what gives its code types to attribute against. */
|
|
224
|
+
private static async parseWithContext(
|
|
212
225
|
templateParts: TemplateStringsArray,
|
|
213
226
|
parameters: Parameter[],
|
|
214
|
-
contextStatements: string[]
|
|
215
|
-
dependencies: Record<string, string
|
|
216
|
-
|
|
217
|
-
|
|
227
|
+
contextStatements: string[],
|
|
228
|
+
dependencies: Record<string, string>,
|
|
229
|
+
types: string[] | undefined
|
|
230
|
+
): Promise<JS.CompilationUnit> {
|
|
231
|
+
// A capture's declared type reaches the parse as a declaration, so it belongs with context.
|
|
218
232
|
const preamble = TemplateEngine.parameterPreamble(parameters);
|
|
219
|
-
|
|
220
|
-
// Build the template string with parameter placeholders
|
|
221
233
|
const templateString = TemplateEngine.buildTemplateString(templateParts, parameters);
|
|
222
|
-
|
|
223
|
-
// Add preamble to context statements (so they're skipped during extraction)
|
|
224
234
|
const contextWithPreamble = preamble.length > 0
|
|
225
235
|
? [...contextStatements, ...preamble]
|
|
226
236
|
: contextStatements;
|
|
237
|
+
return templateCache.getOrParse(templateString, [], contextWithPreamble, dependencies, types);
|
|
238
|
+
}
|
|
227
239
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
240
|
+
/**
|
|
241
|
+
* The modules the template's context binds, for the caller to bind in the file being edited.
|
|
242
|
+
* An `import` or `require` states one; anything else — a `declare`, a helper signature — types
|
|
243
|
+
* the template without asking for a binding.
|
|
244
|
+
*/
|
|
245
|
+
static async getContextBindings(
|
|
246
|
+
templateParts: TemplateStringsArray,
|
|
247
|
+
parameters: Parameter[],
|
|
248
|
+
contextStatements: string[] = [],
|
|
249
|
+
dependencies: Record<string, string> = {},
|
|
250
|
+
types?: string[]
|
|
251
|
+
): Promise<ContextBinding[]> {
|
|
252
|
+
const cu = await TemplateEngine.parseWithContext(templateParts, parameters, contextStatements, dependencies, types);
|
|
253
|
+
// The template's own code is the last statement, so everything ahead of it is context.
|
|
254
|
+
const context = {...cu, statements: cu.statements.slice(0, -1)};
|
|
255
|
+
const attributed = new Set<string>();
|
|
256
|
+
walk(context.statements, node => {
|
|
257
|
+
if (isIdentifier(node) && (node.type !== undefined || node.fieldType !== undefined)) {
|
|
258
|
+
attributed.add(node.simpleName);
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
});
|
|
262
|
+
return moduleScopeBindings(context)
|
|
263
|
+
.filter(b => b.module !== undefined)
|
|
264
|
+
.map(b => ({...b, attributed: attributed.has(b.name)}));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
static async getTemplateTree(
|
|
268
|
+
templateParts: TemplateStringsArray,
|
|
269
|
+
parameters: Parameter[],
|
|
270
|
+
contextStatements: string[] = [],
|
|
271
|
+
dependencies: Record<string, string> = {},
|
|
272
|
+
types?: string[]
|
|
273
|
+
): Promise<J> {
|
|
274
|
+
const cu = await TemplateEngine.parseWithContext(templateParts, parameters, contextStatements, dependencies, types);
|
|
235
275
|
|
|
236
276
|
// Check if there are any statements
|
|
237
277
|
if (!cu.statements || cu.statements.length === 0) {
|
|
238
|
-
throw new Error(`Failed to parse template code (no statements):\n${
|
|
278
|
+
throw new Error(`Failed to parse template code (no statements):\n${TemplateEngine.buildTemplateString(templateParts, parameters)}`);
|
|
239
279
|
}
|
|
240
280
|
|
|
241
281
|
// The template code is always the last statement (after context + preamble)
|
|
@@ -263,7 +303,7 @@ export class TemplateEngine {
|
|
|
263
303
|
* @returns A Promise resolving to the generated AST node
|
|
264
304
|
*/
|
|
265
305
|
static async applyTemplateFromAst(
|
|
266
|
-
ast:
|
|
306
|
+
ast: J,
|
|
267
307
|
parameters: Parameter[],
|
|
268
308
|
cursor: Cursor,
|
|
269
309
|
coordinates: JavaCoordinates,
|
|
@@ -490,7 +530,8 @@ export class TemplateEngine {
|
|
|
490
530
|
templateParts: TemplateStringsArray,
|
|
491
531
|
captures: (Capture | Any | RawCode)[],
|
|
492
532
|
contextStatements: string[] = [],
|
|
493
|
-
dependencies: Record<string, string> = {}
|
|
533
|
+
dependencies: Record<string, string> = {},
|
|
534
|
+
types?: string[]
|
|
494
535
|
): Promise<J> {
|
|
495
536
|
const preamble = TemplateEngine.capturePreamble(captures);
|
|
496
537
|
|
|
@@ -531,7 +572,8 @@ export class TemplateEngine {
|
|
|
531
572
|
templateString,
|
|
532
573
|
actualCaptures,
|
|
533
574
|
contextWithPreamble,
|
|
534
|
-
dependencies
|
|
575
|
+
dependencies,
|
|
576
|
+
types
|
|
535
577
|
);
|
|
536
578
|
|
|
537
579
|
// Check if there are any statements
|