/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://docs.moderne.io/licensing/moderne-source-available-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {NamedStyles, Style} from "../style";
import {randomId} from "../uuid";
import {SourceFile} from "../tree";
import {JS} from "./tree";
import {JavaScriptVisitor} from "./visitor";
import {J} from "../java";
import {
IntelliJ,
SpacesStyle,
StyleKind,
TabsAndIndentsStyle,
WrappingAndBracesStyle,
WrappingAndBracesStyleDetailKind
} from "./style";
import {UUID} from "../uuid";
/**
* Auto-detected styles for JavaScript/TypeScript code.
* Focuses on key formatting variations where projects differ:
* - Tabs vs spaces
* - Indent size (2, 4, etc.)
* - Spaces within ES6 import/export braces
*/
export interface Autodetect extends NamedStyles {
readonly kind: typeof StyleKind.Autodetect;
readonly name: "org.openrewrite.javascript.Autodetect";
readonly displayName: "Auto-detected";
readonly description: "Automatically detect styles from a repository's existing code.";
}
export function autodetect(id: UUID, styles: Style[]): Autodetect {
return {
kind: StyleKind.Autodetect,
id,
name: "org.openrewrite.javascript.Autodetect",
displayName: "Auto-detected",
description: "Automatically detect styles from a repository's existing code.",
tags: [],
styles
};
}
export namespace Autodetect {
export function detector(): Detector {
return new Detector();
}
}
/**
* Collects formatting statistics from source files and builds auto-detected styles.
*/
export class Detector {
private readonly tabsAndIndentsStats = new TabsAndIndentsStatistics();
private readonly spacesStats = new SpacesStatistics();
private readonly wrappingAndBracesStats = new WrappingAndBracesStatistics();
/**
* Sample a source file to collect formatting statistics.
*/
async sample(sourceFile: SourceFile): Promise {
if (sourceFile.kind === JS.Kind.CompilationUnit) {
await this.sampleJavaScript(sourceFile as JS.CompilationUnit);
}
}
/**
* Sample a JavaScript/TypeScript compilation unit.
*/
async sampleJavaScript(cu: JS.CompilationUnit): Promise {
await new FindIndentVisitor(this.tabsAndIndentsStats).visit(cu, {});
await new FindSpacesVisitor(this.spacesStats).visit(cu, {});
await new FindWrappingAndBracesVisitor(this.wrappingAndBracesStats).visit(cu, {});
}
/**
* Build the auto-detected styles from collected statistics.
*/
build(): Autodetect {
return autodetect(randomId(), [
this.tabsAndIndentsStats.getTabsAndIndentsStyle(),
this.spacesStats.getSpacesStyle(),
this.getWrappingAndBracesStyle(),
]);
}
getTabsAndIndentsStyle(): TabsAndIndentsStyle {
return this.tabsAndIndentsStats.getTabsAndIndentsStyle();
}
getSpacesStyle(): SpacesStyle {
return this.spacesStats.getSpacesStyle();
}
getWrappingAndBracesStyle(): WrappingAndBracesStyle {
return this.wrappingAndBracesStats.getWrappingAndBracesStyle();
}
}
// ============================================================================
// Statistics Classes
// ============================================================================
/**
* Tracks indentation patterns to detect tabs vs spaces and indent size.
*/
class TabsAndIndentsStatistics {
private totalSpaceIndents = 0;
private totalTabIndents = 0;
// Track all observed indent sizes to compute GCD
private observedIndents: number[] = [];
recordSpaceIndent(spaceCount: number): void {
this.totalSpaceIndents++;
if (spaceCount > 0) {
this.observedIndents.push(spaceCount);
}
}
recordTabIndent(): void {
this.totalTabIndents++;
}
getTabsAndIndentsStyle(): TabsAndIndentsStyle {
// Determine if using tabs or spaces
const useTabs = this.totalTabIndents > this.totalSpaceIndents;
// Find indent size by computing GCD of all observed indents
// This correctly handles 2-space files where we see 2, 4, 6, 8... (all multiples of 2)
let detectedIndentSize = 4; // Default
if (this.observedIndents.length > 0) {
// Compute GCD of all observed indents
let gcd = this.observedIndents[0];
for (let i = 1; i < this.observedIndents.length; i++) {
gcd = this.computeGcd(gcd, this.observedIndents[i]);
if (gcd === 1) break; // Can't get smaller than 1
}
// Only use common indent sizes (2, 4, 8)
if (gcd === 2 || gcd === 4 || gcd === 8) {
detectedIndentSize = gcd;
} else if (gcd > 0 && gcd % 4 === 0) {
detectedIndentSize = 4;
} else if (gcd > 0 && gcd % 2 === 0) {
detectedIndentSize = 2;
}
}
return {
kind: StyleKind.TabsAndIndentsStyle,
useTabCharacter: useTabs,
tabSize: 4,
indentSize: detectedIndentSize,
continuationIndent: detectedIndentSize * 2,
keepIndentsOnEmptyLines: false,
indentChainedMethods: true,
indentAllChainedCallsInAGroup: false
};
}
private computeGcd(a: number, b: number): number {
while (b !== 0) {
const temp = b;
b = a % b;
a = temp;
}
return a;
}
}
/**
* Tracks spacing patterns around braces and other constructs.
*/
class SpacesStatistics {
// Track spaces within ES6 import/export braces: { a } vs {a}
es6ImportExportBracesWithSpace = 0;
es6ImportExportBracesWithoutSpace = 0;
// Track spaces within object literal braces: { foo: 1 } vs {foo: 1}
objectLiteralBracesWithSpace = 0;
objectLiteralBracesWithoutSpace = 0;
// Track spaces within object literal type braces: { foo: string } vs {foo: string}
objectLiteralTypeBracesWithSpace = 0;
objectLiteralTypeBracesWithoutSpace = 0;
getSpacesStyle(): SpacesStyle {
// Use TypeScript defaults as base since most modern JS/TS projects use similar conventions
// TypeScript defaults include afterTypeReferenceColon: true which is commonly expected
const defaults = IntelliJ.TypeScript.spaces();
return {
...defaults,
within: {
...defaults.within,
es6ImportExportBraces: this.es6ImportExportBracesWithSpace > this.es6ImportExportBracesWithoutSpace,
objectLiteralBraces: this.objectLiteralBracesWithSpace > this.objectLiteralBracesWithoutSpace,
objectLiteralTypeBraces: this.objectLiteralTypeBracesWithSpace > this.objectLiteralTypeBracesWithoutSpace
}
};
}
}
/**
* Tracks wrapping and braces patterns for simple (empty) blocks and methods.
*/
class WrappingAndBracesStatistics {
// Track simple blocks (not method/function bodies): {} vs {\n}
simpleBlocksOnOneLine = 0;
simpleBlocksOnMultipleLines = 0;
// Track simple method/function bodies: {} vs {\n}
simpleMethodsOnOneLine = 0;
simpleMethodsOnMultipleLines = 0;
recordSimpleBlock(isOnOneLine: boolean): void {
if (isOnOneLine) {
this.simpleBlocksOnOneLine++;
} else {
this.simpleBlocksOnMultipleLines++;
}
}
recordSimpleMethod(isOnOneLine: boolean): void {
if (isOnOneLine) {
this.simpleMethodsOnOneLine++;
} else {
this.simpleMethodsOnMultipleLines++;
}
}
getWrappingAndBracesStyle(): WrappingAndBracesStyle {
return {
kind: StyleKind.WrappingAndBracesStyle,
ifStatement: {
kind: WrappingAndBracesStyleDetailKind.WrappingAndBracesStyleIfStatement,
elseOnNewLine: false
},
keepWhenReformatting: {
kind: WrappingAndBracesStyleDetailKind.WrappingAndBracesStyleKeepWhenReformatting,
simpleBlocksInOneLine: this.simpleBlocksOnOneLine > this.simpleBlocksOnMultipleLines,
simpleMethodsInOneLine: this.simpleMethodsOnOneLine > this.simpleMethodsOnMultipleLines
}
};
}
}
// ============================================================================
// Visitor Classes for Collecting Statistics
// ============================================================================
/**
* Detects indentation patterns by examining block contents.
*/
class FindIndentVisitor extends JavaScriptVisitor {
constructor(private stats: TabsAndIndentsStatistics) {
super();
}
protected async visitBlock(block: J.Block, p: any): Promise {
// Check indentation of statements in the block
for (const stmt of block.statements) {
const whitespace = stmt.element.prefix?.whitespace;
if (whitespace) {
this.analyzeIndent(whitespace);
}
}
return super.visitBlock(block, p);
}
private analyzeIndent(whitespace: string): void {
const newlineIndex = whitespace.lastIndexOf('\n');
if (newlineIndex < 0) return;
const indent = whitespace.substring(newlineIndex + 1);
if (indent.length === 0) return;
// Check first character to determine type
if (indent[0] === '\t') {
this.stats.recordTabIndent();
} else if (indent[0] === ' ') {
// Count consecutive spaces
let spaceCount = 0;
for (const char of indent) {
if (char === ' ') spaceCount++;
else break;
}
if (spaceCount > 0) {
this.stats.recordSpaceIndent(spaceCount);
}
}
}
}
/**
* Detects spacing patterns in imports and exports.
*/
class FindSpacesVisitor extends JavaScriptVisitor {
constructor(private stats: SpacesStatistics) {
super();
}
protected async visitImportDeclaration(import_: JS.Import, p: any): Promise {
// Check ES6 import braces spacing: import { a } from 'x' vs import {a} from 'x'
if (import_.importClause?.namedBindings?.kind === JS.Kind.NamedImports) {
const namedImports = import_.importClause.namedBindings as JS.NamedImports;
if (namedImports.elements.elements.length > 0) {
const firstElement = namedImports.elements.elements[0];
const hasSpaceAfterOpenBrace = firstElement.element.prefix?.whitespace?.includes(' ') ?? false;
const lastElement = namedImports.elements.elements[namedImports.elements.elements.length - 1];
const hasSpaceBeforeCloseBrace = lastElement.after?.whitespace?.includes(' ') ?? false;
if (hasSpaceAfterOpenBrace || hasSpaceBeforeCloseBrace) {
this.stats.es6ImportExportBracesWithSpace++;
} else {
this.stats.es6ImportExportBracesWithoutSpace++;
}
}
}
return super.visitImportDeclaration(import_, p);
}
protected async visitExportDeclaration(export_: JS.ExportDeclaration, p: any): Promise {
// Check ES6 export braces spacing
if (export_.exportClause?.kind === JS.Kind.NamedExports) {
const namedExports = export_.exportClause as JS.NamedExports;
if (namedExports.elements.elements.length > 0) {
const firstElement = namedExports.elements.elements[0];
const hasSpaceAfterOpenBrace = firstElement.element.prefix?.whitespace?.includes(' ') ?? false;
const lastElement = namedExports.elements.elements[namedExports.elements.elements.length - 1];
const hasSpaceBeforeCloseBrace = lastElement.after?.whitespace?.includes(' ') ?? false;
if (hasSpaceAfterOpenBrace || hasSpaceBeforeCloseBrace) {
this.stats.es6ImportExportBracesWithSpace++;
} else {
this.stats.es6ImportExportBracesWithoutSpace++;
}
}
}
return super.visitExportDeclaration(export_, p);
}
protected async visitNewClass(newClass: J.NewClass, p: any): Promise {
// Only handle object literals (NewClass with no class/constructor)
if (!newClass.class && newClass.body && newClass.body.statements.length > 0) {
const stmts = newClass.body.statements;
// Check if single-line (no newlines in any element prefix or in end)
const isMultiLine = stmts.some(s => s.element.prefix?.whitespace?.includes('\n')) ||
newClass.body.end?.whitespace?.includes('\n');
if (!isMultiLine) {
const firstElement = stmts[0];
const hasSpaceAfterOpenBrace = firstElement.element.prefix?.whitespace?.includes(' ') ?? false;
// For object literals, the space before } is in body.end, not in last statement's after
const hasSpaceBeforeCloseBrace = newClass.body.end?.whitespace?.includes(' ') ?? false;
if (hasSpaceAfterOpenBrace || hasSpaceBeforeCloseBrace) {
this.stats.objectLiteralBracesWithSpace++;
} else {
this.stats.objectLiteralBracesWithoutSpace++;
}
}
}
return super.visitNewClass(newClass, p);
}
protected async visitTypeLiteral(typeLiteral: JS.TypeLiteral, p: any): Promise {
// Check type literal braces spacing: { foo: string } vs {foo: string}
if (typeLiteral.members && typeLiteral.members.statements.length > 0) {
const stmts = typeLiteral.members.statements;
// Check if single-line (no newlines in any element prefix or in end)
const isMultiLine = stmts.some(s => s.element.prefix?.whitespace?.includes('\n')) ||
typeLiteral.members.end?.whitespace?.includes('\n');
if (!isMultiLine) {
const firstElement = stmts[0];
const hasSpaceAfterOpenBrace = firstElement.element.prefix?.whitespace?.includes(' ') ?? false;
// For type literals, the space before } is in members.end, not in last statement's after
const hasSpaceBeforeCloseBrace = typeLiteral.members.end?.whitespace?.includes(' ') ?? false;
if (hasSpaceAfterOpenBrace || hasSpaceBeforeCloseBrace) {
this.stats.objectLiteralTypeBracesWithSpace++;
} else {
this.stats.objectLiteralTypeBracesWithoutSpace++;
}
}
}
return super.visitTypeLiteral(typeLiteral, p);
}
}
/**
* Detects wrapping and braces patterns for simple (empty) blocks.
*/
class FindWrappingAndBracesVisitor extends JavaScriptVisitor {
constructor(private stats: WrappingAndBracesStatistics) {
super();
}
protected async visitBlock(block: J.Block, p: any): Promise {
// Check if this is a simple block (empty or contains only J.Empty)
const isSimple = block.statements.length === 0 ||
(block.statements.length === 1 && block.statements[0].element.kind === J.Kind.Empty);
if (isSimple) {
// Determine if block is on one line by checking for newlines
const hasNewlineInEnd = block.end?.whitespace?.includes('\n') ?? false;
const hasNewlineInStatements = block.statements.length > 0 &&
(block.statements[0].element.prefix?.whitespace?.includes('\n') ||
block.statements[0].after?.whitespace?.includes('\n'));
const isOnOneLine = !hasNewlineInEnd && !hasNewlineInStatements;
// Determine parent kind to classify as block or method
const parent = this.cursor.parent?.value;
const isMethodOrFunctionBody = parent?.kind === J.Kind.Lambda ||
parent?.kind === J.Kind.MethodDeclaration;
if (isMethodOrFunctionBody) {
this.stats.recordSimpleMethod(isOnOneLine);
} else {
// Skip object literals and type literals
if (parent?.kind !== J.Kind.NewClass && parent?.kind !== JS.Kind.TypeLiteral) {
this.stats.recordSimpleBlock(isOnOneLine);
}
}
}
return super.visitBlock(block, p);
}
}