diff --git a/openrewrite/src/core/execution.ts b/openrewrite/src/core/execution.ts index e33033b0..c45ed2f3 100644 --- a/openrewrite/src/core/execution.ts +++ b/openrewrite/src/core/execution.ts @@ -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 { @@ -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 { 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 { @@ -83,4 +159,4 @@ export class RecipeRunException extends Error { get cursor(): Cursor | undefined { return this._cursor; } -} \ No newline at end of file +} diff --git a/openrewrite/src/core/index.ts b/openrewrite/src/core/index.ts index dd8d415c..83489352 100644 --- a/openrewrite/src/core/index.ts +++ b/openrewrite/src/core/index.ts @@ -3,3 +3,4 @@ export * from './markers'; export * from './parser'; export * from './tree'; export * from './utils'; +export * from './style'; diff --git a/openrewrite/src/core/style.ts b/openrewrite/src/core/style.ts new file mode 100644 index 00000000..23c0d73c --- /dev/null +++ b/openrewrite/src/core/style.ts @@ -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; + styles: Style[]; + + constructor(id: UUID, name: string, displayName: string, description?: string, tags: Set = 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(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; + } + +} diff --git a/openrewrite/src/core/tree.ts b/openrewrite/src/core/tree.ts index 35e5de68..64a3570e 100644 --- a/openrewrite/src/core/tree.ts +++ b/openrewrite/src/core/tree.ts @@ -162,12 +162,12 @@ export class Cursor { private readonly _parent: Cursor | null; private readonly _value: Object; - private _messages: Map; + private _messages: Map; constructor(parent: Cursor | null, value: Object) { this._parent = parent; this._value = value; - this._messages = new Map(); + this._messages = new Map(); } get parent(): Cursor | null { @@ -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(type: Constructor): T | null { let c: Cursor | null = this; @@ -211,6 +222,10 @@ export class Cursor { getMessage(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") diff --git a/openrewrite/src/javascript/format/blankLines.ts b/openrewrite/src/javascript/format/blankLines.ts new file mode 100644 index 00000000..36517dd7 --- /dev/null +++ b/openrewrite/src/javascript/format/blankLines.ts @@ -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 { + 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('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; + } +} diff --git a/openrewrite/src/javascript/parser.ts b/openrewrite/src/javascript/parser.ts index 60800798..a4688cd5 100644 --- a/openrewrite/src/javascript/parser.ts +++ b/openrewrite/src/javascript/parser.ts @@ -861,10 +861,12 @@ export class JavaScriptParserVisitor { } visitPropertySignature(node: ts.PropertySignature) { + const prefix = this.prefix(node); + if (node.questionToken) { return new JS.JSVariableDeclarations( randomId(), - this.prefix(node), + prefix, Markers.EMPTY, [], // no decorators allowed this.mapModifiers(node), @@ -890,7 +892,7 @@ export class JavaScriptParserVisitor { if (nameExpression instanceof J.Identifier) { return new J.VariableDeclarations( randomId(), - this.prefix(node), + prefix, Markers.EMPTY, [], // no decorators allowed this.mapModifiers(node), @@ -913,7 +915,7 @@ export class JavaScriptParserVisitor { } else { return new JS.JSVariableDeclarations( randomId(), - this.prefix(node), + prefix, Markers.EMPTY, [], // no decorators allowed this.mapModifiers(node), @@ -936,10 +938,12 @@ export class JavaScriptParserVisitor { } visitPropertyDeclaration(node: ts.PropertyDeclaration) { + const prefix = this.prefix(node); + if (node.questionToken) { return new JS.JSVariableDeclarations( randomId(), - this.prefix(node), + prefix, Markers.EMPTY, this.mapDecorators(node), this.mapModifiers(node), @@ -963,7 +967,7 @@ export class JavaScriptParserVisitor { if (node.exclamationToken) { return new JS.JSVariableDeclarations( randomId(), - this.prefix(node), + prefix, Markers.EMPTY, this.mapDecorators(node), this.mapModifiers(node), @@ -996,7 +1000,7 @@ export class JavaScriptParserVisitor { if (nameExpression instanceof J.Identifier) { return new J.VariableDeclarations( randomId(), - this.prefix(node), + prefix, Markers.EMPTY, this.mapDecorators(node), this.mapModifiers(node), @@ -1020,7 +1024,7 @@ export class JavaScriptParserVisitor { return new JS.JSVariableDeclarations( randomId(), - this.prefix(node), + prefix, Markers.EMPTY, this.mapDecorators(node), this.mapModifiers(node), @@ -1042,10 +1046,12 @@ export class JavaScriptParserVisitor { } visitMethodSignature(node: ts.MethodSignature) { + const prefix = this.prefix(node); + if (node.questionToken) { return new JS.JSMethodDeclaration( randomId(), - this.prefix(node), + prefix, Markers.EMPTY, [], // no decorators allowed [], // no modifiers allowed @@ -1063,7 +1069,7 @@ export class JavaScriptParserVisitor { if (ts.isComputedPropertyName(node.name)) { return new JS.JSMethodDeclaration( randomId(), - this.prefix(node), + prefix, Markers.EMPTY, [], // no decorators allowed [], // no modifiers allowed @@ -1086,7 +1092,7 @@ export class JavaScriptParserVisitor { return new J.MethodDeclaration( randomId(), - this.prefix(node), + prefix, Markers.EMPTY, [], // no decorators allowed [], // no modifiers allowed @@ -1105,6 +1111,8 @@ export class JavaScriptParserVisitor { } visitMethodDeclaration(node: ts.MethodDeclaration) { + const prefix = this.prefix(node); + if (node.questionToken || node.asteriskToken) { let methodName = node.questionToken ? this.getOptionalUnary(node) : this.visit(node.name); @@ -1121,7 +1129,7 @@ export class JavaScriptParserVisitor { return new JS.JSMethodDeclaration( randomId(), - this.prefix(node), + prefix, Markers.EMPTY, this.mapDecorators(node), this.mapModifiers(node), @@ -1135,11 +1143,12 @@ export class JavaScriptParserVisitor { this.mapMethodType(node) ); } + const name = node.name ? this.visit(node.name) : this.mapIdentifier(node, ""); if (!(name instanceof J.Identifier)) { return new JS.JSMethodDeclaration( randomId(), - this.prefix(node), + prefix, Markers.EMPTY, this.mapDecorators(node), this.mapModifiers(node), @@ -1156,7 +1165,7 @@ export class JavaScriptParserVisitor { return new J.MethodDeclaration( randomId(), - this.prefix(node), + prefix, Markers.EMPTY, this.mapDecorators(node), this.mapModifiers(node), diff --git a/openrewrite/src/javascript/style.ts b/openrewrite/src/javascript/style.ts new file mode 100644 index 00000000..2103c5f7 --- /dev/null +++ b/openrewrite/src/javascript/style.ts @@ -0,0 +1,336 @@ +import { + Style, NamedStyles, randomId +} from "../core"; + +export abstract class JavaScriptStyle extends Style { +} + +export class SpacesStyle extends JavaScriptStyle { + constructor( + readonly beforeParentheses: SpacesStyle.BeforeParentheses, + readonly aroundOperators: SpacesStyle.AroundOperators, + readonly beforeLeftBrace: SpacesStyle.BeforeLeftBrace, + readonly beforeKeywords: SpacesStyle.BeforeKeywords, + readonly within: SpacesStyle.Within, + readonly ternaryOperator: SpacesStyle.TernaryOperator, + readonly other: SpacesStyle.Other + ) { + super(); + } +} + +export namespace SpacesStyle { + export class BeforeParentheses { + constructor( + public readonly functionDeclarationParentheses: boolean, + public readonly functionCallParentheses: boolean, + public readonly ifParentheses: boolean, + public readonly forParentheses: boolean, + public readonly whileParentheses: boolean, + public readonly switchParentheses: boolean, + public readonly catchParentheses: boolean, + public readonly inFunctionCallExpression: boolean, + public readonly inAsyncArrowFunction: boolean + ) { + } + } + + export class AroundOperators { + constructor( + public readonly assignment: boolean, + public readonly logical: boolean, + public readonly equality: boolean, + public readonly relational: boolean, + public readonly bitwise: boolean, + public readonly additive: boolean, + public readonly multiplicative: boolean, + public readonly shift: boolean, + public readonly unary: boolean, + public readonly arrowFunction: boolean, + public readonly beforeUnaryNotAndNotNull: boolean, + public readonly afterUnaryNotAndNotNull: boolean + ) { + } + } + + export class BeforeLeftBrace { + constructor( + public readonly functionLeftBrace: boolean, + public readonly ifLeftBrace: boolean, + public readonly elseLeftBrace: boolean, + public readonly forLeftBrace: boolean, + public readonly whileLeftBrace: boolean, + public readonly doLeftBrace: boolean, + public readonly switchLeftBrace: boolean, + public readonly tryLeftBrace: boolean, + public readonly catchLeftBrace: boolean, + public readonly finallyLeftBrace: boolean, + public readonly classInterfaceModuleLeftBrace: boolean + ) { + } + } + + export class BeforeKeywords { + constructor( + public readonly elseKeyword: boolean, + public readonly whileKeyword: boolean, + public readonly catchKeyword: boolean, + public readonly finallyKeyword: boolean + ) { + } + } + + export class Within { + constructor( + public readonly indexAccessBrackets: boolean, + public readonly groupingParentheses: boolean, + public readonly functionDeclarationParentheses: boolean, + public readonly functionCallParentheses: boolean, + public readonly ifParentheses: boolean, + public readonly forParentheses: boolean, + public readonly whileParentheses: boolean, + public readonly switchParentheses: boolean, + public readonly catchParentheses: boolean, + public readonly objectLiteralBraces: boolean, + public readonly es6ImportExportBraces: boolean, + public readonly arrayBrackets: boolean, + public readonly interpolationExpressions: boolean, + public readonly objectLiteralTypeBraces: boolean, + public readonly unionAndIntersectionTypes: boolean, + public readonly typeAssertions: boolean + ) { + } + } + + export class TernaryOperator { + constructor( + public readonly beforeQuestionMark: boolean, + public readonly afterQuestionMark: boolean, + public readonly beforeColon: boolean, + public readonly afterColon: boolean + ) { + } + } + + export class Other { + constructor( + public readonly beforeComma: boolean, + public readonly afterComma: boolean, + public readonly beforeForSemicolon: boolean, + public readonly beforePropertyNameValueSeparator: boolean, + public readonly afterPropertyNameValueSeparator: boolean, + public readonly afterVarArgInRestOrSpread: boolean, + public readonly beforeAsteriskInGenerator: boolean, + public readonly afterAsteriskInGenerator: boolean, + public readonly beforeTypeReferenceColon: boolean, + public readonly afterTypeReferenceColon: boolean + ) { + } + } +} + +export class WrappingAndBraces extends JavaScriptStyle { +} + +export class TabsAndIndentsStyle extends JavaScriptStyle { + constructor( + public readonly useTabCharacter: boolean, + public readonly tabSize: number, + public readonly indentSize: number, + public readonly continuationIndent: number, + public readonly keepIndentsOnEmptyLines: boolean, + public readonly indentChainedMethods: boolean, + public readonly indentAllChainedCallsInAGroup: boolean + ) { + super(); + } +} + +export class BlankLinesStyle extends JavaScriptStyle { + constructor( + public readonly keepMaximum: BlankLinesStyle.KeepMaximum, + public readonly minimum: BlankLinesStyle.Minimum + ) { + super();} +} + +export namespace BlankLinesStyle { + export class KeepMaximum { + constructor(public readonly inCode: number) {} + } + + export class Minimum { + constructor( + public readonly afterImports: number, + public readonly aroundClass: number, + public readonly aroundFieldInInterface: number | null, + public readonly aroundField: number, + public readonly aroundMethodInInterface: number | null, + public readonly aroundMethod: number, + public readonly aroundFunction: number + ) {} + } +} + +export class ImportsStyle extends JavaScriptStyle { + constructor( + public readonly mergeImportsForMembersFromTheSameModule: boolean, + public readonly usePathRelativeToTheProjectOrResourceOrSourcesRootsOrTsconfigJson: boolean, + public readonly useDirectoryImportsWhenIndexJsIsAvailable: boolean, + public readonly useFileExtensions: ImportsStyle.UseFileExtensions, + public readonly useTypeModifiersInImports: ImportsStyle.UseTypeModifiersInImports | null, + public readonly usePathMappingsFromTSConfigJson: ImportsStyle.UsePathMappingsFromTSConfigJson | null, + public readonly usePathAliases: ImportsStyle.UsePathAliases | null, + public readonly doNotImportExactlyFrom: string[], + public readonly sortImportedMembers: boolean, + public readonly sortImportsByModules: boolean + ) { + super();} +} + +export namespace ImportsStyle { + export enum UseFileExtensions { + Auto, + AlwaysJs, + Never + } + + export enum UsePathAliases { + Always, + OnlyInFilesOutsideSpecifiedPath, + Never + } + + export enum UsePathMappingsFromTSConfigJson { + Always, + OnlyInFilesOutsideSpecifiedPath, + Never + } + + export enum UseTypeModifiersInImports { + Auto, + AlwaysWithType, + Never + } +} + +export class PunctuationStyle extends JavaScriptStyle { + constructor(public readonly trailingComma: PunctuationStyle.TrailingComma) { + super();} +} + +export namespace PunctuationStyle { + export enum TrailingComma { + Keep, + Remove, + AddWhenMultiLine + } +} + + +export class IntelliJ extends NamedStyles { + constructor() { + super( + randomId(), + "org.openrewrite.javascript.style.IntelliJ", + "IntelliJ IDEA", + "IntelliJ IDEA default JS/TS style.", + new Set(), + [ + IntelliJ.JavaScript.spaces(), + IntelliJ.JavaScript.wrappingAndBraces(), + IntelliJ.JavaScript.tabsAndIndents(), + IntelliJ.JavaScript.blankLines(), + IntelliJ.JavaScript.imports(), + IntelliJ.JavaScript.punctuation(), + + IntelliJ.TypeScript.spaces(), + IntelliJ.TypeScript.wrappingAndBraces(), + IntelliJ.TypeScript.tabsAndIndents(), + IntelliJ.TypeScript.blankLines(), + IntelliJ.TypeScript.imports(), + IntelliJ.TypeScript.punctuation() + ] + ); + } + + static JavaScript = class { + static tabsAndIndents(): TabsAndIndentsStyle { + return new TabsAndIndentsStyle(false, 4, 4, 4, false, true, false); + } + + static spaces(): SpacesStyle { + return new SpacesStyle( + new SpacesStyle.BeforeParentheses(false, false, true, true, true, true, true, true, true), + new SpacesStyle.AroundOperators(true, true, true, true, true, true, true, true, false, true, false, false), + new SpacesStyle.BeforeLeftBrace(true, true, true, true, true, true, true, true, true, true, true), + new SpacesStyle.BeforeKeywords(true, true, true, true), + new SpacesStyle.Within(false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false), + new SpacesStyle.TernaryOperator(true, true, true, true), + new SpacesStyle.Other(false, true, false, false, true, false, false, true, false, false) + ); + } + + static blankLines(): BlankLinesStyle { + return new BlankLinesStyle(new BlankLinesStyle.KeepMaximum(2), new BlankLinesStyle.Minimum(1, 1, null, 0, null, 1, 1)); + } + + static imports(): ImportsStyle { + return new ImportsStyle(true, false, true, ImportsStyle.UseFileExtensions.Auto, null, null, ImportsStyle.UsePathAliases.Always, [ + "rxjs/Rx", + "node_modules/**", + "**/node_modules/**", + "@angular/material", + "@angular/material/typings/**" + ], true, false); + } + + static wrappingAndBraces(): WrappingAndBraces { + return new WrappingAndBraces(); + } + + static punctuation(): PunctuationStyle { + return new PunctuationStyle(PunctuationStyle.TrailingComma.Keep); + } + }; + + static TypeScript = class { + static tabsAndIndents(): TabsAndIndentsStyle { + return new TabsAndIndentsStyle(false, 4, 4, 4, false, true, false); + } + + static spaces(): SpacesStyle { + return new SpacesStyle( + new SpacesStyle.BeforeParentheses(false, false, true, true, true, true, true, true, true), + new SpacesStyle.AroundOperators(true, true, true, true, true, true, true, true, false, true, false, false), + new SpacesStyle.BeforeLeftBrace(true, true, true, true, true, true, true, true, true, true, false), + new SpacesStyle.BeforeKeywords(true, true, true, true), + new SpacesStyle.Within(false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, false), + new SpacesStyle.TernaryOperator(true, true, true, true), + new SpacesStyle.Other(false, true, false, false, true, false, false, true, false, true) + ); + } + + static blankLines(): BlankLinesStyle { + return new BlankLinesStyle(new BlankLinesStyle.KeepMaximum(2), new BlankLinesStyle.Minimum(1, 1, 0, 0, 1, 1, 1)); + } + + static imports(): ImportsStyle { + return new ImportsStyle(true, false, true, ImportsStyle.UseFileExtensions.Auto, ImportsStyle.UseTypeModifiersInImports.Auto, ImportsStyle.UsePathMappingsFromTSConfigJson.Always, null, [ + "rxjs/Rx", + "node_modules/**", + "**/node_modules/**", + "@angular/material", + "@angular/material/typings/**" + ], true, false); + } + + static wrappingAndBraces(): WrappingAndBraces { + return new WrappingAndBraces(); + } + + static punctuation(): PunctuationStyle { + return new PunctuationStyle(PunctuationStyle.TrailingComma.Keep); + } + }; +} diff --git a/openrewrite/test/javascript/format/blankLines.test.ts b/openrewrite/test/javascript/format/blankLines.test.ts new file mode 100644 index 00000000..a2860a5a --- /dev/null +++ b/openrewrite/test/javascript/format/blankLines.test.ts @@ -0,0 +1,309 @@ +import { + connect, + disconnect, + rewriteRunWithRecipe, + rewriteRunWithRecipeAndOptions, + typeScript +} from '../testHarness'; +import {BlankLinesFormatVisitor} from "../../../dist/src/javascript/format/blankLines"; +import {fromVisitor, RecipeSpec} from "../recipeHarness"; +import {IntelliJ} from "../../../dist/src/javascript/style"; + +describe('blank lines format test', () => { + beforeAll(() => connect()); + afterAll(() => disconnect()); + + test('leading blank lines', () => { + rewriteRunWithRecipeAndOptions( + {normalizeIndent: false}, + new RecipeSpec().withRecipe(fromVisitor(new BlankLinesFormatVisitor(IntelliJ.TypeScript.blankLines()))), + //language=typescript + typeScript( +` + +let printed = print("sourceFile");`, +'let printed = print("sourceFile");' + ) + ); + }); + + test('blank lines after import and variables', () => { + rewriteRunWithRecipe( + new RecipeSpec().withRecipe(fromVisitor(new BlankLinesFormatVisitor(IntelliJ.TypeScript.blankLines()))), + //language=typescript + typeScript(` + import {Component} from 'React' + import {add, subtract} from 'utils'; + /*a*/ + const x = 10; + `, + ` + import {Component} from 'React' + import {add, subtract} from 'utils'; + + /*a*/ + const x = 10; + ` + ) + ); + }); + + test('blank lines exists after import and variables', () => { + rewriteRunWithRecipe( + new RecipeSpec().withRecipe(fromVisitor(new BlankLinesFormatVisitor(IntelliJ.TypeScript.blankLines()))), + //language=typescript + typeScript(` + import {Component} from 'React' + import {add, subtract} from 'utils'; + + const x = 10; + `, + ` + import {Component} from 'React' + import {add, subtract} from 'utils'; + + const x = 10; + ` + ) + ); + }); + + test('blank lines exists after import and variables large maximum', () => { + rewriteRunWithRecipe( + new RecipeSpec().withRecipe(fromVisitor(new BlankLinesFormatVisitor(IntelliJ.TypeScript.blankLines()))), + //language=typescript + typeScript(` + import {Component} from 'React' + import {add, subtract} from 'utils'; + + + + const x = 10; + `, + ` + import {Component} from 'React' + import {add, subtract} from 'utils'; + + + const x = 10; + ` + ) + ); + }); + + test('blank lines after import and function', () => { + rewriteRunWithRecipe( + new RecipeSpec().withRecipe(fromVisitor(new BlankLinesFormatVisitor(IntelliJ.TypeScript.blankLines()))), + //language=typescript + typeScript(` + import {Component} from 'React' + import {add, subtract} from 'utils'; + function f() { + } + `, + ` + import {Component} from 'React' + import {add, subtract} from 'utils'; + + function f() { + } + ` + ) + ); + }); + + test('blank lines before function, class, interface', () => { + rewriteRunWithRecipe( + new RecipeSpec().withRecipe(fromVisitor(new BlankLinesFormatVisitor(IntelliJ.TypeScript.blankLines()))), + //language=typescript + typeScript(` + const x = 10; + function f() { + } + const y = 10; + class Foo {} + const z = 10; + interface I {} + const h = 10; + `, + ` + const x = 10; + + function f() { + } + + const y = 10; + + class Foo {} + + const z = 10; + + interface I {} + + const h = 10; + ` + ) + ); + }); + + + test('blank lines around class methods', () => { + rewriteRunWithRecipe( + new RecipeSpec().withRecipe(fromVisitor(new BlankLinesFormatVisitor(IntelliJ.TypeScript.blankLines()))), + //language=typescript + typeScript(` + class Foo { + x = 10; + + foo() {} + + y? = 10; + } + + class Foo2 { + x = 10; + foo() {} + y? = 10; + } + `, + ` + class Foo { + x = 10; + + foo() {} + + y? = 10; + } + + class Foo2 { + x = 10; + + foo() {} + + y? = 10; + } + ` + ) + ); + }); + + + test('blank lines around interface methods', () => { + rewriteRunWithRecipe( + new RecipeSpec().withRecipe(fromVisitor(new BlankLinesFormatVisitor(IntelliJ.TypeScript.blankLines()))), + //language=typescript + typeScript(` + interface Foo { + field1: number; + foo(): void; + field2: number; + } + + interface Foo2 { + field1: number; + + foo(): void; + + field2: number; + } + `, + ` + interface Foo { + field1: number; + + foo(): void; + + field2: number; + } + + interface Foo2 { + field1: number; + + foo(): void; + + field2: number; + } + ` + ) + ); + }); + + test('blank lines', () => { + rewriteRunWithRecipeAndOptions( + {normalizeIndent: false}, + new RecipeSpec().withRecipe(fromVisitor(new BlankLinesFormatVisitor(IntelliJ.TypeScript.blankLines()))), + //language=typescript + typeScript(` + /** + * This is a sample file + */ + import {Component} from 'React' + import {add, subtract} from 'utils'; + class Foo { + field1 = 1; + field2 = 2; + + foo() { + console.log('foo') + } + + static bar() { + function hello(n) { + console.log('hello ' + n) + } + + var x = 1; + + + + while (x < 10) { + hello(x) + } + } + } + + interface IFoo { + field: number + field2: number + + foo(): void; + }`, + ` + /** + * This is a sample file + */ + import {Component} from 'React' + import {add, subtract} from 'utils'; + + class Foo { + field1 = 1; + field2 = 2; + + foo() { + console.log('foo') + } + + static bar() { + function hello(n) { + console.log('hello ' + n) + } + + var x = 1; + + + while (x < 10) { + hello(x) + } + } + } + + interface IFoo { + field: number + field2: number + + foo(): void; + }` + ) + ); + }); +}); diff --git a/openrewrite/test/javascript/parser/binary.test.ts b/openrewrite/test/javascript/parser/binary.test.ts index 5097795b..975d1090 100644 --- a/openrewrite/test/javascript/parser/binary.test.ts +++ b/openrewrite/test/javascript/parser/binary.test.ts @@ -11,7 +11,7 @@ describe('arithmetic operator mapping', () => { rewriteRun( //language=typescript typeScript( - '1 + 2', + '1 + 2', undefined, cu => { const binary = (cu.statements[0]).expression; expect((binary.type).kind).toBe(JavaType.PrimitiveKind.Double); @@ -23,7 +23,7 @@ describe('arithmetic operator mapping', () => { rewriteRun( //language=typescript typeScript( - '"1" + 2', + '"1" + 2', undefined, cu => { const binary = (cu.statements[0]).expression; expect((binary.type).kind).toBe(JavaType.PrimitiveKind.String); diff --git a/openrewrite/test/javascript/parser/literal.test.ts b/openrewrite/test/javascript/parser/literal.test.ts index bb1098e3..fd50f3c9 100644 --- a/openrewrite/test/javascript/parser/literal.test.ts +++ b/openrewrite/test/javascript/parser/literal.test.ts @@ -10,28 +10,28 @@ describe('identifier mapping', () => { test('number', () => { rewriteRunWithOptions( {normalizeIndent: false}, - typeScript(' 1', sourceFile => { + typeScript(' 1', undefined, sourceFile => { assertLiteralLst(sourceFile, '1', JavaType.PrimitiveKind.Double); })); }); test('string', () => { rewriteRunWithOptions( {normalizeIndent: false}, - typeScript('"1"', sourceFile => { + typeScript('"1"', undefined, sourceFile => { assertLiteralLst(sourceFile, '"1"', JavaType.PrimitiveKind.String); })); }); test('boolean', () => { rewriteRunWithOptions( {normalizeIndent: false}, - typeScript('true', sourceFile => { + typeScript('true', undefined, sourceFile => { assertLiteralLst(sourceFile, 'true', JavaType.PrimitiveKind.Boolean); })); }); test('null', () => { rewriteRunWithOptions( {normalizeIndent: false}, - typeScript('null', sourceFile => { + typeScript('null', undefined, sourceFile => { assertLiteralLst(sourceFile, 'null', JavaType.PrimitiveKind.Null); })); }); @@ -40,21 +40,21 @@ describe('identifier mapping', () => { // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined#description rewriteRunWithOptions( {normalizeIndent: false}, - typeScript('undefined', sourceFile => { + typeScript('undefined', undefined, sourceFile => { assertLiteralLst(sourceFile, 'undefined', JavaType.PrimitiveKind.None); })); }); test('regex', () => { rewriteRunWithOptions( {normalizeIndent: false}, - typeScript('/hello/gi', sourceFile => { + typeScript('/hello/gi', undefined, sourceFile => { assertLiteralLst(sourceFile, '/hello/gi', JavaType.PrimitiveKind.String); })); }); test('template without substitutions', () => { rewriteRunWithOptions( {normalizeIndent: false}, - typeScript('`hello!`', sourceFile => { + typeScript('`hello!`', undefined, sourceFile => { assertLiteralLst(sourceFile, '`hello!`', JavaType.PrimitiveKind.String); })); }); diff --git a/openrewrite/test/javascript/parser/object.test.ts b/openrewrite/test/javascript/parser/object.test.ts index 9ee40a93..9d3d2fd7 100644 --- a/openrewrite/test/javascript/parser/object.test.ts +++ b/openrewrite/test/javascript/parser/object.test.ts @@ -48,7 +48,7 @@ describe('object literal mapping', () => { rewriteRun( //language=typescript typeScript( - 'const c = { [ 1 + 1 ] : 1 }', + 'const c = { [ 1 + 1 ] : 1 }', undefined, cu => { const literal = (((cu.statements[0]).variables[0]).variables[0].initializer); expect(literal.body).toBeDefined(); diff --git a/openrewrite/test/javascript/parser/variableDeclarations.test.ts b/openrewrite/test/javascript/parser/variableDeclarations.test.ts index f6d28861..3a0195e7 100644 --- a/openrewrite/test/javascript/parser/variableDeclarations.test.ts +++ b/openrewrite/test/javascript/parser/variableDeclarations.test.ts @@ -14,7 +14,7 @@ describe('variable declaration mapping', () => { const c = 1; /* c1*/ /*c2 */ const d = 1; - `, cu => { + `, undefined, cu => { expect(cu).toBeDefined(); expect(cu.statements).toHaveLength(2); cu.statements.forEach(statement => { @@ -35,7 +35,7 @@ describe('variable declaration mapping', () => { const c = 1; /* c1*/ /*c2 */ const d = 1; - `, cu => { + `, undefined, cu => { expect(cu).toBeDefined(); expect(cu.statements).toHaveLength(2); cu.statements.forEach(statement => { diff --git a/openrewrite/test/javascript/parser/void.test.ts b/openrewrite/test/javascript/parser/void.test.ts index 1c29697c..a6714324 100644 --- a/openrewrite/test/javascript/parser/void.test.ts +++ b/openrewrite/test/javascript/parser/void.test.ts @@ -9,7 +9,7 @@ describe('void operator mapping', () => { test('void', () => { rewriteRun( //language=typescript - typeScript('void 1', cu => { + typeScript('void 1', undefined, cu => { const statement = cu.statements[0] as JS.ExpressionStatement; expect(statement.expression).toBeInstanceOf(JS.Void); const type = (statement.expression as JS.Void).type as JavaType.Primitive; diff --git a/openrewrite/test/javascript/recipeHarness.ts b/openrewrite/test/javascript/recipeHarness.ts new file mode 100644 index 00000000..ece31001 --- /dev/null +++ b/openrewrite/test/javascript/recipeHarness.ts @@ -0,0 +1,35 @@ +import { + TreeVisitor, + ExecutionContext, + Recipe, +} from '../../dist/src/core'; + +export class RecipeSpec { + private readonly _recipe?: Recipe; + + constructor(recipe?: Recipe) { + this._recipe = recipe; + } + + get recipe(): Recipe | undefined { + return this._recipe; + } + + withRecipe(recipe: Recipe): RecipeSpec { + return recipe === this._recipe ? this : new RecipeSpec(recipe); + } +} + +export class AdHocRecipe extends Recipe { + constructor(private readonly visitor: TreeVisitor) { + super(); + } + + getVisitor(): TreeVisitor { + return this.visitor; + } +} + +export function fromVisitor(visitor: TreeVisitor): Recipe { + return new AdHocRecipe(visitor); +} diff --git a/openrewrite/test/javascript/testHarness.ts b/openrewrite/test/javascript/testHarness.ts index 6500d45c..f72773ea 100644 --- a/openrewrite/test/javascript/testHarness.ts +++ b/openrewrite/test/javascript/testHarness.ts @@ -13,7 +13,9 @@ import { RecipeRunException, SourceFile, Tree, - TreeVisitor + TreeVisitor, + InMemoryLargeSourceSet, + RecipeRunResult } from '../../dist/src/core'; import * as J from "../../dist/src/java/tree"; import * as JS from "../../dist/src/javascript/tree"; @@ -23,6 +25,7 @@ import net from "net"; import {JavaScriptParser, JavaScriptVisitor, JavaScriptPrinter} from "../../dist/src/javascript"; import {ChildProcessWithoutNullStreams, spawn} from "node:child_process"; import path from "node:path"; +import {RecipeSpec} from "./recipeHarness"; export interface RewriteTestOptions { normalizeIndent?: boolean @@ -30,7 +33,7 @@ export interface RewriteTestOptions { expectUnknowns?: boolean } -export type SourceSpec = (options: RewriteTestOptions) => void; +export type SourceSpec = (options: RewriteTestOptions, recipe?: RecipeSpec) => void; registerJsCodecs(SenderContext, ReceiverContext, RemotingContext) registerJavaCodecs(SenderContext, ReceiverContext, RemotingContext) @@ -159,17 +162,45 @@ export async function disconnect(): Promise { } export function rewriteRun(...sourceSpecs: SourceSpec[]) { - rewriteRunWithOptions({}, ...sourceSpecs); + rewriteRunWithRecipeAndOptions({}, undefined, ...sourceSpecs); } export function rewriteRunWithOptions(options: RewriteTestOptions, ...sourceSpecs: SourceSpec[]) { - sourceSpecs.forEach(sourceSpec => sourceSpec(options)); + rewriteRunWithRecipeAndOptions(options, undefined, ...sourceSpecs); +} + +export function rewriteRunWithRecipe(recipeSpec: RecipeSpec, ...sourceSpecs: SourceSpec[]) { + rewriteRunWithRecipeAndOptions({}, recipeSpec, ...sourceSpecs); +} + +export function rewriteRunWithRecipeAndOptions(options: RewriteTestOptions, recipeSpec?: RecipeSpec, ...sourceSpecs: SourceSpec[]) { + sourceSpecs.forEach(sourceSpec => sourceSpec(options, recipeSpec)); } const parser = JavaScriptParser.builder().build(); -function sourceFile(before: string, defaultPath: string, spec?: (sourceFile: JS.CompilationUnit) => void) { - return (options: RewriteTestOptions) => { +function checkUnknowns(sourceFile: SourceFile, options: RewriteTestOptions) { + try { + let unknowns: J.Unknown[] = []; + new class extends JavaScriptVisitor { + visitUnknown(unknown: J.Unknown, p: number): J.J | null { + unknowns.push(unknown); + return unknown; + } + }().visit(sourceFile, 0); + const expectUnknowns = options.expectUnknowns ?? false; + if (expectUnknowns && unknowns.length == 0) { + throw new Error("No J.Unknown instances were found. Adjust the test expectation."); + } else if (!expectUnknowns && unknowns.length != 0) { + throw new Error("No J.Unknown instances were expected: " + unknowns.map(u => u.source.text)); + } + } catch (e) { + throw e instanceof RecipeRunException ? e.cause : e; + } +} + +function sourceFile(before: string, defaultPath: string, after?: string, spec?: (sourceFile: JS.CompilationUnit) => void) { + return (options: RewriteTestOptions, recipeSpec?: RecipeSpec) => { const ctx = new InMemoryExecutionContext(); before = options.normalizeIndent ?? true ? dedent(before) : before; const [sourceFile] = parser.parseInputs( @@ -184,26 +215,23 @@ function sourceFile(before: string, defaultPath: string, spec?: (sourceFile: JS. if (isParseError(sourceFile)) { throw new Error(`Parsing failed for ${sourceFile.sourcePath}: ${sourceFile.markers.findFirst(ParseExceptionResult)!.message}`); } - try { - let unknowns: J.Unknown[] = []; - new class extends JavaScriptVisitor { - visitUnknown(unknown: J.Unknown, p: number): J.J | null { - unknowns.push(unknown); - return unknown; - } - }().visit(sourceFile, 0); - const expectUnknowns = options.expectUnknowns ?? false; - if (expectUnknowns && unknowns.length == 0) { - throw new Error("No J.Unknown instances were found. Adjust the test expectation."); - } else if (!expectUnknowns && unknowns.length != 0) { - throw new Error("No J.Unknown instances were expected: " + unknowns.map(u => u.source.text)); + checkUnknowns(sourceFile, options); + if (after) { + after = options.normalizeIndent ?? true ? dedent(after) : after; + let printed: string; + if (recipeSpec && recipeSpec.recipe) { + const before = new InMemoryLargeSourceSet([sourceFile]) + const [result] = recipeSpec.recipe.run(before, new InMemoryExecutionContext()) as Iterable; + printed = print(result.after!); + } else { + printed = print(sourceFile); + } + expect(printed).toBe(after); + } else { + if (options.validatePrintIdempotence ?? true) { + let printed = print(sourceFile); + expect(printed).toBe(before); } - } catch (e) { - throw e instanceof RecipeRunException ? e.cause : e; - } - if (options.validatePrintIdempotence ?? true) { - let printed = print(sourceFile); - expect(printed).toBe(before); } if (spec) { spec(sourceFile as JS.CompilationUnit); @@ -211,12 +239,12 @@ function sourceFile(before: string, defaultPath: string, spec?: (sourceFile: JS. }; } -export function javaScript(before: string, spec?: (sourceFile: JS.CompilationUnit) => void): SourceSpec { - return sourceFile(before, 'test.js', spec); +export function javaScript(before: string, after?: string, spec?: (sourceFile: JS.CompilationUnit) => void): SourceSpec { + return sourceFile(before, 'test.js', after, spec); } -export function typeScript(before: string, spec?: (sourceFile: JS.CompilationUnit) => void): SourceSpec { - return sourceFile(before, 'test.ts', spec); +export function typeScript(before: string, after?: string, spec?: (sourceFile: JS.CompilationUnit) => void): SourceSpec { + return sourceFile(before, 'test.ts', after, spec); } function print(parsed: SourceFile) { diff --git a/rewrite-javascript/src/main/java/org/openrewrite/javascript/format/Spaces.java b/rewrite-javascript/src/main/java/org/openrewrite/javascript/format/JavaScriptSpaces.java similarity index 91% rename from rewrite-javascript/src/main/java/org/openrewrite/javascript/format/Spaces.java rename to rewrite-javascript/src/main/java/org/openrewrite/javascript/format/JavaScriptSpaces.java index 49823e70..53f9522b 100644 --- a/rewrite-javascript/src/main/java/org/openrewrite/javascript/format/Spaces.java +++ b/rewrite-javascript/src/main/java/org/openrewrite/javascript/format/JavaScriptSpaces.java @@ -26,7 +26,7 @@ import static java.util.Objects.requireNonNull; @Incubating(since = "1.x") -public class Spaces extends Recipe { +public class JavaScriptSpaces extends Recipe { @Override public String getDisplayName() { @@ -50,9 +50,9 @@ public J visit(@Nullable Tree tree, ExecutionContext ctx) { JS.CompilationUnit cu = (JS.CompilationUnit) requireNonNull(tree); SpacesStyle style = cu.getStyle(SpacesStyle.class); if (style == null) { - style = IntelliJ.spaces(); + style = IntelliJ.TypeScript.spaces(); } - doAfterVisit(new SpacesVisitor<>(style)); + doAfterVisit(new JavaScriptSpacesVisitor<>(style)); } return super.visit(tree, ctx); } diff --git a/rewrite-javascript/src/main/java/org/openrewrite/javascript/format/SpacesVisitor.java b/rewrite-javascript/src/main/java/org/openrewrite/javascript/format/JavaScriptSpacesVisitor.java similarity index 99% rename from rewrite-javascript/src/main/java/org/openrewrite/javascript/format/SpacesVisitor.java rename to rewrite-javascript/src/main/java/org/openrewrite/javascript/format/JavaScriptSpacesVisitor.java index 12760a98..1b496a8b 100644 --- a/rewrite-javascript/src/main/java/org/openrewrite/javascript/format/SpacesVisitor.java +++ b/rewrite-javascript/src/main/java/org/openrewrite/javascript/format/JavaScriptSpacesVisitor.java @@ -25,11 +25,11 @@ import java.util.List; @Incubating(since = "1.x") -public class SpacesVisitor

extends JavaScriptIsoVisitor

{ +public class JavaScriptSpacesVisitor

extends JavaScriptIsoVisitor

{ private final SpacesStyle style; - public SpacesVisitor(SpacesStyle style) { + public JavaScriptSpacesVisitor(SpacesStyle style) { this.style = style; } diff --git a/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/BlankLinesStyle.java b/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/BlankLinesStyle.java new file mode 100644 index 00000000..503fb178 --- /dev/null +++ b/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/BlankLinesStyle.java @@ -0,0 +1,54 @@ +/* + * 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. + */ +package org.openrewrite.javascript.style; + +import lombok.Value; +import lombok.With; +import org.jspecify.annotations.Nullable; +import org.openrewrite.style.Style; +import org.openrewrite.style.StyleHelper; + +@Value +@With +public class BlankLinesStyle implements JavaScriptStyle { + KeepMaximum keepMaximum; + Minimum minimum; + + @Value + @With + public static class KeepMaximum { + Integer inCode; + } + + @Value + @With + public static class Minimum { + Integer afterImports; + Integer aroundClass; + @Nullable + Integer aroundFieldInInterface; + Integer aroundField; + @Nullable + Integer aroundMethodInInterface; + Integer aroundMethod; + Integer aroundFunction; + } + + @Override + public Style applyDefaults() { + return StyleHelper.merge(IntelliJ.TypeScript.blankLines(), this); + } +} diff --git a/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/ImportsStyle.java b/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/ImportsStyle.java new file mode 100644 index 00000000..2f0cfd4d --- /dev/null +++ b/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/ImportsStyle.java @@ -0,0 +1,64 @@ +/* + * 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. + */ +package org.openrewrite.javascript.style; + +import lombok.Value; +import lombok.With; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +@Value +@With +public class ImportsStyle implements JavaScriptStyle { + boolean mergeImportsForMembersFromTheSameModule; + boolean usePathRelativeToTheProjectOrResourceOrSourcesRootsOrTsconfigJson; + boolean useDirectoryImportsWhenIndexJsIsAvailable; + UseFileExtensions useFileExtensions; + @Nullable + UseTypeModifiersInImports useTypeModifiersInImports; + @Nullable + UsePathMappingsFromTSConfigJson usePathMappingsFromTSConfigJson; + @Nullable + UsePathAliases usePathAliases; + List doNotImportExactlyFrom; + boolean sortImportedMembers; + boolean sortImportsByModules; + + public enum UseFileExtensions { + Auto, + AlwaysJs, + Never + } + + public enum UsePathAliases { + Always, + OnlyInFilesOutsideSpecifiedPath, + Never + } + + public enum UsePathMappingsFromTSConfigJson { + Always, + OnlyInFilesOutsideSpecifiedPath, + Never + } + + public enum UseTypeModifiersInImports { + Auto, + AlwaysWithType, + Never + } +} diff --git a/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/IntelliJ.java b/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/IntelliJ.java index 1f46a663..8a9292af 100644 --- a/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/IntelliJ.java +++ b/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/IntelliJ.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * 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. @@ -15,18 +15,157 @@ */ package org.openrewrite.javascript.style; -// TODO: extend NameStyles and implement remaining styles for JS/TS. -public class IntelliJ { - - public static SpacesStyle spaces() { - return new SpacesStyle( - new SpacesStyle.BeforeParentheses(false, false, true, true, true, true, true, true, true), - new SpacesStyle.AroundOperators(true, true, true, true, true, true, true, true, false, true, false, false), - new SpacesStyle.BeforeLeftBrace(true, true, true, true, true, true, true, true, true, true, true), - new SpacesStyle.BeforeKeywords(true, true, true, true), - new SpacesStyle.Within(false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, false), - new SpacesStyle.TernaryOperator(true, true, true, true), - new SpacesStyle.Other(false, true, false, true, false, true, false, false, true, false, true) +import org.openrewrite.style.NamedStyles; +import java.util.Arrays; +import java.util.Collections; +import static org.openrewrite.Tree.randomId; + +public class IntelliJ extends NamedStyles { + + public IntelliJ() { + super(randomId(), + "org.openrewrite.javascript.style.IntelliJ", + "IntelliJ IDEA", + "IntelliJ IDEA default JS/TS style.", + Collections.emptySet(), + Arrays.asList( + JavaScript.spaces(), + JavaScript.wrappingAndBraces(), + JavaScript.tabsAndIndents(), + JavaScript.blankLines(), + JavaScript.imports(), + JavaScript.punctuation(), + + TypeScript.spaces(), + TypeScript.wrappingAndBraces(), + TypeScript.tabsAndIndents(), + TypeScript.blankLines(), + TypeScript.imports(), + TypeScript.punctuation() + ) ); } + + public static class JavaScript { + + public static TabsAndIndentsStyle tabsAndIndents() { + return new TabsAndIndentsStyle( + false, // useTabCharacter + 4, // tabSize + 4, // indentSize + 4, // continuationIndent + false, // keepIndentsOnEmptyLines + true, // indentChainedMethods + false // indentAllChainedCallsInAGroup + ); + } + + public static SpacesStyle spaces() { + return new SpacesStyle( + new SpacesStyle.BeforeParentheses(false, false, true, true, true, true, true, true, true), + new SpacesStyle.AroundOperators(true, true, true, true, true, true, true, true, false, true, false, false), + new SpacesStyle.BeforeLeftBrace(true, true, true, true, true, true, true, true, true, true, true), + new SpacesStyle.BeforeKeywords(true, true, true, true), + new SpacesStyle.Within(false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false), + new SpacesStyle.TernaryOperator(true, true, true, true), + new SpacesStyle.Other(false, true, false, false, true, false, false, true, false, false) + ); + } + + public static BlankLinesStyle blankLines() { + return new BlankLinesStyle( + new BlankLinesStyle.KeepMaximum(2), + new BlankLinesStyle.Minimum(1, 1, null, 0, null, 1, 1) + ); + } + + public static ImportsStyle imports() { + return new ImportsStyle( + true, + false, + true, + ImportsStyle.UseFileExtensions.Auto, + null, // TS specific config + null, // TS specific config + ImportsStyle.UsePathAliases.Always, + Arrays.asList("rxjs/Rx", + "node_modules/**", + "**/node_modules/**", + "@angular/material", + "@angular/material/typings/**"), + true, + false + ); + } + + public static WrappingAndBraces wrappingAndBraces() { + return new WrappingAndBraces(); + } + + public static PunctuationStyle punctuation() { + return new PunctuationStyle(PunctuationStyle.TrailingComma.Keep); + } + } + + public static class TypeScript { + + public static TabsAndIndentsStyle tabsAndIndents() { + return new TabsAndIndentsStyle( + false, // useTabCharacter + 4, // tabSize + 4, // indentSize + 4, // continuationIndent + false, // keepIndentsOnEmptyLines + true, // indentChainedMethods + false // indentAllChainedCallsInAGroup + ); + } + + public static SpacesStyle spaces() { + return new SpacesStyle( + new SpacesStyle.BeforeParentheses(false, false, true, true, true, true, true, true, true), + new SpacesStyle.AroundOperators(true, true, true, true, true, true, true, true, false, true, false, false), + new SpacesStyle.BeforeLeftBrace(true, true, true, true, true, true, true, true, true, true, false), + new SpacesStyle.BeforeKeywords(true, true, true, true), + new SpacesStyle.Within(false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, false), + new SpacesStyle.TernaryOperator(true, true, true, true), + new SpacesStyle.Other(false, true, false, false, true, false, false, true, false, true) + ); + } + + public static BlankLinesStyle blankLines() { + return new BlankLinesStyle( + new BlankLinesStyle.KeepMaximum(2), + new BlankLinesStyle.Minimum(1, 1, 0, 0, 1, 1, 1) + ); + } + + public static ImportsStyle imports() { + return new ImportsStyle( + true, + false, + true, + ImportsStyle.UseFileExtensions.Auto, + ImportsStyle.UseTypeModifiersInImports.Auto, + ImportsStyle.UsePathMappingsFromTSConfigJson.Always, + null, // JS specific config + Arrays.asList("rxjs/Rx", + "node_modules/**", + "**/node_modules/**", + "@angular/material", + "@angular/material/typings/**"), + true, + false + ); + } + + public static WrappingAndBraces wrappingAndBraces() { + return new WrappingAndBraces(); + } + + public static PunctuationStyle punctuation() { + return new PunctuationStyle(PunctuationStyle.TrailingComma.Keep); + } + } + } diff --git a/rewrite-javascript/src/main/java/org/openrewrite/javascript/JavaScriptStyle.java b/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/JavaScriptStyle.java similarity index 88% rename from rewrite-javascript/src/main/java/org/openrewrite/javascript/JavaScriptStyle.java rename to rewrite-javascript/src/main/java/org/openrewrite/javascript/style/JavaScriptStyle.java index ecf17bf5..924c0b1f 100644 --- a/rewrite-javascript/src/main/java/org/openrewrite/javascript/JavaScriptStyle.java +++ b/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/JavaScriptStyle.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.openrewrite.javascript; +package org.openrewrite.javascript.style; import org.openrewrite.style.Style; diff --git a/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/PunctuationStyle.java b/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/PunctuationStyle.java new file mode 100644 index 00000000..b5747541 --- /dev/null +++ b/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/PunctuationStyle.java @@ -0,0 +1,31 @@ +/* + * 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. + */ +package org.openrewrite.javascript.style; + +import lombok.Value; +import lombok.With; + +@Value +@With +public class PunctuationStyle implements JavaScriptStyle { + TrailingComma trailingComma; + + public enum TrailingComma { + Keep, + Remove, + AddWhenMultiLine + } +} diff --git a/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/SpacesStyle.java b/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/SpacesStyle.java index 90187b2c..e8eeea1d 100644 --- a/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/SpacesStyle.java +++ b/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/SpacesStyle.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * 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. @@ -17,7 +17,8 @@ import lombok.Value; import lombok.With; -import org.openrewrite.javascript.JavaScriptStyle; +import org.openrewrite.style.Style; +import org.openrewrite.style.StyleHelper; @Value @With @@ -90,7 +91,7 @@ public static class BeforeKeywords { @Value @With public static class Within { - Boolean brackets; + Boolean indexAccessBrackets; Boolean groupingParentheses; Boolean functionDeclarationParentheses; Boolean functionCallParentheses; @@ -123,7 +124,6 @@ public static class Other { Boolean beforeComma; Boolean afterComma; Boolean beforeForSemicolon; - Boolean afterForSemicolon; Boolean beforePropertyNameValueSeparator; Boolean afterPropertyNameValueSeparator; Boolean afterVarArgInRestOrSpread; @@ -132,4 +132,9 @@ public static class Other { Boolean beforeTypeReferenceColon; Boolean afterTypeReferenceColon; } + + @Override + public Style applyDefaults() { + return StyleHelper.merge(IntelliJ.TypeScript.spaces(), this); + } } diff --git a/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/TabsAndIndentsStyle.java b/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/TabsAndIndentsStyle.java new file mode 100644 index 00000000..d8672f1b --- /dev/null +++ b/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/TabsAndIndentsStyle.java @@ -0,0 +1,39 @@ +/* + * 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. + */ +package org.openrewrite.javascript.style; + +import lombok.Value; +import lombok.With; +import org.openrewrite.style.Style; +import org.openrewrite.style.StyleHelper; + +@Value +@With +public class TabsAndIndentsStyle implements JavaScriptStyle { + + boolean useTabCharacter; + int tabSize; + int indentSize; + int continuationIndent; + boolean keepIndentsOnEmptyLines; + boolean indentChainedMethods; + boolean indentAllChainedCallsInAGroup; + + @Override + public Style applyDefaults() { + return StyleHelper.merge(IntelliJ.TypeScript.tabsAndIndents(), this); + } +} diff --git a/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/WrappingAndBraces.java b/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/WrappingAndBraces.java new file mode 100644 index 00000000..7fcd0f25 --- /dev/null +++ b/rewrite-javascript/src/main/java/org/openrewrite/javascript/style/WrappingAndBraces.java @@ -0,0 +1,31 @@ +/* + * 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. + */ +package org.openrewrite.javascript.style; + +import lombok.Value; +import lombok.With; +import org.openrewrite.style.Style; +import org.openrewrite.style.StyleHelper; + +@Value +@With +public class WrappingAndBraces implements JavaScriptStyle { + + @Override + public Style applyDefaults() { + return StyleHelper.merge(IntelliJ.TypeScript.wrappingAndBraces(), this); + } +} diff --git a/rewrite-javascript/src/test/java/org/openrewrite/javascript/format/SpacesTest.java b/rewrite-javascript/src/test/java/org/openrewrite/javascript/format/JavaScriptSpacesTest.java similarity index 99% rename from rewrite-javascript/src/test/java/org/openrewrite/javascript/format/SpacesTest.java rename to rewrite-javascript/src/test/java/org/openrewrite/javascript/format/JavaScriptSpacesTest.java index 8a550423..9cafb2da 100644 --- a/rewrite-javascript/src/test/java/org/openrewrite/javascript/format/SpacesTest.java +++ b/rewrite-javascript/src/test/java/org/openrewrite/javascript/format/JavaScriptSpacesTest.java @@ -33,13 +33,13 @@ import static org.openrewrite.javascript.Assertions.javaScript; @SuppressWarnings({"JSDuplicatedDeclaration", "ReservedWordAsName", "JSUnusedLocalSymbols", "InfiniteRecursionJS", "JSUnresolvedReference", "TrailingWhitespacesInTextBlock"}) -class SpacesTest implements RewriteTest { +class JavaScriptSpacesTest implements RewriteTest { private static Consumer spaces(UnaryOperator with) { - return spec -> spec.recipe(new Spaces()) + return spec -> spec.recipe(new JavaScriptSpaces()) .parser(JavaScriptParser.builder().styles(singletonList( new NamedStyles(Tree.randomId(), "test", "test", "test", emptySet(), - singletonList(with.apply(IntelliJ.spaces()))) + singletonList(with.apply(IntelliJ.TypeScript.spaces()))) ))); }