@openrewrite/rewrite 8.66.0-SNAPSHOT → 8.67.0-20251103-160333

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.
@@ -2637,11 +2637,10 @@ export class JavaScriptParserVisitor {
2637
2637
  }
2638
2638
 
2639
2639
  visitVariableStatement(node: ts.VariableStatement): JS.ScopedVariableDeclarations | J.VariableDeclarations {
2640
+ const prefix = this.prefix(node);
2640
2641
  return produce(this.visitVariableDeclarationList(node.declarationList), draft => {
2641
- if (node.modifiers) {
2642
- draft.modifiers = this.mapModifiers(node).concat(draft.modifiers);
2643
- }
2644
- draft.prefix = this.prefix(node);
2642
+ draft.prefix = prefix;
2643
+ draft.modifiers = this.mapModifiers(node).concat(draft.modifiers);
2645
2644
  });
2646
2645
  }
2647
2646
 
@@ -3064,7 +3063,7 @@ export class JavaScriptParserVisitor {
3064
3063
  modifiers.push({
3065
3064
  kind: J.Kind.Modifier,
3066
3065
  id: randomId(),
3067
- prefix: this.prefix(kind),
3066
+ prefix: modifiers.length === 0 ? this.prefix(kind) : this.prefix(kind),
3068
3067
  markers: emptyMarkers,
3069
3068
  annotations: [],
3070
3069
  keyword: kind.kind === ts.SyntaxKind.VarKeyword ? 'var' :
@@ -75,16 +75,16 @@ export class JavaScriptPrinter extends JavaScriptVisitor<PrintOutputCapture> {
75
75
  }
76
76
 
77
77
  override async visitExpressionStatement(statement: JS.ExpressionStatement, p: PrintOutputCapture): Promise<J | undefined> {
78
- await this.visitSpace(statement.prefix, p);
79
- await this.visitMarkers(statement.markers, p);
78
+ await this.beforeSyntax(statement, p);
80
79
  await this.visit(statement.expression, p);
80
+ await this.afterSyntax(statement, p);
81
81
  return statement;
82
82
  }
83
83
 
84
84
  override async visitStatementExpression(statementExpression: JS.StatementExpression, p: PrintOutputCapture): Promise<J | undefined> {
85
- await this.visitSpace(statementExpression.prefix, p);
86
- await this.visitMarkers(statementExpression.markers, p);
85
+ await this.beforeSyntax(statementExpression, p);
87
86
  await this.visit(statementExpression.statement, p);
87
+ await this.afterSyntax(statementExpression, p);
88
88
  return statementExpression;
89
89
  }
90
90
 
@@ -1767,7 +1767,7 @@ export class JavaScriptPrinter extends JavaScriptVisitor<PrintOutputCapture> {
1767
1767
  return cursor;
1768
1768
  }
1769
1769
 
1770
- private async afterSyntax(j: J, p: PrintOutputCapture) {
1770
+ protected async afterSyntax(j: J, p: PrintOutputCapture) {
1771
1771
  await this.afterSyntaxMarkers(j.markers, p);
1772
1772
  }
1773
1773
 
@@ -1777,7 +1777,7 @@ export class JavaScriptPrinter extends JavaScriptVisitor<PrintOutputCapture> {
1777
1777
  }
1778
1778
  }
1779
1779
 
1780
- private async beforeSyntax(j: J, p: PrintOutputCapture) {
1780
+ protected async beforeSyntax(j: J, p: PrintOutputCapture) {
1781
1781
  await this.beforeSyntaxExt(j.prefix, j.markers, p);
1782
1782
  }
1783
1783
 
@@ -17,11 +17,11 @@ import {JS} from '.';
17
17
  import {JavaScriptParser} from './parser';
18
18
  import {JavaScriptVisitor} from './visitor';
19
19
  import {Cursor, isTree, Tree} from '..';
20
- import {J, Type} from '../java';
20
+ import {emptySpace, J} from '../java';
21
21
  import {produce} from "immer";
22
- import {JavaScriptComparatorVisitor} from "./comparator";
22
+ import {JavaScriptSemanticComparatorVisitor} from "./comparator";
23
23
  import {DependencyWorkspace} from './dependency-workspace';
24
- import {Marker} from '../markers';
24
+ import {emptyMarkers, Marker} from '../markers';
25
25
  import {randomId} from '../uuid';
26
26
 
27
27
  /**
@@ -118,6 +118,108 @@ class CaptureMarker implements Marker {
118
118
  }
119
119
  }
120
120
 
121
+ /**
122
+ * A comparator for pattern matching that is lenient about optional properties.
123
+ * Allows patterns without type annotations to match actual code with type annotations.
124
+ * Uses semantic comparison to match semantically equivalent code (e.g., isDate() and util.isDate()).
125
+ */
126
+ class PatternMatchingComparator extends JavaScriptSemanticComparatorVisitor {
127
+ constructor(private readonly matcher: { handleCapture: (pattern: J, target: J) => boolean }) {
128
+ super();
129
+ }
130
+
131
+ /**
132
+ * Creates a wildcard identifier that will match any AST node during comparison.
133
+ * The identifier has a CaptureMarker which causes it to match anything without storing the result.
134
+ *
135
+ * @param captureName The name for the capture marker (for debugging purposes)
136
+ * @returns A wildcard identifier
137
+ */
138
+ private createWildcardIdentifier(captureName: string): J.Identifier {
139
+ return {
140
+ id: randomId(),
141
+ kind: J.Kind.Identifier,
142
+ prefix: emptySpace,
143
+ markers: {
144
+ ...emptyMarkers,
145
+ markers: [new CaptureMarker(captureName)]
146
+ },
147
+ annotations: [],
148
+ simpleName: '__wildcard__',
149
+ type: undefined,
150
+ fieldType: undefined
151
+ };
152
+ }
153
+
154
+ override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
155
+ // Check if the pattern node is a capture - this handles captures anywhere in the tree
156
+ if (PlaceholderUtils.isCapture(j as J)) {
157
+ const success = this.matcher.handleCapture(j as J, p);
158
+ if (!success) {
159
+ return this.abort(j) as R;
160
+ }
161
+ return j as R;
162
+ }
163
+
164
+ if (!this.match) {
165
+ return j as R;
166
+ }
167
+
168
+ return super.visit(j, p, parent);
169
+ }
170
+
171
+ override async visitVariableDeclarations(variableDeclarations: J.VariableDeclarations, other: J): Promise<J | undefined> {
172
+ if (!this.match || other.kind !== J.Kind.VariableDeclarations) {
173
+ return this.abort(variableDeclarations);
174
+ }
175
+
176
+ const otherVariableDeclarations = other as J.VariableDeclarations;
177
+
178
+ // LENIENT: If pattern lacks typeExpression but target has one, add a wildcard capture to pattern
179
+ // This allows the pattern to match without requiring us to modify the target (which would corrupt captures)
180
+ if (!variableDeclarations.typeExpression && otherVariableDeclarations.typeExpression) {
181
+ variableDeclarations = produce(variableDeclarations, draft => {
182
+ draft.typeExpression = this.createWildcardIdentifier('__wildcard_type__') as J.Identifier;
183
+ });
184
+ }
185
+
186
+ // Delegate to super implementation
187
+ return super.visitVariableDeclarations(variableDeclarations, otherVariableDeclarations);
188
+ }
189
+
190
+ override async visitMethodDeclaration(methodDeclaration: J.MethodDeclaration, other: J): Promise<J | undefined> {
191
+ if (!this.match || other.kind !== J.Kind.MethodDeclaration) {
192
+ return this.abort(methodDeclaration);
193
+ }
194
+
195
+ const otherMethodDeclaration = other as J.MethodDeclaration;
196
+
197
+ // LENIENT: If pattern lacks returnTypeExpression but target has one, add a wildcard capture to pattern
198
+ // This allows the pattern to match without requiring us to modify the target (which would corrupt captures)
199
+ if (!methodDeclaration.returnTypeExpression && otherMethodDeclaration.returnTypeExpression) {
200
+ methodDeclaration = produce(methodDeclaration, draft => {
201
+ draft.returnTypeExpression = this.createWildcardIdentifier('__wildcard_return_type__') as J.Identifier;
202
+ });
203
+ }
204
+
205
+ // Delegate to super implementation
206
+ return super.visitMethodDeclaration(methodDeclaration, otherMethodDeclaration);
207
+ }
208
+
209
+ protected hasSameKind(j: J, other: J): boolean {
210
+ return super.hasSameKind(j, other) ||
211
+ (j.kind == J.Kind.Identifier && PlaceholderUtils.isCapture(j as J.Identifier));
212
+ }
213
+
214
+ override async visitIdentifier(identifier: J.Identifier, other: J): Promise<J | undefined> {
215
+ if (PlaceholderUtils.isCapture(identifier)) {
216
+ const success = this.matcher.handleCapture(identifier, other);
217
+ return success ? identifier : this.abort(identifier);
218
+ }
219
+ return super.visitIdentifier(identifier, other);
220
+ }
221
+ }
222
+
121
223
  /**
122
224
  * Capture specification for pattern matching.
123
225
  * Represents a placeholder in a template pattern that can capture a part of the AST.
@@ -264,150 +366,6 @@ export class MatchResult implements Pick<Map<string, J>, "get"> {
264
366
  }
265
367
  }
266
368
 
267
- /**
268
- * A comparator visitor that checks semantic equality including type attribution.
269
- * This ensures that patterns only match code with compatible types, not just
270
- * structurally similar code.
271
- */
272
- class JavaScriptTemplateSemanticallyEqualVisitor extends JavaScriptComparatorVisitor {
273
- /**
274
- * Checks if two types are semantically equal.
275
- * For method types, this checks that the declaring type and method name match.
276
- */
277
- private isOfType(target?: Type, source?: Type): boolean {
278
- if (!target || !source) {
279
- return target === source;
280
- }
281
-
282
- if (target.kind !== source.kind) {
283
- return false;
284
- }
285
-
286
- // For method types, check declaring type
287
- // Note: We don't check the name field because it might not be fully resolved in patterns
288
- // The method invocation visitor already checks that simple names match
289
- if (target.kind === Type.Kind.Method && source.kind === Type.Kind.Method) {
290
- const targetMethod = target as Type.Method;
291
- const sourceMethod = source as Type.Method;
292
-
293
- // Only check that declaring types match
294
- return this.isOfType(targetMethod.declaringType, sourceMethod.declaringType);
295
- }
296
-
297
- // For fully qualified types, check the fully qualified name
298
- if (Type.isFullyQualified(target) && Type.isFullyQualified(source)) {
299
- return Type.FullyQualified.getFullyQualifiedName(target) ===
300
- Type.FullyQualified.getFullyQualifiedName(source);
301
- }
302
-
303
- // Default: types are equal if they're the same kind
304
- return true;
305
- }
306
-
307
- /**
308
- * Override method invocation comparison to include type attribution checking.
309
- * When types match semantically, we allow matching even if one has a receiver
310
- * and the other doesn't (e.g., `isDate(x)` vs `util.isDate(x)`).
311
- */
312
- override async visitMethodInvocation(method: J.MethodInvocation, other: J): Promise<J | undefined> {
313
- if (other.kind !== J.Kind.MethodInvocation) {
314
- return method;
315
- }
316
-
317
- const otherMethod = other as J.MethodInvocation;
318
-
319
- // Check basic structural equality first
320
- if (method.name.simpleName !== otherMethod.name.simpleName ||
321
- method.arguments.elements.length !== otherMethod.arguments.elements.length) {
322
- this.abort();
323
- return method;
324
- }
325
-
326
- // Check type attribution
327
- // Both must have method types for semantic equality
328
- if (!method.methodType || !otherMethod.methodType) {
329
- // If template has type but target doesn't, they don't match
330
- if (method.methodType || otherMethod.methodType) {
331
- this.abort();
332
- return method;
333
- }
334
- // If neither has type, fall through to structural comparison
335
- return super.visitMethodInvocation(method, other);
336
- }
337
-
338
- // Both have types - check they match semantically
339
- const typesMatch = this.isOfType(method.methodType, otherMethod.methodType);
340
- if (!typesMatch) {
341
- // Types don't match - abort comparison
342
- this.abort();
343
- return method;
344
- }
345
-
346
- // Types match! Now we can ignore receiver differences and just compare arguments.
347
- // This allows pattern `isDate(x)` to match both `isDate(x)` and `util.isDate(x)`
348
- // when they have the same type attribution.
349
-
350
- // Compare type parameters
351
- if ((method.typeParameters === undefined) !== (otherMethod.typeParameters === undefined)) {
352
- this.abort();
353
- return method;
354
- }
355
-
356
- if (method.typeParameters && otherMethod.typeParameters) {
357
- if (method.typeParameters.elements.length !== otherMethod.typeParameters.elements.length) {
358
- this.abort();
359
- return method;
360
- }
361
- for (let i = 0; i < method.typeParameters.elements.length; i++) {
362
- await this.visit(method.typeParameters.elements[i].element, otherMethod.typeParameters.elements[i].element);
363
- if (!this.match) return method;
364
- }
365
- }
366
-
367
- // Compare name (already checked simpleName above, but visit for markers/prefix)
368
- await this.visit(method.name, otherMethod.name);
369
- if (!this.match) return method;
370
-
371
- // Compare arguments
372
- for (let i = 0; i < method.arguments.elements.length; i++) {
373
- await this.visit(method.arguments.elements[i].element, otherMethod.arguments.elements[i].element);
374
- if (!this.match) return method;
375
- }
376
-
377
- return method;
378
- }
379
-
380
- /**
381
- * Override identifier comparison to include type checking for field access.
382
- */
383
- override async visitIdentifier(identifier: J.Identifier, other: J): Promise<J | undefined> {
384
- if (other.kind !== J.Kind.Identifier) {
385
- return identifier;
386
- }
387
-
388
- const otherIdentifier = other as J.Identifier;
389
-
390
- // Check name matches
391
- if (identifier.simpleName !== otherIdentifier.simpleName) {
392
- return identifier;
393
- }
394
-
395
- // For identifiers with field types, check type attribution
396
- if (identifier.fieldType && otherIdentifier.fieldType) {
397
- if (!this.isOfType(identifier.fieldType, otherIdentifier.fieldType)) {
398
- this.abort();
399
- return identifier;
400
- }
401
- } else if (identifier.fieldType || otherIdentifier.fieldType) {
402
- // If only one has a type, they don't match
403
- this.abort();
404
- return identifier;
405
- }
406
-
407
- return super.visitIdentifier(identifier, other);
408
- }
409
- }
410
-
411
369
  /**
412
370
  * Matcher for checking if a pattern matches an AST node and extracting captured nodes.
413
371
  */
@@ -473,20 +431,11 @@ class Matcher {
473
431
  return false;
474
432
  }
475
433
 
476
- const matcher = this;
477
- return await ((new class extends JavaScriptTemplateSemanticallyEqualVisitor {
478
- protected hasSameKind(j: J, other: J): boolean {
479
- return super.hasSameKind(j, other) || j.kind == J.Kind.Identifier && this.matchesParameter(j as J.Identifier, other);
480
- }
481
-
482
- override async visitIdentifier(identifier: J.Identifier, other: J): Promise<J | undefined> {
483
- return this.matchesParameter(identifier, other) ? identifier : await super.visitIdentifier(identifier, other);
484
- }
485
-
486
- private matchesParameter(identifier: J.Identifier, other: J): boolean {
487
- return PlaceholderUtils.isCapture(identifier) && matcher.handleCapture(identifier, other);
488
- }
489
- }).compare(pattern, target));
434
+ // Use the pattern matching comparator which is lenient about optional properties
435
+ const comparator = new PatternMatchingComparator({
436
+ handleCapture: (p, t) => this.handleCapture(p, t)
437
+ });
438
+ return await comparator.compare(pattern, target);
490
439
  }
491
440
 
492
441
  /**
@@ -19,7 +19,6 @@ import {noopVisitor, TreeVisitor} from "../visitor";
19
19
  import {Parser} from "../parser";
20
20
  import {TreePrinters} from "../print";
21
21
  import {SourceFile} from "../tree";
22
- import dedent from "dedent";
23
22
  import {Result, scheduleRun} from "../run";
24
23
  import {SnowflakeId} from "@akashrajpurohit/snowflake-id";
25
24
  import {mapAsync, trimIndent} from "../util";
@@ -242,6 +241,71 @@ class NoopRecipe extends Recipe {
242
241
 
243
242
  export type AfterRecipeText = string | ((actual: string) => string | undefined) | undefined | null;
244
243
 
244
+ /**
245
+ * Simple dedent implementation that removes common leading whitespace from each line.
246
+ *
247
+ * Behavior:
248
+ * - Removes ONE leading newline if present (for template string ergonomics)
249
+ * - Removes trailing newline + whitespace (for template string ergonomics)
250
+ * - Preserves additional leading/trailing empty lines beyond the first
251
+ * - For lines with content: removes common indentation
252
+ * - For lines with only whitespace: removes common indentation, preserving remaining spaces
253
+ *
254
+ * Examples:
255
+ * - `\n code` → `code` (single leading newline removed)
256
+ * - `\n\n code` → `\ncode` (first newline removed, second preserved)
257
+ * - ` code\n` → `code` (trailing newline removed)
258
+ * - ` code\n\n` → `code\n` (first trailing newline removed, second preserved)
259
+ */
260
+ function dedent(s: string): string {
261
+ if (!s) return s;
262
+
263
+ // Remove single leading newline for ergonomics
264
+ let start = s.charCodeAt(0) === 10 ? 1 : 0; // 10 = '\n'
265
+
266
+ // Remove trailing newline + any trailing whitespace
267
+ let end = s.length;
268
+ for (let i = s.length - 1; i >= start; i--) {
269
+ const ch = s.charCodeAt(i);
270
+ if (ch === 10) { // '\n'
271
+ end = i;
272
+ break;
273
+ }
274
+ if (ch !== 32 && ch !== 9) break; // not ' ' or '\t'
275
+ }
276
+
277
+ if (start >= end) return '';
278
+
279
+ const str = start > 0 || end < s.length ? s.slice(start, end) : s;
280
+ const lines = str.split('\n');
281
+
282
+ // Find minimum indentation (avoid regex for performance)
283
+ let minIndent = Infinity;
284
+ for (const line of lines) {
285
+ let indent = 0;
286
+ for (let j = 0; j < line.length; j++) {
287
+ const ch = line.charCodeAt(j);
288
+ if (ch === 32 || ch === 9) { // ' ' or '\t'
289
+ indent++;
290
+ } else {
291
+ // Found non-whitespace, update minIndent
292
+ if (indent < minIndent) minIndent = indent;
293
+ break;
294
+ }
295
+ }
296
+ }
297
+
298
+ // If all lines are empty or no indentation
299
+ if (minIndent === Infinity || minIndent === 0) {
300
+ return lines.map(line => line.trim() || '').join('\n');
301
+ }
302
+
303
+ // Remove common indentation from each line
304
+ return lines.map(line =>
305
+ line.length >= minIndent ? line.slice(minIndent) : ''
306
+ ).join('\n');
307
+ }
308
+
245
309
  export function dedentAfter(s?: AfterRecipeText): AfterRecipeText {
246
310
  if (s !== null) {
247
311
  if (s === undefined) {