/*
* 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 {JavaScriptVisitor} from "../visitor";
import {J} from "../../java";
import {Cursor, isScope, Tree} from "../../tree";
import {JS} from "../tree";
import {create as produce, Draft} from "mutative";
import {findMarker} from "../../markers";
/**
* Ensures minimum viable spacing between AST elements.
* Adds required spaces where they are missing (e.g., after keywords).
*/
export class MinimumViableSpacingVisitor
extends JavaScriptVisitor
{
constructor(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 visitAwait(await_: JS.Await, p: P): Promise {
const ret = await super.visitAwait(await_, p) as JS.Await;
return produce(ret, draft => {
this.ensureSpace(draft.expression.prefix)
});
}
protected async visitClassDeclaration(classDecl: J.ClassDeclaration, p: P): Promise {
let c = await super.visitClassDeclaration(classDecl, p) as J.ClassDeclaration;
let first = c.leadingAnnotations.length === 0;
if (c.modifiers.length > 0) {
if (!first && c.modifiers[0].prefix.whitespace === "") {
c = produce(c, draft => {
this.ensureSpace(draft.modifiers[0].prefix);
});
}
c = produce(c, draft => {
for (let i = 1; i < draft.modifiers.length; i++) {
this.ensureSpace(draft.modifiers[i].prefix);
}
});
first = false;
}
if (c.classKind.prefix.whitespace === "" && !first) {
c = produce(c, draft => {
this.ensureSpace(draft.classKind.prefix);
});
first = false;
}
// anonymous classes have an empty name
if (c.name.simpleName !== "") {
c = produce(c, draft => {
this.ensureSpace(draft.name.prefix);
});
}
// Note: typeParameters should NOT have space before them - they immediately follow the class name
// e.g., "class DataTable" not "class DataTable "
// Note: body.prefix spacing (space before '{') is handled by SpacesVisitor, not here.
if (c.extends && c.extends.before.whitespace === "") {
c = produce(c, draft => {
this.ensureSpace(draft.extends!.before);
});
}
if (c.implements && c.implements.before.whitespace === "") {
c = produce(c, draft => {
this.ensureSpace(draft.implements!.before);
if (draft.implements != undefined && draft.implements.elements.length > 0) {
this.ensureSpace(draft.implements.elements[0].element.prefix);
}
});
}
return c;
}
protected async visitMethodDeclaration(method: J.MethodDeclaration, p: P): Promise {
let m = await super.visitMethodDeclaration(method, p) as J.MethodDeclaration;
let first = m.leadingAnnotations.length === 0;
if (m.modifiers.length > 0) {
if (!first && m.modifiers[0].prefix.whitespace === "") {
m = produce(m, draft => {
this.ensureSpace(draft.modifiers[0].prefix);
});
}
m = produce(m, draft => {
for (let i = 1; i < draft.modifiers.length; i++) {
this.ensureSpace(draft.modifiers[i].prefix);
}
});
first = false;
}
// FunctionDeclaration marker check must come AFTER modifiers processing
// to avoid adding unwanted space before the first modifier (e.g., 'async')
if (findMarker(method, JS.Markers.FunctionDeclaration)) {
first = false;
}
if (!first && m.name.prefix.whitespace === "") {
m = produce(m, draft => {
this.ensureSpace(draft.name.prefix);
});
}
if (m.throws && m.throws.before.whitespace === "") {
m = produce(m, draft => {
this.ensureSpace(draft.throws!.before);
});
}
return m;
}
protected async visitNamespaceDeclaration(namespaceDeclaration: JS.NamespaceDeclaration, p: P): Promise {
const ret = await super.visitNamespaceDeclaration(namespaceDeclaration, p) as JS.NamespaceDeclaration;
return produce(ret, draft => {
if (draft.modifiers.length > 0) {
draft.keywordType.before.whitespace=" ";
}
this.ensureSpace(draft.name.element.prefix);
});
}
protected async visitNewClass(newClass: J.NewClass, p: P): Promise {
const ret = await super.visitNewClass(newClass, p) as J.NewClass;
return produce(ret, draft => {
if (draft.class) {
if (draft.class.kind == J.Kind.Identifier) {
this.ensureSpace((draft.class as Draft).prefix);
}
}
});
}
protected async visitReturn(returnNode: J.Return, p: P): Promise {
const r = await super.visitReturn(returnNode, p) as J.Return;
if (r.expression && r.expression.prefix.whitespace === "" &&
!r.markers.markers.find(m => m.id === "org.openrewrite.java.marker.ImplicitReturn")) {
return produce(r, draft => {
this.ensureSpace(draft.expression!.prefix);
});
}
return r;
}
protected async visitThrow(thrown: J.Throw, p: P): Promise {
const ret = await super.visitThrow(thrown, p) as J.Throw;
return ret && produce(ret, draft => {
this.ensureSpace(draft.exception.prefix);
});
}
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) {
this.ensureSpace(draft.name.before);
}
this.ensureSpace(draft.name.element.prefix);
});
}
protected async visitTypeOf(typeOf: JS.TypeOf, p: P): Promise {
const ret = await super.visitTypeOf(typeOf, p) as JS.TypeOf;
return produce(ret, draft => {
this.ensureSpace(draft.expression.prefix);
});
}
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 > 0) {
this.ensureSpace(draft.bounds.before);
this.ensureSpace(draft.bounds.elements[0].element.prefix);
}
});
}
protected async visitVariableDeclarations(v: J.VariableDeclarations, p: P): Promise {
let ret = await super.visitVariableDeclarations(v, p) as J.VariableDeclarations;
let first = ret.leadingAnnotations.length === 0;
if (first && ret.modifiers.length > 0) {
ret = produce(ret, draft => {
for (let i = 1; i < draft.modifiers.length; i++) {
this.ensureSpace(draft.modifiers[i].prefix);
}
});
first = false;
}
// `catch { }` has a parameter with no variables
if (!first && ret.variables.length > 0) {
ret = produce(ret, draft => {
this.ensureSpace(draft.variables[0].element.prefix);
});
}
return ret;
}
protected async visitCase(caseNode: J.Case, p: P): Promise {
const c = await super.visitCase(caseNode, p) as J.Case;
if (c.guard && c.caseLabels.elements.length > 0 && c.caseLabels.elements[c.caseLabels.elements.length - 1].after.whitespace === "") {
return produce(c, draft => {
const last = draft.caseLabels.elements.length - 1;
draft.caseLabels.elements[last].after.whitespace = " ";
});
}
return c;
}
private ensureSpace(spaceDraft: Draft) {
if (spaceDraft.whitespace.length === 0 && spaceDraft.comments.length === 0) {
spaceDraft.whitespace = " ";
}
}
}