@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.
Files changed (93) hide show
  1. package/dist/java/type.d.ts +5 -0
  2. package/dist/java/type.d.ts.map +1 -1
  3. package/dist/java/type.js +12 -0
  4. package/dist/java/type.js.map +1 -1
  5. package/dist/javascript/assertions.d.ts.map +1 -1
  6. package/dist/javascript/assertions.js +9 -0
  7. package/dist/javascript/assertions.js.map +1 -1
  8. package/dist/javascript/comparator.d.ts.map +1 -1
  9. package/dist/javascript/comparator.js +5 -9
  10. package/dist/javascript/comparator.js.map +1 -1
  11. package/dist/javascript/{format.d.ts → format/format.d.ts} +15 -33
  12. package/dist/javascript/format/format.d.ts.map +1 -0
  13. package/dist/javascript/{format.js → format/format.js} +56 -313
  14. package/dist/javascript/format/format.js.map +1 -0
  15. package/dist/javascript/format/index.d.ts +3 -0
  16. package/dist/javascript/format/index.d.ts.map +1 -0
  17. package/dist/javascript/format/index.js +38 -0
  18. package/dist/javascript/format/index.js.map +1 -0
  19. package/dist/javascript/format/minimum-viable-spacing-visitor.d.ts +28 -0
  20. package/dist/javascript/format/minimum-viable-spacing-visitor.d.ts.map +1 -0
  21. package/dist/javascript/format/minimum-viable-spacing-visitor.js +308 -0
  22. package/dist/javascript/format/minimum-viable-spacing-visitor.js.map +1 -0
  23. package/dist/javascript/format/normalize-whitespace-visitor.d.ts +14 -0
  24. package/dist/javascript/format/normalize-whitespace-visitor.d.ts.map +1 -0
  25. package/dist/javascript/format/normalize-whitespace-visitor.js +65 -0
  26. package/dist/javascript/format/normalize-whitespace-visitor.js.map +1 -0
  27. package/dist/javascript/format/prettier-config-loader.d.ts +92 -0
  28. package/dist/javascript/format/prettier-config-loader.d.ts.map +1 -0
  29. package/dist/javascript/format/prettier-config-loader.js +419 -0
  30. package/dist/javascript/format/prettier-config-loader.js.map +1 -0
  31. package/dist/javascript/format/prettier-format.d.ts +111 -0
  32. package/dist/javascript/format/prettier-format.d.ts.map +1 -0
  33. package/dist/javascript/format/prettier-format.js +496 -0
  34. package/dist/javascript/format/prettier-format.js.map +1 -0
  35. package/dist/javascript/{tabs-and-indents-visitor.d.ts → format/tabs-and-indents-visitor.d.ts} +4 -4
  36. package/dist/javascript/format/tabs-and-indents-visitor.d.ts.map +1 -0
  37. package/dist/javascript/{tabs-and-indents-visitor.js → format/tabs-and-indents-visitor.js} +7 -7
  38. package/dist/javascript/format/tabs-and-indents-visitor.js.map +1 -0
  39. package/dist/javascript/format/whitespace-reconciler.d.ts +106 -0
  40. package/dist/javascript/format/whitespace-reconciler.d.ts.map +1 -0
  41. package/dist/javascript/format/whitespace-reconciler.js +291 -0
  42. package/dist/javascript/format/whitespace-reconciler.js.map +1 -0
  43. package/dist/javascript/markers.d.ts.map +1 -1
  44. package/dist/javascript/markers.js +21 -0
  45. package/dist/javascript/markers.js.map +1 -1
  46. package/dist/javascript/parser.d.ts +15 -3
  47. package/dist/javascript/parser.d.ts.map +1 -1
  48. package/dist/javascript/parser.js +107 -24
  49. package/dist/javascript/parser.js.map +1 -1
  50. package/dist/javascript/recipes/auto-format.d.ts +3 -0
  51. package/dist/javascript/recipes/auto-format.d.ts.map +1 -1
  52. package/dist/javascript/recipes/auto-format.js +22 -1
  53. package/dist/javascript/recipes/auto-format.js.map +1 -1
  54. package/dist/javascript/style.d.ts +52 -1
  55. package/dist/javascript/style.d.ts.map +1 -1
  56. package/dist/javascript/style.js +43 -2
  57. package/dist/javascript/style.js.map +1 -1
  58. package/dist/test/rewrite-test.d.ts +3 -4
  59. package/dist/test/rewrite-test.d.ts.map +1 -1
  60. package/dist/test/rewrite-test.js +6 -18
  61. package/dist/test/rewrite-test.js.map +1 -1
  62. package/dist/version.txt +1 -1
  63. package/dist/yaml/assertions.d.ts +4 -0
  64. package/dist/yaml/assertions.d.ts.map +1 -0
  65. package/dist/yaml/assertions.js +31 -0
  66. package/dist/yaml/assertions.js.map +1 -0
  67. package/dist/yaml/index.d.ts +2 -1
  68. package/dist/yaml/index.d.ts.map +1 -1
  69. package/dist/yaml/index.js +2 -1
  70. package/dist/yaml/index.js.map +1 -1
  71. package/package.json +5 -4
  72. package/src/java/type.ts +12 -0
  73. package/src/javascript/assertions.ts +9 -0
  74. package/src/javascript/comparator.ts +6 -11
  75. package/src/javascript/{format.ts → format/format.ts} +59 -267
  76. package/src/javascript/format/index.ts +21 -0
  77. package/src/javascript/format/minimum-viable-spacing-visitor.ts +256 -0
  78. package/src/javascript/format/normalize-whitespace-visitor.ts +42 -0
  79. package/src/javascript/format/prettier-config-loader.ts +422 -0
  80. package/src/javascript/format/prettier-format.ts +622 -0
  81. package/src/javascript/{tabs-and-indents-visitor.ts → format/tabs-and-indents-visitor.ts} +8 -8
  82. package/src/javascript/format/whitespace-reconciler.ts +345 -0
  83. package/src/javascript/markers.ts +19 -0
  84. package/src/javascript/parser.ts +107 -20
  85. package/src/javascript/recipes/auto-format.ts +28 -1
  86. package/src/javascript/style.ts +41 -2
  87. package/src/test/rewrite-test.ts +6 -18
  88. package/src/yaml/assertions.ts +28 -0
  89. package/src/yaml/index.ts +2 -1
  90. package/dist/javascript/format.d.ts.map +0 -1
  91. package/dist/javascript/format.js.map +0 -1
  92. package/dist/javascript/tabs-and-indents-visitor.d.ts.map +0 -1
  93. 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
