@openrewrite/rewrite 8.67.0-20251104-132125 → 8.67.0-20251104-141355

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,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 {J} from '../../java';
17
- import {Capture, Any, TemplateParam, CaptureOptions, VariadicOptions} from './types';
16
+ import {J, Type} from '../../java';
17
+ import {Any, Capture, CaptureOptions, TemplateParam, VariadicOptions} from './types';
18
18
 
19
19
  /**
20
20
  * Combines multiple constraints with AND logic.
@@ -70,6 +70,8 @@ export const CAPTURE_VARIADIC_SYMBOL = Symbol('captureVariadic');
70
70
  export const CAPTURE_CONSTRAINT_SYMBOL = Symbol('captureConstraint');
71
71
  // Symbol to access capturing flag without triggering Proxy
72
72
  export const CAPTURE_CAPTURING_SYMBOL = Symbol('captureCapturing');
73
+ // Symbol to access type information without triggering Proxy
74
+ export const CAPTURE_TYPE_SYMBOL = Symbol('captureType');
73
75
 
74
76
  export class CaptureImpl<T = any> implements Capture<T> {
75
77
  public readonly name: string;
@@ -77,6 +79,7 @@ export class CaptureImpl<T = any> implements Capture<T> {
77
79
  [CAPTURE_VARIADIC_SYMBOL]: VariadicOptions | undefined;
78
80
  [CAPTURE_CONSTRAINT_SYMBOL]: ((node: T) => boolean) | undefined;
79
81
  [CAPTURE_CAPTURING_SYMBOL]: boolean;
82
+ [CAPTURE_TYPE_SYMBOL]: string | Type | undefined;
80
83
 
81
84
  constructor(name: string, options?: CaptureOptions<T>, capturing: boolean = true) {
82
85
  this.name = name;
@@ -99,6 +102,11 @@ export class CaptureImpl<T = any> implements Capture<T> {
99
102
  if (options?.constraint) {
100
103
  this[CAPTURE_CONSTRAINT_SYMBOL] = options.constraint;
101
104
  }
105
+
106
+ // Store type if provided
107
+ if (options?.type) {
108
+ this[CAPTURE_TYPE_SYMBOL] = options.type;
109
+ }
102
110
  }
103
111
 
104
112
  getName(): string {
@@ -120,6 +128,10 @@ export class CaptureImpl<T = any> implements Capture<T> {
120
128
  isCapturing(): boolean {
121
129
  return this[CAPTURE_CAPTURING_SYMBOL];
122
130
  }
131
+
132
+ getType(): string | Type | undefined {
133
+ return this[CAPTURE_TYPE_SYMBOL];
134
+ }
123
135
  }
124
136
 
125
137
  export class TemplateParamImpl<T = any> implements TemplateParam<T> {
@@ -291,6 +303,9 @@ function createCaptureProxy<T>(impl: CaptureImpl<T>): any {
291
303
  if (prop === CAPTURE_CAPTURING_SYMBOL) {
292
304
  return target[CAPTURE_CAPTURING_SYMBOL];
293
305
  }
306
+ if (prop === CAPTURE_TYPE_SYMBOL) {
307
+ return target[CAPTURE_TYPE_SYMBOL];
308
+ }
294
309
 
295
310
  // Support using Capture as object key via computed properties {[x]: value}
296
311
  if (prop === Symbol.toPrimitive || prop === 'toString' || prop === 'valueOf') {
@@ -298,7 +313,7 @@ function createCaptureProxy<T>(impl: CaptureImpl<T>): any {
298
313
  }
299
314
 
300
315
  // Allow methods to be called directly on the target
301
- if (prop === 'getName' || prop === 'isVariadic' || prop === 'getVariadicOptions' || prop === 'getConstraint' || prop === 'isCapturing') {
316
+ if (prop === 'getName' || prop === 'isVariadic' || prop === 'getVariadicOptions' || prop === 'getConstraint' || prop === 'isCapturing' || prop === 'getType') {
302
317
  return target[prop].bind(target);
303
318
  }
304
319
 
@@ -335,7 +350,7 @@ function createCaptureProxy<T>(impl: CaptureImpl<T>): any {
335
350
 
336
351
  // Overload 1: Options object with constraint (no variadic)
337
352
  export function capture<T = any>(
338
- options: { name?: string; constraint: (node: T) => boolean } & { variadic?: never }
353
+ options: CaptureOptions<T> & { variadic?: never }
339
354
  ): Capture<T> & T;
340
355
 
341
356
  // Overload 2: Options object with variadic
@@ -14,11 +14,11 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import {Cursor, isTree} from '../..';
17
- import {J} from '../../java';
17
+ import {J, Type} from '../../java';
18
18
  import {JS} from '..';
19
19
  import {produce} from 'immer';
20
- import {TemplateCache, PlaceholderUtils} from './utils';
21
- import {CaptureImpl, TemplateParamImpl, CaptureValue, CAPTURE_NAME_SYMBOL} from './capture';
20
+ import {PlaceholderUtils, TemplateCache} from './utils';
21
+ import {CAPTURE_NAME_SYMBOL, CAPTURE_TYPE_SYMBOL, CaptureImpl, CaptureValue, TemplateParamImpl} from './capture';
22
22
  import {PlaceholderReplacementVisitor} from './placeholder-replacement';
23
23
  import {JavaCoordinates} from './template';
24
24
 
@@ -66,6 +66,9 @@ export class TemplateEngine {
66
66
  contextStatements: string[] = [],
67
67
  dependencies: Record<string, string> = {}
68
68
  ): Promise<J | undefined> {
69
+ // Generate type preamble for captures/parameters with types
70
+ const preamble = TemplateEngine.generateTypePreamble(parameters);
71
+
69
72
  // Build the template string with parameter placeholders
70
73
  const templateString = TemplateEngine.buildTemplateString(templateParts, parameters);
71
74
 
@@ -74,12 +77,17 @@ export class TemplateEngine {
74
77
  return undefined;
75
78
  }
76
79
 
80
+ // Add preamble to context statements (so they're skipped during extraction)
81
+ const contextWithPreamble = preamble.length > 0
82
+ ? [...contextStatements, ...preamble]
83
+ : contextStatements;
84
+
77
85
  // Use cache to get or parse the compilation unit
78
86
  // For templates, we don't have captures, so use empty array
79
87
  const cu = await templateCache.getOrParse(
80
88
  templateString,
81
89
  [], // templates don't have captures in the cache key
82
- contextStatements,
90
+ contextWithPreamble,
83
91
  dependencies
84
92
  );
85
93
 
@@ -88,31 +96,25 @@ export class TemplateEngine {
88
96
  throw new Error(`Failed to parse template code (no statements):\n${templateString}`);
89
97
  }
90
98
 
91
- // Skip context statements to get to the actual template code
92
- const templateStatementIndex = contextStatements.length;
93
- if (templateStatementIndex >= cu.statements.length) {
94
- return undefined;
95
- }
96
-
97
- // Extract the relevant part of the AST
98
- const firstStatement = cu.statements[templateStatementIndex].element;
99
+ // The template code is always the last statement (after context + preamble)
100
+ const lastStatement = cu.statements[cu.statements.length - 1].element;
99
101
  let extracted: J;
100
102
 
101
103
  // Check if this is a wrapped template (function __TEMPLATE__() { ... })
102
- if (firstStatement.kind === J.Kind.MethodDeclaration) {
103
- const func = firstStatement as J.MethodDeclaration;
104
+ if (lastStatement.kind === J.Kind.MethodDeclaration) {
105
+ const func = lastStatement as J.MethodDeclaration;
104
106
  if (func.name.simpleName === '__TEMPLATE__' && func.body) {
105
107
  // __TEMPLATE__ wrapper indicates the original template was a block.
106
108
  // Always return the block to preserve the block structure.
107
109
  extracted = func.body;
108
110
  } else {
109
111
  // Not a __TEMPLATE__ wrapper
110
- extracted = firstStatement;
112
+ extracted = lastStatement;
111
113
  }
112
- } else if (firstStatement.kind === JS.Kind.ExpressionStatement) {
113
- extracted = (firstStatement as JS.ExpressionStatement).expression;
114
+ } else if (lastStatement.kind === JS.Kind.ExpressionStatement) {
115
+ extracted = (lastStatement as JS.ExpressionStatement).expression;
114
116
  } else {
115
- extracted = firstStatement;
117
+ extracted = lastStatement;
116
118
  }
117
119
 
118
120
  // Create a copy to avoid sharing cached AST instances
@@ -133,6 +135,58 @@ export class TemplateEngine {
133
135
  return new TemplateApplier(cursor, coordinates, unsubstitutedAst).apply();
134
136
  }
135
137
 
138
+ /**
139
+ * Generates type preamble declarations for captures/parameters with type annotations.
140
+ *
141
+ * @param parameters The parameters
142
+ * @returns Array of preamble statements
143
+ */
144
+ private static generateTypePreamble(parameters: Parameter[]): string[] {
145
+ const preamble: string[] = [];
146
+
147
+ for (let i = 0; i < parameters.length; i++) {
148
+ const param = parameters[i].value;
149
+ const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
150
+
151
+ // Check for Capture (could be a Proxy, so check for symbol property)
152
+ const isCapture = param instanceof CaptureImpl ||
153
+ (param && typeof param === 'object' && param[CAPTURE_NAME_SYMBOL]);
154
+ const isCaptureValue = param instanceof CaptureValue;
155
+ const isTreeArray = Array.isArray(param) && param.length > 0 && isTree(param[0]);
156
+
157
+ if (isCapture) {
158
+ const captureType = param[CAPTURE_TYPE_SYMBOL];
159
+ if (captureType) {
160
+ const typeString = typeof captureType === 'string'
161
+ ? captureType
162
+ : this.typeToString(captureType);
163
+ preamble.push(`const ${placeholder}: ${typeString};`);
164
+ }
165
+ } else if (isCaptureValue) {
166
+ // For CaptureValue, check if the root capture has a type
167
+ const rootCapture = param.rootCapture;
168
+ if (rootCapture) {
169
+ const captureType = (rootCapture as any)[CAPTURE_TYPE_SYMBOL];
170
+ if (captureType) {
171
+ const typeString = typeof captureType === 'string'
172
+ ? captureType
173
+ : this.typeToString(captureType);
174
+ preamble.push(`const ${placeholder}: ${typeString};`);
175
+ }
176
+ }
177
+ } else if (isTree(param) && !isTreeArray) {
178
+ // For J elements, derive type from the element's type property if it exists
179
+ const jElement = param as J;
180
+ if ((jElement as any).type) {
181
+ const typeString = this.typeToString((jElement as any).type);
182
+ preamble.push(`const ${placeholder}: ${typeString};`);
183
+ }
184
+ }
185
+ }
186
+
187
+ return preamble;
188
+ }
189
+
136
190
  /**
137
191
  * Builds a template string with parameter placeholders.
138
192
  *
@@ -174,6 +228,53 @@ export class TemplateEngine {
174
228
 
175
229
  return result;
176
230
  }
231
+
232
+ /**
233
+ * Converts a Type instance to a TypeScript type string.
234
+ *
235
+ * @param type The Type instance
236
+ * @returns A TypeScript type string
237
+ */
238
+ private static typeToString(type: Type): string {
239
+ // Handle Type.Class and Type.ShallowClass - return their fully qualified names
240
+ if (type.kind === Type.Kind.Class || type.kind === Type.Kind.ShallowClass) {
241
+ const classType = type as Type.Class;
242
+ return classType.fullyQualifiedName;
243
+ }
244
+
245
+ // Handle Type.Primitive - map to TypeScript primitive types
246
+ if (type.kind === Type.Kind.Primitive) {
247
+ const primitiveType = type as Type.Primitive;
248
+ switch (primitiveType.keyword) {
249
+ case 'String':
250
+ return 'string';
251
+ case 'boolean':
252
+ return 'boolean';
253
+ case 'double':
254
+ case 'float':
255
+ case 'int':
256
+ case 'long':
257
+ case 'short':
258
+ case 'byte':
259
+ return 'number';
260
+ case 'void':
261
+ return 'void';
262
+ default:
263
+ return 'any';
264
+ }
265
+ }
266
+
267
+ // Handle Type.Array - render component type plus []
268
+ if (type.kind === Type.Kind.Array) {
269
+ const arrayType = type as Type.Array;
270
+ const componentTypeString = this.typeToString(arrayType.elemType);
271
+ return `${componentTypeString}[]`;
272
+ }
273
+
274
+ // For other types, return 'any' as a fallback
275
+ // TODO: Implement proper Type to string conversion for other Type.Kind values
276
+ return 'any';
277
+ }
177
278
  }
