@openrewrite/rewrite 8.67.0-20251105-091131 → 8.67.0-20251105-105242

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 (35) hide show
  1. package/dist/javascript/comparator.d.ts.map +1 -1
  2. package/dist/javascript/comparator.js +7 -10
  3. package/dist/javascript/comparator.js.map +1 -1
  4. package/dist/javascript/templating/comparator.d.ts +5 -5
  5. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  6. package/dist/javascript/templating/comparator.js +41 -38
  7. package/dist/javascript/templating/comparator.js.map +1 -1
  8. package/dist/javascript/templating/pattern.d.ts +3 -3
  9. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  10. package/dist/javascript/templating/pattern.js +31 -10
  11. package/dist/javascript/templating/pattern.js.map +1 -1
  12. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  13. package/dist/javascript/templating/placeholder-replacement.js +20 -1
  14. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  15. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  16. package/dist/javascript/templating/rewrite.js +24 -4
  17. package/dist/javascript/templating/rewrite.js.map +1 -1
  18. package/dist/javascript/templating/template.d.ts +1 -1
  19. package/dist/javascript/templating/template.js +1 -1
  20. package/dist/javascript/templating/types.d.ts +30 -1
  21. package/dist/javascript/templating/types.d.ts.map +1 -1
  22. package/dist/javascript/templating/utils.d.ts +10 -13
  23. package/dist/javascript/templating/utils.d.ts.map +1 -1
  24. package/dist/javascript/templating/utils.js +0 -40
  25. package/dist/javascript/templating/utils.js.map +1 -1
  26. package/dist/version.txt +1 -1
  27. package/package.json +1 -1
  28. package/src/javascript/comparator.ts +8 -10
  29. package/src/javascript/templating/comparator.ts +48 -39
  30. package/src/javascript/templating/pattern.ts +30 -6
  31. package/src/javascript/templating/placeholder-replacement.ts +22 -1
  32. package/src/javascript/templating/rewrite.ts +25 -5
  33. package/src/javascript/templating/template.ts +1 -1
  34. package/src/javascript/templating/types.ts +31 -1
  35. package/src/javascript/templating/utils.ts +4 -27
@@ -13,7 +13,7 @@
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 '../..';
16
+ import {Cursor, isTree, Tree} from '../..';
17
17
  import {J} from '../../java';
18
18
  import {JS} from '../index';
19
19
  import {JavaScriptSemanticComparatorVisitor} from '../comparator';
@@ -38,26 +38,11 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
38
38
  super(lenientTypeMatching);
39
39
  }
40
40
 
41
- /**
42
- * Extracts the wrapper from the cursor if the parent is a RightPadded.
43
- */
44
- private getWrapperFromCursor(cursor: Cursor): J.RightPadded<J> | undefined {
45
- if (!cursor.parent) {
46
- return undefined;
47
- }
48
- const parent = cursor.parent.value;
49
- // Check if parent is a RightPadded by checking its kind
50
- if ((parent as any).kind === J.Kind.RightPadded) {
51
- return parent as J.RightPadded<J>;
52
- }
53
- return undefined;
54
- }
55
-
56
41
  override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
57
- // Check if the pattern node is a capture - this handles captures anywhere in the tree
42
+ // Check if the pattern node is a capture - this handles unwrapped captures
43
+ // (Wrapped captures in J.RightPadded are handled by visitRightPadded override)
58
44
  if (PlaceholderUtils.isCapture(j as J)) {
59
- const wrapper = this.getWrapperFromCursor(this.cursor);
60
- const success = this.matcher.handleCapture(j as J, p, wrapper);
45
+ const success = this.matcher.handleCapture(j as J, p, undefined);
61
46
  if (!success) {
62
47
  return this.abort(j) as R;
63
48
  }
@@ -68,27 +53,58 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
68
53
  return j as R;
69
54
  }
70
55
 
71
- return super.visit(j, p, parent);
56
+ return await super.visit(j, p, parent);
72
57
  }
73
58
 
74
59
  protected hasSameKind(j: J, other: J): boolean {
75
60
  return super.hasSameKind(j, other) ||
76
- (j.kind == J.Kind.Identifier && PlaceholderUtils.isCapture(j as J.Identifier));
61
+ (j.kind == J.Kind.Identifier && PlaceholderUtils.isCapture(j as J.Identifier));
77
62
  }
78
63
 
