Skip to content

Commit

Permalink
Formatter infrastructure and BlankLines formatter (#213)
Browse files Browse the repository at this point in the history
* Formatter styles classes

* Added BlankLines formatter

* typos

* remove unused imports

---------

Co-authored-by: Andrii Rodionov <andriih@moderne.io>
  • Loading branch information
arodionov and Andrii Rodionov authored Mar 7, 2025
1 parent dd7f5d6 commit 9a81022
Show file tree
Hide file tree
Showing 26 changed files with 1,455 additions and 86 deletions.
80 changes: 78 additions & 2 deletions openrewrite/src/core/execution.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {createTwoFilesPatch} from 'diff';
import {PathLike} from 'fs';
import {Cursor, TreeVisitor} from "./tree";
import {Cursor, SourceFile, TreeVisitor} from "./tree";

export class Result {
static diff(before: string, after: string, path: PathLike): string {
Expand Down Expand Up @@ -60,10 +60,86 @@ export class DelegatingExecutionContext implements ExecutionContext {
}
}

interface LargeSourceSet {
edit(map: (source: SourceFile) => SourceFile | null): LargeSourceSet;
getChangeSet(): RecipeRunResult[];
}

export class InMemoryLargeSourceSet implements LargeSourceSet {
private readonly initialState?: InMemoryLargeSourceSet;
private readonly sources: SourceFile[];
private readonly deletions: SourceFile[];

constructor(sources: SourceFile[], deletions: SourceFile[] = [], initialState?: InMemoryLargeSourceSet) {
this.initialState = initialState;
this.sources = sources;
this.deletions = deletions;
}

edit(map: (source: SourceFile) => SourceFile | null): InMemoryLargeSourceSet {
const mapped: SourceFile[] = [];
const deleted: SourceFile[] = this.initialState ? [...this.initialState.deletions] : [];
let changed = false;

for (const source of this.sources) {
const mappedSource = map(source);
if (mappedSource !== null) {
mapped.push(mappedSource);
changed = mappedSource !== source;
} else {
deleted.push(source);
changed = true;
}
}

return changed ? new InMemoryLargeSourceSet(mapped, deleted, this.initialState ?? this) : this;
}

getChangeSet(): RecipeRunResult[] {
const sourceFileById = new Map(this.initialState?.sources.map(sf => [sf.id, sf]));
const changes: RecipeRunResult[] = [];

for (const source of this.sources) {
const original = sourceFileById.get(source.id) || null;
changes.push(new RecipeRunResult(original, source));
}

for (const source of this.deletions) {
changes.push(new RecipeRunResult(source, null));
}

return changes;
}
}

export class RecipeRunResult {
constructor(
public readonly before: SourceFile | null,
public readonly after: SourceFile | null
) {}
}

export class Recipe {
getVisitor(): TreeVisitor<any, ExecutionContext> {
return TreeVisitor.noop();
}

getRecipeList(): Recipe[] {
return [];
}

run(before: LargeSourceSet, ctx: ExecutionContext): RecipeRunResult[] {
const lss = this.runInternal(before, ctx, new Cursor(null, Cursor.ROOT_VALUE));
return lss.getChangeSet();
}

runInternal(before: LargeSourceSet, ctx: ExecutionContext, root: Cursor): LargeSourceSet {
let after = before.edit((beforeFile) => this.getVisitor().visit(beforeFile, ctx, root));
for (const recipe of this.getRecipeList()) {
after = recipe.runInternal(after, ctx, root);
}
return after;
}
}

export class RecipeRunException extends Error {
Expand All @@ -83,4 +159,4 @@ export class RecipeRunException extends Error {
get cursor(): Cursor | undefined {
return this._cursor;
}
}
}
1 change: 1 addition & 0 deletions openrewrite/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './markers';
export * from './parser';
export * from './tree';
export * from './utils';
export * from './style';
58 changes: 58 additions & 0 deletions openrewrite/src/core/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {Marker, MarkerSymbol} from "./markers";
import {UUID} from "./utils";

export abstract class Style {
merge(lowerPrecedence: Style): Style {
return this;
}

applyDefaults(): Style {
return this;
}
}

export class NamedStyles implements Marker {
[MarkerSymbol] = true;

private readonly _id: UUID;
name: string;
displayName: string;
description?: string;
tags: Set<string>;
styles: Style[];

constructor(id: UUID, name: string, displayName: string, description?: string, tags: Set<string> = new Set(), styles: Style[] = []) {
this._id = id;
this.name = name;
this.displayName = displayName;
this.description = description;
this.tags = tags;
this.styles = styles;
}

public get id(): UUID {
return this._id;
}

public withId(id: UUID): NamedStyles {
return id === this._id ? this : new NamedStyles(id, this.name, this.displayName, this.description, this.tags, this.styles);
}

static merge<S extends Style>(styleClass: new (...args: any[]) => S, namedStyles: NamedStyles[]): S | null {
let merged: S | null = null;

for (const namedStyle of namedStyles) {
if (namedStyle.styles) {
for (let style of namedStyle.styles) {
if (style instanceof styleClass) {
style = style.applyDefaults();
merged = merged ? (merged.merge(style) as S) : (style as S);
}
}
}
}

return merged;
}

}
19 changes: 17 additions & 2 deletions openrewrite/src/core/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,12 @@ export class Cursor {

private readonly _parent: Cursor | null;
private readonly _value: Object;
private _messages: Map<string, Object>;
private _messages: Map<string, any>;

constructor(parent: Cursor | null, value: Object) {
this._parent = parent;
this._value = value;
this._messages = new Map<string, Object>();
this._messages = new Map<string, any>();
}

get parent(): Cursor | null {
Expand All @@ -182,6 +182,17 @@ export class Cursor {
return new Cursor(this._parent === null ? null : this._parent.fork(), this.value);
}

parentTreeCursor(): Cursor {
let c: Cursor | null = this.parent;
while (c && c.parent) {
if (isTree(c.value()) || c.parent.value() === Cursor.ROOT_VALUE) {
return c;
}
c = c.parent;
}
throw new Error(`Expected to find parent tree cursor for ${c}`);
}

firstEnclosing<T>(type: Constructor<T>): T | null {
let c: Cursor | null = this;

Expand Down Expand Up @@ -211,6 +222,10 @@ export class Cursor {
getMessage<T>(key: string, defaultValue?: T | null): T | null {
return this._messages.get(key) as T || defaultValue!;
}

putMessage(key: string, value: any) {
this._messages.set(key, value);
}
}

@LstType("org.openrewrite.Checksum")
Expand Down
139 changes: 139 additions & 0 deletions openrewrite/src/javascript/format/blankLines.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import * as J from '../../java';
import * as JS from '..';
import {ClassDeclaration, Space} from '../../java';
import {Cursor, InMemoryExecutionContext} from "../../core";
import {JavaScriptVisitor} from "..";
import {BlankLinesStyle} from "../style";

export class BlankLinesFormatVisitor extends JavaScriptVisitor<InMemoryExecutionContext> {
private style: BlankLinesStyle;

constructor(style: BlankLinesStyle) {
super();
this.style = style;
this.cursor = new Cursor(null, Cursor.ROOT_VALUE);
}

visitJsCompilationUnit(compilationUnit: JS.CompilationUnit, p: InMemoryExecutionContext): J.J | null {
if (compilationUnit.prefix.comments.length == 0) {
compilationUnit = compilationUnit.withPrefix(Space.EMPTY);
}
return super.visitJsCompilationUnit(compilationUnit, p);
}

visitStatement(statement: J.Statement, p: InMemoryExecutionContext): J.J {
statement = super.visitStatement(statement, p);

const parentCursor = this.cursor.parentTreeCursor();
const topLevel = parentCursor.value() instanceof JS.CompilationUnit;

let prevBlankLine: number | null | undefined;
const blankLines = this.getBlankLines(statement, parentCursor);
if (blankLines) {
prevBlankLine = parentCursor.getMessage('prev_blank_line', undefined);
parentCursor.putMessage('prev_blank_line', blankLines);
} else {
prevBlankLine = parentCursor.getMessage('prev_blank_line', undefined);
if (prevBlankLine) {
parentCursor.putMessage('prev_blank_line', undefined);
}
}

if (topLevel) {
const isFirstStatement = p.getMessage<boolean>('is_first_statement', true) ?? true;
if (isFirstStatement) {
p.putMessage('is_first_statement', false);
} else {
const minLines = statement instanceof JS.JsImport ? 0 : max(prevBlankLine, blankLines);
statement = adjustedLinesForTree(statement, minLines, this.style.keepMaximum.inCode);
}
} else {
const inBlock = parentCursor.value() instanceof J.Block;
const inClass = inBlock && parentCursor.parentTreeCursor().value() instanceof J.ClassDeclaration;
let minLines = 0;

if (inClass) {
const isFirst = (parentCursor.value() as J.Block).statements[0] === statement;
minLines = isFirst ? 0 : max(blankLines, prevBlankLine);
}

statement = adjustedLinesForTree(statement, minLines, this.style.keepMaximum.inCode);
}
return statement;
}

getBlankLines(statement: J.Statement, cursor: Cursor): number | undefined {
const inBlock = cursor.value() instanceof J.Block;
let type;
if (inBlock) {
const val = cursor.parentTreeCursor().value();
if (val instanceof J.ClassDeclaration) {
type = val.padding.kind.type;
}
}

if (type === ClassDeclaration.Kind.Type.Interface && (statement instanceof J.MethodDeclaration || statement instanceof JS.JSMethodDeclaration)) {
return this.style.minimum.aroundMethodInInterface ?? undefined;
} else if (type === ClassDeclaration.Kind.Type.Interface && (statement instanceof J.VariableDeclarations || statement instanceof JS.JSVariableDeclarations)) {
return this.style.minimum.aroundFieldInInterface ?? undefined;
} else if (type === ClassDeclaration.Kind.Type.Class && (statement instanceof J.VariableDeclarations || statement instanceof JS.JSVariableDeclarations)) {
return this.style.minimum.aroundField;
} else if (statement instanceof JS.JsImport) {
return this.style.minimum.afterImports;
} else if (statement instanceof J.ClassDeclaration) {
return this.style.minimum.aroundClass;
} else if (statement instanceof J.MethodDeclaration || statement instanceof JS.JSMethodDeclaration) {
return this.style.minimum.aroundMethod;
} else if (statement instanceof JS.FunctionDeclaration) {
return this.style.minimum.aroundFunction;
} else {
return undefined;
}
}

}

function adjustedLinesForTree(tree: J.J, minLines: number, maxLines: number): J.J {

if (tree instanceof JS.ScopedVariableDeclarations && tree.padding.scope) {
const prefix = tree.padding.scope.before;
return tree.padding.withScope(tree.padding.scope.withBefore(adjustedLinesForSpace(prefix, minLines, maxLines)));
} else {
const prefix = tree.prefix;
return tree.withPrefix(adjustedLinesForSpace(prefix, minLines, maxLines));
}

}

function adjustedLinesForSpace(prefix: Space, minLines: number, maxLines: number): Space {
if (prefix.comments.length == 0 || prefix.whitespace?.includes('\n')) {
return prefix.withWhitespace(adjustedLinesForString(prefix.whitespace ?? '', minLines, maxLines));
}

return prefix;
}

function adjustedLinesForString(whitespace: string, minLines: number, maxLines: number): string {
const existingBlankLines = Math.max(countLineBreaks(whitespace) - 1, 0);
maxLines = Math.max(minLines, maxLines);

if (existingBlankLines >= minLines && existingBlankLines <= maxLines) {
return whitespace;
} else if (existingBlankLines < minLines) {
return '\n'.repeat(minLines - existingBlankLines) + whitespace;
} else {
return '\n'.repeat(maxLines) + whitespace.substring(whitespace.lastIndexOf('\n'));
}
}

function countLineBreaks(whitespace: string): number {
return (whitespace.match(/\n/g) || []).length;
}

function max(x: number | null | undefined, y: number | null | undefined) {
if (x && y) {
return Math.max(x, y);
} else {
return x ? x : y ? y : 0;
}
}
Loading

0 comments on commit 9a81022

Please # to comment.