178
279
 
179
280
  /**
@@ -14,13 +14,13 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import {produce} from 'immer';
17
- import {J} from '../../java';
17
+ import {J, Type} from '../../java';
18
18
  import {JS} from '../index';
19
19
  import {randomId} from '../../uuid';
20
- import {Capture, Any, PatternOptions} from './types';
21
- import {CaptureImpl, CAPTURE_NAME_SYMBOL, CAPTURE_CAPTURING_SYMBOL} from './capture';
20
+ import {Any, Capture, PatternOptions} from './types';
21
+ import {CAPTURE_CAPTURING_SYMBOL, CAPTURE_NAME_SYMBOL, CAPTURE_TYPE_SYMBOL, CaptureImpl} from './capture';
22
22
  import {PatternMatchingComparator} from './comparator';
23
- import {PlaceholderUtils, templateCache, CaptureMarker, CaptureStorageValue, WRAPPERS_MAP_SYMBOL} from './utils';
23
+ import {CaptureMarker, CaptureStorageValue, PlaceholderUtils, templateCache, WRAPPERS_MAP_SYMBOL} from './utils';
24
24
 
25
25
  /**
26
26
  * Builder for creating patterns programmatically.
@@ -514,21 +514,52 @@ class TemplateProcessor {
514
514
  * @returns A Promise resolving to the AST pattern
515
515
  */
