@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.
Files changed (124) hide show
  1. package/dist/execution.d.ts +8 -0
  2. package/dist/execution.d.ts.map +1 -1
  3. package/dist/execution.js +31 -0
  4. package/dist/execution.js.map +1 -1
  5. package/dist/javascript/add-import.d.ts +64 -9
  6. package/dist/javascript/add-import.d.ts.map +1 -1
  7. package/dist/javascript/add-import.js +398 -71
  8. package/dist/javascript/add-import.js.map +1 -1
  9. package/dist/javascript/amd.d.ts +11 -1
  10. package/dist/javascript/amd.d.ts.map +1 -1
  11. package/dist/javascript/amd.js +43 -9
  12. package/dist/javascript/amd.js.map +1 -1
  13. package/dist/javascript/binding.d.ts +10 -2
  14. package/dist/javascript/binding.d.ts.map +1 -1
  15. package/dist/javascript/binding.js +44 -71
  16. package/dist/javascript/binding.js.map +1 -1
  17. package/dist/javascript/comparator.d.ts +2 -0
  18. package/dist/javascript/comparator.d.ts.map +1 -1
  19. package/dist/javascript/comparator.js +5 -1
  20. package/dist/javascript/comparator.js.map +1 -1
  21. package/dist/javascript/format/format.d.ts +5 -2
  22. package/dist/javascript/format/format.d.ts.map +1 -1
  23. package/dist/javascript/format/format.js +51 -11
  24. package/dist/javascript/format/format.js.map +1 -1
  25. package/dist/javascript/format/minimum-viable-spacing-visitor.d.ts +19 -0
  26. package/dist/javascript/format/minimum-viable-spacing-visitor.d.ts.map +1 -1
  27. package/dist/javascript/format/minimum-viable-spacing-visitor.js +180 -7
  28. package/dist/javascript/format/minimum-viable-spacing-visitor.js.map +1 -1
  29. package/dist/javascript/format/prettier-format.d.ts +2 -1
  30. package/dist/javascript/format/prettier-format.d.ts.map +1 -1
  31. package/dist/javascript/format/prettier-format.js +33 -23
  32. package/dist/javascript/format/prettier-format.js.map +1 -1
  33. package/dist/javascript/index.d.ts +3 -3
  34. package/dist/javascript/index.d.ts.map +1 -1
  35. package/dist/javascript/index.js +21 -1
  36. package/dist/javascript/index.js.map +1 -1
  37. package/dist/javascript/parser-utils.d.ts.map +1 -1
  38. package/dist/javascript/parser-utils.js +5 -9
  39. package/dist/javascript/parser-utils.js.map +1 -1
  40. package/dist/javascript/parser.d.ts.map +1 -1
  41. package/dist/javascript/parser.js +48 -3
  42. package/dist/javascript/parser.js.map +1 -1
  43. package/dist/javascript/print.d.ts +1 -0
  44. package/dist/javascript/print.d.ts.map +1 -1
  45. package/dist/javascript/print.js +9 -0
  46. package/dist/javascript/print.js.map +1 -1
  47. package/dist/javascript/recipes/change-import.d.ts +6 -3
  48. package/dist/javascript/recipes/change-import.d.ts.map +1 -1
  49. package/dist/javascript/recipes/change-import.js +17 -277
  50. package/dist/javascript/recipes/change-import.js.map +1 -1
  51. package/dist/javascript/rpc.js +2 -0
  52. package/dist/javascript/rpc.js.map +1 -1
  53. package/dist/javascript/scope.d.ts +40 -7
  54. package/dist/javascript/scope.d.ts.map +1 -1
  55. package/dist/javascript/scope.js +182 -30
  56. package/dist/javascript/scope.js.map +1 -1
  57. package/dist/javascript/templating/bindings.js +2 -20
  58. package/dist/javascript/templating/bindings.js.map +1 -1
  59. package/dist/javascript/templating/comparator.d.ts +2 -0
  60. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  61. package/dist/javascript/templating/comparator.js +25 -0
  62. package/dist/javascript/templating/comparator.js.map +1 -1
  63. package/dist/javascript/templating/engine.d.ts +5 -0
  64. package/dist/javascript/templating/engine.d.ts.map +1 -1
  65. package/dist/javascript/templating/engine.js +30 -12
  66. package/dist/javascript/templating/engine.js.map +1 -1
  67. package/dist/javascript/templating/template.d.ts.map +1 -1
  68. package/dist/javascript/templating/template.js +6 -0
  69. package/dist/javascript/templating/template.js.map +1 -1
  70. package/dist/javascript/templating/types.d.ts +5 -0
  71. package/dist/javascript/templating/types.d.ts.map +1 -1
  72. package/dist/javascript/tree.d.ts +1 -0
  73. package/dist/javascript/tree.d.ts.map +1 -1
  74. package/dist/javascript/tree.js.map +1 -1
  75. package/dist/javascript/type-mapping.d.ts.map +1 -1
  76. package/dist/javascript/type-mapping.js +14 -0
  77. package/dist/javascript/type-mapping.js.map +1 -1
  78. package/dist/javascript/visitor.d.ts.map +1 -1
  79. package/dist/javascript/visitor.js +1 -0
  80. package/dist/javascript/visitor.js.map +1 -1
  81. package/dist/parser.d.ts +6 -0
  82. package/dist/parser.d.ts.map +1 -1
  83. package/dist/parser.js +23 -1
  84. package/dist/parser.js.map +1 -1
  85. package/dist/print.d.ts.map +1 -1
  86. package/dist/print.js +5 -2
  87. package/dist/print.js.map +1 -1
  88. package/dist/rewrite-javascript-version.txt +1 -1
  89. package/dist/rpc/request/parse-project.d.ts +11 -1
  90. package/dist/rpc/request/parse-project.d.ts.map +1 -1
  91. package/dist/rpc/request/parse-project.js +7 -2
  92. package/dist/rpc/request/parse-project.js.map +1 -1
  93. package/dist/rpc/request/parse.d.ts +13 -1
  94. package/dist/rpc/request/parse.d.ts.map +1 -1
  95. package/dist/rpc/request/parse.js +8 -2
  96. package/dist/rpc/request/parse.js.map +1 -1
  97. package/package.json +1 -1
  98. package/src/execution.ts +23 -0
  99. package/src/javascript/add-import.ts +449 -78
  100. package/src/javascript/amd.ts +49 -10
  101. package/src/javascript/binding.ts +60 -74
  102. package/src/javascript/comparator.ts +6 -1
  103. package/src/javascript/format/format.ts +56 -12
  104. package/src/javascript/format/minimum-viable-spacing-visitor.ts +196 -7
  105. package/src/javascript/format/prettier-format.ts +35 -25
  106. package/src/javascript/index.ts +10 -3
  107. package/src/javascript/parser-utils.ts +5 -11
  108. package/src/javascript/parser.ts +65 -6
  109. package/src/javascript/print.ts +10 -0
  110. package/src/javascript/recipes/change-import.ts +19 -349
  111. package/src/javascript/rpc.ts +2 -0
  112. package/src/javascript/scope.ts +210 -31
  113. package/src/javascript/templating/bindings.ts +2 -25
  114. package/src/javascript/templating/comparator.ts +29 -2
  115. package/src/javascript/templating/engine.ts +41 -14
  116. package/src/javascript/templating/template.ts +6 -0
  117. package/src/javascript/templating/types.ts +6 -0
  118. package/src/javascript/tree.ts +1 -0
  119. package/src/javascript/type-mapping.ts +15 -0
  120. package/src/javascript/visitor.ts +1 -0
  121. package/src/parser.ts +24 -1
  122. package/src/print.ts +5 -2
  123. package/src/rpc/request/parse-project.ts +6 -2
  124. package/src/rpc/request/parse.ts +7 -2