+ });
@@ -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 ext = fileName.toLowerCase();
63
- if (ext.endsWith('.tsx')) return ts.ScriptKind.TSX;
64
- if (ext.endsWith('.jsx')) return ts.ScriptKind.JSX;
65
- if (ext.endsWith('.ts')) return ts.ScriptKind.TS;
66
- if (ext.endsWith('.js')) return ts.ScriptKind.JS;
67
- if (ext.endsWith('.mjs')) return ts.ScriptKind.JS;
68
- if (ext.endsWith('.cjs')) return ts.ScriptKind.JS;
69
- if (ext.endsWith('.mts')) return ts.ScriptKind.TS;
70
- if (ext.endsWith('.cts')) return ts.ScriptKind.TS;
71
- return ts.ScriptKind.TS;
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
- const typeChecker = program.getTypeChecker();
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: JavaScriptTypeMapping;
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: JavaScriptTypeMapping) {
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: this.prefix(node.endOfFileToken)
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.type(node);
4374
+ return this.typeMapping?.type(node);
4288
4375
  }
4289
4376
 
4290
4377
  private mapPrimitiveType(node: ts.Node): Type.Primitive {
4291
- return this.typeMapping.primitiveType(node);
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.variableType(node);
4382
+ return this.typeMapping?.variableType(node);
4296
4383
  }
4297
4384
 
4298
4385
  private mapMethodType(node: ts.Node): Type.Method | undefined {
4299
- return this.typeMapping.methodType(node);
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
- await acc.detector.sample(cu);
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
  };