516
516
  async toAstPattern(): Promise<J> {
517
+ // Generate type preamble for captures with types
518
+ const preamble = this.generateTypePreamble();
519
+
517
520
  // Combine template parts and placeholders
518
521
  const templateString = this.buildTemplateString();
519
522
 
523
+ // Add preamble to context statements (so they're skipped during extraction)
524
+ const contextWithPreamble = preamble.length > 0
525
+ ? [...this.contextStatements, ...preamble]
526
+ : this.contextStatements;
527
+
520
528
  // Use cache to get or parse the compilation unit
521
529
  const cu = await templateCache.getOrParse(
522
530
  templateString,
523
531
  this.captures,
524
- this.contextStatements,
532
+ contextWithPreamble,
525
533
  this.dependencies
526
534
  );
527
535
 
528
536
  // Extract the relevant part of the AST
537
+ // The pattern code is always the last statement (after context + preamble)
529
538
  return this.extractPatternFromAst(cu);
530
539
  }
531
540
 
541
+ /**
542
+ * Generates type preamble declarations for captures with type annotations.
543
+ *
544
+ * @returns Array of preamble statements
545
+ */
546
+ private generateTypePreamble(): string[] {
547
+ const preamble: string[] = [];
548
+ for (const capture of this.captures) {
549
+ const captureName = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName();
550
+ const captureType = (capture as any)[CAPTURE_TYPE_SYMBOL];
551
+ if (captureType) {
552
+ // Convert Type to string if needed
553
+ const typeString = typeof captureType === 'string'
554
+ ? captureType
555
+ : this.typeToString(captureType);
556
+ const placeholder = PlaceholderUtils.createCapture(captureName, undefined);
557
+ preamble.push(`const ${placeholder}: ${typeString};`);
558
+ }
559
+ }
560
+ return preamble;
561
+ }
562
+
532
563
  /**
533
564
  * Builds a template string with placeholders for captures.
534
565
  * If the template looks like a block pattern, wraps it in a function.
@@ -561,46 +592,87 @@ class TemplateProcessor {
561
592
  return result;
562
593
  }
563
594
 
595
+ /**
596
+ * Converts a Type instance to a TypeScript type string.
597
+ *
598
+ * @param type The Type instance
599
+ * @returns A TypeScript type string
600
+ */
601
+ private typeToString(type: Type): string {
602
+ // Handle Type.Class and Type.ShallowClass - return their fully qualified names
603
+ if (type.kind === Type.Kind.Class || type.kind === Type.Kind.ShallowClass) {
604
+ const classType = type as Type.Class;
605
+ return classType.fullyQualifiedName;
606
+ }
607
+
608
+ // Handle Type.Primitive - map to TypeScript primitive types
609
+ if (type.kind === Type.Kind.Primitive) {
610
+ const primitiveType = type as Type.Primitive;
611
+ switch (primitiveType.keyword) {
612
+ case 'String':
613
+ return 'string';
614
+ case 'boolean':
615
+ return 'boolean';
616
+ case 'double':
617
+ case 'float':
618
+ case 'int':
619
+ case 'long':
620
+ case 'short':
621
+ case 'byte':
622
+ return 'number';
623
+ case 'void':
624
+ return 'void';
625
+ default:
626
+ return 'any';
627
+ }
628
+ }
629
+
630
+ // Handle Type.Array - render component type plus []
631
+ if (type.kind === Type.Kind.Array) {
632
+ const arrayType = type as Type.Array;
633
+ const componentTypeString = this.typeToString(arrayType.elemType);
634
+ return `${componentTypeString}[]`;
635
+ }
636
+
637
+ // For other types, return 'any' as a fallback
638
+ // TODO: Implement proper Type to string conversion for other Type.Kind values
639
+ return 'any';
640
+ }
641
+
564
642
  /**
565
643
  * Extracts the pattern from the parsed AST.
644
+ * The pattern code is always the last statement in the compilation unit
645
+ * (after all context statements and type preamble declarations).
566
646
  *
567
647
  * @param cu The compilation unit
568
648
  * @returns The extracted pattern
569
649
  */