@@ -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
- /** The names code at some position can reach unqualified. */
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
- names(): ReadonlySet<string>;
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
- * 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.
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
- let resolved: Set<string> | undefined;
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.statements, 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 namesInScope(cursor: Cursor): Set<string> {
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
- for (const name of frameBindings(c.value, c.parent?.value)) {
121
- names.add(name);
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
- /** 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) {
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
- // 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);
241
+ return blockScopedNames((node as J.Block).statements);
137
242
  case J.Kind.MethodDeclaration: {
138
243
  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
- ];
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, and every
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 (!((node.modifiers ?? []) as J.Modifier[]).some(m => blockScoped.has(m.keyword!))) {
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
- /** Visits every LST node under `node`, leaving a subtree unvisited where `visit` returns false. */
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 {Cursor} from '../../tree';
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
- : !namesItsParent(this.cursor, identifier);
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 templateCache.getOrParse(templateString, [], contextWithPreamble, dependencies, types);
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
- return classType.fullyQualifiedName;
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
- // Use cache to get or parse the compilation unit
571
- const cu = await templateCache.getOrParse(
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
  /**
@@ -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.message + ':\n' + e.stack,
114
+ message: e.stack ?? e.message,
92
115
  } satisfies ParseExceptionResult as ParseExceptionResult),
93
116
  text: parserInputRead(input),
94
117
  sourcePath: this.relativePath(input),