@openrewrite/rewrite 8.67.0-20251105-121415 → 8.67.0-20251105-160319

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.
@@ -13,14 +13,16 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import {produce} from 'immer';
17
16
  import {J, Type} from '../../java';
18
17
  import {JS} from '../index';
19
- import {randomId} from '../../uuid';
18
+ import {JavaScriptVisitor} from '../visitor';
19
+ import {produceAsync} from '../../visitor';
20
+ import {updateIfChanged} from '../../util';
20
21
  import {Any, Capture, PatternOptions} from './types';
21
22
  import {CAPTURE_CAPTURING_SYMBOL, CAPTURE_NAME_SYMBOL, CAPTURE_TYPE_SYMBOL, CaptureImpl} from './capture';
22
23
  import {PatternMatchingComparator} from './comparator';
23
24
  import {CaptureMarker, CaptureStorageValue, PlaceholderUtils, templateCache, WRAPPERS_MAP_SYMBOL} from './utils';
25
+ import {isTree, Tree} from "../../tree";
24
26
 
25
27
  /**
26
28
  * Builder for creating patterns programmatically.
@@ -171,7 +173,7 @@ export class Pattern {
171
173
  * })
172
174
  */
173
175
  configure(options: PatternOptions): Pattern {
174
- this._options = { ...this._options, ...options };
176
+ this._options = {...this._options, ...options};
175
177
  return this;
176
178
  }
177
179
 
