Skip to content
This repository was archived by the owner on May 12, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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