@openrewrite/rewrite 8.75.2 → 8.75.4

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 (48) hide show
  1. package/dist/javascript/format/format.d.ts +2 -0
  2. package/dist/javascript/format/format.d.ts.map +1 -1
  3. package/dist/javascript/format/format.js +97 -69
  4. package/dist/javascript/format/format.js.map +1 -1
  5. package/dist/javascript/format/minimum-viable-spacing-visitor.d.ts.map +1 -1
  6. package/dist/javascript/format/minimum-viable-spacing-visitor.js +1 -3
  7. package/dist/javascript/format/minimum-viable-spacing-visitor.js.map +1 -1
  8. package/dist/javascript/format/whitespace-reconciler.d.ts +6 -0
  9. package/dist/javascript/format/whitespace-reconciler.d.ts.map +1 -1
  10. package/dist/javascript/format/whitespace-reconciler.js +31 -1
  11. package/dist/javascript/format/whitespace-reconciler.js.map +1 -1
  12. package/dist/javascript/recipes/index.d.ts +1 -0
  13. package/dist/javascript/recipes/index.d.ts.map +1 -1
  14. package/dist/javascript/recipes/index.js +1 -0
  15. package/dist/javascript/recipes/index.js.map +1 -1
  16. package/dist/javascript/recipes/order-imports.d.ts.map +1 -1
  17. package/dist/javascript/recipes/order-imports.js +5 -0
  18. package/dist/javascript/recipes/order-imports.js.map +1 -1
  19. package/dist/javascript/recipes/remove-dependency.d.ts +29 -0
  20. package/dist/javascript/recipes/remove-dependency.d.ts.map +1 -0
  21. package/dist/javascript/recipes/remove-dependency.js +261 -0
  22. package/dist/javascript/recipes/remove-dependency.js.map +1 -0
  23. package/dist/javascript/remove-import.d.ts.map +1 -1
  24. package/dist/javascript/remove-import.js +9 -8
  25. package/dist/javascript/remove-import.js.map +1 -1
  26. package/dist/rewrite-javascript-version.txt +1 -1
  27. package/dist/rpc/request/prepare-recipe.d.ts +1 -0
  28. package/dist/rpc/request/prepare-recipe.d.ts.map +1 -1
  29. package/dist/rpc/request/prepare-recipe.js +18 -9
  30. package/dist/rpc/request/prepare-recipe.js.map +1 -1
  31. package/dist/test/rewrite-test.d.ts +1 -0
  32. package/dist/test/rewrite-test.d.ts.map +1 -1
  33. package/dist/test/rewrite-test.js +18 -7
  34. package/dist/test/rewrite-test.js.map +1 -1
  35. package/dist/visitor.d.ts.map +1 -1
  36. package/dist/visitor.js +7 -2
  37. package/dist/visitor.js.map +1 -1
  38. package/package.json +12 -11
  39. package/src/javascript/format/format.ts +99 -71
  40. package/src/javascript/format/minimum-viable-spacing-visitor.ts +1 -4
  41. package/src/javascript/format/whitespace-reconciler.ts +27 -2
  42. package/src/javascript/recipes/index.ts +1 -0
  43. package/src/javascript/recipes/order-imports.ts +6 -0
  44. package/src/javascript/recipes/remove-dependency.ts +345 -0
  45. package/src/javascript/remove-import.ts +10 -9
  46. package/src/rpc/request/prepare-recipe.ts +20 -9
  47. package/src/test/rewrite-test.ts +17 -3
  48. package/src/visitor.ts +7 -2
@@ -322,14 +322,14 @@ export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
322
322
 
