@openrewrite/rewrite 8.75.2 → 8.75.3
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.
- package/dist/javascript/format/format.d.ts +2 -0
- package/dist/javascript/format/format.d.ts.map +1 -1
- package/dist/javascript/format/format.js +97 -69
- package/dist/javascript/format/format.js.map +1 -1
- package/dist/javascript/format/minimum-viable-spacing-visitor.d.ts.map +1 -1
- package/dist/javascript/format/minimum-viable-spacing-visitor.js +1 -3
- package/dist/javascript/format/minimum-viable-spacing-visitor.js.map +1 -1
- package/dist/javascript/format/whitespace-reconciler.d.ts +6 -0
- package/dist/javascript/format/whitespace-reconciler.d.ts.map +1 -1
- package/dist/javascript/format/whitespace-reconciler.js +31 -1
- package/dist/javascript/format/whitespace-reconciler.js.map +1 -1
- package/dist/javascript/recipes/order-imports.d.ts.map +1 -1
- package/dist/javascript/recipes/order-imports.js +5 -0
- package/dist/javascript/recipes/order-imports.js.map +1 -1
- package/dist/javascript/remove-import.d.ts.map +1 -1
- package/dist/javascript/remove-import.js +9 -8
- package/dist/javascript/remove-import.js.map +1 -1
- package/dist/rpc/server.js +0 -0
- package/dist/test/rewrite-test.d.ts +1 -0
- package/dist/test/rewrite-test.d.ts.map +1 -1
- package/dist/test/rewrite-test.js +13 -3
- package/dist/test/rewrite-test.js.map +1 -1
- package/dist/visitor.d.ts.map +1 -1
- package/dist/visitor.js +7 -2
- package/dist/visitor.js.map +1 -1
- package/package.json +7 -2
- package/src/javascript/format/format.ts +99 -71
- package/src/javascript/format/minimum-viable-spacing-visitor.ts +1 -4
- package/src/javascript/format/whitespace-reconciler.ts +27 -2
- package/src/javascript/recipes/order-imports.ts +6 -0
- package/src/javascript/remove-import.ts +10 -9
- package/src/rpc/server.ts +0 -0
- package/src/test/rewrite-test.ts +16 -3
- package/src/visitor.ts +7 -2
- package/dist/rewrite-javascript-version.txt +0 -1
|
@@ -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
|
-
//
|
|
329
|
-
//
|
|
330
|
-
|
|
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
|
-
|
|
342
|
-
draft.importClause.
|
|
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
|
-
|
|
349
|
-
ni.elements.elements[0].element.prefix.whitespace =
|
|
350
|
-
ni.elements.elements[ni.elements.elements.length - 1].after.whitespace =
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
if (
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
@@ -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
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {JavaScriptVisitor} from "./visitor";
|
|
2
2
|
import {J} from "../java";
|
|
3
3
|
import {JS, JSX} from "./tree";
|
|
4
|
-
import {mapAsync} from "../util";
|
|
4
|
+
import {mapAsync, updateIfChanged} from "../util";
|
|
5
5
|
import {ElementRemovalFormatter} from "../java";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -146,7 +146,7 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
|
|
|
146
146
|
return this.produceJavaScript(compilationUnit, p, async draft => {
|
|
147
147
|
const formatter = new ElementRemovalFormatter<J>(true); // Preserve file headers from first import
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
const newStatements = await mapAsync(compilationUnit.statements, async (stmt) => {
|
|
150
150
|
const statement = stmt.element;
|
|
151
151
|
|
|
152
152
|
// Handle ES6 imports
|
|
@@ -159,7 +159,7 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
const finalResult = formatter.processKept(result) as JS.Import;
|
|
162
|
-
return
|
|
162
|
+
return updateIfChanged(stmt, {element: finalResult});
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
// Handle CommonJS require statements
|
|
@@ -174,7 +174,7 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
const finalResult = formatter.processKept(result) as J.VariableDeclarations;
|
|
177
|
-
return
|
|
177
|
+
return updateIfChanged(stmt, {element: finalResult});
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
// Handle JS.ScopedVariableDeclarations (multi-variable var/let/const)
|
|
@@ -194,7 +194,7 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
|
|
|
194
194
|
varFormatter.markRemoved(varDecl);
|
|
195
195
|
} else {
|
|
196
196
|
const formattedVarDecl = varFormatter.processKept(result as J.VariableDeclarations);
|
|
197
|
-
filteredVariables.push(
|
|
197
|
+
filteredVariables.push(updateIfChanged(v, {element: formattedVarDecl}));
|
|
198
198
|
}
|
|
199
199
|
} else {
|
|
200
200
|
filteredVariables.push(v);
|
|
@@ -210,20 +210,21 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
|
|
|
210
210
|
? formatter.processKept({...scopedVarDecl, variables: filteredVariables})
|
|
211
211
|
: formatter.processKept(statement);
|
|
212
212
|
|
|
213
|
-
return
|
|
213
|
+
return updateIfChanged(stmt, {element: finalElement});
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
// For any other statement type, apply prefix from removed elements
|
|
217
217
|
if (statement) {
|
|
218
218
|
const finalStatement = formatter.processKept(statement);
|
|
219
|
-
return
|
|
219
|
+
return updateIfChanged(stmt, {element: finalStatement});
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
return stmt;
|
|
223
223
|
});
|
|
224
224
|
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
draft.statements = newStatements.some(s => s === undefined)
|
|
226
|
+
? newStatements.filter(s => s !== undefined)
|
|
227
|
+
: newStatements;
|
|
227
228
|
draft.eof = await this.visitSpace(compilationUnit.eof, p);
|
|
228
229
|
});
|
|
229
230
|
}
|
package/src/rpc/server.ts
CHANGED
|
File without changes
|
package/src/test/rewrite-test.ts
CHANGED
|
@@ -40,6 +40,7 @@ export interface SourceSpec<T extends SourceFile> {
|
|
|
40
40
|
|
|
41
41
|
export class RecipeSpec {
|
|
42
42
|
checkParsePrintIdempotence: boolean = true
|
|
43
|
+
allowEmptyDiff: boolean = false
|
|
43
44
|
|
|
44
45
|
recipe: Recipe = new NoopRecipe()
|
|
45
46
|
|
|
@@ -162,9 +163,21 @@ export class RecipeSpec {
|
|
|
162
163
|
|
|
163
164
|
if (!spec.after) {
|
|
164
165
|
if (after && after !== result?.before) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
const actual = await TreePrinters.print(after);
|
|
167
|
+
const expected = dedent(spec.before!);
|
|
168
|
+
if (actual === expected) {
|
|
169
|
+
if (!this.allowEmptyDiff) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
"An empty diff was generated. The recipe incorrectly " +
|
|
172
|
+
"changed the AST without changing the printed output."
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
throw new Error(
|
|
177
|
+
"Expected no change but recipe modified the file.\n" +
|
|
178
|
+
`Expected:\n${expected}\n\nActual:\n${actual}`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
168
181
|
}
|
|
169
182
|
if (spec.afterRecipe) {
|
|
170
183
|
await spec.afterRecipe(matchingSpec![1]);
|
package/src/visitor.ts
CHANGED
|
@@ -145,13 +145,18 @@ export abstract class TreeVisitor<T extends Tree, P> {
|
|
|
145
145
|
const newMarkers = await this.visitMarkers(before.markers, p);
|
|
146
146
|
|
|
147
147
|
if (recipe) {
|
|
148
|
-
// Remove markers before Mutative drafting to avoid cycles, then restore after
|
|
148
|
+
// Remove markers before Mutative drafting to avoid cycles, then restore after.
|
|
149
|
+
// The spread cost is paid unconditionally, but it enables the identity check below.
|
|
149
150
|
const withoutMarkers = { ...before, markers: emptyMarkers };
|
|
150
151
|
const result = await produceAsync(withoutMarkers, recipe);
|
|
151
152
|
if (result === undefined) {
|
|
152
153
|
return undefined;
|
|
153
154
|
}
|
|
154
|
-
//
|
|
155
|
+
// Mutative's produceAsync returns the same reference when no draft mutations occurred
|
|
156
|
+
// (structural sharing), so reference equality is a reliable no-change check.
|
|
157
|
+
if (result === withoutMarkers && newMarkers === before.markers) {
|
|
158
|
+
return before;
|
|
159
|
+
}
|
|
155
160
|
return { ...result, markers: newMarkers } as T;
|
|
156
161
|
}
|
|
157
162
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
8.75.2
|