@openrewrite/rewrite 8.66.1 → 8.66.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 (106) hide show
  1. package/dist/java/tree.d.ts +10 -1
  2. package/dist/java/tree.d.ts.map +1 -1
  3. package/dist/java/tree.js +21 -5
  4. package/dist/java/tree.js.map +1 -1
  5. package/dist/java/type-visitor.d.ts +1 -1
  6. package/dist/java/type-visitor.d.ts.map +1 -1
  7. package/dist/java/visitor.d.ts +2 -2
  8. package/dist/java/visitor.d.ts.map +1 -1
  9. package/dist/java/visitor.js +8 -2
  10. package/dist/java/visitor.js.map +1 -1
  11. package/dist/javascript/assertions.d.ts +6 -0
  12. package/dist/javascript/assertions.d.ts.map +1 -1
  13. package/dist/javascript/assertions.js +14 -6
  14. package/dist/javascript/assertions.js.map +1 -1
  15. package/dist/javascript/comparator.d.ts +154 -7
  16. package/dist/javascript/comparator.d.ts.map +1 -1
  17. package/dist/javascript/comparator.js +623 -180
  18. package/dist/javascript/comparator.js.map +1 -1
  19. package/dist/javascript/format.d.ts +5 -3
  20. package/dist/javascript/format.d.ts.map +1 -1
  21. package/dist/javascript/format.js +85 -43
  22. package/dist/javascript/format.js.map +1 -1
  23. package/dist/javascript/index.d.ts +1 -0
  24. package/dist/javascript/index.d.ts.map +1 -1
  25. package/dist/javascript/index.js +1 -0
  26. package/dist/javascript/index.js.map +1 -1
  27. package/dist/javascript/parser.d.ts +2 -1
  28. package/dist/javascript/parser.d.ts.map +1 -1
  29. package/dist/javascript/parser.js +39 -30
  30. package/dist/javascript/parser.js.map +1 -1
  31. package/dist/javascript/templating/capture.d.ts +81 -14
  32. package/dist/javascript/templating/capture.d.ts.map +1 -1
  33. package/dist/javascript/templating/capture.js +98 -8
  34. package/dist/javascript/templating/capture.js.map +1 -1
  35. package/dist/javascript/templating/comparator.d.ts +125 -15
  36. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  37. package/dist/javascript/templating/comparator.js +946 -118
  38. package/dist/javascript/templating/comparator.js.map +1 -1
  39. package/dist/javascript/templating/engine.d.ts +58 -25
  40. package/dist/javascript/templating/engine.d.ts.map +1 -1
  41. package/dist/javascript/templating/engine.js +527 -94
  42. package/dist/javascript/templating/engine.js.map +1 -1
  43. package/dist/javascript/templating/index.d.ts +3 -3
  44. package/dist/javascript/templating/index.d.ts.map +1 -1
  45. package/dist/javascript/templating/index.js +3 -1
  46. package/dist/javascript/templating/index.js.map +1 -1
  47. package/dist/javascript/templating/pattern.d.ts +121 -16
  48. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  49. package/dist/javascript/templating/pattern.js +528 -257
  50. package/dist/javascript/templating/pattern.js.map +1 -1
  51. package/dist/javascript/templating/placeholder-replacement.d.ts +30 -5
  52. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  53. package/dist/javascript/templating/placeholder-replacement.js +183 -81
  54. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  55. package/dist/javascript/templating/rewrite.d.ts +56 -11
  56. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  57. package/dist/javascript/templating/rewrite.js +143 -16
  58. package/dist/javascript/templating/rewrite.js.map +1 -1
  59. package/dist/javascript/templating/template.d.ts +31 -5
  60. package/dist/javascript/templating/template.d.ts.map +1 -1
  61. package/dist/javascript/templating/template.js +89 -15
  62. package/dist/javascript/templating/template.js.map +1 -1
  63. package/dist/javascript/templating/types.d.ts +359 -12
  64. package/dist/javascript/templating/types.d.ts.map +1 -1
  65. package/dist/javascript/templating/utils.d.ts +52 -35
  66. package/dist/javascript/templating/utils.d.ts.map +1 -1
  67. package/dist/javascript/templating/utils.js +107 -109
  68. package/dist/javascript/templating/utils.js.map +1 -1
  69. package/dist/javascript/type-mapping.d.ts.map +1 -1
  70. package/dist/javascript/type-mapping.js +21 -11
  71. package/dist/javascript/type-mapping.js.map +1 -1
  72. package/dist/json/rpc.js +2 -2
  73. package/dist/json/rpc.js.map +1 -1
  74. package/dist/recipe/order-imports.js.map +1 -1
  75. package/dist/test/rewrite-test.d.ts.map +1 -1
  76. package/dist/test/rewrite-test.js +10 -6
  77. package/dist/test/rewrite-test.js.map +1 -1
  78. package/dist/version.txt +1 -1
  79. package/dist/visitor.d.ts +4 -4
  80. package/dist/visitor.d.ts.map +1 -1
  81. package/dist/visitor.js +8 -3
  82. package/dist/visitor.js.map +1 -1
  83. package/package.json +4 -2
  84. package/src/java/tree.ts +10 -3
  85. package/src/java/type-visitor.ts +1 -1
  86. package/src/java/visitor.ts +11 -5
  87. package/src/javascript/assertions.ts +9 -3
  88. package/src/javascript/comparator.ts +676 -185
  89. package/src/javascript/format.ts +72 -34
  90. package/src/javascript/index.ts +1 -0
  91. package/src/javascript/parser.ts +51 -31
  92. package/src/javascript/templating/capture.ts +107 -15
  93. package/src/javascript/templating/comparator.ts +1087 -134
  94. package/src/javascript/templating/engine.ts +601 -103
  95. package/src/javascript/templating/index.ts +9 -2
  96. package/src/javascript/templating/pattern.ts +655 -281
  97. package/src/javascript/templating/placeholder-replacement.ts +183 -80
  98. package/src/javascript/templating/rewrite.ts +152 -18
  99. package/src/javascript/templating/template.ts +110 -22
  100. package/src/javascript/templating/types.ts +386 -12
  101. package/src/javascript/templating/utils.ts +116 -102
  102. package/src/javascript/type-mapping.ts +20 -11
  103. package/src/json/rpc.ts +2 -2
  104. package/src/recipe/order-imports.ts +1 -1
  105. package/src/test/rewrite-test.ts +12 -7
  106. package/src/visitor.ts +14 -6
