@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,622 @@
|
|
|
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 {JS} from '../tree';
|
|
17
|
+
import {J, Statement} from '../../java';
|
|
18
|
+
import {Cursor, Tree} from '../../tree';
|
|
19
|
+
import {TreePrinters} from '../../print';
|
|
20
|
+
import {JavaScriptParser} from '../parser';
|
|
21
|
+
import {WhitespaceReconciler} from './whitespace-reconciler';
|
|
22
|
+
import {produce} from 'immer';
|
|
23
|
+
import {randomId} from '../../uuid';
|
|
24
|
+
import {PrettierStyle, StyleKind} from '../style';
|
|
25
|
+
import {NamedStyles} from '../../style';
|
|
26
|
+
import {findMarker} from '../../markers';
|
|
27
|
+
import {NormalizeWhitespaceVisitor} from './normalize-whitespace-visitor';
|
|
28
|
+
import {MinimumViableSpacingVisitor} from './minimum-viable-spacing-visitor';
|
|
29
|
+
import {loadPrettierVersion} from './prettier-config-loader';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Loads Prettier for formatting.
|
|
33
|
+
*
|
|
34
|
+
* We use the main Prettier module (not standalone) because:
|
|
35
|
+
* 1. It automatically handles parser resolution
|
|
36
|
+
* 2. Works better with CommonJS (avoids ESM issues in Jest)
|
|
37
|
+
* 3. Simpler - no need to manually load plugins
|
|
38
|
+
*/
|
|
39
|
+
async function loadPrettierFormatting(version?: string): Promise<typeof import('prettier')> {
|
|
40
|
+
if (version) {
|
|
41
|
+
// Ensure the version is installed and get it from cache
|
|
42
|
+
return await loadPrettierVersion(version);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Use bundled Prettier
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
47
|
+
return require('prettier');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Options for Prettier formatting.
|
|
52
|
+
*/
|
|
53
|
+
export interface PrettierFormatOptions {
|
|
54
|
+
/**
|
|
55
|
+
* Tab width for indentation. Defaults to 2.
|
|
56
|
+
*/
|
|
57
|
+
tabWidth?: number;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Use tabs instead of spaces. Defaults to false.
|
|
61
|
+
*/
|
|
62
|
+
useTabs?: boolean;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Print semicolons at the ends of statements. Defaults to true.
|
|
66
|
+
*/
|
|
67
|
+
semi?: boolean;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Use single quotes instead of double quotes. Defaults to false.
|
|
71
|
+
*/
|
|
72
|
+
singleQuote?: boolean;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Print trailing commas wherever possible. Defaults to 'all'.
|
|
76
|
+
*/
|
|
77
|
+
trailingComma?: 'all' | 'es5' | 'none';
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Print width for line wrapping. Defaults to 80.
|
|
81
|
+
*/
|
|
82
|
+
printWidth?: number;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Change when properties in objects are quoted.
|
|
86
|
+
* - "as-needed" - Only add quotes around object properties where required.
|
|
87
|
+
* - "consistent" - If at least one property in an object requires quotes, quote all properties.
|
|
88
|
+
* - "preserve" - Respect the input use of quotes in object properties.
|
|
89
|
+
* Defaults to "as-needed".
|
|
90
|
+
*/
|
|
91
|
+
quoteProps?: 'as-needed' | 'consistent' | 'preserve';
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The Prettier version to use (e.g., "3.4.2").
|
|
95
|
+
* If specified, loads that version from cache or installs it.
|
|
96
|
+
* If not specified, uses the bundled Prettier.
|
|
97
|
+
*/
|
|
98
|
+
prettierVersion?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Formats a JavaScript/TypeScript AST using Prettier.
|
|
103
|
+
*
|
|
104
|
+
* This function:
|
|
105
|
+
* 1. Prints the AST to a string
|
|
106
|
+
* 2. Formats the string using Prettier
|
|
107
|
+
* 3. Parses the formatted string back to an AST (without type attribution for performance)
|
|
108
|
+
* 4. Reconciles the whitespace from the formatted AST back into the original AST
|
|
109
|
+
*
|
|
110
|
+
* The result preserves the original AST's structure, types, and markers while
|
|
111
|
+
* applying Prettier's formatting rules for whitespace.
|
|
112
|
+
*
|
|
113
|
+
* @param sourceFile The source file to format
|
|
114
|
+
* @param options Prettier formatting options
|
|
115
|
+
* @param stopAfter Optional node to stop formatting after. Once this node is exited,
|
|
116
|
+
* no more whitespace changes are applied to subsequent nodes.
|
|
117
|
+
* @returns The formatted source file with reconciled whitespace
|
|
118
|
+
*/
|
|
119
|
+
export async function prettierFormat(
|
|
120
|
+
sourceFile: JS.CompilationUnit,
|
|
121
|
+
options: PrettierFormatOptions = {},
|
|
122
|
+
stopAfter?: J
|
|
123
|
+
): Promise<JS.CompilationUnit> {
|
|
124
|
+
// Load Prettier - either specific version or bundled
|
|
125
|
+
let prettier: typeof import('prettier');
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
prettier = await loadPrettierFormatting(options.prettierVersion);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
console.error('Failed to load Prettier:', e);
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Prettier is not installed or failed to load. Please install it with: npm install prettier. Error: ${e}`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Step 1: Print the AST to string
|
|
137
|
+
const originalSource = await TreePrinters.print(sourceFile);
|
|
138
|
+
|
|
139
|
+
// Step 2: Determine parser based on source path
|
|
140
|
+
const parser = getParserForPath(sourceFile.sourcePath);
|
|
141
|
+
|
|
142
|
+
// Step 3: Format with Prettier
|
|
143
|
+
// Using the main Prettier module - parsers are resolved automatically
|
|
144
|
+
// Only set parser and filepath - pass through all other options without defaults
|
|
145
|
+
// This lets Prettier use its own defaults for any unspecified options
|
|
146
|
+
const prettierOptions: Record<string, unknown> = {
|
|
147
|
+
parser,
|
|
148
|
+
filepath: sourceFile.sourcePath, // Important: tells Prettier the file type for proper formatting
|
|
149
|
+
...options,
|
|
150
|
+
};
|
|
151
|
+
// Remove our internal option that Prettier doesn't understand
|
|
152
|
+
delete prettierOptions.prettierVersion;
|
|
153
|
+
|
|
154
|
+
const formattedSource = await prettier.format(originalSource, prettierOptions);
|
|
155
|
+
|
|
156
|
+
// Step 4: Parse the formatted string using parseOnly() for maximum performance
|
|
157
|
+
// (bypasses TypeScript's type checker entirely)
|
|
158
|
+
const formattedParser = new JavaScriptParser();
|
|
159
|
+
const formattedAst = await formattedParser.parseOnly({
|
|
160
|
+
sourcePath: sourceFile.sourcePath,
|
|
161
|
+
text: formattedSource
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (!formattedAst) {
|
|
165
|
+
console.warn('Prettier formatting: Failed to parse formatted output, returning original');
|
|
166
|
+
return sourceFile;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Step 5: Reconcile whitespace from formatted AST to original AST
|
|
170
|
+
// Note: For subtree formatting with pruned trees, the structure may differ
|
|
171
|
+
// (e.g., Prettier removes empty placeholder statements). In such cases,
|
|
172
|
+
// we return the formatted AST directly and let the caller handle
|
|
173
|
+
// subtree-level reconciliation.
|
|
174
|
+
const reconciler = new WhitespaceReconciler();
|
|
175
|
+
const formattedCu = formattedAst as JS.CompilationUnit;
|
|
176
|
+
const result = reconciler.reconcile(sourceFile, formattedCu, undefined, stopAfter);
|
|
177
|
+
|
|
178
|
+
// If reconciliation succeeded, return the reconciled original with updated whitespace
|
|
179
|
+
// If it failed (structure mismatch), return the formatted AST for subtree reconciliation
|
|
180
|
+
return reconciler.isCompatible() ? result as JS.CompilationUnit : formattedCu;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Maps file extensions to Prettier parser names.
|
|
185
|
+
*/
|
|
186
|
+
const PRETTIER_PARSER_BY_EXTENSION: Record<string, string> = {
|
|
187
|
+
'.ts': 'typescript',
|
|
188
|
+
'.tsx': 'typescript',
|
|
189
|
+
'.js': 'babel',
|
|
190
|
+
'.jsx': 'babel',
|
|
191
|
+
'.mjs': 'babel',
|
|
192
|
+
'.cjs': 'babel',
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Determines the Prettier parser to use based on file extension.
|
|
197
|
+
*/
|
|
198
|
+
function getParserForPath(filePath: string): string {
|
|
199
|
+
const lower = filePath.toLowerCase();
|
|
200
|
+
for (const [ext, parser] of Object.entries(PRETTIER_PARSER_BY_EXTENSION)) {
|
|
201
|
+
if (lower.endsWith(ext)) {
|
|
202
|
+
return parser;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return 'babel'; // Default parser
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Represents a segment of the path from root to a target node.
|
|
210
|
+
*/
|
|
211
|
+
interface PathSegment {
|
|
212
|
+
/** The property name containing the child */
|
|
213
|
+
property: string;
|
|
214
|
+
/** For array properties, the index of the element */
|
|
215
|
+
index?: number;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Result of extracting a path from cursor.
|
|
220
|
+
*/
|
|
221
|
+
interface PathExtractionResult {
|
|
222
|
+
/** The compilation unit (root of the tree) */
|
|
223
|
+
compilationUnit: JS.CompilationUnit | undefined;
|
|
224
|
+
/** The path from root to target */
|
|
225
|
+
path: PathSegment[];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Extracts the path from a CompilationUnit to a target node using the cursor.
|
|
230
|
+
* Returns the path segments in order from root to target.
|
|
231
|
+
*
|
|
232
|
+
* @param cursor The cursor, which may not include the target (e.g., when passing cursor.parent)
|
|
233
|
+
* @param target The target node we're looking for
|
|
234
|
+
*/
|
|
235
|
+
function extractPathFromCursor(cursor: Cursor, target: any): PathExtractionResult {
|
|
236
|
+
const pathNodes = cursor.asArray().reverse(); // root to target
|
|
237
|
+
const segments: PathSegment[] = [];
|
|
238
|
+
let compilationUnit: JS.CompilationUnit | undefined;
|
|
239
|
+
|
|
240
|
+
// Helper to check if two nodes are the same (by identity or ID)
|
|
241
|
+
const isSameNode = (a: any, b: any): boolean => {
|
|
242
|
+
if (a === b) return true;
|
|
243
|
+
if (a && b && typeof a === 'object' && typeof b === 'object' && 'id' in a && 'id' in b) {
|
|
244
|
+
return a.id === b.id;
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Helper to find a child in a parent and return the segment
|
|
250
|
+
const findChildInParent = (parent: any, child: any): PathSegment | undefined => {
|
|
251
|
+
if (!parent || typeof parent !== 'object') return undefined;
|
|
252
|
+
|
|
253
|
+
for (const key of Object.keys(parent)) {
|
|
254
|
+
const value = (parent as any)[key];
|
|
255
|
+
if (value == null) continue;
|
|
256
|
+
|
|
257
|
+
if (Array.isArray(value)) {
|
|
258
|
+
for (let idx = 0; idx < value.length; idx++) {
|
|
259
|
+
const item = value[idx];
|
|
260
|
+
if (isSameNode(item, child)) {
|
|
261
|
+
return { property: key, index: idx };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
} else if (isSameNode(value, child)) {
|
|
265
|
+
return { property: key };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return undefined;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
for (let i = 0; i < pathNodes.length - 1; i++) {
|
|
272
|
+
const parent = pathNodes[i];
|
|
273
|
+
const child = pathNodes[i + 1];
|
|
274
|
+
|
|
275
|
+
// Check if this node is the CompilationUnit
|
|
276
|
+
if (parent?.kind === JS.Kind.CompilationUnit) {
|
|
277
|
+
compilationUnit = parent as JS.CompilationUnit;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const segment = findChildInParent(parent, child);
|
|
281
|
+
if (segment) {
|
|
282
|
+
segments.push(segment);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check the last node for CompilationUnit
|
|
287
|
+
const lastNode = pathNodes[pathNodes.length - 1];
|
|
288
|
+
if (lastNode?.kind === JS.Kind.CompilationUnit) {
|
|
289
|
+
compilationUnit = lastNode as JS.CompilationUnit;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// If the cursor doesn't include the target, add the final segment
|
|
293
|
+
// This handles the case when autoFormat is called with cursor.parent
|
|
294
|
+
if (lastNode && !isSameNode(lastNode, target)) {
|
|
295
|
+
const finalSegment = findChildInParent(lastNode, target);
|
|
296
|
+
if (finalSegment) {
|
|
297
|
+
segments.push(finalSegment);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return { compilationUnit, path: segments };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Creates a "null" identifier placeholder for use in pruned trees.
|
|
306
|
+
* Using "null" instead of an empty statement ensures Prettier sees similar
|
|
307
|
+
* line lengths and doesn't collapse multi-line code to single-line.
|
|
308
|
+
*/
|
|
309
|
+
function createNullPlaceholder(prefix: J.Space): J.Identifier {
|
|
310
|
+
return {
|
|
311
|
+
kind: J.Kind.Identifier,
|
|
312
|
+
id: randomId(),
|
|
313
|
+
markers: { kind: "org.openrewrite.marker.Markers", id: randomId(), markers: [] },
|
|
314
|
+
prefix: prefix,
|
|
315
|
+
annotations: [],
|
|
316
|
+
simpleName: "null",
|
|
317
|
+
type: undefined,
|
|
318
|
+
fieldType: undefined
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Prunes a compilation unit for efficient Prettier formatting of a subtree,
|
|
324
|
+
* and substitutes the (potentially modified) target at the path location.
|
|
325
|
+
*
|
|
326
|
+
* For J.Block#statements along the path to the target:
|
|
327
|
+
* - Prior siblings are replaced with "null" identifier placeholders (to maintain line length)
|
|
328
|
+
* - Following siblings are omitted entirely
|
|
329
|
+
*
|
|
330
|
+
* This optimization reduces the amount of code Prettier needs to process
|
|
331
|
+
* while maintaining approximate line positions so Prettier doesn't collapse
|
|
332
|
+
* multi-line code.
|
|
333
|
+
*
|
|
334
|
+
* @param cu The compilation unit to prune
|
|
335
|
+
* @param path The path from root to the target subtree
|
|
336
|
+
* @param target The (potentially modified) target to substitute at the path location
|
|
337
|
+
* @returns A pruned copy of the compilation unit with the target substituted
|
|
338
|
+
*/
|
|
339
|
+
function pruneTreeForSubtree(cu: JS.CompilationUnit, path: PathSegment[], target: any): JS.CompilationUnit {
|
|
340
|
+
return pruneNode(cu, path, 0, target) as JS.CompilationUnit;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Recursively prunes a node, following the path, pruning J.Block#statements,
|
|
345
|
+
* and substituting the target at the final location.
|
|
346
|
+
*/
|
|
347
|
+
function pruneNode(node: any, path: PathSegment[], pathIndex: number, target: any): any {
|
|
348
|
+
if (pathIndex >= path.length) {
|
|
349
|
+
// Reached the target location - substitute with the (potentially modified) target
|
|
350
|
+
return target;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const segment = path[pathIndex];
|
|
354
|
+
const value = node[segment.property];
|
|
355
|
+
|
|
356
|
+
if (value == null) {
|
|
357
|
+
return node;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Handle J.Block#statements specially - prune siblings
|
|
361
|
+
if (node.kind === J.Kind.Block && segment.property === 'statements' && segment.index !== undefined) {
|
|
362
|
+
const statements = value as J.RightPadded<Statement>[];
|
|
363
|
+
const targetIndex = segment.index;
|
|
364
|
+
|
|
365
|
+
// Create pruned statements array:
|
|
366
|
+
// - Prior siblings: replace with "null" placeholders (to maintain line length)
|
|
367
|
+
// - Target: recurse into it (following the path through RightPadded.element)
|
|
368
|
+
// - Following siblings: omit entirely
|
|
369
|
+
const prunedStatements: J.RightPadded<Statement>[] = [];
|
|
370
|
+
|
|
371
|
+
for (let i = 0; i <= targetIndex; i++) {
|
|
372
|
+
if (i < targetIndex) {
|
|
373
|
+
// Prior sibling - replace with "null" placeholder
|
|
374
|
+
// Preserve the original prefix to maintain line positions
|
|
375
|
+
const originalPrefix = statements[i].element.prefix;
|
|
376
|
+
const placeholder = createNullPlaceholder(originalPrefix);
|
|
377
|
+
prunedStatements.push({
|
|
378
|
+
kind: J.Kind.RightPadded,
|
|
379
|
+
element: placeholder,
|
|
380
|
+
after: statements[i].after,
|
|
381
|
+
markers: statements[i].markers
|
|
382
|
+
} as J.RightPadded<Statement>);
|
|
383
|
+
} else {
|
|
384
|
+
// Target - recurse into the RightPadded (path will handle .element)
|
|
385
|
+
const prunedRightPadded = pruneNode(statements[i], path, pathIndex + 1, target);
|
|
386
|
+
prunedStatements.push(prunedRightPadded);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// Following siblings are omitted
|
|
390
|
+
|
|
391
|
+
return produce(node, (draft: any) => {
|
|
392
|
+
draft.statements = prunedStatements;
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// For other properties, just recurse without pruning
|
|
397
|
+
if (Array.isArray(value) && segment.index !== undefined) {
|
|
398
|
+
const childNode = value[segment.index];
|
|
399
|
+
const prunedChild = pruneNode(childNode, path, pathIndex + 1, target);
|
|
400
|
+
|
|
401
|
+
if (prunedChild !== childNode) {
|
|
402
|
+
return produce(node, (draft: any) => {
|
|
403
|
+
draft[segment.property][segment.index!] = prunedChild;
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
} else if (!Array.isArray(value)) {
|
|
407
|
+
const prunedChild = pruneNode(value, path, pathIndex + 1, target);
|
|
408
|
+
|
|
409
|
+
if (prunedChild !== value) {
|
|
410
|
+
return produce(node, (draft: any) => {
|
|
411
|
+
draft[segment.property] = prunedChild;
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return node;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Finds a node in a tree by following a path of segments.
|
|
421
|
+
* Used to locate the target node in the formatted tree.
|
|
422
|
+
*
|
|
423
|
+
* For block statements, the target is always at the last index since
|
|
424
|
+
* following siblings are omitted during pruning.
|
|
425
|
+
*/
|
|
426
|
+
function findByPath(tree: any, path: PathSegment[]): any {
|
|
427
|
+
let current = tree;
|
|
428
|
+
|
|
429
|
+
for (const segment of path) {
|
|
430
|
+
if (current == null) return undefined;
|
|
431
|
+
|
|
432
|
+
const value = current[segment.property];
|
|
433
|
+
if (value == null) return undefined;
|
|
434
|
+
|
|
435
|
+
if (Array.isArray(value) && segment.index !== undefined) {
|
|
436
|
+
// For block statements, target is always at the last index
|
|
437
|
+
// since following siblings are omitted during pruning
|
|
438
|
+
const isBlockStatements = current.kind === J.Kind.Block && segment.property === 'statements';
|
|
439
|
+
const index = isBlockStatements ? value.length - 1 : segment.index;
|
|
440
|
+
const item = value[index];
|
|
441
|
+
if (item == null) return undefined;
|
|
442
|
+
current = item;
|
|
443
|
+
} else {
|
|
444
|
+
current = value;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return current;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Formats a subtree of a JavaScript/TypeScript AST using Prettier.
|
|
453
|
+
*
|
|
454
|
+
* This function is optimized for formatting a small part of a larger tree:
|
|
455
|
+
* 1. Extracts the path from compilation unit to target
|
|
456
|
+
* 2. Prunes the tree (replaces siblings with placeholders)
|
|
457
|
+
* 3. Formats the pruned tree with Prettier
|
|
458
|
+
* 4. Finds the target in the formatted tree
|
|
459
|
+
* 5. Reconciles only the target subtree's whitespace
|
|
460
|
+
*
|
|
461
|
+
* @param target The subtree to format
|
|
462
|
+
* @param cursor The cursor pointing to or near the target
|
|
463
|
+
* @param options Prettier formatting options
|
|
464
|
+
* @param stopAfter Optional node to stop formatting after
|
|
465
|
+
* @returns The formatted subtree, or undefined if formatting failed
|
|
466
|
+
*/
|
|
467
|
+
export async function prettierFormatSubtree<T extends J>(
|
|
468
|
+
target: T,
|
|
469
|
+
cursor: Cursor,
|
|
470
|
+
options: PrettierFormatOptions = {},
|
|
471
|
+
stopAfter?: J
|
|
472
|
+
): Promise<T | undefined> {
|
|
473
|
+
// Extract the path and compilation unit in a single cursor traversal
|
|
474
|
+
const { compilationUnit: cu, path } = extractPathFromCursor(cursor, target);
|
|
475
|
+
|
|
476
|
+
if (!cu) {
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Prune the tree for efficient formatting and substitute the (potentially modified) target.
|
|
481
|
+
// This ensures that if the visitor modified the target before calling autoFormat,
|
|
482
|
+
// we format the modified content, not the original from the cursor.
|
|
483
|
+
const prunedCu = pruneTreeForSubtree(cu, path, target);
|
|
484
|
+
|
|
485
|
+
// Format the pruned compilation unit with Prettier
|
|
486
|
+
const formattedPrunedCu = await prettierFormat(prunedCu, options);
|
|
487
|
+
|
|
488
|
+
// Find the target node in the formatted tree using the path
|
|
489
|
+
const formattedTarget = findByPath(formattedPrunedCu, path);
|
|
490
|
+
if (!formattedTarget) {
|
|
491
|
+
return undefined;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Reconcile only the target subtree, optionally stopping after a specific node
|
|
495
|
+
const reconciler = new WhitespaceReconciler();
|
|
496
|
+
const reconciled = reconciler.reconcile(target as J, formattedTarget as J, undefined, stopAfter);
|
|
497
|
+
|
|
498
|
+
return reconciled as T;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Gets the PrettierStyle from the styles array or source file markers.
|
|
503
|
+
*
|
|
504
|
+
* @param tree The tree being formatted
|
|
505
|
+
* @param cursor Optional cursor for walking up to find source file
|
|
506
|
+
* @param styles Optional styles array to check first
|
|
507
|
+
* @returns PrettierStyle if found, undefined otherwise
|
|
508
|
+
*/
|
|
509
|
+
export function getPrettierStyle(
|
|
510
|
+
tree: Tree,
|
|
511
|
+
cursor?: Cursor,
|
|
512
|
+
styles?: NamedStyles<string>[]
|
|
513
|
+
): PrettierStyle | undefined {
|
|
514
|
+
// First check the styles array
|
|
515
|
+
if (styles) {
|
|
516
|
+
const fromStyles = styles.find(s => (s as any).kind === StyleKind.PrettierStyle);
|
|
517
|
+
if (fromStyles) {
|
|
518
|
+
return fromStyles as unknown as PrettierStyle;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Then check for PrettierStyle marker on source file
|
|
523
|
+
let sourceFile: JS.CompilationUnit | undefined;
|
|
524
|
+
|
|
525
|
+
if (tree.kind === JS.Kind.CompilationUnit) {
|
|
526
|
+
sourceFile = tree as JS.CompilationUnit;
|
|
527
|
+
} else if (cursor) {
|
|
528
|
+
// Walk up the cursor to find the compilation unit
|
|
529
|
+
let current: Cursor | undefined = cursor;
|
|
530
|
+
while (current) {
|
|
531
|
+
if (current.value?.kind === JS.Kind.CompilationUnit) {
|
|
532
|
+
sourceFile = current.value as JS.CompilationUnit;
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
current = current.parent;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (!sourceFile) {
|
|
540
|
+
return undefined;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return findMarker(sourceFile, StyleKind.PrettierStyle) as PrettierStyle | undefined;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Applies Prettier formatting to a tree.
|
|
548
|
+
*
|
|
549
|
+
* Configuration is resolved from the PrettierStyle marker on the source file.
|
|
550
|
+
*
|
|
551
|
+
* For compilation units, formats and reconciles the entire tree.
|
|
552
|
+
* For subtrees, uses prettierFormatSubtree which prunes the tree for efficiency,
|
|
553
|
+
* formats the pruned tree, and reconciles only the target subtree.
|
|
554
|
+
*
|
|
555
|
+
* @param tree The tree to format
|
|
556
|
+
* @param prettierStyle The PrettierStyle containing config
|
|
557
|
+
* @param p The visitor parameter
|
|
558
|
+
* @param cursor Optional cursor for subtree formatting
|
|
559
|
+
* @param stopAfter Optional tree to stop after
|
|
560
|
+
* @returns The formatted tree
|
|
561
|
+
*/
|
|
562
|
+
export async function applyPrettierFormatting<R extends J, P>(
|
|
563
|
+
tree: R,
|
|
564
|
+
prettierStyle: PrettierStyle,
|
|
565
|
+
p: P,
|
|
566
|
+
cursor?: Cursor,
|
|
567
|
+
stopAfter?: Tree
|
|
568
|
+
): Promise<R | undefined> {
|
|
569
|
+
// Run only the essential visitors first
|
|
570
|
+
const essentialVisitors = [
|
|
571
|
+
new NormalizeWhitespaceVisitor(stopAfter),
|
|
572
|
+
new MinimumViableSpacingVisitor(stopAfter),
|
|
573
|
+
];
|
|
574
|
+
|
|
575
|
+
let t: R | undefined = tree;
|
|
576
|
+
for (const visitor of essentialVisitors) {
|
|
577
|
+
t = await visitor.visit(t, p, cursor);
|
|
578
|
+
if (t === undefined) {
|
|
579
|
+
return undefined;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// If file is in .prettierignore, skip formatting entirely
|
|
584
|
+
if (prettierStyle.ignored) {
|
|
585
|
+
return t;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Build options for Prettier
|
|
589
|
+
// Pass through the entire resolved config - let Prettier use its own defaults for unspecified options
|
|
590
|
+
const prettierOpts: PrettierFormatOptions = {
|
|
591
|
+
...prettierStyle.config as PrettierFormatOptions,
|
|
592
|
+
prettierVersion: prettierStyle.prettierVersion,
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
if (t.kind === JS.Kind.CompilationUnit) {
|
|
597
|
+
// Format and reconcile the entire compilation unit
|
|
598
|
+
const formatted = await prettierFormat(t as unknown as JS.CompilationUnit, prettierOpts, stopAfter as J | undefined);
|
|
599
|
+
return formatted as unknown as R;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (!cursor) {
|
|
603
|
+
// No cursor provided - can't use subtree formatting, return with essential formatting
|
|
604
|
+
console.warn('Prettier formatting: No cursor provided for subtree, returning with essential formatting only');
|
|
605
|
+
return t;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Use prettierFormatSubtree for subtree formatting
|
|
609
|
+
const formatted = await prettierFormatSubtree(t, cursor, prettierOpts, stopAfter as J | undefined);
|
|
610
|
+
if (formatted) {
|
|
611
|
+
return formatted as R;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Subtree formatting failed, return with essential formatting applied
|
|
615
|
+
console.warn('Prettier formatting: Subtree formatting failed, returning with essential formatting only');
|
|
616
|
+
return t;
|
|
617
|
+
} catch (e) {
|
|
618
|
+
// If Prettier fails, return tree with essential formatting applied
|
|
619
|
+
console.warn('Prettier formatting failed, returning with essential formatting only:', e);
|
|
620
|
+
return t;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import {JS, JSX} from "
|
|
17
|
-
import {JavaScriptVisitor} from "
|
|
16
|
+
import {JS, JSX} from "../tree";
|
|
17
|
+
import {JavaScriptVisitor} from "../visitor";
|
|
18
18
|
import {
|
|
19
19
|
isJava,
|
|
20
20
|
isSpace,
|
|
@@ -25,13 +25,13 @@ import {
|
|
|
25
25
|
replaceLastWhitespace,
|
|
26
26
|
spaceContainsNewline,
|
|
27
27
|
stripLeadingIndent
|
|
28
|
-
} from "
|
|
28
|
+
} from "../../java";
|
|
29
29
|
import {produce} from "immer";
|
|
30
|
-
import {Cursor, isScope, isTree, Tree} from "
|
|
31
|
-
import {mapAsync} from "
|
|
32
|
-
import {produceAsync} from "
|
|
33
|
-
import {TabsAndIndentsStyle} from "
|
|
34
|
-
import {findMarker} from "
|
|
30
|
+
import {Cursor, isScope, isTree, Tree} from "../../tree";
|
|
31
|
+
import {mapAsync} from "../../util";
|
|
32
|
+
import {produceAsync} from "../../visitor";
|
|
33
|
+
import {TabsAndIndentsStyle} from "../style";
|
|
34
|
+
import {findMarker} from "../../markers";
|
|
35
35
|
|
|
36
36
|
type IndentKind = 'block' | 'continuation' | 'align';
|
|
37
37
|
type IndentContext = [number, IndentKind]; // [indent, kind]
|