79
- override async visitIdentifier(identifier: J.Identifier, other: J): Promise<J | undefined> {
80
- if (PlaceholderUtils.isCapture(identifier)) {
81
- const wrapper = this.getWrapperFromCursor(this.cursor);
82
- const success = this.matcher.handleCapture(identifier, other, wrapper);
83
- return success ? identifier : this.abort(identifier);
64
+ /**
65
+ * Override visitRightPadded to check if this wrapper has a CaptureMarker.
66
+ * If so, capture the entire wrapper (to preserve markers like semicolons).
67
+ */
68
+ override async visitRightPadded<T extends J | boolean>(right: J.RightPadded<T>, p: J): Promise<J.RightPadded<T>> {
69
+ if (!this.match) {
70
+ return right;
71
+ }
72
+
73
+ // Check if this RightPadded or its element has a CaptureMarker (attached during pattern construction)
74
+ const element = right.element;
75
+ let captureMarker = undefined;
76
+
77
+ // Check RightPadded itself
78
+ if (right.markers) {
79
+ captureMarker = PlaceholderUtils.getCaptureMarker(right);
80
+ }
81
+
82
+ // Check element if it's a J node
83
+ if (!captureMarker && isTree(element)) {
84
+ captureMarker = PlaceholderUtils.getCaptureMarker(element);
85
+ }
86
+ if (captureMarker) {
87
+ // Extract the target wrapper if it's also a RightPadded
88
+ const isRightPadded = (p as any).kind === J.Kind.RightPadded;
89
+ const targetWrapper = isRightPadded ? (p as unknown) as J.RightPadded<T> : undefined;
90
+ const targetElement = isRightPadded ? targetWrapper!.element : p;
91
+
92
+ // Handle the capture with the wrapper - use the element for pattern matching
93
+ const success = this.matcher.handleCapture(element as J, targetElement as J, targetWrapper as J.RightPadded<J> | undefined);
94
+ if (!success) {
95
+ return this.abort(right);
96
+ }
97
+ return right;
84
98
  }
85
- return super.visitIdentifier(identifier, other);
99
+
100
+ // Not a capture wrapper - use parent implementation
101
+ return super.visitRightPadded(right, p);
86
102
  }
87
103
 
88
104
  override async visitMethodInvocation(methodInvocation: J.MethodInvocation, other: J): Promise<J | undefined> {
89
105
  // Check if any arguments are variadic captures
90
106
  const hasVariadicCapture = methodInvocation.arguments.elements.some(arg =>
91
- PlaceholderUtils.isVariadicCapture(arg.element)
107
+ PlaceholderUtils.isVariadicCapture(arg)
92
108
  );
93
109
 
94
110
  // If no variadic captures, use parent implementation (which includes semantic/type-aware matching)
@@ -172,10 +188,9 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
172
188
  }
173
189
 
174
190
  override async visitJsCompilationUnit(compilationUnit: JS.CompilationUnit, other: J): Promise<J | undefined> {
175
- // Check if any statements are variadic captures (unwrap ExpressionStatement wrappers first)
191
+ // Check if any statements are variadic captures
176
192
  const hasVariadicCapture = compilationUnit.statements.some(stmt => {
177
- const unwrapped = PlaceholderUtils.unwrapStatementCapture(stmt.element);
178
- return PlaceholderUtils.isVariadicCapture(unwrapped);
193
+ return PlaceholderUtils.isVariadicCapture(stmt);
179
194
  });
180
195
 
181
196
  // If no variadic captures, use parent implementation
@@ -394,14 +409,8 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
394
409
  const savedMatch = this.match;
395
410
  const savedState = this.matcher.saveState();
396
411
 
397
- // Push wrapper onto cursor so captures can access it
398
- const savedCursor = this.cursor;
399
- this.cursor = new Cursor(targetWrapper, this.cursor);
400
- try {
401
- await this.visit(patternElement, targetElement);
402
- } finally {
403
- this.cursor = savedCursor;
404
- }
412
+ await this.visit(patternElement, targetElement);
413
+
405
414
  if (!this.match) {
406
415
  // Restore state on match failure
407
416
  this.match = savedMatch;
@@ -164,10 +164,10 @@ export class Pattern {
164
164
  * @returns This pattern for method chaining
165
165
  *
166
166
  * @example
167
- * pattern`isDate(${capture('date')})`
167
+ * pattern`forwardRef((${props}, ${ref}) => ${body})`
168
168
  * .configure({
169
- * imports: ['import { isDate } from \"util\"'],
170
- * dependencies: { 'util': '^1.0.0' }
169
+ * context: ['import { forwardRef } from "react"'],
170
+ * dependencies: {'@types/react': '^18.0.0'}
171
171
  * })
172
172
  */
173
173
  configure(options: PatternOptions): Pattern {
@@ -387,7 +387,7 @@ class Matcher {
387
387
  // Default to true for backward compatibility with existing patterns
388
388
  const lenientTypeMatching = this.pattern.options.lenientTypeMatching ?? true;
389
389
  const comparator = new PatternMatchingComparator({
390
- handleCapture: (p, t) => this.handleCapture(p, t),
390
+ handleCapture: (p, t, w) => this.handleCapture(p, t, w),
391
391
  handleVariadicCapture: (p, ts, ws) => this.handleVariadicCapture(p, ts, ws),
392
392
  saveState: () => this.saveState(),
393
393
  restoreState: (state) => this.restoreState(state)
@@ -709,9 +709,33 @@ class TemplateProcessor {
709
709
  // Mark as visited to avoid cycles
710
710
  visited.add(node);
711
711
 
712
+ // Check if this is a RightPadded containing a capture identifier
713
+ // Attach marker to the wrapper to preserve markers (like semicolons) during capture
714
+ if (node.kind === J.Kind.RightPadded &&
715
+ node.element?.kind === J.Kind.Identifier &&
716
+ node.element.simpleName?.startsWith(PlaceholderUtils.CAPTURE_PREFIX)) {
717
+
718
+ const captureInfo = PlaceholderUtils.parseCapture(node.element.simpleName);
719
+ if (captureInfo) {
720
+ // Initialize markers on the RightPadded
721
+ if (!node.markers) {
722
+ node.markers = { kind: 'org.openrewrite.marker.Markers', id: randomId(), markers: [] };
723
+ }
724
+ if (!node.markers.markers) {
725
+ node.markers.markers = [];
726
+ }
727
+
728
+ // Find the original capture object to get variadic options
729
+ const captureObj = this.captures.find(c => c.getName() === captureInfo.name);
730
+ const variadicOptions = captureObj?.getVariadicOptions();
731
+
732
+ // Add CaptureMarker to the RightPadded
733
+ node.markers.markers.push(new CaptureMarker(captureInfo.name, variadicOptions));
734
+ }
735
+ }
712
736
  // Check if this is an ExpressionStatement containing a capture identifier
713
737
  // For statement-level captures, we attach the marker to the ExpressionStatement itself
714
- if (node.kind === JS.Kind.ExpressionStatement &&
738
+ else if (node.kind === JS.Kind.ExpressionStatement &&
715
739
  node.expression?.kind === J.Kind.Identifier &&
716
740
  node.expression.simpleName?.startsWith(PlaceholderUtils.CAPTURE_PREFIX)) {
717
741
 
@@ -733,7 +757,7 @@ class TemplateProcessor {
733
757
  node.markers.markers.push(new CaptureMarker(captureInfo.name, variadicOptions));
734
758
  }
735
759
  }
736
- // For non-statement captures (expressions), attach marker to the identifier
760
+ // For non-statement, non-wrapped captures (expressions), attach marker to the identifier
737
761
  else if (node.kind === J.Kind.Identifier && node.simpleName?.startsWith(PlaceholderUtils.CAPTURE_PREFIX)) {
738
762
  const captureInfo = PlaceholderUtils.parseCapture(node.simpleName);
739
763
  if (captureInfo) {
@@ -167,7 +167,28 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
167
167
  // Not a placeholder (or expansion failed) - process normally
168
168
  const replacedElement = await this.visit(element, p);
169
169
  if (replacedElement) {
170
- newElements.push(produce(wrapped, draft => {
170
+ // Check if the replacement came from a capture with a wrapper (to preserve markers)
171
+ const placeholderNode = unwrapElement(element);
172
+ const placeholderText = this.getPlaceholderText(placeholderNode);
173
+ let wrapperToUse = wrapped;
174
+
175
+ if (placeholderText && this.isPlaceholder(placeholderNode)) {
176
+ const param = this.substitutions.get(placeholderText);
177
+ if (param) {
178
+ const isCapture = param.value instanceof CaptureImpl ||
179
+ (param.value && typeof param.value === 'object' && param.value[CAPTURE_NAME_SYMBOL]);
180
+ if (isCapture) {
181
+ const name = param.value[CAPTURE_NAME_SYMBOL] || param.value.name;
182
+ const wrapper = this.wrappersMap.get(name);
183
+ // Use captured wrapper if available and not an array (non-variadic)
184
+ if (wrapper && !Array.isArray(wrapper)) {
185
+ wrapperToUse = wrapper as J.RightPadded<J>;
186
+ }
187
+ }
188
+ }
189
+ }
190
+
191
+ newElements.push(produce(wrapperToUse, draft => {
171
192
  draft.element = replacedElement;
172
193
  }));
173
194
  }
@@ -25,7 +25,7 @@ import {Template} from './template';
25
25
  class RewriteRuleImpl implements RewriteRule {
26
26
  constructor(
27
27
  private readonly before: Pattern[],
28
- private readonly after: Template | ((match: MatchResult) => Promise<J>)
28
+ private readonly after: Template | ((match: MatchResult) => Template)
29
29
  ) {
30
30
  }
31
31
 
@@ -36,8 +36,9 @@ class RewriteRuleImpl implements RewriteRule {
36
36
  let result: J | undefined;
37
37
 
38
38
  if (typeof this.after === 'function') {
39
- // Call the function directly with the match result
40
- result = await this.after(match);
39
+ // Call the function to get a template, then apply it
40
+ const template = this.after(match);
41
+ result = await template.apply(cursor, node, match);
41
42
  } else {
42
43
  // Use template.apply() as before
43
44
  result = await this.after.apply(cursor, node, match);
@@ -59,7 +60,7 @@ class RewriteRuleImpl implements RewriteRule {
59
60
  constructor() {
60
61
  // Pass empty patterns and a function that will never be called
61
62
  // since we override tryOn
62
- super([], async () => undefined as unknown as J);
63
+ super([], () => undefined as unknown as Template);
63
64
  }
64
65
 
65
66
  async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
@@ -72,6 +73,25 @@ class RewriteRuleImpl implements RewriteRule {
72
73
  }
73
74
  })();
74
75
  }
76
+
77
+ orElse(alternative: RewriteRule): RewriteRule {
78
+ const first = this;
79
+ return new (class extends RewriteRuleImpl {
80
+ constructor() {
81
+ // Pass empty patterns and a function that will never be called
82
+ // since we override tryOn
83
+ super([], () => undefined as unknown as Template);
84
+ }
85
+
86
+ async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
87
+ const firstResult = await first.tryOn(cursor, node);
88
+ if (firstResult !== undefined) {
89
+ return firstResult;
90
+ }
91
+ return await alternative.tryOn(cursor, node);
92
+ }
93
+ })();
94
+ }
75
95
  }
76
96
 
77
97
  /**
@@ -165,7 +185,7 @@ export const fromRecipe = (recipe: Recipe, ctx: ExecutionContext): RewriteRule =
165
185
  constructor() {
166
186
  // Pass empty patterns and a function that will never be called
167
187
  // since we override tryOn
168
- super([], async () => undefined as unknown as J);
188
+ super([], () => undefined as unknown as Template);
169
189
  }
170
190
 
171
191
  async tryOn(cursor: Cursor, tree: J): Promise<J | undefined> {
@@ -210,7 +210,7 @@ export class Template {
210
210
  * @example
211
211
  * template`isDate(${capture('date')})`
212
212
  * .configure({
213
- * imports: ['import { isDate } from "util"'],
213
+ * context: ['import { isDate } from "util"'],
214
214
  * dependencies: { 'util': '^1.0.0' }
215
215
  * })
216
216
  */
@@ -331,6 +331,36 @@ export interface RewriteRule {
331
331
  * ```
332
332
  */
333
333
  andThen(next: RewriteRule): RewriteRule;
334
+
335
+ /**
336
+ * Creates a composite rule that tries this rule first, and if it doesn't match, tries an alternative rule.
337
+ *
338
+ * The resulting rule:
339
+ * 1. First applies this rule to the input node
340
+ * 2. If this rule matches and transforms the node, returns the result
341
+ * 3. If this rule returns undefined (no match), tries the alternative rule on the original node
342
+ *
343
+ * @param alternative The rule to try if this rule doesn't match
344
+ * @returns A new RewriteRule that tries both rules with fallback behavior
345
+ *
346
+ * @example
347
+ * ```typescript
348
+ * // Try specific pattern first, fall back to general pattern
349
+ * const specific = rewrite(() => ({
350
+ * before: pattern`foo(${capture('x')}, 0)`,
351
+ * after: template`bar(${capture('x')})`
352
+ * }));
353
+ *
354
+ * const general = rewrite(() => ({
355
+ * before: pattern`foo(${capture('x')}, ${capture('y')})`,
356
+ * after: template`baz(${capture('x')}, ${capture('y')})`
357
+ * }));
358
+ *
359
+ * const combined = specific.orElse(general);
360
+ * // Will try specific pattern first, if no match, try general pattern
361
+ * ```
362
+ */
363
+ orElse(alternative: RewriteRule): RewriteRule;
334
364
  }
335
365
 
336
366
  /**
@@ -338,5 +368,5 @@ export interface RewriteRule {
338
368
  */
339
369
  export interface RewriteConfig {
340
370
  before: Pattern | Pattern[],
341
- after: Template | ((match: MatchResult) => Promise<J>)
371
+ after: Template | ((match: MatchResult) => Template)
342
372
  }
@@ -17,7 +17,7 @@ import {J} from '../../java';
17
17
  import {JS} from '../index';
18
18
  import {JavaScriptParser} from '../parser';
19
19
  import {DependencyWorkspace} from '../dependency-workspace';
20
- import {Marker} from '../../markers';
20
+ import {Marker, Markers} from '../../markers';
21
21
  import {randomId} from '../../uuid';
22
22
  import {VariadicOptions, Capture, Any} from './types';
23
23
 
@@ -177,7 +177,7 @@ export class PlaceholderUtils {
177
177
  * @param node The node to check
178
178
  * @returns The CaptureMarker or undefined
179
179
  */
180
- static getCaptureMarker(node: J): CaptureMarker | undefined {
180
+ static getCaptureMarker(node: { markers: Markers }): CaptureMarker | undefined {
181
181
  for (const marker of node.markers.markers) {
182
182
  if (marker instanceof CaptureMarker) {
183
183
  return marker;
@@ -235,7 +235,7 @@ export class PlaceholderUtils {
235
235
  * @param node The node to check
236
236
  * @returns true if the node has a variadic CaptureMarker, false otherwise
237
237
  */
238
- static isVariadicCapture(node: J): boolean {
238
+ static isVariadicCapture(node: { markers: Markers }): boolean {
239
239
  for (const marker of node.markers.markers) {
240
240
  if (marker instanceof CaptureMarker && marker.variadicOptions) {
241
241
  return true;
@@ -250,7 +250,7 @@ export class PlaceholderUtils {
250
250
  * @param node The node to extract variadic options from
251
251
  * @returns The VariadicOptions, or undefined if not a variadic capture
252
252
  */
253
- static getVariadicOptions(node: J): VariadicOptions | undefined {
253
+ static getVariadicOptions(node: { markers: Markers }): VariadicOptions | undefined {
254
254
  for (const marker of node.markers.markers) {
255
255
  if (marker instanceof CaptureMarker) {
256
256
  return marker.variadicOptions;
@@ -258,27 +258,4 @@ export class PlaceholderUtils {
258
258
  }
259
259
  return undefined;
260
260
  }
261
-
262
- /**
263
- * Checks if a statement is an ExpressionStatement wrapping a capture identifier.
264
- * When a capture placeholder appears in statement position, the parser wraps it as
265
- * an ExpressionStatement. This method unwraps it to get the identifier.
266
- *
267
- * @param stmt The statement to check
268
- * @returns The unwrapped capture identifier, or the original statement if not wrapped
269
- */
270
- static unwrapStatementCapture(stmt: J): J {
271
- // Check if it's an ExpressionStatement containing a capture identifier
272
- if (stmt.kind === JS.Kind.ExpressionStatement) {
273
- const exprStmt = stmt as JS.ExpressionStatement;
274
- if (exprStmt.expression?.kind === J.Kind.Identifier) {
275
- const identifier = exprStmt.expression as J.Identifier;
276
- // Check if this is a capture placeholder
277
- if (identifier.simpleName?.startsWith(this.CAPTURE_PREFIX)) {
278
- return identifier;
279
- }
280
- }
281
- }
282
- return stmt;
283
- }
284
261
  }