570
650
  private extractPatternFromAst(cu: JS.CompilationUnit): J {
571
- // Skip context statements to get to the actual pattern code
572
- const patternStatementIndex = this.contextStatements.length;
573
-
574
- // Check if we have any statements at the pattern index
575
- if (!cu.statements || patternStatementIndex >= cu.statements.length) {
576
- // If there's no statement at the index, but we have exactly one statement
577
- // and it's a block, it might be the pattern itself (e.g., pattern`{ ... }`)
578
- if (cu.statements && cu.statements.length === 1 && cu.statements[0].element.kind === J.Kind.Block) {
579
- return this.attachCaptureMarkers(cu.statements[0].element);
580
- }
581
- throw new Error(`No statement found at index ${patternStatementIndex} in compilation unit with ${cu.statements?.length || 0} statements`);
651
+ // Check if we have any statements
652
+ if (!cu.statements || cu.statements.length === 0) {
653
+ throw new Error(`No statements found in compilation unit`);
582
654
  }
583
655
 
584
- // Extract the relevant part of the AST based on the template content
585
- const firstStatement = cu.statements[patternStatementIndex].element;
656
+ // The pattern code is always the last statement
657
+ const lastStatement = cu.statements[cu.statements.length - 1].element;
586
658
 
587
659
  let extracted: J;
588
660
 
589
661
  // Check if this is our wrapper function for block patterns
590
- if (firstStatement.kind === J.Kind.MethodDeclaration) {
591
- const method = firstStatement as J.MethodDeclaration;
662
+ if (lastStatement.kind === J.Kind.MethodDeclaration) {
663
+ const method = lastStatement as J.MethodDeclaration;
592
664
  if (method.name?.simpleName === '__PATTERN__' && method.body) {
593
665
  // Extract the block from the wrapper function
594
666
  extracted = method.body;
595
667
  } else {
596
- extracted = firstStatement;
668
+ extracted = lastStatement;
597
669
  }
598
- } else if (firstStatement.kind === JS.Kind.ExpressionStatement) {
599
- // If the first statement is an expression statement, extract the expression
600
- extracted = (firstStatement as JS.ExpressionStatement).expression;
670
+ } else if (lastStatement.kind === JS.Kind.ExpressionStatement) {
671
+ // If the statement is an expression statement, extract the expression
672
+ extracted = (lastStatement as JS.ExpressionStatement).expression;
601
673
  } else {
602
674
  // Otherwise, return the statement itself
603
- extracted = firstStatement;
675
+ extracted = lastStatement;
604
676
  }
605
677
 
606
678
  // Attach CaptureMarkers to capture identifiers
@@ -14,7 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import {Cursor, Tree} from '../..';
17
- import {J} from '../../java';
17
+ import {J, Type} from '../../java';
18
18
 
19
19
  /**
20
20
  * Options for variadic captures that match zero or more nodes in a sequence.
@@ -43,6 +43,16 @@ export interface CaptureOptions<T = any> {
43
43
  name?: string;
44
44
  variadic?: boolean | VariadicOptions;
45
45
  constraint?: (node: T) => boolean;
46
+ /**
47
+ * Type annotation for this capture. When provided, the template engine will generate
48
+ * a preamble declaring the capture identifier with this type annotation, allowing
49
+ * the TypeScript parser/compiler to produce a properly type-attributed AST.
50
+ *
51
+ * Can be specified as:
52
+ * - A string type annotation (e.g., "boolean", "string", "number")
53
+ * - A Type instance from the AST (the type will be inferred from the Type)
54
+ */
55
+ type?: string | Type;
46
56
  }
47
57
 
48
58
  /**
@@ -175,7 +175,7 @@ export class RecipeSpec {
175
175
  (spec.after as (actual: string) => string)(actualAfter) : spec.after as string;
176
176
  expect(actualAfter).toEqual(afterSource);
177
177
  if (spec.afterRecipe) {
178
- await spec.afterRecipe(actualAfter);
178
+ await spec.afterRecipe(after);
179
179
  }
180
180
  }
181
181