@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.
Files changed (96) hide show
  1. package/dist/javascript/add-import.d.ts +68 -34
  2. package/dist/javascript/add-import.d.ts.map +1 -1
  3. package/dist/javascript/add-import.js +322 -160
  4. package/dist/javascript/add-import.js.map +1 -1
  5. package/dist/javascript/amd.d.ts +138 -0
  6. package/dist/javascript/amd.d.ts.map +1 -0
  7. package/dist/javascript/amd.js +768 -0
  8. package/dist/javascript/amd.js.map +1 -0
  9. package/dist/javascript/binding.d.ts +94 -0
  10. package/dist/javascript/binding.d.ts.map +1 -0
  11. package/dist/javascript/binding.js +335 -0
  12. package/dist/javascript/binding.js.map +1 -0
  13. package/dist/javascript/index.d.ts +8 -1
  14. package/dist/javascript/index.d.ts.map +1 -1
  15. package/dist/javascript/index.js +26 -1
  16. package/dist/javascript/index.js.map +1 -1
  17. package/dist/javascript/parser-utils.d.ts.map +1 -1
  18. package/dist/javascript/parser-utils.js +2 -0
  19. package/dist/javascript/parser-utils.js.map +1 -1
  20. package/dist/javascript/parser.d.ts +7 -1
  21. package/dist/javascript/parser.d.ts.map +1 -1
  22. package/dist/javascript/parser.js +7 -1
  23. package/dist/javascript/parser.js.map +1 -1
  24. package/dist/javascript/recipes/change-import.d.ts +2 -4
  25. package/dist/javascript/recipes/change-import.d.ts.map +1 -1
  26. package/dist/javascript/recipes/change-import.js +8 -240
  27. package/dist/javascript/recipes/change-import.js.map +1 -1
  28. package/dist/javascript/remove-import.d.ts +2 -28
  29. package/dist/javascript/remove-import.d.ts.map +1 -1
  30. package/dist/javascript/remove-import.js +19 -57
  31. package/dist/javascript/remove-import.js.map +1 -1
  32. package/dist/javascript/scope.d.ts +49 -0
  33. package/dist/javascript/scope.d.ts.map +1 -0
  34. package/dist/javascript/scope.js +324 -0
  35. package/dist/javascript/scope.js.map +1 -0
  36. package/dist/javascript/templating/bindings.d.ts +0 -11
  37. package/dist/javascript/templating/bindings.d.ts.map +1 -1
  38. package/dist/javascript/templating/bindings.js +0 -29
  39. package/dist/javascript/templating/bindings.js.map +1 -1
  40. package/dist/javascript/templating/comparator.d.ts +10 -0
  41. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  42. package/dist/javascript/templating/comparator.js +43 -14
  43. package/dist/javascript/templating/comparator.js.map +1 -1
  44. package/dist/javascript/templating/engine.d.ts +17 -4
  45. package/dist/javascript/templating/engine.d.ts.map +1 -1
  46. package/dist/javascript/templating/engine.js +43 -17
  47. package/dist/javascript/templating/engine.js.map +1 -1
  48. package/dist/javascript/templating/index.d.ts +1 -1
  49. package/dist/javascript/templating/index.d.ts.map +1 -1
  50. package/dist/javascript/templating/index.js.map +1 -1
  51. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  52. package/dist/javascript/templating/pattern.js +2 -2
  53. package/dist/javascript/templating/pattern.js.map +1 -1
  54. package/dist/javascript/templating/placeholder-replacement.d.ts +2 -0
  55. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  56. package/dist/javascript/templating/placeholder-replacement.js +10 -5
  57. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  58. package/dist/javascript/templating/precedence.d.ts +12 -4
  59. package/dist/javascript/templating/precedence.d.ts.map +1 -1
  60. package/dist/javascript/templating/precedence.js +18 -11
  61. package/dist/javascript/templating/precedence.js.map +1 -1
  62. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  63. package/dist/javascript/templating/rewrite.js +7 -1
  64. package/dist/javascript/templating/rewrite.js.map +1 -1
  65. package/dist/javascript/templating/template.d.ts +7 -1
  66. package/dist/javascript/templating/template.d.ts.map +1 -1
  67. package/dist/javascript/templating/template.js +48 -28
  68. package/dist/javascript/templating/template.js.map +1 -1
  69. package/dist/javascript/templating/types.d.ts +10 -18
  70. package/dist/javascript/templating/types.d.ts.map +1 -1
  71. package/dist/javascript/templating/utils.d.ts +1 -1
  72. package/dist/javascript/templating/utils.d.ts.map +1 -1
  73. package/dist/javascript/templating/utils.js +3 -2
  74. package/dist/javascript/templating/utils.js.map +1 -1
  75. package/dist/rewrite-javascript-version.txt +1 -1
  76. package/package.json +2 -2
  77. package/src/javascript/add-import.ts +342 -198
  78. package/src/javascript/amd.ts +915 -0
  79. package/src/javascript/binding.ts +449 -0
  80. package/src/javascript/index.ts +12 -1
  81. package/src/javascript/parser-utils.ts +2 -0
  82. package/src/javascript/parser.ts +13 -0
  83. package/src/javascript/recipes/change-import.ts +10 -260
  84. package/src/javascript/remove-import.ts +20 -56
  85. package/src/javascript/scope.ts +338 -0
  86. package/src/javascript/templating/bindings.ts +0 -30
  87. package/src/javascript/templating/comparator.ts +52 -14
  88. package/src/javascript/templating/engine.ts +70 -28
  89. package/src/javascript/templating/index.ts +0 -1
  90. package/src/javascript/templating/pattern.ts +4 -2
  91. package/src/javascript/templating/placeholder-replacement.ts +16 -6
  92. package/src/javascript/templating/precedence.ts +21 -13
  93. package/src/javascript/templating/rewrite.ts +9 -1
  94. package/src/javascript/templating/template.ts +62 -29
  95. package/src/javascript/templating/types.ts +11 -18
  96. 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 captureMarker = PlaceholderUtils.getCaptureMarker(j)!;
120
- if (captureMarker) {
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(p, this.targetCursor)
127
- : new Cursor(p);
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(p, context)) {
171
+ if (!captureMarker.constraint(target, context)) {
135
172
  const captureName = captureMarker.captureName || 'unnamed';
136
- const targetKind = (p as any).kind || 'unknown';
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, p, undefined);
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 captureMarker = PlaceholderUtils.getCaptureMarker(j)!;
900
- if (captureMarker) {
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(p, this.targetCursor)
904
- : new Cursor(p);
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(p, this.buildConstraintContext(cursorAtCapturedNode));
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 ${(p as any).kind}`, `Constraint evaluation returned false`);
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, p, undefined);
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
- const key = workspaceDir ?? "";
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
- static async getTemplateTree(
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
- ): Promise<J> {
217
- // Generate type preamble for captures/parameters with types
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
- // Use cache to get or parse the compilation unit
229
- const cu = await templateCache.getOrParse(
230
- templateString,
231
- [],
232
- contextWithPreamble,
233
- dependencies
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${templateString}`);
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: JS.CompilationUnit,
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