@openrewrite/rewrite 8.69.0-20251207-220615 → 8.69.0-20251208-071356
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/javascript/autodetect.d.ts +45 -0
- package/dist/javascript/autodetect.d.ts.map +1 -0
- package/dist/javascript/autodetect.js +303 -0
- package/dist/javascript/autodetect.js.map +1 -0
- package/dist/javascript/format.d.ts +13 -2
- package/dist/javascript/format.d.ts.map +1 -1
- package/dist/javascript/format.js +100 -21
- package/dist/javascript/format.js.map +1 -1
- package/dist/javascript/index.d.ts +1 -0
- package/dist/javascript/index.d.ts.map +1 -1
- package/dist/javascript/index.js +1 -0
- package/dist/javascript/index.js.map +1 -1
- package/dist/javascript/recipes/auto-format.d.ts +25 -13
- package/dist/javascript/recipes/auto-format.d.ts.map +1 -1
- package/dist/javascript/recipes/auto-format.js +38 -13
- package/dist/javascript/recipes/auto-format.js.map +1 -1
- package/dist/javascript/style.d.ts +9 -0
- package/dist/javascript/style.d.ts.map +1 -1
- package/dist/javascript/style.js +30 -0
- package/dist/javascript/style.js.map +1 -1
- package/dist/run.d.ts +6 -5
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +36 -37
- package/dist/run.js.map +1 -1
- package/dist/test/rewrite-test.d.ts +17 -0
- package/dist/test/rewrite-test.d.ts.map +1 -1
- package/dist/test/rewrite-test.js +1 -0
- package/dist/test/rewrite-test.js.map +1 -1
- package/dist/version.txt +1 -1
- package/package.json +1 -1
- package/src/javascript/autodetect.ts +302 -0
- package/src/javascript/format.ts +97 -28
- package/src/javascript/index.ts +1 -0
- package/src/javascript/recipes/auto-format.ts +49 -14
- package/src/javascript/style.ts +32 -0
- package/src/run.ts +20 -20
- package/src/test/rewrite-test.ts +1 -1
|
@@ -0,0 +1,302 @@
|
|
|
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
|
+
|
|
17
|
+
import {NamedStyles, Style} from "../style";
|
|
18
|
+
import {randomId} from "../uuid";
|
|
19
|
+
import {SourceFile} from "../tree";
|
|
20
|
+
import {JS} from "./tree";
|
|
21
|
+
import {JavaScriptVisitor} from "./visitor";
|
|
22
|
+
import {J} from "../java";
|
|
23
|
+
import {
|
|
24
|
+
IntelliJ,
|
|
25
|
+
SpacesStyle,
|
|
26
|
+
StyleKind,
|
|
27
|
+
TabsAndIndentsStyle,
|
|
28
|
+
WrappingAndBracesStyle,
|
|
29
|
+
WrappingAndBracesStyleDetailKind
|
|
30
|
+
} from "./style";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Auto-detected styles for JavaScript/TypeScript code.
|
|
34
|
+
* Focuses on key formatting variations where projects differ:
|
|
35
|
+
* - Tabs vs spaces
|
|
36
|
+
* - Indent size (2, 4, etc.)
|
|
37
|
+
* - Spaces within ES6 import/export braces
|
|
38
|
+
*/
|
|
39
|
+
export class Autodetect implements NamedStyles {
|
|
40
|
+
readonly kind = "org.openrewrite.marker.NamedStyles" as const;
|
|
41
|
+
readonly id: string;
|
|
42
|
+
readonly name = "org.openrewrite.javascript.Autodetect";
|
|
43
|
+
readonly displayName = "Auto-detected";
|
|
44
|
+
readonly description = "Automatically detect styles from a repository's existing code.";
|
|
45
|
+
readonly tags: string[] = [];
|
|
46
|
+
readonly styles: Style[];
|
|
47
|
+
|
|
48
|
+
constructor(id: string, styles: Style[]) {
|
|
49
|
+
this.id = id;
|
|
50
|
+
this.styles = styles;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static detector(): Detector {
|
|
54
|
+
return new Detector();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Collects formatting statistics from source files and builds auto-detected styles.
|
|
60
|
+
*/
|
|
61
|
+
export class Detector {
|
|
62
|
+
private readonly tabsAndIndentsStats = new TabsAndIndentsStatistics();
|
|
63
|
+
private readonly spacesStats = new SpacesStatistics();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Sample a source file to collect formatting statistics.
|
|
67
|
+
*/
|
|
68
|
+
async sample(sourceFile: SourceFile): Promise<void> {
|
|
69
|
+
if (sourceFile.kind === JS.Kind.CompilationUnit) {
|
|
70
|
+
await this.sampleJavaScript(sourceFile as JS.CompilationUnit);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sample a JavaScript/TypeScript compilation unit.
|
|
76
|
+
*/
|
|
77
|
+
async sampleJavaScript(cu: JS.CompilationUnit): Promise<void> {
|
|
78
|
+
await new FindIndentVisitor(this.tabsAndIndentsStats).visit(cu, {});
|
|
79
|
+
await new FindSpacesVisitor(this.spacesStats).visit(cu, {});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build the auto-detected styles from collected statistics.
|
|
84
|
+
*/
|
|
85
|
+
build(): Autodetect {
|
|
86
|
+
return new Autodetect(randomId(), [
|
|
87
|
+
this.tabsAndIndentsStats.getTabsAndIndentsStyle(),
|
|
88
|
+
this.spacesStats.getSpacesStyle(),
|
|
89
|
+
this.getWrappingAndBracesStyle(),
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getTabsAndIndentsStyle(): TabsAndIndentsStyle {
|
|
94
|
+
return this.tabsAndIndentsStats.getTabsAndIndentsStyle();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getSpacesStyle(): SpacesStyle {
|
|
98
|
+
return this.spacesStats.getSpacesStyle();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getWrappingAndBracesStyle(): WrappingAndBracesStyle {
|
|
102
|
+
return {
|
|
103
|
+
kind: StyleKind.WrappingAndBracesStyle,
|
|
104
|
+
ifStatement: {
|
|
105
|
+
kind: WrappingAndBracesStyleDetailKind.WrappingAndBracesStyleIfStatement,
|
|
106
|
+
elseOnNewLine: false
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// Statistics Classes
|
|
114
|
+
// ============================================================================
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Tracks indentation patterns to detect tabs vs spaces and indent size.
|
|
118
|
+
*/
|
|
119
|
+
class TabsAndIndentsStatistics {
|
|
120
|
+
private totalSpaceIndents = 0;
|
|
121
|
+
private totalTabIndents = 0;
|
|
122
|
+
|
|
123
|
+
// Track all observed indent sizes to compute GCD
|
|
124
|
+
private observedIndents: number[] = [];
|
|
125
|
+
|
|
126
|
+
recordSpaceIndent(spaceCount: number): void {
|
|
127
|
+
this.totalSpaceIndents++;
|
|
128
|
+
if (spaceCount > 0) {
|
|
129
|
+
this.observedIndents.push(spaceCount);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
recordTabIndent(): void {
|
|
134
|
+
this.totalTabIndents++;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getTabsAndIndentsStyle(): TabsAndIndentsStyle {
|
|
138
|
+
// Determine if using tabs or spaces
|
|
139
|
+
const useTabs = this.totalTabIndents > this.totalSpaceIndents;
|
|
140
|
+
|
|
141
|
+
// Find indent size by computing GCD of all observed indents
|
|
142
|
+
// This correctly handles 2-space files where we see 2, 4, 6, 8... (all multiples of 2)
|
|
143
|
+
let detectedIndentSize = 4; // Default
|
|
144
|
+
if (this.observedIndents.length > 0) {
|
|
145
|
+
// Compute GCD of all observed indents
|
|
146
|
+
let gcd = this.observedIndents[0];
|
|
147
|
+
for (let i = 1; i < this.observedIndents.length; i++) {
|
|
148
|
+
gcd = this.computeGcd(gcd, this.observedIndents[i]);
|
|
149
|
+
if (gcd === 1) break; // Can't get smaller than 1
|
|
150
|
+
}
|
|
151
|
+
// Only use common indent sizes (2, 4, 8)
|
|
152
|
+
if (gcd === 2 || gcd === 4 || gcd === 8) {
|
|
153
|
+
detectedIndentSize = gcd;
|
|
154
|
+
} else if (gcd > 0 && gcd % 4 === 0) {
|
|
155
|
+
detectedIndentSize = 4;
|
|
156
|
+
} else if (gcd > 0 && gcd % 2 === 0) {
|
|
157
|
+
detectedIndentSize = 2;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
kind: StyleKind.TabsAndIndentsStyle,
|
|
163
|
+
useTabCharacter: useTabs,
|
|
164
|
+
tabSize: 4,
|
|
165
|
+
indentSize: detectedIndentSize,
|
|
166
|
+
continuationIndent: detectedIndentSize * 2,
|
|
167
|
+
keepIndentsOnEmptyLines: false,
|
|
168
|
+
indentChainedMethods: true,
|
|
169
|
+
indentAllChainedCallsInAGroup: false
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private computeGcd(a: number, b: number): number {
|
|
174
|
+
while (b !== 0) {
|
|
175
|
+
const temp = b;
|
|
176
|
+
b = a % b;
|
|
177
|
+
a = temp;
|
|
178
|
+
}
|
|
179
|
+
return a;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Tracks spacing patterns around ES6 import/export braces.
|
|
185
|
+
*/
|
|
186
|
+
class SpacesStatistics {
|
|
187
|
+
// Track spaces within ES6 import/export braces: { a } vs {a}
|
|
188
|
+
es6ImportExportBracesWithSpace = 0;
|
|
189
|
+
es6ImportExportBracesWithoutSpace = 0;
|
|
190
|
+
|
|
191
|
+
getSpacesStyle(): SpacesStyle {
|
|
192
|
+
// Use TypeScript defaults as base since most modern JS/TS projects use similar conventions
|
|
193
|
+
// TypeScript defaults include afterTypeReferenceColon: true which is commonly expected
|
|
194
|
+
const defaults = IntelliJ.TypeScript.spaces();
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
...defaults,
|
|
198
|
+
within: {
|
|
199
|
+
...defaults.within,
|
|
200
|
+
es6ImportExportBraces: this.es6ImportExportBracesWithSpace > this.es6ImportExportBracesWithoutSpace
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ============================================================================
|
|
207
|
+
// Visitor Classes for Collecting Statistics
|
|
208
|
+
// ============================================================================
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Detects indentation patterns by examining block contents.
|
|
212
|
+
*/
|
|
213
|
+
class FindIndentVisitor extends JavaScriptVisitor<any> {
|
|
214
|
+
constructor(private stats: TabsAndIndentsStatistics) {
|
|
215
|
+
super();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
protected async visitBlock(block: J.Block, p: any): Promise<J | undefined> {
|
|
219
|
+
// Check indentation of statements in the block
|
|
220
|
+
for (const stmt of block.statements) {
|
|
221
|
+
const whitespace = stmt.element.prefix?.whitespace;
|
|
222
|
+
if (whitespace) {
|
|
223
|
+
this.analyzeIndent(whitespace);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return super.visitBlock(block, p);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private analyzeIndent(whitespace: string): void {
|
|
230
|
+
const newlineIndex = whitespace.lastIndexOf('\n');
|
|
231
|
+
if (newlineIndex < 0) return;
|
|
232
|
+
|
|
233
|
+
const indent = whitespace.substring(newlineIndex + 1);
|
|
234
|
+
if (indent.length === 0) return;
|
|
235
|
+
|
|
236
|
+
// Check first character to determine type
|
|
237
|
+
if (indent[0] === '\t') {
|
|
238
|
+
this.stats.recordTabIndent();
|
|
239
|
+
} else if (indent[0] === ' ') {
|
|
240
|
+
// Count consecutive spaces
|
|
241
|
+
let spaceCount = 0;
|
|
242
|
+
for (const char of indent) {
|
|
243
|
+
if (char === ' ') spaceCount++;
|
|
244
|
+
else break;
|
|
245
|
+
}
|
|
246
|
+
if (spaceCount > 0) {
|
|
247
|
+
this.stats.recordSpaceIndent(spaceCount);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Detects spacing patterns in imports and exports.
|
|
255
|
+
*/
|
|
256
|
+
class FindSpacesVisitor extends JavaScriptVisitor<any> {
|
|
257
|
+
constructor(private stats: SpacesStatistics) {
|
|
258
|
+
super();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
protected async visitImportDeclaration(import_: JS.Import, p: any): Promise<J | undefined> {
|
|
262
|
+
// Check ES6 import braces spacing: import { a } from 'x' vs import {a} from 'x'
|
|
263
|
+
if (import_.importClause?.namedBindings?.kind === JS.Kind.NamedImports) {
|
|
264
|
+
const namedImports = import_.importClause.namedBindings as JS.NamedImports;
|
|
265
|
+
if (namedImports.elements.elements.length > 0) {
|
|
266
|
+
const firstElement = namedImports.elements.elements[0];
|
|
267
|
+
const hasSpaceAfterOpenBrace = firstElement.element.prefix?.whitespace?.includes(' ') ?? false;
|
|
268
|
+
|
|
269
|
+
const lastElement = namedImports.elements.elements[namedImports.elements.elements.length - 1];
|
|
270
|
+
const hasSpaceBeforeCloseBrace = lastElement.after?.whitespace?.includes(' ') ?? false;
|
|
271
|
+
|
|
272
|
+
if (hasSpaceAfterOpenBrace || hasSpaceBeforeCloseBrace) {
|
|
273
|
+
this.stats.es6ImportExportBracesWithSpace++;
|
|
274
|
+
} else {
|
|
275
|
+
this.stats.es6ImportExportBracesWithoutSpace++;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return super.visitImportDeclaration(import_, p);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
protected async visitExportDeclaration(export_: JS.ExportDeclaration, p: any): Promise<J | undefined> {
|
|
283
|
+
// Check ES6 export braces spacing
|
|
284
|
+
if (export_.exportClause?.kind === JS.Kind.NamedExports) {
|
|
285
|
+
const namedExports = export_.exportClause as JS.NamedExports;
|
|
286
|
+
if (namedExports.elements.elements.length > 0) {
|
|
287
|
+
const firstElement = namedExports.elements.elements[0];
|
|
288
|
+
const hasSpaceAfterOpenBrace = firstElement.element.prefix?.whitespace?.includes(' ') ?? false;
|
|
289
|
+
|
|
290
|
+
const lastElement = namedExports.elements.elements[namedExports.elements.elements.length - 1];
|
|
291
|
+
const hasSpaceBeforeCloseBrace = lastElement.after?.whitespace?.includes(' ') ?? false;
|
|
292
|
+
|
|
293
|
+
if (hasSpaceAfterOpenBrace || hasSpaceBeforeCloseBrace) {
|
|
294
|
+
this.stats.es6ImportExportBracesWithSpace++;
|
|
295
|
+
} else {
|
|
296
|
+
this.stats.es6ImportExportBracesWithoutSpace++;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return super.visitExportDeclaration(export_, p);
|
|
301
|
+
}
|
|
302
|
+
}
|
package/src/javascript/format.ts
CHANGED
|
@@ -18,14 +18,8 @@ import {JavaScriptVisitor} from "./visitor";
|
|
|
18
18
|
import {Comment, J, lastWhitespace, replaceLastWhitespace, Statement} from "../java";
|
|
19
19
|
import {Draft, produce} from "immer";
|
|
20
20
|
import {Cursor, isScope, Tree} from "../tree";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
SpacesStyle,
|
|
24
|
-
styleFromSourceFile,
|
|
25
|
-
StyleKind,
|
|
26
|
-
TabsAndIndentsStyle,
|
|
27
|
-
WrappingAndBracesStyle
|
|
28
|
-
} from "./style";
|
|
21
|
+
import {BlankLinesStyle, getStyle, SpacesStyle, StyleKind, TabsAndIndentsStyle, WrappingAndBracesStyle} from "./style";
|
|
22
|
+
import {NamedStyles} from "../style";
|
|
29
23
|
import {produceAsync} from "../visitor";
|
|
30
24
|
import {findMarker} from "../markers";
|
|
31
25
|
import {Generator} from "./markers";
|
|
@@ -40,22 +34,33 @@ export const maybeAutoFormat = async <J2 extends J, P>(before: J2, after: J2, p:
|
|
|
40
34
|
return after;
|
|
41
35
|
}
|
|
42
36
|
|
|
43
|
-
export const autoFormat = async <J2 extends J, P>(j: J2, p: P, stopAfter?: J, parent?: Cursor): Promise<J2> =>
|
|
44
|
-
(await new AutoformatVisitor(stopAfter).visit(j, p, parent) as J2);
|
|
37
|
+
export const autoFormat = async <J2 extends J, P>(j: J2, p: P, stopAfter?: J, parent?: Cursor, styles?: NamedStyles[]): Promise<J2> =>
|
|
38
|
+
(await new AutoformatVisitor(stopAfter, styles).visit(j, p, parent) as J2);
|
|
45
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Formats JavaScript/TypeScript code using a comprehensive set of formatting rules.
|
|
42
|
+
*
|
|
43
|
+
* Style resolution order (first match wins):
|
|
44
|
+
* 1. Styles passed to the constructor
|
|
45
|
+
* 2. Styles from source file markers (NamedStyles)
|
|
46
|
+
* 3. IntelliJ defaults
|
|
47
|
+
*/
|
|
46
48
|
export class AutoformatVisitor<P> extends JavaScriptVisitor<P> {
|
|
47
|
-
|
|
49
|
+
private readonly styles?: NamedStyles[];
|
|
50
|
+
|
|
51
|
+
constructor(private stopAfter?: Tree, styles?: NamedStyles[]) {
|
|
48
52
|
super();
|
|
53
|
+
this.styles = styles;
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
async visit<R extends J>(tree: Tree, p: P, cursor?: Cursor): Promise<R | undefined> {
|
|
52
57
|
const visitors = [
|
|
53
58
|
new NormalizeWhitespaceVisitor(this.stopAfter),
|
|
54
59
|
new MinimumViableSpacingVisitor(this.stopAfter),
|
|
55
|
-
new BlankLinesVisitor(
|
|
56
|
-
new WrappingAndBracesVisitor(
|
|
57
|
-
new SpacesVisitor(
|
|
58
|
-
new TabsAndIndentsVisitor(
|
|
60
|
+
new BlankLinesVisitor(getStyle(StyleKind.BlankLinesStyle, tree, this.styles) as BlankLinesStyle, this.stopAfter),
|
|
61
|
+
new WrappingAndBracesVisitor(getStyle(StyleKind.WrappingAndBracesStyle, tree, this.styles) as WrappingAndBracesStyle, this.stopAfter),
|
|
62
|
+
new SpacesVisitor(getStyle(StyleKind.SpacesStyle, tree, this.styles) as SpacesStyle, this.stopAfter),
|
|
63
|
+
new TabsAndIndentsVisitor(getStyle(StyleKind.TabsAndIndentsStyle, tree, this.styles) as TabsAndIndentsStyle, this.stopAfter),
|
|
59
64
|
]
|
|
60
65
|
|
|
61
66
|
let t: R | undefined = tree as R;
|
|
@@ -208,7 +213,19 @@ export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
208
213
|
public async visitContainer<T extends J>(container: J.Container<T>, p: P): Promise<J.Container<T>> {
|
|
209
214
|
const ret = await super.visitContainer(container, p) as J.Container<T>;
|
|
210
215
|
return produce(ret, draft => {
|
|
216
|
+
if (draft.elements.length > 0) {
|
|
217
|
+
// Apply beforeComma rule to all elements except the last
|
|
218
|
+
// (last element's after is before closing bracket, not a comma)
|
|
219
|
+
for (let i = 0; i < draft.elements.length - 1; i++) {
|
|
220
|
+
const afterWs = draft.elements[i].after.whitespace;
|
|
221
|
+
// Preserve newlines - only adjust when on same line
|
|
222
|
+
if (!afterWs.includes("\n")) {
|
|
223
|
+
draft.elements[i].after.whitespace = this.style.other.beforeComma ? " " : "";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
211
227
|
if (draft.elements.length > 1) {
|
|
228
|
+
// Apply afterComma rule to elements after the first
|
|
212
229
|
for (let i = 1; i < draft.elements.length; i++) {
|
|
213
230
|
const currentWs = draft.elements[i].element.prefix.whitespace;
|
|
214
231
|
// Preserve original newlines - only adjust spacing when elements are on same line
|
|
@@ -228,8 +245,20 @@ export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
228
245
|
if (draft.exportClause.kind == JS.Kind.NamedExports) {
|
|
229
246
|
const ne = (draft.exportClause as Draft<JS.NamedExports>);
|
|
230
247
|
if (ne.elements.elements.length > 0) {
|
|
231
|
-
|
|
232
|
-
ne.elements.elements
|
|
248
|
+
// Check if this is a multi-line export (any element's prefix has a newline)
|
|
249
|
+
const isMultiLine = ne.elements.elements.some(e => e.element.prefix.whitespace.includes("\n"));
|
|
250
|
+
if (!isMultiLine) {
|
|
251
|
+
// Single-line: adjust brace spacing
|
|
252
|
+
ne.elements.elements[0].element.prefix.whitespace = this.style.within.es6ImportExportBraces ? " " : "";
|
|
253
|
+
ne.elements.elements[ne.elements.elements.length - 1].after.whitespace = this.style.within.es6ImportExportBraces ? " " : "";
|
|
254
|
+
} else {
|
|
255
|
+
// Multi-line: apply beforeComma rule to last element's after (for trailing commas)
|
|
256
|
+
// If it has only spaces (no newline), it's the space before a trailing comma
|
|
257
|
+
const lastAfter = ne.elements.elements[ne.elements.elements.length - 1].after.whitespace;
|
|
258
|
+
if (!lastAfter.includes("\n") && lastAfter.trim() === "") {
|
|
259
|
+
ne.elements.elements[ne.elements.elements.length - 1].after.whitespace = this.style.other.beforeComma ? " " : "";
|
|
260
|
+
}
|
|
261
|
+
}
|
|
233
262
|
}
|
|
234
263
|
}
|
|
235
264
|
}
|
|
@@ -290,8 +319,20 @@ export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
290
319
|
draft.importClause.namedBindings.prefix.whitespace = " ";
|
|
291
320
|
if (draft.importClause.namedBindings.kind == JS.Kind.NamedImports) {
|
|
292
321
|
const ni = draft.importClause.namedBindings as Draft<JS.NamedImports>;
|
|
293
|
-
|
|
294
|
-
ni.elements.elements
|
|
322
|
+
// Check if this is a multi-line import (any element's prefix has a newline)
|
|
323
|
+
const isMultiLine = ni.elements.elements.some(e => e.element.prefix.whitespace.includes("\n"));
|
|
324
|
+
if (!isMultiLine) {
|
|
325
|
+
// Single-line: adjust brace spacing
|
|
326
|
+
ni.elements.elements[0].element.prefix.whitespace = this.style.within.es6ImportExportBraces ? " " : "";
|
|
327
|
+
ni.elements.elements[ni.elements.elements.length - 1].after.whitespace = this.style.within.es6ImportExportBraces ? " " : "";
|
|
328
|
+
} else {
|
|
329
|
+
// Multi-line: apply beforeComma rule to last element's after (for trailing commas)
|
|
330
|
+
// If it has only spaces (no newline), it's the space before a trailing comma
|
|
331
|
+
const lastAfter = ni.elements.elements[ni.elements.elements.length - 1].after.whitespace;
|
|
332
|
+
if (!lastAfter.includes("\n") && lastAfter.trim() === "") {
|
|
333
|
+
ni.elements.elements[ni.elements.elements.length - 1].after.whitespace = this.style.other.beforeComma ? " " : "";
|
|
334
|
+
}
|
|
335
|
+
}
|
|
295
336
|
}
|
|
296
337
|
}
|
|
297
338
|
}
|
|
@@ -363,9 +404,14 @@ export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
363
404
|
|
|
364
405
|
protected async visitPropertyAssignment(propertyAssignment: JS.PropertyAssignment, p: P): Promise<J | undefined> {
|
|
365
406
|
const pa = await super.visitPropertyAssignment(propertyAssignment, p) as JS.PropertyAssignment;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
407
|
+
// Only adjust the space before the colon if there's an initializer (not a shorthand property)
|
|
408
|
+
// For shorthand properties like { headers }, name.after.whitespace is the space before }
|
|
409
|
+
if (pa.initializer) {
|
|
410
|
+
return produceAsync(pa, draft => {
|
|
411
|
+
draft.name.after.whitespace = this.style.other.beforePropertyNameValueSeparator ? " " : "";
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
return pa;
|
|
369
415
|
}
|
|
370
416
|
|
|
371
417
|
protected async visitSwitch(switchNode: J.Switch, p: P): Promise<J | undefined> {
|
|
@@ -432,6 +478,21 @@ export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
432
478
|
});
|
|
433
479
|
}
|
|
434
480
|
|
|
481
|
+
protected async visitTypeLiteral(typeLiteral: JS.TypeLiteral, p: P): Promise<J | undefined> {
|
|
482
|
+
const ret = await super.visitTypeLiteral(typeLiteral, p) as JS.TypeLiteral;
|
|
483
|
+
// Apply objectLiteralTypeBraces spacing for single-line type literals
|
|
484
|
+
const isSingleLine = !ret.members.end.whitespace.includes("\n") &&
|
|
485
|
+
ret.members.statements.every(s => !s.element.prefix.whitespace.includes("\n"));
|
|
486
|
+
if (isSingleLine && ret.members.statements.length > 0) {
|
|
487
|
+
return produce(ret, draft => {
|
|
488
|
+
const space = this.style.within.objectLiteralTypeBraces ? " " : "";
|
|
489
|
+
draft.members.statements[0].element.prefix.whitespace = space;
|
|
490
|
+
draft.members.end.whitespace = space;
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return ret;
|
|
494
|
+
}
|
|
495
|
+
|
|
435
496
|
protected async visitUnary(unary: J.Unary, p: P): Promise<J | undefined> {
|
|
436
497
|
const ret = await super.visitUnary(unary, p) as J.Unary;
|
|
437
498
|
return produce(ret, draft => {
|
|
@@ -681,9 +742,10 @@ export class WrappingAndBracesVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
681
742
|
const b = await super.visitBlock(block, p) as J.Block;
|
|
682
743
|
return produce(b, draft => {
|
|
683
744
|
if (!draft.end.whitespace.includes("\n") && (draft.statements.length == 0 || !draft.statements[draft.statements.length - 1].after.whitespace.includes("\n"))) {
|
|
684
|
-
// Skip newline for object literals and empty lambda/function bodies
|
|
745
|
+
// Skip newline for object literals, type literals, and empty lambda/function bodies
|
|
685
746
|
const parentKind = this.cursor.parent?.value.kind;
|
|
686
747
|
if (parentKind !== J.Kind.NewClass &&
|
|
748
|
+
parentKind !== JS.Kind.TypeLiteral &&
|
|
687
749
|
!(draft.statements.length === 0 && (parentKind === J.Kind.Lambda || parentKind === J.Kind.MethodDeclaration))) {
|
|
688
750
|
draft.end = this.withNewlineSpace(draft.end);
|
|
689
751
|
}
|
|
@@ -1012,14 +1074,21 @@ export class BlankLinesVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
1012
1074
|
if (statements.length > 0) {
|
|
1013
1075
|
this.keepMaximumBlankLines(draft.body.statements[0].element, 0);
|
|
1014
1076
|
|
|
1077
|
+
const isInterface = draft.classKind.type === J.ClassDeclaration.Kind.Type.Interface;
|
|
1015
1078
|
for (let i = 1; i < statements.length; i++) {
|
|
1016
1079
|
const previousElement = statements[i - 1].element;
|
|
1017
1080
|
let currentElement = statements[i].element;
|
|
1018
1081
|
if (previousElement.kind == J.Kind.VariableDeclarations || currentElement.kind == J.Kind.VariableDeclarations) {
|
|
1019
|
-
|
|
1082
|
+
const fieldBlankLines = isInterface
|
|
1083
|
+
? this.style.minimum.aroundFieldInInterface ?? 0
|
|
1084
|
+
: this.style.minimum.aroundField;
|
|
1085
|
+
this.minimumBlankLines(currentElement, fieldBlankLines);
|
|
1020
1086
|
}
|
|
1021
1087
|
if (previousElement.kind == J.Kind.MethodDeclaration || currentElement.kind == J.Kind.MethodDeclaration) {
|
|
1022
|
-
|
|
1088
|
+
const methodBlankLines = isInterface
|
|
1089
|
+
? this.style.minimum.aroundMethodInInterface ?? 0
|
|
1090
|
+
: this.style.minimum.aroundMethod;
|
|
1091
|
+
this.minimumBlankLines(currentElement, methodBlankLines);
|
|
1023
1092
|
}
|
|
1024
1093
|
this.keepMaximumBlankLines(currentElement, this.style.keepMaximum.inCode);
|
|
1025
1094
|
draft.body.statements[i].element = currentElement;
|
|
@@ -1072,7 +1141,7 @@ export class BlankLinesVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
1072
1141
|
}
|
|
1073
1142
|
this.keepMaximumBlankLines(draft, this.style.keepMaximum.inCode);
|
|
1074
1143
|
}
|
|
1075
|
-
} else if (parent?.kind === J.Kind.Block && grandparent?.kind !== J.Kind.NewClass ||
|
|
1144
|
+
} else if (parent?.kind === J.Kind.Block && grandparent?.kind !== J.Kind.NewClass && grandparent?.kind !== JS.Kind.TypeLiteral ||
|
|
1076
1145
|
(parent?.kind === JS.Kind.CompilationUnit && (parent! as JS.CompilationUnit).statements[0].element.id != draft.id) ||
|
|
1077
1146
|
(parent?.kind === J.Kind.Case)) {
|
|
1078
1147
|
if (draft.kind != J.Kind.Case) {
|
|
@@ -1086,8 +1155,8 @@ export class BlankLinesVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
1086
1155
|
const b = await super.visitBlock(block, p) as J.Block;
|
|
1087
1156
|
return produce(b, draft => {
|
|
1088
1157
|
const parentKind = this.cursor.parent?.value.kind;
|
|
1089
|
-
// Skip newline
|
|
1090
|
-
if (parentKind != J.Kind.NewClass) {
|
|
1158
|
+
// Skip newline for object literals (NewClass) and type literals (TypeLiteral)
|
|
1159
|
+
if (parentKind != J.Kind.NewClass && parentKind != JS.Kind.TypeLiteral) {
|
|
1091
1160
|
draft.end = replaceLastWhitespace(draft.end, ws =>
|
|
1092
1161
|
ws.includes("\n") ? ws : ws.replace(/[ \t]+$/, '') + "\n"
|
|
1093
1162
|
);
|
package/src/javascript/index.ts
CHANGED
|
@@ -14,31 +14,66 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import {ScanningRecipe} from "../../recipe";
|
|
18
18
|
import {TreeVisitor} from "../../visitor";
|
|
19
19
|
import {ExecutionContext} from "../../execution";
|
|
20
20
|
import {AutoformatVisitor} from "../format";
|
|
21
|
+
import {Autodetect, Detector} from "../autodetect";
|
|
22
|
+
import {JavaScriptVisitor} from "../visitor";
|
|
23
|
+
import {JS} from "../tree";
|
|
24
|
+
import {J} from "../../java";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Accumulator for the AutoFormat scanning recipe.
|
|
28
|
+
* Holds the Detector that collects formatting statistics during the scan phase.
|
|
29
|
+
*/
|
|
30
|
+
interface AutoFormatAccumulator {
|
|
31
|
+
detector: Detector;
|
|
32
|
+
detectedStyles?: Autodetect;
|
|
33
|
+
}
|
|
21
34
|
|
|
22
35
|
/**
|
|
23
36
|
* Formats JavaScript/TypeScript code using a comprehensive set of formatting rules.
|
|
24
37
|
*
|
|
25
|
-
* This
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* -
|
|
31
|
-
* -
|
|
38
|
+
* This is a scanning recipe that:
|
|
39
|
+
* 1. Scans all source files to detect the project's existing formatting style
|
|
40
|
+
* 2. Applies consistent formatting based on the detected style
|
|
41
|
+
*
|
|
42
|
+
* The detected formatting includes:
|
|
43
|
+
* - Tabs vs spaces preference
|
|
44
|
+
* - Indent size (2, 4, etc.)
|
|
45
|
+
* - ES6 import/export brace spacing
|
|
32
46
|
*
|
|
33
|
-
*
|
|
34
|
-
* or defaults to IntelliJ IDEA style if no custom style is specified.
|
|
47
|
+
* If no clear style is detected, defaults to IntelliJ IDEA style.
|
|
35
48
|
*/
|
|
36
|
-
export class AutoFormat extends
|
|
49
|
+
export class AutoFormat extends ScanningRecipe<AutoFormatAccumulator> {
|
|
37
50
|
readonly name = "org.openrewrite.javascript.format.auto-format";
|
|
38
51
|
readonly displayName = "Auto-format JavaScript/TypeScript code";
|
|
39
|
-
readonly description = "Format JavaScript and TypeScript code using
|
|
52
|
+
readonly description = "Format JavaScript and TypeScript code using formatting rules auto-detected from the project's existing code style.";
|
|
53
|
+
|
|
54
|
+
initialValue(_ctx: ExecutionContext): AutoFormatAccumulator {
|
|
55
|
+
return {
|
|
56
|
+
detector: Autodetect.detector()
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async scanner(acc: AutoFormatAccumulator): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
61
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
62
|
+
protected async visitJsCompilationUnit(cu: JS.CompilationUnit, ctx: ExecutionContext): Promise<J | undefined> {
|
|
63
|
+
await acc.detector.sample(cu);
|
|
64
|
+
return cu;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async editorWithData(acc: AutoFormatAccumulator): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
70
|
+
// Build detected styles once (lazily on first edit)
|
|
71
|
+
if (!acc.detectedStyles) {
|
|
72
|
+
acc.detectedStyles = acc.detector.build();
|
|
73
|
+
}
|
|
40
74
|
|
|
41
|
-
|
|
42
|
-
|
|
75
|
+
// Pass detected styles to the AutoformatVisitor
|
|
76
|
+
// Autodetect is a NamedStyles, so pass it as an array
|
|
77
|
+
return new AutoformatVisitor(undefined, [acc.detectedStyles]);
|
|
43
78
|
}
|
|
44
79
|
}
|
package/src/javascript/style.ts
CHANGED
|
@@ -498,3 +498,35 @@ export function styleFromSourceFile(styleKind: string, sourceFile: Tree): Style
|
|
|
498
498
|
}
|
|
499
499
|
return IntelliJ.TypeScript.defaults.styles.find(style => style.kind === styleKind) as Style;
|
|
500
500
|
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Get a style by kind, with passed-in styles taking precedence over source file styles.
|
|
504
|
+
* Falls back to IntelliJ defaults if no style is found.
|
|
505
|
+
*
|
|
506
|
+
* @param styleKind The kind of style to retrieve
|
|
507
|
+
* @param sourceFile The source file to check for styles
|
|
508
|
+
* @param styles Optional array of NamedStyles that take precedence over source file styles
|
|
509
|
+
*/
|
|
510
|
+
export function getStyle(styleKind: string, sourceFile: Tree, styles?: NamedStyles[]): Style | undefined {
|
|
511
|
+
// First check passed-in styles (highest precedence)
|
|
512
|
+
if (styles) {
|
|
513
|
+
for (const namedStyle of styles) {
|
|
514
|
+
const found = namedStyle.styles.find(s => s.kind === styleKind);
|
|
515
|
+
if (found) {
|
|
516
|
+
return found;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Then check source file markers
|
|
522
|
+
const namedStyles = sourceFile.markers.markers.filter(marker => marker.kind === MarkersKind.NamedStyles) as NamedStyles[];
|
|
523
|
+
for (const namedStyle of namedStyles) {
|
|
524
|
+
const found = namedStyle.styles.find(s => s.kind === styleKind);
|
|
525
|
+
if (found) {
|
|
526
|
+
return found;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Fall back to defaults
|
|
531
|
+
return IntelliJ.TypeScript.defaults.styles.find(style => style.kind === styleKind) as Style;
|
|
532
|
+
}
|