Skip to content
This repository was archived by the owner on May 12, 2025. It is now read-only.

Commit 9a81022

Browse files
arodionovAndrii Rodionov
andauthored
Formatter infrastructure and BlankLines formatter (#213)
* Formatter styles classes * Added BlankLines formatter * typos * remove unused imports --------- Co-authored-by: Andrii Rodionov <andriih@moderne.io>
1 parent dd7f5d6 commit 9a81022

File tree

26 files changed

+1455
-86
lines changed

26 files changed

+1455
-86
lines changed

openrewrite/src/core/execution.ts

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {createTwoFilesPatch} from 'diff';
22
import {PathLike} from 'fs';
3-
import {Cursor, TreeVisitor} from "./tree";
3+
import {Cursor, SourceFile, TreeVisitor} from "./tree";
44

55
export class Result {
66
static diff(before: string, after: string, path: PathLike): string {
@@ -60,10 +60,86 @@ export class DelegatingExecutionContext implements ExecutionContext {
6060
}
6161
}
6262

63+
interface LargeSourceSet {
64+
edit(map: (source: SourceFile) => SourceFile | null): LargeSourceSet;
65+
getChangeSet(): RecipeRunResult[];
66+
}
67+
68+
export class InMemoryLargeSourceSet implements LargeSourceSet {
69+
private readonly initialState?: InMemoryLargeSourceSet;
70+
private readonly sources: SourceFile[];
71+
private readonly deletions: SourceFile[];
72+
73+
constructor(sources: SourceFile[], deletions: SourceFile[] = [], initialState?: InMemoryLargeSourceSet) {
74+
this.initialState = initialState;
75+
this.sources = sources;
76+
this.deletions = deletions;
77+
}
78+
79+
edit(map: (source: SourceFile) => SourceFile | null): InMemoryLargeSourceSet {
80+
const mapped: SourceFile[] = [];
81+
const deleted: SourceFile[] = this.initialState ? [...this.initialState.deletions] : [];
82+
let changed = false;
83+
84+
for (const source of this.sources) {
85+
const mappedSource = map(source);
86+
if (mappedSource !== null) {
87+
mapped.push(mappedSource);
88+
changed = mappedSource !== source;
89+
} else {
90+
deleted.push(source);
91+
changed = true;
92+
}
93+
}
94+
95+
return changed ? new InMemoryLargeSourceSet(mapped, deleted, this.initialState ?? this) : this;
96+
}
97+
98+
getChangeSet(): RecipeRunResult[] {
99+
const sourceFileById = new Map(this.initialState?.sources.map(sf => [sf.id, sf]));
100+
const changes: RecipeRunResult[] = [];
101+
102+
for (const source of this.sources) {
103+
const original = sourceFileById.get(source.id) || null;
104+
changes.push(new RecipeRunResult(original, source));
105+
}
106+
107+
for (const source of this.deletions) {
108+
changes.push(new RecipeRunResult(source, null));
109+
}
110+
111+
return changes;
112+
}
113+
}
114+
115+
export class RecipeRunResult {
116+
constructor(
117+
public readonly before: SourceFile | null,
118+
public readonly after: SourceFile | null
119+
) {}
120+
}
121+
63122
export class Recipe {
64123
getVisitor(): TreeVisitor<any, ExecutionContext> {
65124
return TreeVisitor.noop();
66125
}
126+
127+
getRecipeList(): Recipe[] {
128+
return [];
129+
}
130+
131+
run(before: LargeSourceSet, ctx: ExecutionContext): RecipeRunResult[] {
132+
const lss = this.runInternal(before, ctx, new Cursor(null, Cursor.ROOT_VALUE));
133+
return lss.getChangeSet();
134+
}
135+
136+
runInternal(before: LargeSourceSet, ctx: ExecutionContext, root: Cursor): LargeSourceSet {
137+
let after = before.edit((beforeFile) => this.getVisitor().visit(beforeFile, ctx, root));
138+
for (const recipe of this.getRecipeList()) {
139+
after = recipe.runInternal(after, ctx, root);
140+
}
141+
return after;
142+
}
67143
}
68144

69145
export class RecipeRunException extends Error {
@@ -83,4 +159,4 @@ export class RecipeRunException extends Error {
83159
get cursor(): Cursor | undefined {
84160
return this._cursor;
85161
}
86-
}
162+
}

openrewrite/src/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from './markers';
33
export * from './parser';
44
export * from './tree';
55
export * from './utils';
6+
export * from './style';

openrewrite/src/core/style.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {Marker, MarkerSymbol} from "./markers";
2+
import {UUID} from "./utils";
3+
4+
export abstract class Style {
5+
merge(lowerPrecedence: Style): Style {
6+
return this;
7+
}
8+
9+
applyDefaults(): Style {
10+
return this;
11+
}
12+
}
13+
14+
export class NamedStyles implements Marker {
15+
[MarkerSymbol] = true;
16+
17+
private readonly _id: UUID;
18+
name: string;
19+
displayName: string;
20+
description?: string;
21+
tags: Set<string>;
22+
styles: Style[];
23+
24+
constructor(id: UUID, name: string, displayName: string, description?: string, tags: Set<string> = new Set(), styles: Style[] = []) {
25+
this._id = id;
26+
this.name = name;
27+
this.displayName = displayName;
28+
this.description = description;
29+
this.tags = tags;
30+
this.styles = styles;
31+
}
32+
33+
public get id(): UUID {
34+
return this._id;
35+
}
36+
37+
public withId(id: UUID): NamedStyles {
38+
return id === this._id ? this : new NamedStyles(id, this.name, this.displayName, this.description, this.tags, this.styles);
39+
}
40+
41+
static merge<S extends Style>(styleClass: new (...args: any[]) => S, namedStyles: NamedStyles[]): S | null {
42+
let merged: S | null = null;
43+
44+
for (const namedStyle of namedStyles) {
45+
if (namedStyle.styles) {
46+
for (let style of namedStyle.styles) {
47+
if (style instanceof styleClass) {
48+
style = style.applyDefaults();
49+
merged = merged ? (merged.merge(style) as S) : (style as S);
50+
}
51+
}
52+
}
53+
}
54+
55+
return merged;
56+
}
57+
58+
}

openrewrite/src/core/tree.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,12 @@ export class Cursor {
162162

163163
private readonly _parent: Cursor | null;
164164
private readonly _value: Object;
165-
private _messages: Map<string, Object>;
165+
private _messages: Map<string, any>;
166166

167167
constructor(parent: Cursor | null, value: Object) {
168168
this._parent = parent;
169169
this._value = value;
170-
this._messages = new Map<string, Object>();
170+
this._messages = new Map<string, any>();
171171
}
172172

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

185+
parentTreeCursor(): Cursor {
186+
let c: Cursor | null = this.parent;
187+
while (c && c.parent) {
188+
if (isTree(c.value()) || c.parent.value() === Cursor.ROOT_VALUE) {
189+
return c;
190+
}
191+
c = c.parent;
192+
}
193+
throw new Error(`Expected to find parent tree cursor for ${c}`);
194+
}
195+
185196
firstEnclosing<T>(type: Constructor<T>): T | null {
186197
let c: Cursor | null = this;
187198

@@ -211,6 +222,10 @@ export class Cursor {
211222
getMessage<T>(key: string, defaultValue?: T | null): T | null {
212223
return this._messages.get(key) as T || defaultValue!;
213224
}
225+
226+
putMessage(key: string, value: any) {
227+
this._messages.set(key, value);
228+
}
214229
}
215230

216231
@LstType("org.openrewrite.Checksum")
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import * as J from '../../java';
2+
import * as JS from '..';
3+
import {ClassDeclaration, Space} from '../../java';
4+
import {Cursor, InMemoryExecutionContext} from "../../core";
5+
import {JavaScriptVisitor} from "..";
6+
import {BlankLinesStyle} from "../style";
7+
8+
export class BlankLinesFormatVisitor extends JavaScriptVisitor<InMemoryExecutionContext> {
9+
private style: BlankLinesStyle;
10+
11+
constructor(style: BlankLinesStyle) {
12+
super();
13+
this.style = style;
14+
this.cursor = new Cursor(null, Cursor.ROOT_VALUE);
15+
}
16+
17+
visitJsCompilationUnit(compilationUnit: JS.CompilationUnit, p: InMemoryExecutionContext): J.J | null {
18+
if (compilationUnit.prefix.comments.length == 0) {
19+
compilationUnit = compilationUnit.withPrefix(Space.EMPTY);
20+
}
21+
return super.visitJsCompilationUnit(compilationUnit, p);
22+
}
23+
24+
visitStatement(statement: J.Statement, p: InMemoryExecutionContext): J.J {
25+
statement = super.visitStatement(statement, p);
26+
27+
const parentCursor = this.cursor.parentTreeCursor();
28+
const topLevel = parentCursor.value() instanceof JS.CompilationUnit;
29+
30+
let prevBlankLine: number | null | undefined;
31+
const blankLines = this.getBlankLines(statement, parentCursor);
32+
if (blankLines) {
33+
prevBlankLine = parentCursor.getMessage('prev_blank_line', undefined);
34+
parentCursor.putMessage('prev_blank_line', blankLines);
35+
} else {
36+
prevBlankLine = parentCursor.getMessage('prev_blank_line', undefined);
37+
if (prevBlankLine) {
38+
parentCursor.putMessage('prev_blank_line', undefined);
39+
}
40+
}
41+
42+
if (topLevel) {
43+
const isFirstStatement = p.getMessage<boolean>('is_first_statement', true) ?? true;
44+
if (isFirstStatement) {
45+
p.putMessage('is_first_statement', false);
46+
} else {
47+
const minLines = statement instanceof JS.JsImport ? 0 : max(prevBlankLine, blankLines);
48+
statement = adjustedLinesForTree(statement, minLines, this.style.keepMaximum.inCode);
49+
}
50+
} else {
51+
const inBlock = parentCursor.value() instanceof J.Block;
52+
const inClass = inBlock && parentCursor.parentTreeCursor().value() instanceof J.ClassDeclaration;
53+
let minLines = 0;
54+
55+
if (inClass) {
56+
const isFirst = (parentCursor.value() as J.Block).statements[0] === statement;
57+
minLines = isFirst ? 0 : max(blankLines, prevBlankLine);
58+
}
59+
60+
statement = adjustedLinesForTree(statement, minLines, this.style.keepMaximum.inCode);
61+
}
62+
return statement;
63+
}
64+
65+
getBlankLines(statement: J.Statement, cursor: Cursor): number | undefined {
66+
const inBlock = cursor.value() instanceof J.Block;
67+
let type;
68+
if (inBlock) {
69+
const val = cursor.parentTreeCursor().value();
70+
if (val instanceof J.ClassDeclaration) {
71+
type = val.padding.kind.type;
72+
}
73+
}
74+
75+
if (type === ClassDeclaration.Kind.Type.Interface && (statement instanceof J.MethodDeclaration || statement instanceof JS.JSMethodDeclaration)) {
76+
return this.style.minimum.aroundMethodInInterface ?? undefined;
77+
} else if (type === ClassDeclaration.Kind.Type.Interface && (statement instanceof J.VariableDeclarations || statement instanceof JS.JSVariableDeclarations)) {
78+
return this.style.minimum.aroundFieldInInterface ?? undefined;
79+
} else if (type === ClassDeclaration.Kind.Type.Class && (statement instanceof J.VariableDeclarations || statement instanceof JS.JSVariableDeclarations)) {
80+
return this.style.minimum.aroundField;
81+
} else if (statement instanceof JS.JsImport) {
82+
return this.style.minimum.afterImports;
83+
} else if (statement instanceof J.ClassDeclaration) {
84+
return this.style.minimum.aroundClass;
85+
} else if (statement instanceof J.MethodDeclaration || statement instanceof JS.JSMethodDeclaration) {
86+
return this.style.minimum.aroundMethod;
87+
} else if (statement instanceof JS.FunctionDeclaration) {
88+
return this.style.minimum.aroundFunction;
89+
} else {
90+
return undefined;
91+
}
92+
}
93+
94+
}
95+
96+
function adjustedLinesForTree(tree: J.J, minLines: number, maxLines: number): J.J {
97+
98+
if (tree instanceof JS.ScopedVariableDeclarations && tree.padding.scope) {
99+
const prefix = tree.padding.scope.before;
100+
return tree.padding.withScope(tree.padding.scope.withBefore(adjustedLinesForSpace(prefix, minLines, maxLines)));
101+
} else {
102+
const prefix = tree.prefix;
103+
return tree.withPrefix(adjustedLinesForSpace(prefix, minLines, maxLines));
104+
}
105+
106+
}
107+
108+
function adjustedLinesForSpace(prefix: Space, minLines: number, maxLines: number): Space {
109+
if (prefix.comments.length == 0 || prefix.whitespace?.includes('\n')) {
110+
return prefix.withWhitespace(adjustedLinesForString(prefix.whitespace ?? '', minLines, maxLines));
111+
}
112+
113+
return prefix;
114+
}
115+
116+
function adjustedLinesForString(whitespace: string, minLines: number, maxLines: number): string {
117+
const existingBlankLines = Math.max(countLineBreaks(whitespace) - 1, 0);
118+
maxLines = Math.max(minLines, maxLines);
119+
120+
if (existingBlankLines >= minLines && existingBlankLines <= maxLines) {
121+
return whitespace;
122+
} else if (existingBlankLines < minLines) {
123+
return '\n'.repeat(minLines - existingBlankLines) + whitespace;
124+
} else {
125+
return '\n'.repeat(maxLines) + whitespace.substring(whitespace.lastIndexOf('\n'));
126+
}
127+
}
128+
129+
function countLineBreaks(whitespace: string): number {
130+
return (whitespace.match(/\n/g) || []).length;
131+
}
132+
133+
function max(x: number | null | undefined, y: number | null | undefined) {
134+
if (x && y) {
135+
return Math.max(x, y);
136+
} else {
137+
return x ? x : y ? y : 0;
138+
}
139+
}

0 commit comments

Comments
 (0)