@@ -375,7 +377,7 @@ class Matcher {
375
377
  private async matchNode(pattern: J, target: J): Promise<boolean> {
376
378
  // Check if pattern is a capture placeholder
377
379
  if (PlaceholderUtils.isCapture(pattern)) {
378
- return this.handleCapture(pattern, target);
380
+ return this.handleCapture(PlaceholderUtils.getCaptureMarker(pattern)!, target);
379
381
  }
380
382
 
381
383
  // Check if nodes have the same kind
@@ -387,8 +389,8 @@ class Matcher {
387
389
  // Default to true for backward compatibility with existing patterns
388
390
  const lenientTypeMatching = this.pattern.options.lenientTypeMatching ?? true;
389
391
  const comparator = new PatternMatchingComparator({
390
- handleCapture: (p, t, w) => this.handleCapture(p, t, w),
391
- handleVariadicCapture: (p, ts, ws) => this.handleVariadicCapture(p, ts, ws),
392
+ handleCapture: (capture, t, w) => this.handleCapture(capture, t, w),
393
+ handleVariadicCapture: (capture, ts, ws) => this.handleVariadicCapture(capture, ts, ws),
392
394
  saveState: () => this.saveState(),
393
395
  restoreState: (state) => this.restoreState(state)
394
396
  }, lenientTypeMatching);
@@ -417,13 +419,13 @@ class Matcher {
417
419
  /**
418
420
  * Handles a capture placeholder.
419
421
  *
420
- * @param pattern The pattern node
422
+ * @param capture The pattern node capture
421
423
  * @param target The target node
422
424
  * @param wrapper Optional wrapper containing the target (for preserving markers)
423
425
  * @returns true if the capture is successful, false otherwise
424
426
  */
425
- private handleCapture(pattern: J, target: J, wrapper?: J.RightPadded<J>): boolean {
426
- const captureName = PlaceholderUtils.getCaptureName(pattern);
427
+ private handleCapture(capture: CaptureMarker, target: J, wrapper?: J.RightPadded<J>): boolean {
428
+ const captureName = capture.captureName;
427
429
 
428
430
  if (!captureName) {
429
431
  return false;
@@ -451,13 +453,13 @@ class Matcher {
451
453
  /**
452
454
  * Handles a variadic capture placeholder.
453
455
  *
454
- * @param pattern The pattern node (the variadic capture)
456
+ * @param capture The pattern node capture (the variadic capture)
455
457
  * @param targets The target nodes that were matched
456
458
  * @param wrappers Optional wrappers to preserve markers
457
459
  * @returns true if the capture is successful, false otherwise
458
460
  */
459
- private handleVariadicCapture(pattern: J, targets: J[], wrappers?: J.RightPadded<J>[]): boolean {
460
- const captureName = PlaceholderUtils.getCaptureName(pattern);
461
+ private handleVariadicCapture(capture: CaptureMarker, targets: J[], wrappers?: J.RightPadded<J>[]): boolean {
462
+ const captureName = capture.captureName;
461
463
 
462
464
  if (!captureName) {
463
465
  return false;
@@ -487,6 +489,101 @@ class Matcher {
487
489
  }
488
490
  }
489
491
 
492
+ /**
493
+ * Visitor that attaches CaptureMarkers to capture identifiers in the AST.
494
+ * Markers are attached to Identifiers, then moved up to wrappers (RightPadded, ExpressionStatement).
495
+ * Uses JavaScriptVisitor to properly handle AST traversal and avoid cycles in Type objects.
496
+ */
497
+ class MarkerAttachmentVisitor extends JavaScriptVisitor<undefined> {
498
+ constructor(private readonly captures: (Capture | Any<any>)[]) {
499
+ super();
500
+ }
501
+
502
+ /**
503
+ * Attaches CaptureMarker to capture identifiers.
504
+ */
505
+ protected override async visitIdentifier(ident: J.Identifier, p: undefined): Promise<J | undefined> {
506
+ // First call parent to handle standard visitation
507
+ const visited = await super.visitIdentifier(ident, p);
508
+ if (!visited || visited.kind !== J.Kind.Identifier) {
509
+ return visited;
510
+ }
511
+ ident = visited as J.Identifier;
512
+
513
+ // Check if this is a capture placeholder
514
+ if (ident.simpleName?.startsWith(PlaceholderUtils.CAPTURE_PREFIX)) {
515
+ const captureInfo = PlaceholderUtils.parseCapture(ident.simpleName);
516
+ if (captureInfo) {
517
+ // Find the original capture object to get variadic options
518
+ const captureObj = this.captures.find(c => c.getName() === captureInfo.name);
519
+ const variadicOptions = captureObj?.getVariadicOptions();
520
+
521
+ // Add CaptureMarker to the Identifier
522
+ const marker = new CaptureMarker(captureInfo.name, variadicOptions);
523
+ return updateIfChanged(ident, {
524
+ markers: {
525
+ ...ident.markers,
526
+ markers: [...ident.markers.markers, marker]
527
+ }
528
+ });
529
+ }
530
+ }
531
+
532
+ return ident;
533
+ }
534
+
535
+ /**
536
+ * Propagates markers from element to RightPadded wrapper.
537
+ */
538
+ public override async visitRightPadded<T extends J | boolean>(right: J.RightPadded<T>, p: undefined): Promise<J.RightPadded<T>> {
539
+ if (!isTree(right.element)) {
540
+ return right;
541
+ }
542
+
543
+ const visitedElement = await this.visit(right.element as J, p);
544
+ if (visitedElement && visitedElement !== right.element as Tree) {
545
+ return produceAsync<J.RightPadded<T>>(right, async (draft: any) => {
546
+ // Visit element first
547
+ if (right.element && (right.element as any).kind) {
548
+ // Check if element has a CaptureMarker
549
+ const elementMarker = PlaceholderUtils.getCaptureMarker(visitedElement);
550
+ if (elementMarker) {
551
+ draft.markers.markers.push(elementMarker);
552
+ } else {
553
+ draft.element = visitedElement;
554
+ }
555
+ }
556
+ });
557
+ }
558
+
559
+ return right;
560
+ }
561
+
562
+ /**
563
+ * Propagates markers from expression to ExpressionStatement.
564
+ */
565
+ protected override async visitExpressionStatement(expressionStatement: JS.ExpressionStatement, p: undefined): Promise<J | undefined> {
566
+ // Visit the expression
567
+ const visitedExpression = await this.visit(expressionStatement.expression, p);
568
+
569
+ // Check if expression has a CaptureMarker
570
+ const expressionMarker = PlaceholderUtils.getCaptureMarker(visitedExpression as any);
571
+ if (expressionMarker) {
572
+ return updateIfChanged(expressionStatement, {
573
+ markers: {
574
+ ...expressionStatement.markers,
575
+ markers: [...expressionStatement.markers.markers, expressionMarker]
576
+ },
577
+ });
578
+ }
579
+
580
+ // No marker to move, just update with visited expression
581
+ return updateIfChanged(expressionStatement, {
582
+ expression: visitedExpression
583
+ });
584
+ }
585
+ }
586
+
490
587
  /**
491
588
  * Processor for template strings.
492
589
  * Converts a template string with captures into an AST pattern.
@@ -535,7 +632,7 @@ class TemplateProcessor {
535
632
 
536
633
  // Extract the relevant part of the AST
537
634
  // The pattern code is always the last statement (after context + preamble)
538
- return this.extractPatternFromAst(cu);
635
+ return await this.extractPatternFromAst(cu);
539
636
  }
540
637
 
541
638
  /**
@@ -647,7 +744,7 @@ class TemplateProcessor {
647
744
  * @param cu The compilation unit
648
745
  * @returns The extracted pattern
649
746
  */
650
- private extractPatternFromAst(cu: JS.CompilationUnit): J {
747
+ private async extractPatternFromAst(cu: JS.CompilationUnit): Promise<J> {
651
748
  // Check if we have any statements
652
749
  if (!cu.statements || cu.statements.length === 0) {
653
750
  throw new Error(`No statements found in compilation unit`);
@@ -676,119 +773,20 @@ class TemplateProcessor {
676
773
  }
677
774
 
678
775
  // Attach CaptureMarkers to capture identifiers
679
- return this.attachCaptureMarkers(extracted);
776
+ return await this.attachCaptureMarkers(extracted);
680
777
  }
681
778
 
682
779
  /**
683
780
  * Attaches CaptureMarkers to capture identifiers in the AST.
684
781
  * This allows efficient capture detection without string parsing.
782
+ * Uses JavaScriptVisitor to properly handle AST traversal and avoid cycles in Type objects.
685
783
  *
686
784
  * @param ast The AST to process
687
785
  * @returns The AST with CaptureMarkers attached
688
786
  */
689
- private attachCaptureMarkers(ast: J): J {
690
- const visited = new Set<J | object>();
691
- return produce(ast, draft => {
692
- this.visitAndAttachMarkers(draft, visited);
693
- });
694
- }
695
-
696
- /**
697
- * Recursively visits AST nodes and attaches CaptureMarkers to capture identifiers.
698
- * For statement-level captures (identifiers in ExpressionStatement), the marker
699
- * is attached to the ExpressionStatement itself rather than the nested identifier.
700
- *
701
- * @param node The node to visit
702
- * @param visited Set of already visited nodes to avoid cycles
703
- */
704
- private visitAndAttachMarkers(node: any, visited: Set<J | object>): void {
705
- if (!node || typeof node !== 'object' || visited.has(node)) {
706
- return;
707
- }
708
-
709
- // Mark as visited to avoid cycles
710
- visited.add(node);
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
- }
736
- // Check if this is an ExpressionStatement containing a capture identifier
737
- // For statement-level captures, we attach the marker to the ExpressionStatement itself
738
- else if (node.kind === JS.Kind.ExpressionStatement &&
739
- node.expression?.kind === J.Kind.Identifier &&
740
- node.expression.simpleName?.startsWith(PlaceholderUtils.CAPTURE_PREFIX)) {
741
-
742
- const captureInfo = PlaceholderUtils.parseCapture(node.expression.simpleName);
743
- if (captureInfo) {
744
- // Initialize markers on the ExpressionStatement
745
- if (!node.markers) {
746
- node.markers = { kind: 'org.openrewrite.marker.Markers', id: randomId(), markers: [] };
747
- }
748
- if (!node.markers.markers) {
749
- node.markers.markers = [];
750
- }
751
-
752
- // Find the original capture object to get variadic options
753
- const captureObj = this.captures.find(c => c.getName() === captureInfo.name);
754
- const variadicOptions = captureObj?.getVariadicOptions();
755
-
756
- // Add CaptureMarker to the ExpressionStatement
757
- node.markers.markers.push(new CaptureMarker(captureInfo.name, variadicOptions));
758
- }
759
- }
760
- // For non-statement, non-wrapped captures (expressions), attach marker to the identifier
761
- else if (node.kind === J.Kind.Identifier && node.simpleName?.startsWith(PlaceholderUtils.CAPTURE_PREFIX)) {
762
- const captureInfo = PlaceholderUtils.parseCapture(node.simpleName);
763
- if (captureInfo) {
764
- // Initialize markers if needed
765
- if (!node.markers) {
766
- node.markers = { kind: 'org.openrewrite.marker.Markers', id: randomId(), markers: [] };
767
- }
768
- if (!node.markers.markers) {
769
- node.markers.markers = [];
770
- }
771
-
772
- // Find the original capture object to get variadic options
773
- const captureObj = this.captures.find(c => c.getName() === captureInfo.name);
774
- const variadicOptions = captureObj?.getVariadicOptions();
775
-
776
- // Add CaptureMarker with variadic options if available
777
- node.markers.markers.push(new CaptureMarker(captureInfo.name, variadicOptions));
778
- }
779
- }
780
-
781
- // Recursively visit all properties
782
- for (const key in node) {
783
- if (node.hasOwnProperty(key)) {
784
- const value = node[key];
785
- if (Array.isArray(value)) {
786
- value.forEach(item => this.visitAndAttachMarkers(item, visited));
787
- } else if (typeof value === 'object' && value !== null) {
788
- this.visitAndAttachMarkers(value, visited);
789
- }
790
- }
791
- }
787
+ private async attachCaptureMarkers(ast: J): Promise<J> {
788
+ const visitor = new MarkerAttachmentVisitor(this.captures);
789
+ return (await visitor.visit(ast, undefined))!;
792
790
  }
793
791
  }
794
792