@openrewrite/rewrite 8.69.0 → 8.69.1
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/java/type.d.ts +5 -0
- package/dist/java/type.d.ts.map +1 -1
- package/dist/java/type.js +12 -0
- package/dist/java/type.js.map +1 -1
- package/dist/javascript/assertions.d.ts.map +1 -1
- package/dist/javascript/assertions.js +9 -0
- package/dist/javascript/assertions.js.map +1 -1
- package/dist/javascript/comparator.d.ts.map +1 -1
- package/dist/javascript/comparator.js +5 -9
- package/dist/javascript/comparator.js.map +1 -1
- package/dist/javascript/{format.d.ts → format/format.d.ts} +15 -33
- package/dist/javascript/format/format.d.ts.map +1 -0
- package/dist/javascript/{format.js → format/format.js} +56 -313
- package/dist/javascript/format/format.js.map +1 -0
- package/dist/javascript/format/index.d.ts +3 -0
- package/dist/javascript/format/index.d.ts.map +1 -0
- package/dist/javascript/format/index.js +38 -0
- package/dist/javascript/format/index.js.map +1 -0
- package/dist/javascript/format/minimum-viable-spacing-visitor.d.ts +28 -0
- package/dist/javascript/format/minimum-viable-spacing-visitor.d.ts.map +1 -0
- package/dist/javascript/format/minimum-viable-spacing-visitor.js +308 -0
- package/dist/javascript/format/minimum-viable-spacing-visitor.js.map +1 -0
- package/dist/javascript/format/normalize-whitespace-visitor.d.ts +14 -0
- package/dist/javascript/format/normalize-whitespace-visitor.d.ts.map +1 -0
- package/dist/javascript/format/normalize-whitespace-visitor.js +65 -0
- package/dist/javascript/format/normalize-whitespace-visitor.js.map +1 -0
- package/dist/javascript/format/prettier-config-loader.d.ts +92 -0
- package/dist/javascript/format/prettier-config-loader.d.ts.map +1 -0
- package/dist/javascript/format/prettier-config-loader.js +419 -0
- package/dist/javascript/format/prettier-config-loader.js.map +1 -0
- package/dist/javascript/format/prettier-format.d.ts +111 -0
- package/dist/javascript/format/prettier-format.d.ts.map +1 -0
- package/dist/javascript/format/prettier-format.js +496 -0
- package/dist/javascript/format/prettier-format.js.map +1 -0
- package/dist/javascript/{tabs-and-indents-visitor.d.ts → format/tabs-and-indents-visitor.d.ts} +4 -4
- package/dist/javascript/format/tabs-and-indents-visitor.d.ts.map +1 -0
- package/dist/javascript/{tabs-and-indents-visitor.js → format/tabs-and-indents-visitor.js} +7 -7
- package/dist/javascript/format/tabs-and-indents-visitor.js.map +1 -0
- package/dist/javascript/format/whitespace-reconciler.d.ts +106 -0
- package/dist/javascript/format/whitespace-reconciler.d.ts.map +1 -0
- package/dist/javascript/format/whitespace-reconciler.js +291 -0
- package/dist/javascript/format/whitespace-reconciler.js.map +1 -0
- package/dist/javascript/markers.d.ts.map +1 -1
- package/dist/javascript/markers.js +21 -0
- package/dist/javascript/markers.js.map +1 -1
- package/dist/javascript/parser.d.ts +15 -3
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +107 -24
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/recipes/auto-format.d.ts +3 -0
- package/dist/javascript/recipes/auto-format.d.ts.map +1 -1
- package/dist/javascript/recipes/auto-format.js +22 -1
- package/dist/javascript/recipes/auto-format.js.map +1 -1
- package/dist/javascript/style.d.ts +52 -1
- package/dist/javascript/style.d.ts.map +1 -1
- package/dist/javascript/style.js +43 -2
- package/dist/javascript/style.js.map +1 -1
- package/dist/test/rewrite-test.d.ts +3 -4
- package/dist/test/rewrite-test.d.ts.map +1 -1
- package/dist/test/rewrite-test.js +6 -18
- package/dist/test/rewrite-test.js.map +1 -1
- package/dist/version.txt +1 -1
- package/dist/yaml/assertions.d.ts +4 -0
- package/dist/yaml/assertions.d.ts.map +1 -0
- package/dist/yaml/assertions.js +31 -0
- package/dist/yaml/assertions.js.map +1 -0
- package/dist/yaml/index.d.ts +2 -1
- package/dist/yaml/index.d.ts.map +1 -1
- package/dist/yaml/index.js +2 -1
- package/dist/yaml/index.js.map +1 -1
- package/package.json +5 -4
- package/src/java/type.ts +12 -0
- package/src/javascript/assertions.ts +9 -0
- package/src/javascript/comparator.ts +6 -11
- package/src/javascript/{format.ts → format/format.ts} +59 -267
- package/src/javascript/format/index.ts +21 -0
- package/src/javascript/format/minimum-viable-spacing-visitor.ts +256 -0
- package/src/javascript/format/normalize-whitespace-visitor.ts +42 -0
- package/src/javascript/format/prettier-config-loader.ts +422 -0
- package/src/javascript/format/prettier-format.ts +622 -0
- package/src/javascript/{tabs-and-indents-visitor.ts → format/tabs-and-indents-visitor.ts} +8 -8
- package/src/javascript/format/whitespace-reconciler.ts +345 -0
- package/src/javascript/markers.ts +19 -0
- package/src/javascript/parser.ts +107 -20
- package/src/javascript/recipes/auto-format.ts +28 -1
- package/src/javascript/style.ts +41 -2
- package/src/test/rewrite-test.ts +6 -18
- package/src/yaml/assertions.ts +28 -0
- package/src/yaml/index.ts +2 -1
- package/dist/javascript/format.d.ts.map +0 -1
- package/dist/javascript/format.js.map +0 -1
- package/dist/javascript/tabs-and-indents-visitor.d.ts.map +0 -1
- package/dist/javascript/tabs-and-indents-visitor.js.map +0 -1
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 the original author or authors.
|
|
3
|
+
* <p>
|
|
4
|
+
* Licensed under the Moderne Source Available License (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
* <p>
|
|
8
|
+
* https://docs.moderne.io/licensing/moderne-source-available-license
|
|
9
|
+
* <p>
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import {isIdentifier, isLiteral, isSpace, J, Type} from '../../java';
|
|
17
|
+
import {produce} from "immer";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Union type for all tree node types that the reconciler handles.
|
|
21
|
+
* This includes J nodes and their wrapper types.
|
|
22
|
+
*/
|
|
23
|
+
type TreeNode = J | J.RightPadded<J> | J.LeftPadded<J> | J.Container<J> | J.Space;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Type guard to check if a value has a kind property (is a tree node or wrapper).
|
|
27
|
+
*/
|
|
28
|
+
function hasKind(value: unknown): value is { kind: string } {
|
|
29
|
+
return value !== null &&
|
|
30
|
+
typeof value === 'object' &&
|
|
31
|
+
'kind' in value &&
|
|
32
|
+
typeof (value as { kind: unknown }).kind === 'string';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Tree nodes that can be visited by visitNode (excludes Space which is handled separately).
|
|
37
|
+
*/
|
|
38
|
+
type VisitableNode = Exclude<TreeNode, J.Space>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Type guard to check if a value is a VisitableNode (J, LeftPadded, RightPadded, or Container).
|
|
42
|
+
* This is used after hasKind() to further narrow the type for visitNode().
|
|
43
|
+
*/
|
|
44
|
+
function isVisitableNode(value: { kind: string }): value is VisitableNode {
|
|
45
|
+
const kind = value.kind;
|
|
46
|
+
// Check for wrapper kinds first (more specific)
|
|
47
|
+
if (kind === J.Kind.LeftPadded || kind === J.Kind.RightPadded || kind === J.Kind.Container) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
// All other non-Space tree nodes with valid kind strings are J nodes
|
|
51
|
+
// Space is handled separately before this check
|
|
52
|
+
return kind !== J.Kind.Space && !kind.startsWith('org.openrewrite.java.tree.JavaType$');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A visitor that reconciles whitespace from a formatted tree into the original tree.
|
|
57
|
+
* Walks both trees in parallel and copies whitespace (prefix, before, after) from
|
|
58
|
+
* the formatted tree to the original.
|
|
59
|
+
*
|
|
60
|
+
* The result preserves the original AST's structure, types, and markers while
|
|
61
|
+
* applying the formatted tree's whitespace.
|
|
62
|
+
*
|
|
63
|
+
* When a target subtree is specified, the reconciler only applies whitespace changes
|
|
64
|
+
* to that subtree and its descendants, leaving surrounding code unchanged.
|
|
65
|
+
*
|
|
66
|
+
* When stopAfter is specified, the reconciler stops applying changes after exiting
|
|
67
|
+
* that node, leaving all subsequent nodes with their original whitespace.
|
|
68
|
+
*/
|
|
69
|
+
export class WhitespaceReconciler {
|
|
70
|
+
/**
|
|
71
|
+
* Flag indicating whether the trees have compatible structure.
|
|
72
|
+
*/
|
|
73
|
+
private compatible: boolean = true;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The subtree to reconcile (by reference). If undefined, reconcile everything.
|
|
77
|
+
* Can be a J node, RightPadded, LeftPadded, or Container.
|
|
78
|
+
*/
|
|
79
|
+
private targetSubtree?: TreeNode;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The node to stop after (by reference). Once we exit this node, stop reconciling.
|
|
83
|
+
* Can be a J node, RightPadded, LeftPadded, or Container.
|
|
84
|
+
*/
|
|
85
|
+
private stopAfterNode?: TreeNode;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Tracks the reconciliation state:
|
|
89
|
+
* - 'searching': Walking but not yet inside target subtree
|
|
90
|
+
* - 'reconciling': Inside target subtree, applying changes
|
|
91
|
+
* - 'done': Exited target subtree or stopAfter node, no more changes
|
|
92
|
+
*/
|
|
93
|
+
private reconcileState: 'searching' | 'reconciling' | 'done' = 'reconciling';
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Reconciles whitespace from a formatted tree into the original tree.
|
|
97
|
+
*
|
|
98
|
+
* @param original The original tree (with types, markers, etc.)
|
|
99
|
+
* @param formatted The formatted tree (with desired whitespace)
|
|
100
|
+
* @param targetSubtree Optional subtree to limit reconciliation to (by reference).
|
|
101
|
+
* Can be a J node, RightPadded, LeftPadded, or Container.
|
|
102
|
+
* If provided, only this subtree and its descendants will have
|
|
103
|
+
* whitespace and markers applied.
|
|
104
|
+
* @param stopAfter Optional node to stop reconciliation after (by reference).
|
|
105
|
+
* Once we exit this node, no more changes are applied.
|
|
106
|
+
* @returns The original tree with whitespace from the formatted tree
|
|
107
|
+
*/
|
|
108
|
+
reconcile(original: J, formatted: J, targetSubtree?: TreeNode, stopAfter?: TreeNode): J {
|
|
109
|
+
this.compatible = true;
|
|
110
|
+
this.targetSubtree = targetSubtree;
|
|
111
|
+
this.stopAfterNode = stopAfter;
|
|
112
|
+
this.reconcileState = targetSubtree ? 'searching' : 'reconciling';
|
|
113
|
+
|
|
114
|
+
// We know original and formatted are J nodes, so result will be J
|
|
115
|
+
return this.visitNode(original, formatted) as J;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Returns whether the reconciliation was successful (structures were compatible).
|
|
120
|
+
*/
|
|
121
|
+
isCompatible(): boolean {
|
|
122
|
+
return this.compatible;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Marks structure as incompatible and returns the original unchanged.
|
|
127
|
+
*/
|
|
128
|
+
private structureMismatch<T>(t: T): T {
|
|
129
|
+
this.compatible = false;
|
|
130
|
+
return t;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Visit a property value, handling all the different types appropriately.
|
|
135
|
+
* This is the central entry point for visiting any node, including wrappers.
|
|
136
|
+
*
|
|
137
|
+
* @returns The reconciled value (original structure with formatted whitespace)
|
|
138
|
+
*/
|
|
139
|
+
private visitProperty(original: unknown, formatted: unknown): unknown {
|
|
140
|
+
// Handle null/undefined
|
|
141
|
+
if (original == null || formatted == null) {
|
|
142
|
+
if (original !== formatted) {
|
|
143
|
+
return this.structureMismatch(original);
|
|
144
|
+
}
|
|
145
|
+
return original;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Type nodes - short-circuit, keep original (types are expensive to compute)
|
|
149
|
+
// Check this first as isType already validates the kind property
|
|
150
|
+
if (Type.isType(original)) {
|
|
151
|
+
return original;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check if this is a tree node (has a kind property)
|
|
155
|
+
if (!hasKind(original)) {
|
|
156
|
+
// Primitive values or non-tree objects - copy from formatted when reconciling
|
|
157
|
+
// This handles things like valueSource (quote style) which is formatting
|
|
158
|
+
if (this.shouldReconcile() && formatted !== original) {
|
|
159
|
+
return formatted;
|
|
160
|
+
}
|
|
161
|
+
return original;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Space nodes - copy when reconciling, don't recurse
|
|
165
|
+
if (isSpace(original)) {
|
|
166
|
+
return this.shouldReconcile() ? formatted : original;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Track entering target subtree (using referential equality)
|
|
170
|
+
const isTargetSubtree = this.targetSubtree !== undefined && original === this.targetSubtree;
|
|
171
|
+
const isStopAfterNode = this.stopAfterNode !== undefined && original === this.stopAfterNode;
|
|
172
|
+
const previousState = this.reconcileState;
|
|
173
|
+
if (isTargetSubtree && this.reconcileState === 'searching') {
|
|
174
|
+
this.reconcileState = 'reconciling';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
// All tree nodes (J, RightPadded, LeftPadded, Container) go through visitNode
|
|
179
|
+
// After hasKind() and isSpace()/isType() checks, we know this is a VisitableNode
|
|
180
|
+
if (!isVisitableNode(original) || !hasKind(formatted) || !isVisitableNode(formatted)) {
|
|
181
|
+
return this.structureMismatch(original);
|
|
182
|
+
}
|
|
183
|
+
return this.visitNode(original, formatted);
|
|
184
|
+
} finally {
|
|
185
|
+
// Track exiting the target subtree
|
|
186
|
+
if (isTargetSubtree && previousState === 'searching') {
|
|
187
|
+
this.reconcileState = 'done';
|
|
188
|
+
}
|
|
189
|
+
// Track exiting the stopAfter node - stop reconciling after this
|
|
190
|
+
if (isStopAfterNode && previousState === 'reconciling') {
|
|
191
|
+
this.reconcileState = 'done';
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Visit all properties of a tree node (J, RightPadded, LeftPadded, Container).
|
|
198
|
+
* Copies Space values and markers when reconciling, visits everything else.
|
|
199
|
+
*
|
|
200
|
+
* Note: The return type may differ from the input type in cases of semantic
|
|
201
|
+
* equivalence (e.g., Identifier↔Literal with quoteProps), but will always
|
|
202
|
+
* be a valid VisitableNode.
|
|
203
|
+
*
|
|
204
|
+
* @param original Tree node with kind property
|
|
205
|
+
* @param formatted Corresponding formatted tree node
|
|
206
|
+
* @returns The original with whitespace from formatted applied
|
|
207
|
+
*/
|
|
208
|
+
private visitNode(
|
|
209
|
+
original: VisitableNode,
|
|
210
|
+
formatted: VisitableNode
|
|
211
|
+
): VisitableNode {
|
|
212
|
+
if (!this.compatible) {
|
|
213
|
+
return original;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check if kinds match
|
|
217
|
+
if (original.kind !== formatted.kind) {
|
|
218
|
+
// Check if this is a valid semantic equivalence (e.g., quoteProps changing Identifier↔Literal)
|
|
219
|
+
if (this.shouldReconcile() && this.isSemanticEquivalent(original, formatted)) {
|
|
220
|
+
// Use the formatted node but preserve type information from original
|
|
221
|
+
// isSemanticEquivalent only returns true for Identifier/Literal pairs
|
|
222
|
+
return this.copyWithPreservedTypes(
|
|
223
|
+
original as J.Identifier | J.Literal,
|
|
224
|
+
formatted as J.Identifier | J.Literal
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return this.structureMismatch(original);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let result: VisitableNode = original;
|
|
231
|
+
|
|
232
|
+
// Visit all properties
|
|
233
|
+
for (const key of Object.keys(original)) {
|
|
234
|
+
// Skip: kind, id, type properties
|
|
235
|
+
if (key === 'kind' || key === 'id' ||
|
|
236
|
+
key === 'type' || key === 'fieldType' || key === 'variableType' ||
|
|
237
|
+
key === 'methodType' || key === 'constructorType') {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const originalValue = (original as Record<string, unknown>)[key];
|
|
242
|
+
const formattedValue = (formatted as Record<string, unknown>)[key];
|
|
243
|
+
|
|
244
|
+
// Space values and markers: copy from formatted when reconciling
|
|
245
|
+
if ((isSpace(originalValue)) || key === 'markers') {
|
|
246
|
+
if (this.shouldReconcile() && formattedValue !== originalValue) {
|
|
247
|
+
result = produce(result, (draft) => {
|
|
248
|
+
(draft as Record<string, unknown>)[key] = formattedValue;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Handle arrays
|
|
255
|
+
if (Array.isArray(originalValue)) {
|
|
256
|
+
if (!Array.isArray(formattedValue) || originalValue.length !== formattedValue.length) {
|
|
257
|
+
return this.structureMismatch(original);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const newArray: unknown[] = [];
|
|
261
|
+
let changed = false;
|
|
262
|
+
for (let i = 0; i < originalValue.length; i++) {
|
|
263
|
+
const visited = this.visitProperty(originalValue[i], formattedValue[i]);
|
|
264
|
+
if (!this.compatible) return original;
|
|
265
|
+
newArray.push(visited);
|
|
266
|
+
if (visited !== originalValue[i]) {
|
|
267
|
+
changed = true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (changed) {
|
|
272
|
+
result = produce(result, (draft) => {
|
|
273
|
+
(draft as Record<string, unknown>)[key] = newArray;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
// Visit the property
|
|
278
|
+
const visited = this.visitProperty(originalValue, formattedValue);
|
|
279
|
+
if (!this.compatible) return original;
|
|
280
|
+
|
|
281
|
+
if (visited !== originalValue) {
|
|
282
|
+
result = produce(result, (draft) => {
|
|
283
|
+
(draft as Record<string, unknown>)[key] = visited;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Check if we should apply whitespace changes at the current position.
|
|
294
|
+
*/
|
|
295
|
+
private shouldReconcile(): boolean {
|
|
296
|
+
return this.reconcileState === 'reconciling';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Checks if two nodes with different kinds are semantically equivalent.
|
|
301
|
+
* This handles cases like Prettier's quoteProps option which can change
|
|
302
|
+
* property names between Identifier and Literal forms.
|
|
303
|
+
*/
|
|
304
|
+
private isSemanticEquivalent(original: VisitableNode, formatted: VisitableNode): boolean {
|
|
305
|
+
// Identifier → Literal equivalence (for property names with quoteProps)
|
|
306
|
+
if (isIdentifier(original) && isLiteral(formatted)) {
|
|
307
|
+
return original.simpleName === formatted.value;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Literal → Identifier equivalence
|
|
311
|
+
if (isLiteral(original) && isIdentifier(formatted)) {
|
|
312
|
+
return original.value === formatted.simpleName;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Creates a copy of the formatted node with type information preserved from the original.
|
|
320
|
+
* Used when we accept a structural change from Prettier (Identifier ↔ Literal)
|
|
321
|
+
* but need to keep type attribution from the original.
|
|
322
|
+
*
|
|
323
|
+
* Only preserves `type` and `fieldType` since those are the only type-related
|
|
324
|
+
* fields on J.Identifier and J.Literal.
|
|
325
|
+
*/
|
|
326
|
+
private copyWithPreservedTypes(
|
|
327
|
+
original: J.Identifier | J.Literal,
|
|
328
|
+
formatted: J.Identifier | J.Literal
|
|
329
|
+
): J.Identifier | J.Literal {
|
|
330
|
+
const result: Record<string, unknown> = { ...formatted };
|
|
331
|
+
|
|
332
|
+
// Preserve type attribution - both Identifier and Literal have `type`
|
|
333
|
+
if (original.type !== undefined) {
|
|
334
|
+
result.type = original.type;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Preserve fieldType - only Identifier has this, but safe to check
|
|
338
|
+
if ('fieldType' in original && original.fieldType !== undefined) {
|
|
339
|
+
result.fieldType = original.fieldType;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Cast via unknown since we're modifying a copy of a valid Identifier/Literal
|
|
343
|
+
return result as unknown as J.Identifier | J.Literal;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
@@ -18,6 +18,7 @@ import {J} from "../java";
|
|
|
18
18
|
import {JS} from "./tree";
|
|
19
19
|
import {RpcCodecs, RpcReceiveQueue, RpcSendQueue} from "../rpc";
|
|
20
20
|
import {createDraft, finishDraft} from "immer";
|
|
21
|
+
import {PrettierStyle, StyleKind} from "./style";
|
|
21
22
|
|
|
22
23
|
declare module "./tree" {
|
|
23
24
|
namespace JS {
|
|
@@ -104,3 +105,21 @@ registerPrefixedMarkerCodec<Generator>(JS.Markers.Generator);
|
|
|
104
105
|
registerPrefixedMarkerCodec<NonNullAssertion>(JS.Markers.NonNullAssertion);
|
|
105
106
|
registerPrefixedMarkerCodec<Spread>(JS.Markers.Spread);
|
|
106
107
|
registerPrefixedMarkerCodec<FunctionDeclaration>(JS.Markers.FunctionDeclaration);
|
|
108
|
+
|
|
109
|
+
// Register codec for PrettierStyle (a NamedStyles that contains Prettier configuration)
|
|
110
|
+
// Only serialize the variable fields; constant fields are defined in the class
|
|
111
|
+
RpcCodecs.registerCodec(StyleKind.PrettierStyle, {
|
|
112
|
+
async rpcReceive(before: PrettierStyle, q: RpcReceiveQueue): Promise<PrettierStyle> {
|
|
113
|
+
const draft = createDraft(before);
|
|
114
|
+
(draft as any).id = await q.receive(before.id);
|
|
115
|
+
(draft as any).config = await q.receive(before.config);
|
|
116
|
+
(draft as any).prettierVersion = await q.receive(before.prettierVersion);
|
|
117
|
+
return finishDraft(draft) as PrettierStyle;
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
async rpcSend(after: PrettierStyle, q: RpcSendQueue): Promise<void> {
|
|
121
|
+
await q.getAndSend(after, a => a.id);
|
|
122
|
+
await q.getAndSend(after, a => a.config);
|
|
123
|
+
await q.getAndSend(after, a => a.prettierVersion);
|
|
124
|
+
}
|
|
125
|
+
});
|
package/src/javascript/parser.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
} from "./parser-utils";
|
|
49
49
|
import {JavaScriptTypeMapping} from "./type-mapping";
|
|
50
50
|
import {produce} from "immer";
|
|
51
|
+
import {PrettierConfigLoader} from "./format/prettier-config-loader";
|
|
51
52
|
import Kind = JS.Kind;
|
|
52
53
|
import ComputedPropertyName = JS.ComputedPropertyName;
|
|
53
54
|
import Attribute = JSX.Attribute;
|
|
@@ -55,20 +56,24 @@ import SpreadAttribute = JSX.SpreadAttribute;
|
|
|
55
56
|
|
|
56
57
|
export interface JavaScriptParserOptions extends ParserOptions {
|
|
57
58
|
styles?: NamedStyles[],
|
|
58
|
-
sourceFileCache?: Map<string, ts.SourceFile
|
|
59
|
+
sourceFileCache?: Map<string, ts.SourceFile>,
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
function getScriptKindFromFileName(fileName: string): ts.ScriptKind {
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
const dotIndex = fileName.lastIndexOf('.');
|
|
64
|
+
if (dotIndex === -1) return ts.ScriptKind.TS;
|
|
65
|
+
const ext = fileName.slice(dotIndex);
|
|
66
|
+
switch (ext) {
|
|
67
|
+
case '.tsx': return ts.ScriptKind.TSX;
|
|
68
|
+
case '.jsx': return ts.ScriptKind.JSX;
|
|
69
|
+
case '.ts':
|
|
70
|
+
case '.mts':
|
|
71
|
+
case '.cts': return ts.ScriptKind.TS;
|
|
72
|
+
case '.js':
|
|
73
|
+
case '.mjs':
|
|
74
|
+
case '.cjs': return ts.ScriptKind.JS;
|
|
75
|
+
default: return ts.ScriptKind.TS;
|
|
76
|
+
}
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
export class JavaScriptParser extends Parser {
|
|
@@ -83,7 +88,7 @@ export class JavaScriptParser extends Parser {
|
|
|
83
88
|
ctx,
|
|
84
89
|
relativeTo,
|
|
85
90
|
styles,
|
|
86
|
-
sourceFileCache
|
|
91
|
+
sourceFileCache,
|
|
87
92
|
}: JavaScriptParserOptions = {},
|
|
88
93
|
) {
|
|
89
94
|
super({ctx, relativeTo});
|
|
@@ -91,6 +96,7 @@ export class JavaScriptParser extends Parser {
|
|
|
91
96
|
target: ts.ScriptTarget.Latest,
|
|
92
97
|
module: ts.ModuleKind.CommonJS,
|
|
93
98
|
moduleResolution: ts.ModuleResolutionKind.Node10,
|
|
99
|
+
noEmit: true,
|
|
94
100
|
allowJs: true,
|
|
95
101
|
checkJs: true,
|
|
96
102
|
esModuleInterop: true,
|
|
@@ -105,6 +111,75 @@ export class JavaScriptParser extends Parser {
|
|
|
105
111
|
this.sourceFileCache = sourceFileCache;
|
|
106
112
|
}
|
|
107
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Parses a single source file using only ts.createSourceFile(), bypassing
|
|
116
|
+
* the TypeScript type checker entirely. This is significantly faster than
|
|
117
|
+
* the full parse() method when type information is not needed.
|
|
118
|
+
*
|
|
119
|
+
* Use this method for formatting-only operations where AST structure is
|
|
120
|
+
* needed but type attribution is not required.
|
|
121
|
+
*
|
|
122
|
+
* @param input The parser input containing the source code
|
|
123
|
+
* @returns The parsed SourceFile, or a ParseExceptionResult if parsing failed
|
|
124
|
+
*/
|
|
125
|
+
async parseOnly(input: ParserInput): Promise<SourceFile> {
|
|
126
|
+
const filePath = parserInputFile(input);
|
|
127
|
+
const sourcePath = this.relativeTo && !path.isAbsolute(filePath)
|
|
128
|
+
? path.join(this.relativeTo, filePath)
|
|
129
|
+
: filePath;
|
|
130
|
+
|
|
131
|
+
const sourceText = parserInputRead(input);
|
|
132
|
+
const scriptKind = getScriptKindFromFileName(sourcePath);
|
|
133
|
+
|
|
134
|
+
// Create source file directly without a Program
|
|
135
|
+
const sourceFileOptions: ts.CreateSourceFileOptions = {
|
|
136
|
+
languageVersion: ts.ScriptTarget.Latest,
|
|
137
|
+
jsDocParsingMode: ts.JSDocParsingMode.ParseNone
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const tsSourceFile = ts.createSourceFile(
|
|
141
|
+
sourcePath,
|
|
142
|
+
sourceText,
|
|
143
|
+
sourceFileOptions,
|
|
144
|
+
true, // setParentNodes
|
|
145
|
+
scriptKind
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Check for parse-time syntax errors (accessible via internal parseDiagnostics)
|
|
149
|
+
// TypeScript stores parse errors directly on the source file
|
|
150
|
+
const parseDiagnostics = (tsSourceFile as any).parseDiagnostics as ts.Diagnostic[] | undefined;
|
|
151
|
+
if (parseDiagnostics && parseDiagnostics.length > 0) {
|
|
152
|
+
const errors = parseDiagnostics.filter(d => d.category === ts.DiagnosticCategory.Error);
|
|
153
|
+
if (errors.length > 0) {
|
|
154
|
+
const errorMessages = errors.map(e => {
|
|
155
|
+
if (e.file && e.start !== undefined) {
|
|
156
|
+
const { line, character } = ts.getLineAndCharacterOfPosition(e.file, e.start);
|
|
157
|
+
const message = ts.flattenDiagnosticMessageText(e.messageText, "\n");
|
|
158
|
+
return `(${line + 1},${character + 1}): ${message} [${e.code}]`;
|
|
159
|
+
}
|
|
160
|
+
return `${ts.flattenDiagnosticMessageText(e.messageText, "\n")} [${e.code}]`;
|
|
161
|
+
}).join('; ');
|
|
162
|
+
return this.error(input, new SyntaxError(`Compiler error(s): ${errorMessages}`));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
// Parse without type mapping (no type checker available)
|
|
168
|
+
const result = new JavaScriptParserVisitor(tsSourceFile, this.relativePath(input), undefined)
|
|
169
|
+
.visit(tsSourceFile) as SourceFile;
|
|
170
|
+
|
|
171
|
+
if (this.styles) {
|
|
172
|
+
const styles = this.styles;
|
|
173
|
+
return produce(result, draft => {
|
|
174
|
+
draft.markers.markers = draft.markers.markers.concat(styles);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return this.error(input, error instanceof Error ? error : new Error('Parser threw unknown error: ' + error));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
108
183
|
// noinspection JSUnusedGlobalSymbols
|
|
109
184
|
reset(): this {
|
|
110
185
|
this.sourceFileCache && this.sourceFileCache.clear();
|
|
@@ -255,11 +330,16 @@ export class JavaScriptParser extends Parser {
|
|
|
255
330
|
// Update the oldProgram reference
|
|
256
331
|
this.oldProgram = program;
|
|
257
332
|
|
|
258
|
-
|
|
333
|
+
// Detect Prettier config for the project
|
|
334
|
+
const prettierLoader = this.relativeTo ? new PrettierConfigLoader(this.relativeTo) : undefined;
|
|
335
|
+
if (prettierLoader) {
|
|
336
|
+
await prettierLoader.detectPrettier();
|
|
337
|
+
}
|
|
259
338
|
|
|
260
339
|
// Create a single JavaScriptTypeMapping instance to be shared across all files in this parse batch.
|
|
261
340
|
// This ensures that TypeScript types with the same type.id map to the same Type instance,
|
|
262
341
|
// preventing duplicate Type.Class, Type.Parameterized, etc. instances.
|
|
342
|
+
const typeChecker = program.getTypeChecker();
|
|
263
343
|
const typeMapping = new JavaScriptTypeMapping(typeChecker);
|
|
264
344
|
|
|
265
345
|
for (const input of inputFiles.values()) {
|
|
@@ -283,6 +363,9 @@ export class JavaScriptParser extends Parser {
|
|
|
283
363
|
}
|
|
284
364
|
|
|
285
365
|
try {
|
|
366
|
+
// Get Prettier config marker for this file (if Prettier is available)
|
|
367
|
+
const prettierConfigMarker = await prettierLoader?.getConfigMarker(filePath);
|
|
368
|
+
|
|
286
369
|
yield produce(
|
|
287
370
|
new JavaScriptParserVisitor(sourceFile, this.relativePath(input), typeMapping)
|
|
288
371
|
.visit(sourceFile) as SourceFile,
|
|
@@ -290,6 +373,9 @@ export class JavaScriptParser extends Parser {
|
|
|
290
373
|
if (this.styles) {
|
|
291
374
|
draft.markers.markers = draft.markers.markers.concat(this.styles);
|
|
292
375
|
}
|
|
376
|
+
if (prettierConfigMarker) {
|
|
377
|
+
draft.markers.markers = draft.markers.markers.concat([prettierConfigMarker]);
|
|
378
|
+
}
|
|
293
379
|
});
|
|
294
380
|
} catch (error) {
|
|
295
381
|
yield this.error(input, error instanceof Error ? error : new Error('Parser threw unknown error: ' + error));
|
|
@@ -309,12 +395,12 @@ for (const [key, value] of Object.entries(ts.SyntaxKind)) {
|
|
|
309
395
|
|
|
310
396
|
// noinspection JSUnusedGlobalSymbols
|
|
311
397
|
export class JavaScriptParserVisitor {
|
|
312
|
-
private readonly typeMapping
|
|
398
|
+
private readonly typeMapping?: JavaScriptTypeMapping;
|
|
313
399
|
|
|
314
400
|
constructor(
|
|
315
401
|
private readonly sourceFile: ts.SourceFile,
|
|
316
402
|
private readonly sourcePath: string,
|
|
317
|
-
typeMapping
|
|
403
|
+
typeMapping?: JavaScriptTypeMapping) {
|
|
318
404
|
this.typeMapping = typeMapping;
|
|
319
405
|
}
|
|
320
406
|
|
|
@@ -396,6 +482,7 @@ export class JavaScriptParserVisitor {
|
|
|
396
482
|
});
|
|
397
483
|
}
|
|
398
484
|
|
|
485
|
+
const eof = this.prefix(node.endOfFileToken);
|
|
399
486
|
let statements = this.semicolonPaddedStatementList(node.statements);
|
|
400
487
|
|
|
401
488
|
// If there's trailing whitespace/comments after the shebang, prepend to first statement's prefix
|
|
@@ -425,7 +512,7 @@ export class JavaScriptParserVisitor {
|
|
|
425
512
|
statements: shebangStatement
|
|
426
513
|
? [shebangStatement, ...statements]
|
|
427
514
|
: statements,
|
|
428
|
-
eof
|
|
515
|
+
eof
|
|
429
516
|
};
|
|
430
517
|
}
|
|
431
518
|
|
|
@@ -4284,19 +4371,19 @@ export class JavaScriptParserVisitor {
|
|
|
4284
4371
|
}
|
|
4285
4372
|
|
|
4286
4373
|
private mapType(node: ts.Node): Type | undefined {
|
|
4287
|
-
return this.typeMapping
|
|
4374
|
+
return this.typeMapping?.type(node);
|
|
4288
4375
|
}
|
|
4289
4376
|
|
|
4290
4377
|
private mapPrimitiveType(node: ts.Node): Type.Primitive {
|
|
4291
|
-
return this.typeMapping
|
|
4378
|
+
return this.typeMapping?.primitiveType(node) ?? Type.Primitive.None;
|
|
4292
4379
|
}
|
|
4293
4380
|
|
|
4294
4381
|
private mapVariableType(node: ts.NamedDeclaration): Type.Variable | undefined {
|
|
4295
|
-
return this.typeMapping
|
|
4382
|
+
return this.typeMapping?.variableType(node);
|
|
4296
4383
|
}
|
|
4297
4384
|
|
|
4298
4385
|
private mapMethodType(node: ts.Node): Type.Method | undefined {
|
|
4299
|
-
return this.typeMapping
|
|
4386
|
+
return this.typeMapping?.methodType(node);
|
|
4300
4387
|
}
|
|
4301
4388
|
|
|
4302
4389
|
private mapCommaSeparatedList<T extends J>(nodes: readonly ts.Node[]): J.Container<T> {
|
|
@@ -22,6 +22,9 @@ import {Autodetect, Detector} from "../autodetect";
|
|
|
22
22
|
import {JavaScriptVisitor} from "../visitor";
|
|
23
23
|
import {JS} from "../tree";
|
|
24
24
|
import {J} from "../../java";
|
|
25
|
+
import {findMarker, MarkersKind} from "../../markers";
|
|
26
|
+
import {StyleKind} from "../style";
|
|
27
|
+
import {NamedStyles} from "../../style";
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
30
|
* Accumulator for the AutoFormat scanning recipe.
|
|
@@ -32,6 +35,24 @@ interface AutoFormatAccumulator {
|
|
|
32
35
|
detectedStyles?: Autodetect;
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Checks if a compilation unit already has formatting styles attached.
|
|
40
|
+
* Returns true if either PrettierStyle or Autodetect markers are present.
|
|
41
|
+
*/
|
|
42
|
+
function hasFormattingStyles(cu: JS.CompilationUnit): boolean {
|
|
43
|
+
// Check for PrettierStyle marker
|
|
44
|
+
if (findMarker(cu, StyleKind.PrettierStyle)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check for Autodetect marker (NamedStyles with specific name)
|
|
49
|
+
const namedStyles = cu.markers.markers.filter(
|
|
50
|
+
m => m.kind === MarkersKind.NamedStyles
|
|
51
|
+
) as NamedStyles[];
|
|
52
|
+
|
|
53
|
+
return namedStyles.some(s => s.name === "org.openrewrite.javascript.Autodetect");
|
|
54
|
+
}
|
|
55
|
+
|
|
35
56
|
/**
|
|
36
57
|
* Formats JavaScript/TypeScript code using a comprehensive set of formatting rules.
|
|
37
58
|
*
|
|
@@ -45,6 +66,9 @@ interface AutoFormatAccumulator {
|
|
|
45
66
|
* - ES6 import/export brace spacing
|
|
46
67
|
*
|
|
47
68
|
* If no clear style is detected, defaults to IntelliJ IDEA style.
|
|
69
|
+
*
|
|
70
|
+
* Files with existing formatting styles (PrettierStyle or Autodetect markers)
|
|
71
|
+
* are skipped during style detection since they already have their formatting configured.
|
|
48
72
|
*/
|
|
49
73
|
export class AutoFormat extends ScanningRecipe<AutoFormatAccumulator> {
|
|
50
74
|
readonly name = "org.openrewrite.javascript.format.auto-format";
|
|
@@ -60,7 +84,10 @@ export class AutoFormat extends ScanningRecipe<AutoFormatAccumulator> {
|
|
|
60
84
|
async scanner(acc: AutoFormatAccumulator): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
61
85
|
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
62
86
|
protected async visitJsCompilationUnit(cu: JS.CompilationUnit, ctx: ExecutionContext): Promise<J | undefined> {
|
|
63
|
-
|
|
87
|
+
// Skip sampling files that already have formatting styles attached
|
|
88
|
+
if (!hasFormattingStyles(cu)) {
|
|
89
|
+
await acc.detector.sample(cu);
|
|
90
|
+
}
|
|
64
91
|
return cu;
|
|
65
92
|
}
|
|
66
93
|
};
|