323
323
  protected async visitImportDeclaration(jsImport: JS.Import, p: P): Promise<J | undefined> {
324
324
  const ret = await super.visitImportDeclaration(jsImport, p) as JS.Import;
325
+ // Mutative detects same-value assignments and returns the original reference
326
+ // when nothing actually changed, so no guard function is needed.
325
327
  return produce(ret, draft => {
326
328
  if (draft.importClause) {
327
- // Space after 'import' keyword:
328
- // - If there's a default import (name), space goes in importClause.prefix
329
- // - If typeOnly (import type ...), space goes in importClause.prefix (before 'type')
330
- // - If only namedBindings (no default, no type), space goes in namedBindings.prefix (importClause.prefix is empty)
331
- const hasDefaultImport = !!draft.importClause.name;
332
- draft.importClause.prefix.whitespace = (hasDefaultImport || draft.importClause.typeOnly) ? " " : "";
329
+ // Space after 'import' keyword always goes on importClause.prefix.
330
+ // The parser is consistent here: both ImportClause and NamedBindings share
331
+ // the same trivia span, but ImportClause consumes it first.
332
+ draft.importClause.prefix.whitespace = " ";
333
333
  if (draft.importClause.name) {
334
334
  // For import equals declarations (import X = Y), use assignment spacing
335
335
  // For regular imports (import X from 'Y'), no space after name
@@ -338,22 +338,22 @@ export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
338
338
  : "";
339
339
  }
340
340
  if (draft.importClause.namedBindings) {
341
- // Space before namedBindings - always needed
342
- draft.importClause.namedBindings.prefix.whitespace = " ";
341
+ const hasDefaultImport = !!draft.importClause.name;
342
+ if (hasDefaultImport || draft.importClause.typeOnly) {
343
+ draft.importClause.namedBindings.prefix.whitespace = " ";
344
+ }
343
345
  if (draft.importClause.namedBindings.kind == JS.Kind.NamedImports) {
344
346
  const ni = draft.importClause.namedBindings as Draft<JS.NamedImports>;
345
- // Check if this is a multi-line import (any element's prefix has a newline)
346
347
  const isMultiLine = ni.elements.elements.some(e => e.element.prefix.whitespace.includes("\n"));
347
348
  if (!isMultiLine) {
348
- // Single-line: adjust brace spacing
349
- ni.elements.elements[0].element.prefix.whitespace = this.style.within.es6ImportExportBraces ? " " : "";
350
- ni.elements.elements[ni.elements.elements.length - 1].after.whitespace = this.style.within.es6ImportExportBraces ? " " : "";
349
+ const braceSpace = this.style.within.es6ImportExportBraces ? " " : "";
350
+ ni.elements.elements[0].element.prefix.whitespace = braceSpace;
351
+ ni.elements.elements[ni.elements.elements.length - 1].after.whitespace = braceSpace;
351
352
  } else {
352
353
  // Multi-line: apply beforeComma rule to last element's after (for trailing commas)
353
- // If it has only spaces (no newline), it's the space before a trailing comma
354
- const lastAfter = ni.elements.elements[ni.elements.elements.length - 1].after.whitespace;
355
- if (!lastAfter.includes("\n") && lastAfter.trim() === "") {
356
- ni.elements.elements[ni.elements.elements.length - 1].after.whitespace = this.style.other.beforeComma ? " " : "";
354
+ const lastAfter = ni.elements.elements[ni.elements.elements.length - 1].after;
355
+ if (!lastAfter.whitespace.includes("\n") && lastAfter.whitespace.trim() === "") {
356
+ lastAfter.whitespace = this.style.other.beforeComma ? " " : "";
357
357
  }
358
358
  }
359
359
  }
@@ -751,6 +751,9 @@ export class WrappingAndBracesVisitor<P> extends JavaScriptVisitor<P> {
751
751
 
752
752
  protected async visitVariableDeclarations(multiVariable: J.VariableDeclarations, p: P): Promise<J.VariableDeclarations> {
753
753
  const v = await super.visitVariableDeclarations(multiVariable, p) as J.VariableDeclarations;
754
+ if (v.leadingAnnotations.length === 0) {
755
+ return v;
756
+ }
754
757
  const parent = this.cursor.parentTree()?.value;
755
758
  if (parent?.kind === J.Kind.Block) {
756
759
  return produce(v, draft => {
@@ -769,6 +772,9 @@ export class WrappingAndBracesVisitor<P> extends JavaScriptVisitor<P> {
769
772
 
770
773
  protected async visitMethodDeclaration(method: J.MethodDeclaration, p: P): Promise<J.MethodDeclaration> {
771
774
  const m = await super.visitMethodDeclaration(method, p) as J.MethodDeclaration;
775
+ if (m.leadingAnnotations.length === 0) {
776
+ return m;
777
+ }
772
778
  return produce(m, draft => {
773
779
  draft.leadingAnnotations = this.withNewlines(draft.leadingAnnotations);
774
780
  if (draft.leadingAnnotations.length > 0) {
@@ -789,21 +795,29 @@ export class WrappingAndBracesVisitor<P> extends JavaScriptVisitor<P> {
789
795
  const e = await super.visitElse(elsePart, p) as J.If.Else;
790
796
  const hasBody = e.body.element.kind === J.Kind.Block || e.body.element.kind === J.Kind.If;
791
797
 
792
- return produce(e, draft => {
793
- if (hasBody) {
794
- const shouldHaveNewline = this.style.ifStatement.elseOnNewLine;
795
- const hasNewline = draft.prefix.whitespace.includes("\n");
798
+ if (!hasBody) {
799
+ return e;
800
+ }
801
+
802
+ const shouldHaveNewline = this.style.ifStatement.elseOnNewLine;
803
+ const hasNewline = e.prefix.whitespace.includes("\n");
804
+ if ((shouldHaveNewline && !hasNewline) || (!shouldHaveNewline && hasNewline)) {
805
+ return produce(e, draft => {
796
806
  if (shouldHaveNewline && !hasNewline) {
797
807
  draft.prefix.whitespace = "\n" + draft.prefix.whitespace;
798
- } else if (!shouldHaveNewline && hasNewline) {
808
+ } else {
799
809
  draft.prefix.whitespace = "";
800
810
  }
801
- }
802
- });
811
+ });
812
+ }
813
+ return e;
803
814
  }
804
815
 
805
816
  protected async visitClassDeclaration(classDecl: J.ClassDeclaration, p: P): Promise<J.ClassDeclaration> {
806
817
  const j = await super.visitClassDeclaration(classDecl, p) as J.ClassDeclaration;
818
+ if (j.leadingAnnotations.length === 0) {
819
+ return j;
820
+ }
807
821
  return produce(j, draft => {
808
822
  draft.leadingAnnotations = this.withNewlines(draft.leadingAnnotations);
809
823
  if (draft.leadingAnnotations.length > 0) {
@@ -821,61 +835,75 @@ export class WrappingAndBracesVisitor<P> extends JavaScriptVisitor<P> {
821
835
 
822
836
  protected async visitBlock(block: J.Block, p: P): Promise<J.Block> {
823
837
  const b = await super.visitBlock(block, p) as J.Block;
824
- return produce(b, draft => {
825
- const parentKind = this.cursor.parent?.value.kind;
826
-
827
- // Check if this is a "simple" block (empty or contains only a single J.Empty)
828
- const isSimpleBlock = draft.statements.length === 0 ||
829
- (draft.statements.length === 1 && draft.statements[0].element.kind === J.Kind.Empty);
838
+ const parentKind = this.cursor.parent?.value.kind;
830
839
 
831
- // Helper to format block on one line
832
- const formatOnOneLine = () => {
833
- // Format as {} - remove any newlines from end whitespace
834
- if (draft.end.whitespace.includes("\n")) {
835
- draft.end.whitespace = draft.end.whitespace.replace(/\n\s*/g, "");
836
- }
837
- // Also remove newlines from statement padding if there's a J.Empty
838
- if (draft.statements.length === 1) {
839
- if (draft.statements[0].element.prefix.whitespace.includes("\n")) {
840
- draft.statements[0].element.prefix.whitespace = "";
841
- }
842
- if (draft.statements[0].after.whitespace.includes("\n")) {
843
- draft.statements[0].after.whitespace = "";
844
- }
845
- }
846
- };
840
+ // Check if this is a "simple" block (empty or contains only a single J.Empty)
841
+ const isSimpleBlock = b.statements.length === 0 ||
842
+ (b.statements.length === 1 && b.statements[0].element.kind === J.Kind.Empty);
847
843
 
848
- // Object literals and type literals: always format empty ones as {} on single line
849
- if (parentKind === J.Kind.NewClass || parentKind === JS.Kind.TypeLiteral) {
850
- if (isSimpleBlock) {
851
- formatOnOneLine();
852
- }
853
- return;
844
+ // Object literals and type literals: always format empty ones as {} on single line
845
+ if (parentKind === J.Kind.NewClass || parentKind === JS.Kind.TypeLiteral) {
846
+ if (isSimpleBlock && this.blockIsMultiLine(b)) {
847
+ return produce(b, draft => {
848
+ this.formatBlockOnOneLine(draft);
849
+ });
854
850
  }
851
+ return b;
852
+ }
855
853
 
856
- if (isSimpleBlock) {
857
- // Determine which style option applies based on parent
858
- const isMethodOrFunctionBody = parentKind === J.Kind.Lambda ||
859
- parentKind === J.Kind.MethodDeclaration;
860
- const keepInOneLine = isMethodOrFunctionBody
861
- ? this.style.keepWhenReformatting.simpleMethodsInOneLine
862
- : this.style.keepWhenReformatting.simpleBlocksInOneLine;
863
-
864
- if (keepInOneLine) {
865
- formatOnOneLine();
866
- } else {
867
- // Format with newline between { and }
868
- if (!draft.end.whitespace.includes("\n")) {
869
- draft.end = this.withNewlineSpace(draft.end);
870
- }
854
+ if (isSimpleBlock) {
855
+ const isMethodOrFunctionBody = parentKind === J.Kind.Lambda ||
856
+ parentKind === J.Kind.MethodDeclaration;
857
+ const keepInOneLine = isMethodOrFunctionBody
858
+ ? this.style.keepWhenReformatting.simpleMethodsInOneLine
859
+ : this.style.keepWhenReformatting.simpleBlocksInOneLine;
860
+
861
+ if (keepInOneLine) {
862
+ if (this.blockIsMultiLine(b)) {
863
+ return produce(b, draft => {
864
+ this.formatBlockOnOneLine(draft);
865
+ });
871
866
  }
872
867
  } else {
873
- // Non-simple blocks: ensure closing brace is on its own line
874
- if (!draft.end.whitespace.includes("\n") && !draft.statements[draft.statements.length - 1].after.whitespace.includes("\n")) {
875
- draft.end = this.withNewlineSpace(draft.end);
868
+ if (!b.end.whitespace.includes("\n")) {
869
+ return produce(b, draft => {
870
+ draft.end = this.withNewlineSpace(draft.end);
871
+ });
876
872
  }
877
873
  }
878
- });
874
+ } else {
875
+ // Non-simple blocks: ensure closing brace is on its own line
876
+ if (!b.end.whitespace.includes("\n") && !b.statements[b.statements.length - 1].after.whitespace.includes("\n")) {
877
+ return produce(b, draft => {
878
+ draft.end = this.withNewlineSpace(draft.end);
879
+ });
880
+ }
881
+ }
882
+
883
+ return b;
884
+ }
885
+
886
+ private blockIsMultiLine(b: J.Block): boolean {
887
+ if (b.end.whitespace.includes("\n")) return true;
888
+ if (b.statements.length === 1) {
889
+ if (b.statements[0].element.prefix.whitespace.includes("\n")) return true;
890
+ if (b.statements[0].after.whitespace.includes("\n")) return true;
891
+ }
892
+ return false;
893
+ }
894
+
895
+ private formatBlockOnOneLine(draft: Draft<J.Block>): void {
896
+ if (draft.end.whitespace.includes("\n")) {
897
+ draft.end.whitespace = draft.end.whitespace.replace(/\n\s*/g, "");
898
+ }
899
+ if (draft.statements.length === 1) {
900
+ if (draft.statements[0].element.prefix.whitespace.includes("\n")) {
901
+ draft.statements[0].element.prefix.whitespace = "";
902
+ }
903
+ if (draft.statements[0].after.whitespace.includes("\n")) {
904
+ draft.statements[0].after.whitespace = "";
905
+ }
906
+ }
879
907
  }
880
908
 
881
909
  protected async visitSwitch(aSwitch: J.Switch, p: P): Promise<J | undefined> {
@@ -1100,7 +1128,7 @@ export class BlankLinesVisitor<P> extends JavaScriptVisitor<P> {
1100
1128
  }
1101
1129
  }
1102
1130
 
1103
- private ensurePrefixHasNewLine<T extends J>(node: Draft<J>) {
1131
+ private ensurePrefixHasNewLine(node: Draft<J>) {
1104
1132
  if (!node.prefix) return;
1105
1133
 
1106
1134
  // Check if newline already exists in the effective last whitespace
@@ -84,6 +84,7 @@ export class MinimumViableSpacingVisitor<P> extends JavaScriptVisitor<P> {
84
84
 
85
85
  // Note: typeParameters should NOT have space before them - they immediately follow the class name
86
86
  // e.g., "class DataTable<Row>" not "class DataTable <Row>"
87
+ // Note: body.prefix spacing (space before '{') is handled by SpacesVisitor, not here.
87
88
 
88
89
  if (c.extends && c.extends.before.whitespace === "") {
89
90
  c = produce(c, draft => {
@@ -100,10 +101,6 @@ export class MinimumViableSpacingVisitor<P> extends JavaScriptVisitor<P> {
100
101
  });
101
102
  }
102
103
 
103
- c = produce(c, draft => {
104
- draft.body.prefix.whitespace = "";
105
- });
106
-
107
104
  return c;
108
105
  }
109
106
 
@@ -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 {isIdentifier, isLiteral, isSpace, J, Type} from '../../java';
16
+ import {isIdentifier, isLiteral, isSpace, J, TextComment, Type} from '../../java';
17
17
  import {JS} from "../tree";
18
18
 
19
19
  /**
@@ -163,7 +163,9 @@ export class WhitespaceReconciler {
163
163
 
164
164
  // Space nodes - copy when reconciling, don't recurse
165
165
  if (isSpace(original)) {
166
- return this.shouldReconcile() ? formatted : original;
166
+ if (!this.shouldReconcile()) return original;
167
+ if (isSpace(formatted) && this.spacesEqual(original, formatted as J.Space)) return original;
168
+ return formatted;
167
169
  }
168
170
 
169
171
  // Track entering target subtree (using referential equality)
@@ -247,6 +249,10 @@ export class WhitespaceReconciler {
247
249
  // Space values and markers: copy from formatted when reconciling
248
250
  if ((isSpace(originalValue)) || key === 'markers') {
249
251
  if (this.shouldReconcile() && formattedValue !== originalValue) {
252
+ // For spaces, check structural equality to avoid unnecessary new objects
253
+ if (isSpace(originalValue) && isSpace(formattedValue) && this.spacesEqual(originalValue, formattedValue as J.Space)) {
254
+ continue;
255
+ }
250
256
  result = { ...result, [key]: formattedValue } as VisitableNode;
251
257
  }
252
258
  continue;
@@ -297,6 +303,25 @@ export class WhitespaceReconciler {
297
303
  return this.reconcileState === 'reconciling';
298
304
  }
299
305
 
306
+ /**
307
+ * Structurally compare two Space objects for equality.
308
+ * Currently only TextComment has additional properties (text, multiline);
309
+ * if new comment types are added, extend the comparison here.
310
+ */
311
+ private spacesEqual(a: J.Space, b: J.Space): boolean {
312
+ if (a.whitespace !== b.whitespace) return false;
313
+ if (a.comments.length !== b.comments.length) return false;
314
+ for (let i = 0; i < a.comments.length; i++) {
315
+ const ca = a.comments[i], cb = b.comments[i];
316
+ if (ca.kind !== cb.kind || ca.suffix !== cb.suffix) return false;
317
+ if (ca.kind === J.Kind.TextComment) {
318
+ const ta = ca as TextComment, tb = cb as TextComment;
319
+ if (ta.text !== tb.text || ta.multiline !== tb.multiline) return false;
320
+ }
321
+ }
322
+ return true;
323
+ }
324
+
300
325
  /**
301
326
  * Checks if two nodes with different kinds are semantically equivalent.
302
327
  * This handles cases like Prettier's quoteProps option which can change
@@ -17,6 +17,7 @@
17
17
  export * from "./add-dependency";
18
18
  export * from "./async-callback-in-sync-array-method";
19
19
  export * from "./auto-format";
20
+ export * from "./remove-dependency";
20
21
  export * from "./upgrade-dependency-version";
21
22
  export * from "./upgrade-transitive-dependency-version";
22
23
  export * from "./order-imports";
@@ -88,6 +88,12 @@ export class OrderImports extends Recipe {
88
88
  return originalImportPosition[aPadded.element.id] - originalImportPosition[bPadded.element.id];
89
89
  });
90
90
 
91
+ // Check if anything actually changed
92
+ const alreadySorted = sortedSpecifiers.every((s, i) => s === imports[i]);
93
+ if (alreadySorted) {
94
+ return cu;
95
+ }
96
+
91
97
  const cuWithImportsSorted = await produceAsync(cu, async draft => {
92
98
  draft.statements = [...sortedSpecifiers, ...restStatements];
93
99
  });