/*
* 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 {JS} from "../tree";
import {JavaScriptVisitor} from "../visitor";
import {Comment, J, lastWhitespace, replaceLastWhitespace, Statement} from "../../java";
import {create as produce, Draft} from "mutative";
import {Cursor, isScope, Tree} from "../../tree";
import {BlankLinesStyle, getStyle, SpacesStyle, StyleKind, TabsAndIndentsStyle, WrappingAndBracesStyle} from "../style";
import {NamedStyles} from "../../style";
import {produceAsync} from "../../visitor";
import {Generator} from "../markers";
import {TabsAndIndentsVisitor} from "./tabs-and-indents-visitor";
import {NormalizeWhitespaceVisitor} from "./normalize-whitespace-visitor";
import {MinimumViableSpacingVisitor} from "./minimum-viable-spacing-visitor";
import {applyPrettierFormatting, getPrettierStyle} from "./prettier-format";
export {TabsAndIndentsVisitor} from "./tabs-and-indents-visitor";
export {NormalizeWhitespaceVisitor} from "./normalize-whitespace-visitor";
export {MinimumViableSpacingVisitor} from "./minimum-viable-spacing-visitor";
export const maybeAutoFormat = async (before: J2, after: J2, p: P, stopAfter?: J, parent?: Cursor): Promise => {
if (before !== after) {
return autoFormat(after, p, stopAfter, parent);
}
return after;
}
export const autoFormat = async (
j: J2,
p: P,
stopAfter?: J,
parent?: Cursor,
styles?: NamedStyles[]
): Promise =>
(await new AutoformatVisitor(stopAfter, styles).visit(j, p, parent) as J2);
/**
* Formats JavaScript/TypeScript code using a comprehensive set of formatting rules.
*
* Style resolution order (first match wins):
* 1. Styles passed to the constructor
* 2. Styles from source file markers (NamedStyles)
* 3. IntelliJ defaults
*
* When a PrettierStyle is present (either in the styles array or as a marker on the source file),
* Prettier is used for formatting. Otherwise, built-in formatting visitors are used.
*/
export class AutoformatVisitor extends JavaScriptVisitor
{
private readonly styles?: NamedStyles[];
constructor(private stopAfter?: Tree, styles?: NamedStyles[]) {
super();
this.styles = styles;
}
async visit(tree: Tree, p: P, cursor?: Cursor): Promise {
// Check for PrettierStyle in styles array or as marker on source file
// If found, delegate entirely to Prettier (skip other formatting visitors)
const prettierStyle = getPrettierStyle(tree, cursor, this.styles);
if (prettierStyle) {
return applyPrettierFormatting(tree as R, prettierStyle, p, cursor, this.stopAfter);
}
const visitors = [
new NormalizeWhitespaceVisitor(this.stopAfter),
new MinimumViableSpacingVisitor(this.stopAfter),
new BlankLinesVisitor(getStyle(StyleKind.BlankLinesStyle, tree, this.styles) as BlankLinesStyle, this.stopAfter),
new WrappingAndBracesVisitor(getStyle(StyleKind.WrappingAndBracesStyle, tree, this.styles) as WrappingAndBracesStyle, this.stopAfter),
new SpacesVisitor(getStyle(StyleKind.SpacesStyle, tree, this.styles) as SpacesStyle, this.stopAfter),
new TabsAndIndentsVisitor(getStyle(StyleKind.TabsAndIndentsStyle, tree, this.styles) as TabsAndIndentsStyle, this.stopAfter),
]
let t: R | undefined = tree as R;
for (const visitor of visitors) {
t = await visitor.visit(t, p, cursor);
if (t === undefined) {
return undefined;
}
}
return t;
}
}
export class SpacesVisitor extends JavaScriptVisitor
{
constructor(private style: SpacesStyle, private stopAfter?: Tree) {
super();
}
override async visit(tree: Tree, p: P, parent?: Cursor): Promise {
if (this.cursor?.getNearestMessage("stop") != null) {
return tree as R;
}
return super.visit(tree, p, parent);
}
override async postVisit(tree: J, p: P): Promise {
if (this.stopAfter != null && isScope(this.stopAfter, tree)) {
this.cursor?.root.messages.set("stop", true);
}
return super.postVisit(tree, p);
}
protected async visitAlias(alias: JS.Alias, p: P): Promise {
const ret = await super.visitAlias(alias, p) as JS.Alias;
return produce(ret, draft => {
draft.propertyName.after.whitespace = " ";
});
}
protected async visitArrayAccess(arrayAccess: J.ArrayAccess, p: P): Promise {
const ret = await super.visitArrayAccess(arrayAccess, p) as J.ArrayAccess;
return produce(ret, draft => {
// Preserve newlines - only modify if no newlines present
if (!draft.dimension.index.element.prefix.whitespace.includes("\n")) {
draft.dimension.index.element.prefix.whitespace = this.style.within.arrayBrackets ? " " : "";
}
if (!draft.dimension.index.after.whitespace.includes("\n")) {
draft.dimension.index.after.whitespace = this.style.within.arrayBrackets ? " " : "";
}
});
}
protected async visitAssignment(assignment: J.Assignment, p: P): Promise {
const ret = await super.visitAssignment(assignment, p) as J.Assignment;
return produce(ret, draft => {
// Preserve newlines - only modify if no newlines present
if (!draft.assignment.before.whitespace.includes("\n")) {
draft.assignment.before.whitespace = this.style.aroundOperators.assignment ? " " : "";
}
if (!draft.assignment.element.prefix.whitespace.includes("\n")) {
draft.assignment.element.prefix.whitespace = this.style.aroundOperators.assignment ? " " : "";
}
});
}
protected async visitBinary(binary: J.Binary, p: P): Promise {
const ret = await super.visitBinary(binary, p) as J.Binary;
let property = false;
switch (ret.operator.element.valueOf()) {
case J.Binary.Type.And:
case J.Binary.Type.Or:
property = this.style.aroundOperators.logical
break;
case J.Binary.Type.Equal:
case J.Binary.Type.NotEqual:
property = this.style.aroundOperators.equality
break;
case J.Binary.Type.LessThan:
case J.Binary.Type.LessThanOrEqual:
case J.Binary.Type.GreaterThan:
case J.Binary.Type.GreaterThanOrEqual:
property = this.style.aroundOperators.relational
break;
case J.Binary.Type.BitAnd:
case J.Binary.Type.BitOr:
case J.Binary.Type.BitXor:
property = this.style.aroundOperators.bitwise
break;
case J.Binary.Type.Addition:
case J.Binary.Type.Subtraction:
property = this.style.aroundOperators.additive
break;
case J.Binary.Type.Multiplication:
case J.Binary.Type.Division:
case J.Binary.Type.Modulo:
property = this.style.aroundOperators.multiplicative
break;
case J.Binary.Type.LeftShift:
case J.Binary.Type.RightShift:
case J.Binary.Type.UnsignedRightShift:
property = this.style.aroundOperators.shift
break;
default:
throw new Error("Unsupported operator type " + ret.operator.element.valueOf());
}
return produce(ret, draft => {
// Preserve newlines - only modify if no newlines present
if (!draft.operator.before.whitespace.includes("\n")) {
draft.operator.before.whitespace = property ? " " : "";
}
if (!draft.right.prefix.whitespace.includes("\n")) {
draft.right.prefix.whitespace = property ? " " : "";
}
}) as J.Binary;
}
protected async visitCase(aCase: J.Case, p: P): Promise {
const ret = await super.visitCase(aCase, p) as J.Case;
return ret && produce(ret, draft => {
if (draft.caseLabels.elements[0].element.kind != J.Kind.Identifier || (draft.caseLabels.elements[0].element as J.Identifier).simpleName != "default") {
// Preserve newlines - only set to space if no newline exists
if (!draft.caseLabels.before.whitespace.includes("\n")) {
draft.caseLabels.before.whitespace = " ";
}
}
});
}
protected async visitClassDeclaration(classDecl: J.ClassDeclaration, p: P): Promise {
const ret = await super.visitClassDeclaration(classDecl, p) as J.ClassDeclaration;
// TODO typeParameters - IntelliJ doesn't seem to provide a setting for angleBrackets spacing for Typescript (while it does for Java),
// thus we either introduce our own setting or just enforce the natural spacing with no setting
return produce(ret, draft => {
draft.body.prefix.whitespace = this.style.beforeLeftBrace.classInterfaceModuleLeftBrace ? " " : "";
}) as J.ClassDeclaration;
}
public async visitContainer(container: J.Container, p: P): Promise> {
const ret = await super.visitContainer(container, p) as J.Container;
return produce(ret, draft => {
if (draft.elements.length > 0) {
// Apply beforeComma rule to all elements except the last
// (last element's after is before closing bracket, not a comma)
for (let i = 0; i < draft.elements.length - 1; i++) {
const afterWs = draft.elements[i].after.whitespace;
// Preserve newlines - only adjust when on same line
if (!afterWs.includes("\n")) {
draft.elements[i].after.whitespace = this.style.other.beforeComma ? " " : "";
}
}
}
if (draft.elements.length > 1) {
// Apply afterComma rule to elements after the first
for (let i = 1; i < draft.elements.length; i++) {
const currentWs = draft.elements[i].element.prefix.whitespace;
// Preserve original newlines - only adjust spacing when elements are on same line
if (!currentWs.includes("\n")) {
draft.elements[i].element.prefix.whitespace = this.style.other.afterComma ? " " : "";
}
}
}
});
}
protected async visitExportDeclaration(exportDeclaration: JS.ExportDeclaration, p: P): Promise {
const ret = await super.visitExportDeclaration(exportDeclaration, p) as JS.ExportDeclaration;
return produce(ret, draft => {
if (draft.exportClause) {
draft.exportClause.prefix.whitespace = " ";
if (draft.exportClause.kind == JS.Kind.NamedExports) {
const ne = (draft.exportClause as Draft);
if (ne.elements.elements.length > 0) {
// Check if this is a multi-line export (any element's prefix has a newline)
const isMultiLine = ne.elements.elements.some(e => e.element.prefix.whitespace.includes("\n"));
if (!isMultiLine) {
// Single-line: adjust brace spacing
ne.elements.elements[0].element.prefix.whitespace = this.style.within.es6ImportExportBraces ? " " : "";
ne.elements.elements[ne.elements.elements.length - 1].after.whitespace = this.style.within.es6ImportExportBraces ? " " : "";
} else {
// Multi-line: apply beforeComma rule to last element's after (for trailing commas)
// If it has only spaces (no newline), it's the space before a trailing comma
const lastAfter = ne.elements.elements[ne.elements.elements.length - 1].after.whitespace;
if (!lastAfter.includes("\n") && lastAfter.trim() === "") {
ne.elements.elements[ne.elements.elements.length - 1].after.whitespace = this.style.other.beforeComma ? " " : "";
}
}
}
}
}
draft.typeOnly.before.whitespace = draft.typeOnly.element ? " " : "";
if (draft.moduleSpecifier) {
draft.moduleSpecifier.before.whitespace = " ";
draft.moduleSpecifier.element.prefix.whitespace = " ";
}
})
}
protected async visitForLoop(forLoop: J.ForLoop, p: P): Promise {
const ret = await super.visitForLoop(forLoop, p) as J.ForLoop;
return produceAsync(ret, async draft => {
draft.control.prefix.whitespace = this.style.beforeParentheses.forParentheses ? " " : "";
draft.control.init = await Promise.all(draft.control.init.map(async (oneInit, index) => {
if (oneInit.element.kind === J.Kind.VariableDeclarations) {
const vd = oneInit.element as Draft;
if (vd.modifiers && vd.modifiers.length > 0) {
vd.modifiers[0].prefix.whitespace = "";
}
}
oneInit.after.whitespace = "";
this.spaceBeforeRightPaddedElementDraft(oneInit, index === 0 ? this.style.within.forParentheses : true);
return oneInit;
}));
if (draft.control.condition) {
draft.control.condition.element.prefix.whitespace = " ";
draft.control.condition.after.whitespace = this.style.other.beforeForSemicolon ? " " : "";
}
draft.control.update.forEach((oneUpdate, index) => {
oneUpdate.element.prefix.whitespace = " ";
oneUpdate.after.whitespace = (index === draft.control.update.length - 1 ? this.style.within.forParentheses : this.style.other.beforeForSemicolon) ? " " : "";
});
this.spaceBeforeRightPaddedElementDraft(draft.body, this.style.beforeLeftBrace.forLeftBrace);
this.spaceAfterRightPaddedDraft(draft.body, false);
});
}
protected async visitIf(iff: J.If, p: P): Promise {
const ret = await super.visitIf(iff, p) as J.If;
return produceAsync(ret, async draft => {
this.spaceBeforeDraft(draft.ifCondition, this.style.beforeParentheses.ifParentheses);
this.spaceBeforeRightPaddedElementDraft(draft.ifCondition.tree, this.style.within.ifParentheses);
this.spaceAfterRightPaddedDraft(draft.ifCondition.tree, this.style.within.ifParentheses);
this.spaceBeforeRightPaddedElementDraft(draft.thenPart, this.style.beforeLeftBrace.ifLeftBrace);
this.spaceAfterRightPaddedDraft(draft.thenPart, false);
});
}
protected async visitImportDeclaration(jsImport: JS.Import, p: P): Promise {
const ret = await super.visitImportDeclaration(jsImport, p) as JS.Import;
// Mutative detects same-value assignments and returns the original reference
// when nothing actually changed, so no guard function is needed.
return produce(ret, draft => {
if (draft.importClause) {
// Space after 'import' keyword always goes on importClause.prefix.
// The parser is consistent here: both ImportClause and NamedBindings share
// the same trivia span, but ImportClause consumes it first.
draft.importClause.prefix.whitespace = " ";
if (draft.importClause.name) {
// For import equals declarations (import X = Y), use assignment spacing
// For regular imports (import X from 'Y'), no space after name
draft.importClause.name.after.whitespace = draft.initializer
? (this.style.aroundOperators.assignment ? " " : "")
: "";
}
if (draft.importClause.namedBindings) {
const hasDefaultImport = !!draft.importClause.name;
if (hasDefaultImport || draft.importClause.typeOnly) {
draft.importClause.namedBindings.prefix.whitespace = " ";
}
if (draft.importClause.namedBindings.kind == JS.Kind.NamedImports) {
const ni = draft.importClause.namedBindings as Draft;
const isMultiLine = ni.elements.elements.some(e => e.element.prefix.whitespace.includes("\n"));
if (!isMultiLine) {
const braceSpace = this.style.within.es6ImportExportBraces ? " " : "";
ni.elements.elements[0].element.prefix.whitespace = braceSpace;
ni.elements.elements[ni.elements.elements.length - 1].after.whitespace = braceSpace;
} else {
// Multi-line: apply beforeComma rule to last element's after (for trailing commas)
const lastAfter = ni.elements.elements[ni.elements.elements.length - 1].after;
if (!lastAfter.whitespace.includes("\n") && lastAfter.whitespace.trim() === "") {
lastAfter.whitespace = this.style.other.beforeComma ? " " : "";
}
}
}
}
}
if (draft.moduleSpecifier) {
draft.moduleSpecifier.before.whitespace = " ";
draft.moduleSpecifier.element.prefix.whitespace = draft.importClause ? " " : "";
}
})
}
protected async visitIndexSignatureDeclaration(indexSignatureDeclaration: JS.IndexSignatureDeclaration, p: P): Promise {
const ret = await super.visitIndexSignatureDeclaration(indexSignatureDeclaration, p) as JS.IndexSignatureDeclaration;
return produce(ret, draft => {
draft.typeExpression.before.whitespace = this.style.other.beforeTypeReferenceColon ? " " : "";
draft.typeExpression.element.prefix.whitespace = this.style.other.afterTypeReferenceColon ? " " : "";
});
}
protected async visitMethodDeclaration(methodDecl: J.MethodDeclaration, p: P): Promise {
const ret = await super.visitMethodDeclaration(methodDecl, p) as J.MethodDeclaration;
return produceAsync(ret, async draft => {
if (draft.body) {
this.spaceBeforeDraft(draft.body, this.style.beforeLeftBrace.functionLeftBrace);
}
if (draft.parameters.elements.length > 0 && draft.parameters.elements[0].element.kind != J.Kind.Empty) {
draft.parameters.elements.forEach((param, index) => {
this.spaceAfterRightPaddedDraft(param, index === draft.parameters.elements.length - 1 ? this.style.within.functionDeclarationParentheses : this.style.other.beforeComma);
this.spaceBeforeDraft(param.element, index === 0 ? this.style.within.functionDeclarationParentheses : this.style.other.afterComma);
(param.element as Draft).variables[0].element.name.prefix.whitespace = "";
(param.element as Draft).variables[0].after.whitespace = "";
});
} else if (draft.parameters.elements.length == 1) {
this.spaceBeforeRightPaddedElementDraft(draft.parameters.elements[0], this.style.within.functionDeclarationParentheses);
this.spaceAfterRightPaddedDraft(draft.parameters.elements[0], false);
}
this.spaceBeforeContainerDraft(draft.parameters, this.style.beforeParentheses.functionDeclarationParentheses);
// Handle generator asterisk spacing
// - space before * is in the Generator marker's prefix
// - space after * is in the method name's prefix
const generatorIndex = ret.markers.markers.findIndex(m => m.kind === JS.Markers.Generator);
if (generatorIndex >= 0) {
const generator = draft.markers.markers[generatorIndex] as Draft;
generator.prefix.whitespace = this.style.other.beforeAsteriskInGenerator ? " " : "";
draft.name.prefix.whitespace = this.style.other.afterAsteriskInGenerator ? " " : "";
}
// TODO typeParameters handling - see visitClassDeclaration
});
}
protected async visitMethodInvocation(methodInv: J.MethodInvocation, p: P): Promise {
const ret = await super.visitMethodInvocation(methodInv, p) as J.MethodInvocation;
return produceAsync(ret, async draft => {
if (draft.select) {
this.spaceBeforeContainerDraft(draft.arguments, this.style.beforeParentheses.functionCallParentheses);
}
if (ret.arguments.elements.length > 0 && ret.arguments.elements[0].element.kind != J.Kind.Empty) {
draft.arguments.elements.forEach((arg, index) => {
this.spaceAfterRightPaddedDraft(arg, index === draft.arguments.elements.length - 1 ? this.style.within.functionCallParentheses : this.style.other.beforeComma);
this.spaceBeforeDraft(arg.element, index === 0 ? this.style.within.functionCallParentheses : this.style.other.afterComma);
});
} else if (ret.arguments.elements.length == 1) {
this.spaceBeforeRightPaddedElementDraft(draft.arguments.elements[0], this.style.within.functionCallParentheses);
this.spaceAfterRightPaddedDraft(draft.arguments.elements[0], false);
}
// TODO typeParameters handling - see visitClassDeclaration
});
}
protected async visitPropertyAssignment(propertyAssignment: JS.PropertyAssignment, p: P): Promise {
const pa = await super.visitPropertyAssignment(propertyAssignment, p) as JS.PropertyAssignment;
// Only adjust the space before the colon/equals if there's an initializer (not a shorthand property)
if (pa.initializer) {
return produceAsync(pa, draft => {
draft.name.after.whitespace = this.style.other.beforePropertyNameValueSeparator ? " " : "";
});
}
return pa;
}
protected async visitSwitch(switchNode: J.Switch, p: P): Promise {
const ret = await super.visitSwitch(switchNode, p) as J.Switch;
return produceAsync(ret, async draft => {
this.spaceBeforeDraft(draft.selector, this.style.beforeParentheses.switchParentheses);
this.spaceBeforeRightPaddedElementDraft(draft.selector.tree, this.style.within.switchParentheses);
this.spaceAfterRightPaddedDraft(draft.selector.tree, this.style.within.switchParentheses);
this.spaceBeforeDraft(draft.cases, this.style.beforeLeftBrace.switchLeftBrace);
for (const case_ of draft.cases.statements) {
if (case_.element.kind === J.Kind.Case) {
(case_.element as Draft).caseLabels.elements[0].after.whitespace = "";
}
}
});
}
protected async visitTernary(ternary: J.Ternary, p: P): Promise {
const ret = await super.visitTernary(ternary, p) as J.Ternary;
return produceAsync(ret, async draft => {
this.spaceBeforeLeftPaddedElementDraft(draft.truePart, this.style.ternaryOperator.beforeQuestionMark, this.style.ternaryOperator.afterQuestionMark);
this.spaceBeforeLeftPaddedElementDraft(draft.falsePart, this.style.ternaryOperator.beforeColon, this.style.ternaryOperator.afterColon);
});
}
protected async visitTry(try_: J.Try, p: P): Promise {
const ret = await super.visitTry(try_, p) as J.Try;
return produceAsync(ret, async draft => {
draft.body.prefix.whitespace = this.style.beforeLeftBrace.tryLeftBrace ? " " : "";
draft.catches.forEach(catch_ => {
this.spaceBeforeDraft(catch_, this.style.beforeKeywords.catchKeyword);
catch_.parameter.prefix.whitespace = this.style.beforeParentheses.catchParentheses ? " " : "";
this.spaceBeforeRightPaddedElementDraft(catch_.parameter.tree, this.style.within.catchParentheses);
this.spaceAfterRightPaddedDraft(catch_.parameter.tree, this.style.within.catchParentheses);
if (catch_.parameter.tree.element.variables.length > 0) {
catch_.parameter.tree.element.variables[catch_.parameter.tree.element.variables.length - 1].after.whitespace = "";
}
catch_.body.prefix.whitespace = this.style.beforeLeftBrace.catchLeftBrace ? " " : "";
});
if (draft.finally) {
this.spaceBeforeLeftPaddedElementDraft(draft.finally, this.style.beforeKeywords.finallyKeyword, this.style.beforeLeftBrace.finallyLeftBrace);
}
});
}
protected async visitTypeDeclaration(typeDeclaration: JS.TypeDeclaration, p: P): Promise {
const ret = await super.visitTypeDeclaration(typeDeclaration, p) as JS.TypeDeclaration;
return produce(ret, draft => {
if (draft.modifiers.length > 0) {
draft.name.before.whitespace = " ";
}
draft.name.element.prefix.whitespace = " ";
// Preserve newlines - only modify if no newlines present
if (!draft.initializer.before.whitespace.includes("\n")) {
draft.initializer.before.whitespace = this.style.aroundOperators.assignment ? " " : "";
}
if (!draft.initializer.element.prefix.whitespace.includes("\n")) {
draft.initializer.element.prefix.whitespace = this.style.aroundOperators.assignment ? " " : "";
}
});
}
protected async visitTypeInfo(typeInfo: JS.TypeInfo, p: P): Promise {
const ret = await super.visitTypeInfo(typeInfo, p) as JS.TypeInfo;
return produceAsync(ret, async draft => {
draft.prefix.whitespace = this.style.other.beforeTypeReferenceColon ? " " : "";
this.spaceBeforeDraft(draft.typeIdentifier, this.style.other.afterTypeReferenceColon);
});
}
protected async visitTypeLiteral(typeLiteral: JS.TypeLiteral, p: P): Promise {
const ret = await super.visitTypeLiteral(typeLiteral, p) as JS.TypeLiteral;
// Apply objectLiteralTypeBraces spacing for single-line type literals
if (ret.members && ret.members.statements.length > 0) {
const stmts = ret.members.statements;
const isSingleLine = !ret.members.end.whitespace.includes("\n") &&
stmts.every(s => !s.element.prefix.whitespace.includes("\n"));
if (isSingleLine) {
return produce(ret, draft => {
const space = this.style.within.objectLiteralTypeBraces ? " " : "";
draft.members.statements[0].element.prefix.whitespace = space;
// For type literals, the space before } is in members.end, not in last statement's after
draft.members.end.whitespace = space;
});
}
}
return ret;
}
protected async visitUnary(unary: J.Unary, p: P): Promise {
const ret = await super.visitUnary(unary, p) as J.Unary;
return produce(ret, draft => {
const spacing = this.style.aroundOperators.unary;
switch (draft.operator.element) {
case J.Unary.Type.Not:
draft.expression.prefix.whitespace = this.style.aroundOperators.afterUnaryNotAndNotNull ? " " : "";
break;
case J.Unary.Type.PreIncrement:
case J.Unary.Type.PreDecrement:
case J.Unary.Type.Negative:
case J.Unary.Type.Positive:
case J.Unary.Type.Complement:
draft.expression.prefix.whitespace = spacing ? " " : "";
break;
case J.Unary.Type.PostIncrement:
case J.Unary.Type.PostDecrement:
// postfix: don't add space after operand
break;
}
});
}
protected async visitVariable(variable: J.VariableDeclarations.NamedVariable, p: P): Promise {
const ret = await super.visitVariable(variable, p) as J.VariableDeclarations.NamedVariable;
if (variable.initializer?.element?.kind == JS.Kind.StatementExpression
&& (variable.initializer.element as JS.StatementExpression).statement.kind == J.Kind.MethodDeclaration) {
return ret;
}
return produceAsync(ret, async draft => {
if (draft.initializer) {
// Preserve newlines - only modify if no newlines present
if (!draft.initializer.before.whitespace.includes("\n")) {
draft.initializer.before.whitespace = this.style.aroundOperators.assignment ? " " : "";
}
if (!draft.initializer.element.prefix.whitespace.includes("\n")) {
draft.initializer.element.prefix.whitespace = this.style.aroundOperators.assignment ? " " : "";
}
}
});
}
protected async visitWhileLoop(whileLoop: J.WhileLoop, p: P): Promise {
const ret = await super.visitWhileLoop(whileLoop, p) as J.WhileLoop;
return produceAsync(ret, async draft => {
this.spaceBeforeRightPaddedElementDraft(draft.body, this.style.beforeLeftBrace.whileLeftBrace);
this.spaceAfterRightPaddedDraft(draft.body, false);
this.spaceBeforeDraft(draft.condition, this.style.beforeParentheses.whileParentheses);
this.spaceBeforeRightPaddedElementDraft(draft.condition.tree, this.style.within.whileParentheses);
this.spaceAfterRightPaddedDraft(draft.condition.tree, this.style.within.whileParentheses);
});
}
protected async visitTypeParameter(typeParam: J.TypeParameter, p: P): Promise {
const ret = await super.visitTypeParameter(typeParam, p) as J.TypeParameter;
return produce(ret, draft => {
if (draft.bounds && draft.bounds.elements.length >= 2) {
const constraintType = draft.bounds.elements[0];
const defaultType = draft.bounds.elements[1];
const hasConstraint = constraintType.element.kind !== J.Kind.Empty;
const hasDefault = defaultType.element.kind !== J.Kind.Empty;
if (hasConstraint) {
// Space before '=' for default type (in the `after` of constraint type)
if (hasDefault && !constraintType.after.whitespace.includes("\n")) {
constraintType.after.whitespace = this.style.aroundOperators.assignment ? " " : "";
}
} else if (hasDefault) {
// No constraint, just default: space before '=' is in bounds.before
if (!draft.bounds.before.whitespace.includes("\n")) {
draft.bounds.before.whitespace = this.style.aroundOperators.assignment ? " " : "";
}
}
}
});
}
/**
* Modifies the after space of a RightPadded draft in place.
*/
private spaceAfterRightPaddedDraft(draft: Draft>, spaceAfter: boolean): void {
if (draft.after.comments.length > 0) {
// Perform the space rule for the suffix of the last comment only. Same as IntelliJ.
SpacesVisitor.spaceLastCommentSuffixDraft(draft.after.comments, spaceAfter);
return;
}
if (spaceAfter && SpacesVisitor.isNotSingleSpace(draft.after.whitespace)) {
draft.after.whitespace = " ";
} else if (!spaceAfter && SpacesVisitor.isOnlySpacesAndNotEmpty(draft.after.whitespace)) {
draft.after.whitespace = "";
}
}
/**
* Modifies the before space of a LeftPadded draft and the element's prefix in place.
*/
private spaceBeforeLeftPaddedElementDraft(draft: Draft>, spaceBeforePadding: boolean, spaceBeforeElement: boolean): void {
if (draft.before.comments.length == 0) {
// Preserve newlines - only modify if no newlines present
if (!draft.before.whitespace.includes("\n")) {
draft.before.whitespace = spaceBeforePadding ? " " : "";
}
}
this.spaceBeforeDraft(draft.element, spaceBeforeElement);
}
/**
* Modifies the element's prefix of a RightPadded draft in place.
*/
private spaceBeforeRightPaddedElementDraft(draft: Draft>, spaceBefore: boolean): void {
this.spaceBeforeDraft(draft.element, spaceBefore);
}
/**
* Modifies the prefix whitespace of a J draft in place.
*/
private spaceBeforeDraft(draft: Draft, spaceBefore: boolean): void {
if (draft.prefix.comments.length > 0) {
return;
}
if (spaceBefore && SpacesVisitor.isNotSingleSpace(draft.prefix.whitespace)) {
draft.prefix.whitespace = " ";
} else if (!spaceBefore && SpacesVisitor.isOnlySpacesAndNotEmpty(draft.prefix.whitespace)) {
draft.prefix.whitespace = "";
}
}
/**
* Modifies the before space of a Container draft in place.
*/
private spaceBeforeContainerDraft(draft: Draft>, spaceBefore: boolean): void {
if (draft.before.comments.length > 0) {
// Perform the space rule for the suffix of the last comment only. Same as IntelliJ.
SpacesVisitor.spaceLastCommentSuffixDraft(draft.before.comments, spaceBefore);
return;
}
if (spaceBefore && SpacesVisitor.isNotSingleSpace(draft.before.whitespace)) {
draft.before.whitespace = " ";
} else if (!spaceBefore && SpacesVisitor.isOnlySpacesAndNotEmpty(draft.before.whitespace)) {
draft.before.whitespace = "";
}
}
/**
* Modifies the suffix of the last comment in an array of draft comments in place.
*/
private static spaceLastCommentSuffixDraft(comments: Draft[], spaceSuffix: boolean): void {
this.spaceSuffixDraft(comments[comments.length - 1], spaceSuffix);
}
/**
* Modifies the suffix of a Comment draft in place.
*/
private static spaceSuffixDraft(draft: Draft, spaceSuffix: boolean): void {
if (spaceSuffix && this.isNotSingleSpace(draft.suffix)) {
draft.suffix = " ";
} else if (!spaceSuffix && this.isOnlySpacesAndNotEmpty(draft.suffix)) {
draft.suffix = "";
}
}
private static isOnlySpaces(str: string): boolean {
return str.split('').every(char => char === ' ');
}
private static isOnlySpacesAndNotEmpty(s: string): boolean {
return s !== "" && this.isOnlySpaces(s);
}
private static isNotSingleSpace(str: string): boolean {
return this.isOnlySpaces(str) && str !== " ";
}
protected async visitNewClass(newClass: J.NewClass, p: P): Promise {
const ret = await super.visitNewClass(newClass, p) as J.NewClass;
// Only handle object literals (NewClass with no class/constructor)
if (ret.class) {
return ret;
}
// Handle object literal brace spacing: { foo: 1 } vs {foo: 1}
if (ret.body && ret.body.statements.length > 0) {
return produce(ret, draft => {
const stmts = draft.body!.statements;
// Check if this is a multi-line object literal
const isMultiLine = stmts.some(s => s.element.prefix.whitespace.includes("\n")) ||
draft.body!.end.whitespace.includes("\n");
if (!isMultiLine) {
// Single-line: apply objectLiteralBraces spacing
const space = this.style.within.objectLiteralBraces ? " " : "";
stmts[0].element.prefix.whitespace = space;
draft.body!.end.whitespace = space;
}
});
}
return ret;
}
}
export class WrappingAndBracesVisitor extends JavaScriptVisitor
{
constructor(private readonly style: WrappingAndBracesStyle, private stopAfter?: Tree) {
super();
}
override async visit(tree: Tree, p: P, parent?: Cursor): Promise {
if (this.cursor?.getNearestMessage("stop") != null) {
return tree as R;
}
return super.visit(tree, p, parent);
}
override async postVisit(tree: J, p: P): Promise {
if (this.stopAfter != null && isScope(this.stopAfter, tree)) {
this.cursor?.root.messages.set("stop", true);
}
return super.postVisit(tree, p);
}
protected async visitVariableDeclarations(multiVariable: J.VariableDeclarations, p: P): Promise {
const v = await super.visitVariableDeclarations(multiVariable, p) as J.VariableDeclarations;
if (v.leadingAnnotations.length === 0) {
return v;
}
const parent = this.cursor.parentTree()?.value;
if (parent?.kind === J.Kind.Block) {
return produce(v, draft => {
draft.leadingAnnotations = this.withNewlines(draft.leadingAnnotations);
if (draft.leadingAnnotations.length > 0) {
if (draft.modifiers.length > 0) {
draft.modifiers = this.withNewlineModifiers(draft.modifiers);
} else if (draft.typeExpression && !draft.typeExpression.prefix.whitespace.includes("\n")) {
draft.typeExpression.prefix.whitespace = "\n" + draft.typeExpression.prefix.whitespace;
}
}
});
}
return v;
}
protected async visitMethodDeclaration(method: J.MethodDeclaration, p: P): Promise {
const m = await super.visitMethodDeclaration(method, p) as J.MethodDeclaration;
if (m.leadingAnnotations.length === 0) {
return m;
}
return produce(m, draft => {
draft.leadingAnnotations = this.withNewlines(draft.leadingAnnotations);
if (draft.leadingAnnotations.length > 0) {
if (draft.modifiers.length > 0) {
draft.modifiers = this.withNewlineModifiers(draft.modifiers);
} else if (draft.typeParameters && !draft.typeParameters.prefix.whitespace.includes("\n")) {
draft.typeParameters.prefix.whitespace = "\n" + draft.typeParameters.prefix.whitespace;
} else if (draft.returnTypeExpression && !draft.returnTypeExpression.prefix.whitespace.includes("\n")) {
draft.returnTypeExpression.prefix.whitespace = "\n" + draft.returnTypeExpression.prefix.whitespace;
} else if (!draft.name.prefix.whitespace.includes("\n")) {
draft.name.prefix.whitespace = "\n" + draft.name.prefix.whitespace;
}
}
});
}
protected async visitElse(elsePart: J.If.Else, p: P): Promise {
const e = await super.visitElse(elsePart, p) as J.If.Else;
const hasBody = e.body.element.kind === J.Kind.Block || e.body.element.kind === J.Kind.If;
if (!hasBody) {
return e;
}
const shouldHaveNewline = this.style.ifStatement.elseOnNewLine;
const hasNewline = e.prefix.whitespace.includes("\n");
if ((shouldHaveNewline && !hasNewline) || (!shouldHaveNewline && hasNewline)) {
return produce(e, draft => {
if (shouldHaveNewline && !hasNewline) {
draft.prefix.whitespace = "\n" + draft.prefix.whitespace;
} else {
draft.prefix.whitespace = "";
}
});
}
return e;
}
protected async visitClassDeclaration(classDecl: J.ClassDeclaration, p: P): Promise {
const j = await super.visitClassDeclaration(classDecl, p) as J.ClassDeclaration;
if (j.leadingAnnotations.length === 0) {
return j;
}
return produce(j, draft => {
draft.leadingAnnotations = this.withNewlines(draft.leadingAnnotations);
if (draft.leadingAnnotations.length > 0) {
if (draft.modifiers.length > 0) {
draft.modifiers = this.withNewlineModifiers(draft.modifiers);
} else {
const kind = draft.classKind;
if (!kind.prefix.whitespace.includes("\n")) {
kind.prefix.whitespace = "\n" + kind.prefix.whitespace;
}
}
}
});
}
protected async visitBlock(block: J.Block, p: P): Promise {
const b = await super.visitBlock(block, p) as J.Block;
const parentKind = this.cursor.parent?.value.kind;
// Check if this is a "simple" block (empty or contains only a single J.Empty)
const isSimpleBlock = b.statements.length === 0 ||
(b.statements.length === 1 && b.statements[0].element.kind === J.Kind.Empty);
// Object literals and type literals: always format empty ones as {} on single line
if (parentKind === J.Kind.NewClass || parentKind === JS.Kind.TypeLiteral) {
if (isSimpleBlock && this.blockIsMultiLine(b)) {
return produce(b, draft => {
this.formatBlockOnOneLine(draft);
});
}
return b;
}
if (isSimpleBlock) {
const isMethodOrFunctionBody = parentKind === J.Kind.Lambda ||
parentKind === J.Kind.MethodDeclaration;
const keepInOneLine = isMethodOrFunctionBody
? this.style.keepWhenReformatting.simpleMethodsInOneLine
: this.style.keepWhenReformatting.simpleBlocksInOneLine;
if (keepInOneLine) {
if (this.blockIsMultiLine(b)) {
return produce(b, draft => {
this.formatBlockOnOneLine(draft);
});
}
} else {
if (!b.end.whitespace.includes("\n")) {
return produce(b, draft => {
draft.end = this.withNewlineSpace(draft.end);
});
}
}
} else {
// Non-simple blocks: ensure closing brace is on its own line
if (!b.end.whitespace.includes("\n") && !b.statements[b.statements.length - 1].after.whitespace.includes("\n")) {
return produce(b, draft => {
draft.end = this.withNewlineSpace(draft.end);
});
}
}
return b;
}
private blockIsMultiLine(b: J.Block): boolean {
if (b.end.whitespace.includes("\n")) return true;
if (b.statements.length === 1) {
if (b.statements[0].element.prefix.whitespace.includes("\n")) return true;
if (b.statements[0].after.whitespace.includes("\n")) return true;
}
return false;
}
private formatBlockOnOneLine(draft: Draft): void {
if (draft.end.whitespace.includes("\n")) {
draft.end.whitespace = draft.end.whitespace.replace(/\n\s*/g, "");
}
if (draft.statements.length === 1) {
if (draft.statements[0].element.prefix.whitespace.includes("\n")) {
draft.statements[0].element.prefix.whitespace = "";
}
if (draft.statements[0].after.whitespace.includes("\n")) {
draft.statements[0].after.whitespace = "";
}
}
}
protected async visitSwitch(aSwitch: J.Switch, p: P): Promise {
return super.visitSwitch(aSwitch, p);
}
private withNewlines(list: T[]): T[] {
return list.map((a, index) => {
if (index > 0 && !a.prefix.whitespace.includes("\n")) {
return produce(a, draft => {
draft.prefix.whitespace = "\n" + draft.prefix.whitespace;
});
}
return a;
});
}
private withNewlineSpace(space: J.Space): J.Space {
if (space.comments.length === 0) {
space = produce(space, draft => {
draft.whitespace = "\n" + space.whitespace;
});
}
// TODO clarify the situation with Comment.isMultiline and then add this case
// else if (space.comments[space.comments.length - 1].isMultiline) {
// space = space.withComments(ListUtils.mapLast(space.comments, c => c.withSuffix("\n")));
// }
return space;
}
private withNewlineModifiers(modifiers: J.Modifier[]): J.Modifier[] {
if (modifiers.length === 0) {
return modifiers;
}
const first = modifiers[0];
if (!first.prefix.whitespace.includes("\n")) {
return [
produce(first, draft => {
draft.prefix.whitespace = "\n" + draft.prefix.whitespace;
}),
...modifiers.slice(1),
];
}
return modifiers;
}
}
export class BlankLinesVisitor extends JavaScriptVisitor
{
constructor(private readonly style: BlankLinesStyle, private stopAfter?: Tree) {
super();
}
protected async preVisit(tree: J, p: P): Promise {
let ret = await super.preVisit(tree, p) as J;
if (ret.kind === JS.Kind.CompilationUnit) {
ret = produce(ret as JS.CompilationUnit, draft => {
if (draft.prefix.comments.length == 0) {
draft.prefix.whitespace = "";
}
});
}
if (ret.kind === J.Kind.MethodDeclaration
&& this.cursor.parent?.parent?.parent?.value.kind === J.Kind.ClassDeclaration) {
ret = produce(ret as JS.StatementExpression, draft => {
this.ensurePrefixHasNewLine(draft);
});
}
return ret;
}
override async visit(tree: Tree, p: P, cursor?: Cursor): Promise {
if (this.cursor?.getNearestMessage("stop") != null) {
return tree as R;
}
return super.visit(tree, p, cursor);
}
protected async visitClassDeclaration(classDecl: J.ClassDeclaration, p: P): Promise {
let ret = await super.visitClassDeclaration(classDecl, p) as J.ClassDeclaration;
if (!ret.body) return ret;
return produce(ret, draft => {
const statements = draft.body.statements;
if (statements.length > 0) {
this.keepMaximumBlankLines(draft.body.statements[0].element, 0);
const isInterface = draft.classKind.type === J.ClassDeclaration.Kind.Type.Interface;
for (let i = 1; i < statements.length; i++) {
const previousElement = statements[i - 1].element;
let currentElement = statements[i].element;
if (previousElement.kind == J.Kind.VariableDeclarations || currentElement.kind == J.Kind.VariableDeclarations) {
const fieldBlankLines = isInterface
? this.style.minimum.aroundFieldInInterface ?? 0
: this.style.minimum.aroundField;
this.minimumBlankLines(currentElement, fieldBlankLines);
}
if (previousElement.kind == J.Kind.MethodDeclaration || currentElement.kind == J.Kind.MethodDeclaration) {
const methodBlankLines = isInterface
? this.style.minimum.aroundMethodInInterface ?? 0
: this.style.minimum.aroundMethod;
this.minimumBlankLines(currentElement, methodBlankLines);
}
this.keepMaximumBlankLines(currentElement, this.style.keepMaximum.inCode);
draft.body.statements[i].element = currentElement;
}
}
const cu = this.cursor.firstEnclosing((x: any): x is JS.CompilationUnit => x.kind === JS.Kind.CompilationUnit) as JS.CompilationUnit | undefined;
const topLevelIndex = cu?.statements.findIndex(s => s.element.id == draft.id);
if (topLevelIndex !== undefined) {
const isImportJustBeforeThis = topLevelIndex > 0 && cu?.statements[topLevelIndex - 1].element.kind === JS.Kind.Import;
if (isImportJustBeforeThis) {
this.minimumBlankLines(draft, this.style.minimum.afterImports ?? 0);
}
if (topLevelIndex > 0) {
this.minimumBlankLines(draft, this.style.minimum.aroundClass);
}
this.keepMaximumBlankLines(draft, this.style.keepMaximum.inCode);
}
});
}
override async visitStatement(statement: Statement, p: P): Promise {
const ret = await super.visitStatement(statement, p) as Statement;
const parent = this.cursor.parentTree()?.value;
const grandparent = this.cursor.parentTree()?.parent?.value;
return produce(ret, draft => {
if (grandparent?.kind === J.Kind.ClassDeclaration && parent?.kind === J.Kind.Block) {
const classDecl = grandparent as J.ClassDeclaration;
const block = parent as J.Block;
const isFirst = block.statements.length > 0 && block.statements[0].element.id === draft.id;
if (!isFirst) {
if (draft.kind === J.Kind.VariableDeclarations) {
const declMax = classDecl.classKind.type === J.ClassDeclaration.Kind.Type.Interface
? this.style.minimum.aroundFieldInInterface ?? 0
: this.style.minimum.aroundField;
this.minimumBlankLines(draft, declMax);
} else if (draft.kind === J.Kind.MethodDeclaration) {
const declMax = classDecl.classKind.type === J.ClassDeclaration.Kind.Type.Interface
? this.style.minimum.aroundMethodInInterface ?? 0
: this.style.minimum.aroundMethod;
this.minimumBlankLines(draft, declMax);
} else if (draft.kind === J.Kind.Block) {
this.minimumBlankLines(draft, this.style.minimum.aroundFunction);
} else if (draft.kind === J.Kind.ClassDeclaration) {
this.minimumBlankLines(draft, this.style.minimum.aroundClass);
}
this.keepMaximumBlankLines(draft, this.style.keepMaximum.inCode);
}
} else if (parent?.kind === J.Kind.Block && grandparent?.kind !== J.Kind.NewClass && grandparent?.kind !== JS.Kind.TypeLiteral ||
(parent?.kind === JS.Kind.CompilationUnit && (parent! as JS.CompilationUnit).statements[0].element.id != draft.id) ||
(parent?.kind === J.Kind.Case && this.isInsideCaseStatements(parent as J.Case, draft))) {
if (draft.kind != J.Kind.Case) {
this.ensurePrefixHasNewLine(draft);
}
}
});
}
protected async visitBlock(block: J.Block, p: P): Promise {
const b = await super.visitBlock(block, p) as J.Block;
return produce(b, draft => {
const parentKind = this.cursor.parent?.value.kind;
// Skip newline for object literals (NewClass) and type literals (TypeLiteral)
if (parentKind != J.Kind.NewClass && parentKind != JS.Kind.TypeLiteral) {
draft.end = replaceLastWhitespace(draft.end, ws =>
ws.includes("\n") ? ws : ws.replace(/[ \t]+$/, '') + "\n"
);
}
});
}
protected async visitEnumValue(enumValue: J.EnumValue, p: P): Promise {
const e = await super.visitEnumValue(enumValue, p) as J.EnumValue;
this.keepMaximumBlankLines(e, this.style.keepMaximum.inCode);
return e;
}
/**
* Check if a statement is inside a Case's statements container or body,
* not inside the caseLabels container.
*/
private isInsideCaseStatements(caseNode: J.Case, statement: J): boolean {
// Check if statement is in the statements container
if (caseNode.statements.elements.some(s => s.element.id === statement.id)) {
return true;
}
// Check if statement is the body
if (caseNode.body?.element.id === statement.id) {
return true;
}
return false;
}
private keepMaximumBlankLines(node: Draft, max: number) {
const whitespace = node.prefix.whitespace;
const blankLines = BlankLinesVisitor.countNewlines(whitespace) - 1;
if (blankLines > max) {
let idx = 0;
for (let i = 0; i < blankLines - max + 1; i++, idx++) {
idx = whitespace.indexOf('\n', idx);
}
idx--;
node.prefix.whitespace = whitespace.substring(idx);
}
}
private minimumBlankLines(node: Draft, min: number) {
if (min <= 0) {
return;
}
const currentNewlines = BlankLinesVisitor.countNewlines(node.prefix.whitespace);
const needed = min - currentNewlines + 1;
if (needed > 0) {
node.prefix.whitespace = "\n".repeat(needed) + node.prefix.whitespace;
}
}
private ensurePrefixHasNewLine(node: Draft) {
if (!node.prefix) return;
// Check if newline already exists in the effective last whitespace
if (lastWhitespace(node.prefix).includes("\n")) {
return; // Already has a newline
}
if (node.kind === JS.Kind.ExpressionStatement) {
this.ensurePrefixHasNewLine((node as JS.ExpressionStatement).expression);
} else {
node.prefix = replaceLastWhitespace(node.prefix, () => "\n");
}
}
private static countNewlines(s: string): number {
return [...s].filter(c => c === "\n").length;
}
override async postVisit(tree: J, p: P): Promise {
if (this.stopAfter != null && isScope(this.stopAfter, tree)) {
this.cursor?.root.messages.set("stop", true);
}
return super.postVisit(tree, p);
}
}
// Re-export prettier formatting utilities
export {prettierFormat} from "./prettier-format";