@@ -20,7 +20,7 @@ import {JavaScriptVisitor} from '../visitor';
20
20
  import {produce} from 'immer';
21
21
  import {PlaceholderUtils} from './utils';
22
22
  import {CaptureImpl, TemplateParamImpl, CaptureValue, CAPTURE_NAME_SYMBOL} from './capture';
23
- import {Parameter} from './engine';
23
+ import {Parameter} from './types';
24
24
 
25
25
  /**
26
26
  * Visitor that replaces placeholder nodes with actual parameter values.
@@ -34,6 +34,123 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
34
34
  super();
35
35
  }
36
36
 
37
+ async visit<R extends J>(tree: J, p: any, parent?: Cursor): Promise<R | undefined> {
38
+ // Check if this node is a placeholder
39
+ // BUT: Don't handle `JS.BindingElement` here - let `visitBindingElement` preserve `propertyName`
40
+ if (tree.kind !== JS.Kind.BindingElement && this.isPlaceholder(tree)) {
41
+ const replacement = this.replacePlaceholder(tree);
42
+ if (replacement !== tree) {
43
+ return replacement as R;
44
+ }
45
+ }
46
+
47
+ // Continue with normal traversal
48
+ return super.visit(tree, p, parent);
49
+ }
50
+
51
+ /**
52
+ * Override visitBindingElement to preserve propertyName from template when replacing.
53
+ * For example, in `{ ref: ${ref} }`, we want to preserve `ref:` when replacing ${ref}.
54
+ */
55
+ override async visitBindingElement(bindingElement: JS.BindingElement, p: any): Promise<J | undefined> {
56
+ // Visit the name to potentially replace placeholders
57
+ const visitedName = await this.visit(bindingElement.name, p);
58
+
59
+ // If the name changed (placeholder was replaced), preserve the BindingElement structure
60
+ // including the propertyName from the template
61
+ if (visitedName !== bindingElement.name) {
62
+ return produce(bindingElement, draft => {
63
+ draft.name = visitedName as any;
64
+ // propertyName is already set from the template and will be preserved by produce
65
+ });
66
+ }
67
+
68
+ return bindingElement;
69
+ }
70
+
71
+ /**
72
+ * Override visitContainer to handle variadic expansion for containers.
73
+ * This handles J.Container instances anywhere in the AST (method arguments, etc.).
74
+ */
75
+ override async visitContainer<T extends J>(container: J.Container<T>, p: any): Promise<J.Container<T>> {
76
+ // Check if any elements are placeholders (possibly variadic)
77
+ const hasPlaceholder = container.elements.some(elem => this.isPlaceholder(elem.element));
78
+
79
+ if (!hasPlaceholder) {
80
+ return super.visitContainer(container, p);
81
+ }
82
+
83
+ // Expand variadic placeholders in the container's elements
84
+ const newElements = await this.expandVariadicElements(container.elements, undefined, p);
85
+
86
+ return produce(container, draft => {
87
+ draft.elements = newElements as any;
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Override visitRightPadded to handle single placeholder replacements.
93
+ * The base implementation will visit the element, which triggers our visit() override
94
+ * for placeholder detection and replacement.
95
+ */
96
+ override async visitRightPadded<T extends J | boolean>(right: J.RightPadded<T>, p: any): Promise<J.RightPadded<T> | undefined> {
97
+ return super.visitRightPadded(right, p);
98
+ }
99
+
100
+ /**
101
+ * Override visitBlock to handle variadic expansion in block statements.
102
+ * Block.statements is J.RightPadded<Statement>[] (not a Container), so we need
103
+ * array-level access for variadic expansion.
104
+ */
105
+ override async visitBlock(block: J.Block, p: any): Promise<J | undefined> {
106
+ const hasPlaceholder = block.statements.some(stmt => {
107
+ const stmtElement = stmt.element;
108
+ // Check if it's an ExpressionStatement containing a placeholder
109
+ if (stmtElement.kind === JS.Kind.ExpressionStatement) {
110
+ const exprStmt = stmtElement as JS.ExpressionStatement;
111
+ return this.isPlaceholder(exprStmt.expression);
112
+ }
113
+ return this.isPlaceholder(stmtElement);
114
+ });
115
+
116
+ if (!hasPlaceholder) {
117
+ return super.visitBlock(block, p);
118
+ }
119
+
120
+ // Unwrap function to extract placeholder from ExpressionStatement
121
+ const unwrapStatement = (element: J): J => {
122
+ if (element.kind === JS.Kind.ExpressionStatement) {
123
+ return (element as JS.ExpressionStatement).expression;
124
+ }
125
+ return element;
126
+ };
127
+
128
+ const newStatements = await this.expandVariadicElements(block.statements, unwrapStatement, p);
129
+
130
+ return produce(block, draft => {
131
+ draft.statements = newStatements;
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Override visitJsCompilationUnit to handle variadic expansion in top-level statements.
137
+ * CompilationUnit.statements is J.RightPadded<Statement>[] (not a Container), so we need
138
+ * array-level access for variadic expansion.
139
+ */
140
+ override async visitJsCompilationUnit(compilationUnit: JS.CompilationUnit, p: any): Promise<J | undefined> {
141
+ const hasPlaceholder = compilationUnit.statements.some(stmt => this.isPlaceholder(stmt.element));
142
+
143
+ if (!hasPlaceholder) {
144
+ return super.visitJsCompilationUnit(compilationUnit, p);
145
+ }
146
+
147
+ const newStatements = await this.expandVariadicElements(compilationUnit.statements, undefined, p);
148
+
149
+ return produce(compilationUnit, draft => {
150
+ draft.statements = newStatements;
151
+ });
152
+ }
153
+
37
154
  /**
38
155
  * Merges prefixes by preserving comments from the source element
39
156
  * while using whitespace from the template placeholder.
@@ -83,8 +200,15 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
83
200
  if (param) {
84
201
  let arrayToExpand: J[] | J.RightPadded<J>[] | undefined = undefined;
85
202
 
203
+ // Check if it's a J.Container
204
+ const isContainer = param.value && typeof param.value === 'object' &&
205
+ param.value.kind === J.Kind.Container;
206
+ if (isContainer) {
207
+ // Extract elements from J.Container
208
+ arrayToExpand = param.value.elements as J.RightPadded<J>[];
209
+ }
86
210
  // Check if it's a direct Tree[] array
87
- if (Array.isArray(param.value)) {
211
+ else if (Array.isArray(param.value)) {
88
212
  arrayToExpand = param.value as J[];
89
213
  }
90
214
  // Check if it's a CaptureValue
@@ -97,7 +221,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
97
221
  // Check if it's a direct variadic capture
98
222
  else {
99
223
  const isCapture = param.value instanceof CaptureImpl ||
100
- (param.value && typeof param.value === 'object' && param.value[CAPTURE_NAME_SYMBOL]);
224
+ (param.value && typeof param.value === 'object' && param.value[CAPTURE_NAME_SYMBOL]);
101
225
  if (isCapture) {
102
226
  const name = param.value[CAPTURE_NAME_SYMBOL] || param.value.name;
103
227
  const capture = Array.from(this.substitutions.values())
@@ -167,7 +291,28 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
167
291
  // Not a placeholder (or expansion failed) - process normally
168
292
  const replacedElement = await this.visit(element, p);
169
293
  if (replacedElement) {
170
- newElements.push(produce(wrapped, draft => {
294
+ // Check if the replacement came from a capture with a wrapper (to preserve markers)
295
+ const placeholderNode = unwrapElement(element);
296
+ const placeholderText = this.getPlaceholderText(placeholderNode);
297
+ let wrapperToUse = wrapped;
298
+
299
+ if (placeholderText && this.isPlaceholder(placeholderNode)) {
300
+ const param = this.substitutions.get(placeholderText);
301
+ if (param) {
302
+ const isCapture = param.value instanceof CaptureImpl ||
303
+ (param.value && typeof param.value === 'object' && param.value[CAPTURE_NAME_SYMBOL]);
304
+ if (isCapture) {
305
+ const name = param.value[CAPTURE_NAME_SYMBOL] || param.value.name;
306
+ const wrapper = this.wrappersMap.get(name);
307
+ // Use captured wrapper if available and not an array (non-variadic)
308
+ if (wrapper && !Array.isArray(wrapper)) {
309
+ wrapperToUse = wrapper as J.RightPadded<J>;
310
+ }
311
+ }
312
+ }
313
+ }
314
+
315
+ newElements.push(produce(wrapperToUse, draft => {
171
316
  draft.element = replacedElement;
172
317
  }));
173
318
  }
@@ -176,80 +321,6 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
176
321
  return newElements;
177
322
  }
178
323
 
179
- async visit<R extends J>(tree: J, p: any, parent?: Cursor): Promise<R | undefined> {
180
- // Check if this node is a placeholder
181
- if (this.isPlaceholder(tree)) {
182
- const replacement = this.replacePlaceholder(tree);
183
- if (replacement !== tree) {
184
- return replacement as R;
185
- }
186
- }
187
-
188
- // Continue with normal traversal
189
- return super.visit(tree, p, parent);
190
- }
191
-
192
- override async visitMethodInvocation(method: J.MethodInvocation, p: any): Promise<J | undefined> {
193
- // Check if any arguments are placeholders (possibly variadic)
194
- const hasPlaceholder = method.arguments.elements.some(arg => this.isPlaceholder(arg.element));
195
-
196
- if (!hasPlaceholder) {
197
- return super.visitMethodInvocation(method, p);
198
- }
199
-
200
- const newArguments = await this.expandVariadicElements(method.arguments.elements, undefined, p);
201
-
202
- return produce(method, draft => {
203
- draft.arguments.elements = newArguments;
204
- });
205
- }
206
-
207
- override async visitBlock(block: J.Block, p: any): Promise<J | undefined> {
208
- // Check if any statements are placeholders (possibly variadic)
209
- const hasPlaceholder = block.statements.some(stmt => {
210
- const stmtElement = stmt.element;
211
- // Check if it's an ExpressionStatement containing a placeholder
212
- if (stmtElement.kind === JS.Kind.ExpressionStatement) {
213
- const exprStmt = stmtElement as JS.ExpressionStatement;
214
- return this.isPlaceholder(exprStmt.expression);
215
- }
216
- return this.isPlaceholder(stmtElement);
217
- });
218
-
219
- if (!hasPlaceholder) {
220
- return super.visitBlock(block, p);
221
- }
222
-
223
- // Unwrap function to extract placeholder from ExpressionStatement
224
- const unwrapStatement = (element: J): J => {
225
- if (element.kind === JS.Kind.ExpressionStatement) {
226
- return (element as JS.ExpressionStatement).expression;
227
- }
228
- return element;
229
- };
230
-
231
- const newStatements = await this.expandVariadicElements(block.statements, unwrapStatement, p);
232
-
233
- return produce(block, draft => {
234
- draft.statements = newStatements;
235
- });
236
- }
237
-
238
- override async visitJsCompilationUnit(compilationUnit: JS.CompilationUnit, p: any): Promise<J | undefined> {
239
- // Check if any statements are placeholders (possibly variadic)
240
- const hasPlaceholder = compilationUnit.statements.some(stmt => this.isPlaceholder(stmt.element));
241
-
242
- if (!hasPlaceholder) {
243
- return super.visitJsCompilationUnit(compilationUnit, p);
244
- }
245
-
246
- const newStatements = await this.expandVariadicElements(compilationUnit.statements, undefined, p);
247
-
248
- return produce(compilationUnit, draft => {
249
- draft.statements = newStatements;
250
- });
251
- }
252
-
253
324
  /**
254
325
  * Checks if a node is a placeholder.
255
326
  *
@@ -263,6 +334,10 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
263
334
  } else if (node.kind === J.Kind.Literal) {
264
335
  const literal = node as J.Literal;
265
336
  return literal.valueSource?.startsWith(PlaceholderUtils.PLACEHOLDER_PREFIX) || false;
337
+ } else if (node.kind === JS.Kind.BindingElement) {
338
+ // Check if the BindingElement's name is a placeholder
339
+ const bindingElement = node as JS.BindingElement;
340
+ return this.isPlaceholder(bindingElement.name);
266
341
  }
267
342
  return false;
268
343
  }
@@ -323,13 +398,13 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
323
398
 
324
399
  // Check if the parameter value is a Capture (could be a Proxy) or TemplateParam
325
400
  const isCapture = param.value instanceof CaptureImpl ||
326
- (param.value && typeof param.value === 'object' && param.value[CAPTURE_NAME_SYMBOL]);
401
+ (param.value && typeof param.value === 'object' && param.value[CAPTURE_NAME_SYMBOL]);
327
402
  const isTemplateParam = param.value instanceof TemplateParamImpl;
328
403
 
329
404
  if (isCapture || isTemplateParam) {
330
405
  // Simple capture/template param (no property path for template params)
331
406
  const name = isTemplateParam ? param.value.name :
332
- (param.value[CAPTURE_NAME_SYMBOL] || param.value.name);
407
+ (param.value[CAPTURE_NAME_SYMBOL] || param.value.name);
333
408
  const matchedNode = this.values.get(name);
334
409
  if (matchedNode && !Array.isArray(matchedNode)) {
335
410
  return produce(matchedNode, draft => {
@@ -342,6 +417,30 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
342
417
  return placeholder;
343
418
  }
344
419
 
420
+ // Check if the parameter value is a J.RightPadded wrapper
421
+ const isRightPadded = param.value && typeof param.value === 'object' &&
422
+ param.value.kind === J.Kind.RightPadded && isTree(param.value.element);
423
+
424
+ if (isRightPadded) {
425
+ // Extract the element from the J.RightPadded wrapper
426
+ const element = param.value.element as J;
427
+ return produce(element, draft => {
428
+ draft.markers = placeholder.markers;
429
+ draft.prefix = this.mergePrefix(element.prefix, placeholder.prefix);
430
+ });
431
+ }
432
+
433
+ // Check if the parameter value is a J.Container
434
+ const isContainer = param.value && typeof param.value === 'object' &&
435
+ param.value.kind === J.Kind.Container;
436
+
437
+ if (isContainer) {
438
+ // J.Container should be handled by expandVariadicElements
439
+ // For now, return placeholder - the expansion will happen at a higher level
440
+ // This should not happen in normal usage, as containers are typically used in argument positions
441
+ return placeholder;
442
+ }
443
+
345
444
  // If the parameter value is an AST node, use it directly
346
445
  if (isTree(param.value)) {
347
446
  // Return the AST node, preserving comments from the source
@@ -365,6 +464,10 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
365
464
  return (node as J.Identifier).simpleName;
366
465
  } else if (node.kind === J.Kind.Literal) {
367
466
  return (node as J.Literal).valueSource || null;
467
+ } else if (node.kind === JS.Kind.BindingElement) {
468
+ // Extract placeholder text from the BindingElement's name
469
+ const bindingElement = node as JS.BindingElement;
470
+ return this.getPlaceholderText(bindingElement.name);
368
471
  }
369
472
  return null;
370
473
  }
@@ -13,10 +13,10 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import {Cursor} from '../..';
16
+ import {Cursor, ExecutionContext, Recipe} from '../..';
17
17
  import {J} from '../../java';
18
18
  import {RewriteRule, RewriteConfig} from './types';
19
- import {Pattern} from './pattern';
19
+ import {Pattern, MatchResult} from './pattern';
20
20
  import {Template} from './template';
21
21
 
22
22
  /**
@@ -25,24 +25,92 @@ import {Template} from './template';
25
25
  class RewriteRuleImpl implements RewriteRule {
26
26
  constructor(
27
27
  private readonly before: Pattern[],
28
- private readonly after: Template
28
+ private readonly after: Template | ((match: MatchResult) => Template),
29
+ private readonly where?: (node: J, cursor: Cursor) => boolean | Promise<boolean>,
30
+ private readonly whereNot?: (node: J, cursor: Cursor) => boolean | Promise<boolean>
29
31
  ) {
30
32
  }
31
33
 
32
34
  async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
33
35
  for (const pattern of this.before) {
34
- const match = await pattern.match(node);
36
+ // Pass cursor to pattern.match() for context-aware capture constraints
37
+ const match = await pattern.match(node, cursor);
35
38
  if (match) {
36
- const result = await this.after.apply(cursor, node, match);
39
+ // Evaluate context predicates after structural match
40
+ if (this.where) {
41
+ const whereResult = await this.where(node, cursor);
42
+ if (!whereResult) {
43
+ continue; // Pattern matched but context doesn't, try next pattern
44
+ }
45
+ }
46
+
47
+ if (this.whereNot) {
48
+ const whereNotResult = await this.whereNot(node, cursor);
49
+ if (whereNotResult) {
50
+ continue; // Pattern matched but context is excluded, try next pattern
51
+ }
52
+ }
53
+
54
+ // Apply transformation
55
+ let result: J | undefined;
56
+
57
+ if (typeof this.after === 'function') {
58
+ // Call the function to get a template, then apply it
59
+ const template = this.after(match);
60
+ result = await template.apply(cursor, node, match);
61
+ } else {
62
+ // Use template.apply() as before
63
+ result = await this.after.apply(cursor, node, match);
64
+ }
65
+
37
66
  if (result) {
38
67
  return result;
39
68
  }
40
69
  }
41
70
  }
42
71
 
43
- // Return undefined if no patterns match
72
+ // Return undefined if no patterns match or all context checks failed
44
73
  return undefined;
45
74
  }
75
+
76
+ andThen(next: RewriteRule): RewriteRule {
77
+ const first = this;
78
+ return new (class extends RewriteRuleImpl {
79
+ constructor() {
80
+ // Pass empty patterns and a function that will never be called
81
+ // since we override tryOn
82
+ super([], () => undefined as unknown as Template);
83
+ }
84
+
85
+ async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
86
+ const firstResult = await first.tryOn(cursor, node);
87
+ if (firstResult !== undefined) {
88
+ const secondResult = await next.tryOn(cursor, firstResult);
89
+ return secondResult ?? firstResult;
90
+ }
91
+ return undefined;
92
+ }
93
+ })();
94
+ }
95
+
96
+ orElse(alternative: RewriteRule): RewriteRule {
97
+ const first = this;
98
+ return new (class extends RewriteRuleImpl {
99
+ constructor() {
100
+ // Pass empty patterns and a function that will never be called
101
+ // since we override tryOn
102
+ super([], () => undefined as unknown as Template);
103
+ }
104
+
105
+ async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
106
+ const firstResult = await first.tryOn(cursor, node);
107
+ if (firstResult !== undefined) {
108
+ return firstResult;
109
+ }
110
+ return await alternative.tryOn(cursor, node);
111
+ }
112
+ })();
113
+ }
46
114
  }
47
115
 
48
116
  /**
@@ -53,20 +121,26 @@ class RewriteRuleImpl implements RewriteRule {
53
121
  *
54
122
  * @example
55
123
  * // Single pattern
56
- * const swapOperands = rewrite(() => ({
57
- * before: pattern`${"left"} + ${"right"}`,
58
- * after: template`${"right"} + ${"left"}`
59
- * }));
124
+ * const swapOperands = rewrite(() => {
125
+ * const { left, right } = { left: capture(), right: capture() };
126
+ * return {
127
+ * before: pattern`${left} + ${right}`,
128
+ * after: template`${right} + ${left}`
129
+ * };
130
+ * });
60
131
  *
61
132
  * @example
62
133
  * // Multiple patterns
63
- * const normalizeComparisons = rewrite(() => ({
64
- * before: [
65
- * pattern`${"left"} == ${"right"}`,
66
- * pattern`${"left"} === ${"right"}`
67
- * ],
68
- * after: template`${"left"} === ${"right"}`
69
- * }));
134
+ * const normalizeComparisons = rewrite(() => {
135
+ * const { left, right } = { left: capture(), right: capture() };
136
+ * return {
137
+ * before: [
138
+ * pattern`${left} == ${right}`,
139
+ * pattern`${left} === ${right}`
140
+ * ],
141
+ * after: template`${left} === ${right}`
142
+ * };
143
+ * });
70
144
  *
71
145
  * @example
72
146
  * // Using in a visitor - IMPORTANT: use `|| node` to handle undefined when no match
@@ -91,5 +165,65 @@ export function rewrite(
91
165
  throw new Error('Builder function must return an object with before and after properties');
92
166
  }
93
167
 
94
- return new RewriteRuleImpl(Array.isArray(config.before) ? config.before : [config.before], config.after);
168
+ return new RewriteRuleImpl(
169
+ Array.isArray(config.before) ? config.before : [config.before],
170
+ config.after,
171
+ config.where,
172
+ config.whereNot
173
+ );
174
+ }
175
+
176
+ /**
177
+ * Creates a RewriteRule from a Recipe by using its editor visitor.
178
+ *
179
+ * This allows recipes to be used in the same chaining pattern as other rewrite rules,
180
+ * enabling composition with `andThen()`.
181
+ *
182
+ * @param recipe The recipe whose editor will be used to transform nodes
183
+ * @param ctx The execution context to pass to the recipe's editor
184
+ * @returns A RewriteRule that applies the recipe's editor to nodes
185
+ *
186
+ * @example
187
+ * ```typescript
188
+ * class MyRecipe extends Recipe {
189
+ * name = "my.recipe";
190
+ * displayName = "My Recipe";
191
+ * description = "Transforms code.";
192
+ *
193
+ * async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
194
+ * return new MyVisitor();
195
+ * }
196
+ * }
197
+ *
198
+ * // In a visitor:
199
+ * override async visitBinary(binary: J.Binary, p: ExecutionContext): Promise<J | undefined> {
200
+ * const rule1 = rewrite(() => ({
201
+ * before: pattern`${capture('a')} + ${capture('b')}`,
202
+ * after: template`${capture('b')} + ${capture('a')}`
203
+ * }));
204
+ *
205
+ * const rule2 = fromRecipe(new MyRecipe(), p);
206
+ *
207
+ * // Chain the pattern-based rule with the recipe
208
+ * const combined = rule1.andThen(rule2);
209
+ * return await combined.tryOn(this.cursor, binary) || binary;
210
+ * }
211
+ * ```
212
+ */
213
+ export const fromRecipe = (recipe: Recipe, ctx: ExecutionContext): RewriteRule => {
214
+ return new (class extends RewriteRuleImpl {
215
+ constructor() {
216
+ // Pass empty patterns and a function that will never be called
217
+ // since we override tryOn
218
+ super([], () => undefined as unknown as Template);
219
+ }
220
+
221
+ async tryOn(cursor: Cursor, tree: J): Promise<J | undefined> {
222
+ const visitor = await recipe.editor();
223
+ const result = await visitor.visit<J>(tree, ctx, cursor);
224
+
225
+ // Return undefined if the visitor didn't change the node
226
+ return result !== tree ? result : undefined;
227
+ }
228
+ })();
95
229
  }