diff --git a/src/options.ts b/src/options.ts index afdb6b98..5400045b 100644 --- a/src/options.ts +++ b/src/options.ts @@ -31,5 +31,21 @@ export default { { value: "end", description: "" } ], description: "Where to print operators when binary expressions wrap lines." + }, + braceStyle: { + type: "choice", + category: "Java", + default: "same-line", + choices: [ + { + value: "same-line", + description: "Put opening braces on same line (K&R style)" + }, + { + value: "next-line", + description: "Put opening braces on new lines (Allman style)" + } + ], + description: "Placement of opening braces." } } satisfies SupportOptions; diff --git a/src/printers/blocks-and-statements.ts b/src/printers/blocks-and-statements.ts index 6bf8a7ac..1d6e328e 100644 --- a/src/printers/blocks-and-statements.ts +++ b/src/printers/blocks-and-statements.ts @@ -25,8 +25,8 @@ const { } = builders; export default { - block(path, print) { - return printBlock(path, printBlockStatements(path, print)); + block(path, print, options) { + return printBlock(path, printBlockStatements(path, print), options); }, local_variable_declaration: printVariableDeclaration, @@ -47,7 +47,7 @@ export default { : [expression, ";"]; }, - if_statement(path, print) { + if_statement(path, print, options) { const statement = ["if ", path.call(print, "conditionNode")]; if (path.node.consequenceNode.type === ";") { @@ -63,6 +63,8 @@ export default { const danglingComments = printDanglingComments(path); if (danglingComments.length) { statement.push(hardline, ...danglingComments, hardline); + } else if (options?.braceStyle === "next-line") { + statement.push(hardline); } else { const ifHasBlock = path.node.consequenceNode.type === SyntaxType.Block; statement.push(ifHasBlock ? " " : hardline); @@ -91,8 +93,8 @@ export default { ]); }, - switch_block(path, print) { - return printBlock(path, path.map(print, "namedChildren")); + switch_block(path, print, options) { + return printBlock(path, path.map(print, "namedChildren"), options); }, switch_block_statement_group(path, print) { @@ -192,15 +194,16 @@ export default { } }, - do_statement(path, print) { + do_statement(path, print, options) { const hasEmptyStatement = path.node.bodyNode.type === ";"; - return [ - "do", - hasEmptyStatement ? ";" : [" ", path.call(print, "bodyNode")], - " while ", - path.call(print, "conditionNode"), - ";" - ]; + const isNextLine = options?.braceStyle === "next-line"; + const whilePrefix = isNextLine ? [hardline, "while "] : " while "; + const body = hasEmptyStatement + ? ";" + : isNextLine + ? path.call(print, "bodyNode") + : [" ", path.call(print, "bodyNode")]; + return ["do", body, whilePrefix, path.call(print, "conditionNode"), ";"]; }, for_statement(path, print) { @@ -330,7 +333,7 @@ export default { ]; }, - try_statement(path, print) { + try_statement(path, print, options) { const parts = ["try", path.call(print, "bodyNode")]; path.each(child => { @@ -342,23 +345,26 @@ export default { } }, "namedChildren"); + if (options?.braceStyle === "next-line") { + return parts; + } return join(" ", parts); }, - catch_clause(path, print) { + catch_clause(path, print, options) { const catchFormalParameterIndex = path.node.namedChildren.findIndex( ({ type }) => type === SyntaxType.CatchFormalParameter ); - return [ - "catch ", - group( - indentInParentheses( - path.call(print, "namedChildren", catchFormalParameterIndex) - ) - ), - " ", - path.call(print, "bodyNode") - ]; + const params = group( + indentInParentheses( + path.call(print, "namedChildren", catchFormalParameterIndex) + ) + ); + const body = path.call(print, "bodyNode"); + if (options?.braceStyle === "next-line") { + return [hardline, "catch ", params, body]; + } + return ["catch ", params, " ", body]; }, catch_formal_parameter(path, print) { @@ -383,27 +389,31 @@ export default { return join([line, "| "], path.map(print, "namedChildren")); }, - finally_clause(path, print) { + finally_clause(path, print, options) { + if (options?.braceStyle === "next-line") { + return [hardline, "finally", path.call(print, "namedChildren", 0)]; + } return ["finally ", path.call(print, "namedChildren", 0)]; }, - try_with_resources_statement(path, print) { - const parts = [ - "try", - path.call(print, "resourcesNode"), - path.call(print, "bodyNode") - ]; + try_with_resources_statement(path, print, options) { + const resources = path.call(print, "resourcesNode"); + const body = path.call(print, "bodyNode"); + const clauses: Doc[] = []; path.each(child => { if ( child.node.type === SyntaxType.CatchClause || child.node.type === SyntaxType.FinallyClause ) { - parts.push(print(child)); + clauses.push(print(child)); } }, "namedChildren"); - return join(" ", parts); + if (options?.braceStyle === "next-line") { + return ["try ", resources, body, ...clauses]; + } + return join(" ", ["try", resources, body, ...clauses]); }, resource_specification(path, print) { diff --git a/src/printers/classes.ts b/src/printers/classes.ts index 2348c7c7..0c863569 100644 --- a/src/printers/classes.ts +++ b/src/printers/classes.ts @@ -19,7 +19,7 @@ const { group, hardline, indent, indentIfBreak, join, line, softline } = builders; export default { - class_declaration(path, print) { + class_declaration(path, print, options) { const parts: Doc[] = ["class ", path.call(print, "nameNode")]; const definedClauses = definedKeys(path.node, [ "superclassNode", @@ -42,12 +42,14 @@ export default { hasChild(path, clause) ? [separator, path.call(print, clause)] : [] ); const hasBody = path.node.bodyNode.namedChildren.length > 0; + const afterClauses = + options?.braceStyle === "next-line" ? "" : hasBody ? separator : " "; const clauseGroup = [ hasTypeParameters && !hasMultipleClauses ? clauses : indent(clauses), - hasBody ? separator : " " + afterClauses ]; parts.push(hasMultipleClauses ? clauseGroup : group(clauseGroup)); - } else { + } else if (options?.braceStyle !== "next-line") { parts.push(" "); } @@ -89,14 +91,16 @@ export default { return join([",", line], path.map(print, "namedChildren")); }, - class_body(path, print) { + class_body(path, print, options) { return printBlock( path, printBodyDeclarations( path, print, - (path.parent as NamedNode | null)?.type === SyntaxType.ClassDeclaration - ) + (path.parent as NamedNode | null)?.type === + SyntaxType.ClassDeclaration && options?.braceStyle !== "next-line" + ), + options ); }, @@ -261,8 +265,8 @@ export default { return [modifiers, group(declaration), " ", path.call(print, "bodyNode")]; }, - constructor_body(path, print) { - return printBlock(path, printBlockStatements(path, print)); + constructor_body(path, print, options) { + return printBlock(path, printBlockStatements(path, print), options); }, explicit_constructor_invocation(path, print) { @@ -379,7 +383,7 @@ export default { if (declarations.length) { contents.push(";", hardline, ...declarations); } - return printBlock(path, contents.length ? [contents] : []); + return printBlock(path, contents.length ? [contents] : [], options); }, enum_constant(path, print) { diff --git a/src/printers/helpers.ts b/src/printers/helpers.ts index 6d4d2094..b6a12992 100644 --- a/src/printers/helpers.ts +++ b/src/printers/helpers.ts @@ -129,9 +129,22 @@ export function printArrayInitializer( ) { if (!path.node.namedChildren.length) { const danglingComments = printDanglingComments(path); - return danglingComments.length - ? ["{", indent([hardline, ...danglingComments]), hardline, "}"] - : "{}"; + if (danglingComments.length) { + if (options.braceStyle === "next-line") { + const isNested = + path.parent?.type === SyntaxType.ArrayInitializer || + path.parent?.type === SyntaxType.ElementValueArrayInitializer; + const block = [ + "{", + indent([hardline, ...danglingComments]), + hardline, + "}" + ]; + return isNested ? block : [hardline, ...block]; + } + return ["{", indent([hardline, ...danglingComments]), hardline, "}"]; + } + return "{}"; } const list = join([",", line], path.map(print, "namedChildren")); @@ -140,10 +153,26 @@ export function printArrayInitializer( list.push(ifBreak(",")); } + if (options.braceStyle === "next-line") { + const isNested = + path.parent?.type === SyntaxType.ArrayInitializer || + path.parent?.type === SyntaxType.ElementValueArrayInitializer; + const block = ["{", indent([softline, ...list]), softline, "}"]; + return isNested ? block : [hardline, ...block]; + } + return group(["{", indent([softline, ...list]), softline, "}"]); } -export function printBlock(path: NamedNodePath, contents: Doc[]) { +export function printBlock( + path: NamedNodePath, + contents: Doc[], + options?: JavaParserOptions +) { + if (options?.braceStyle === "next-line") { + return printBlockNextLine(path, contents); + } + if (contents.length) { return group([ "{", @@ -195,6 +224,40 @@ export function printBlock(path: NamedNodePath, contents: Doc[]) { : ["{", hardline, "}"]; } +function printBlockNextLine(path: NamedNodePath, contents: Doc[]) { + const parentType = path.parent?.type; + const isChildOfBodyDeclaration = + parentType != null && + [ + SyntaxType.AnnotationTypeBody, + SyntaxType.ClassBody, + SyntaxType.EnumBody, + SyntaxType.InterfaceBody + ].includes(parentType); + const prefix = isChildOfBodyDeclaration ? [] : [hardline]; + + if (contents.length) { + return [ + ...prefix, + "{", + indent([hardline, ...join(hardline, contents)]), + hardline, + "}" + ]; + } + const danglingComments = printDanglingComments(path); + if (danglingComments.length) { + return [ + ...prefix, + "{", + indent([hardline, ...danglingComments]), + hardline, + "}" + ]; + } + return [...prefix, "{", hardline, "}"]; +} + export function printBlockStatements( path: NamedNodePath< | SyntaxType.Block diff --git a/src/printers/interfaces.ts b/src/printers/interfaces.ts index c1057c01..c40e443d 100644 --- a/src/printers/interfaces.ts +++ b/src/printers/interfaces.ts @@ -72,8 +72,8 @@ export default { ]); }, - interface_body(path, print) { - return printBlock(path, printBodyDeclarations(path, print)); + interface_body(path, print, options) { + return printBlock(path, printBodyDeclarations(path, print), options); }, constant_declaration: printVariableDeclaration, @@ -91,8 +91,8 @@ export default { return parts; }, - annotation_type_body(path, print) { - return printBlock(path, printBodyDeclarations(path, print)); + annotation_type_body(path, print, options) { + return printBlock(path, printBodyDeclarations(path, print), options); }, annotation_type_element_declaration(path, print) { diff --git a/src/printers/packages-and-modules.ts b/src/printers/packages-and-modules.ts index 7d8e45e9..45dadfac 100644 --- a/src/printers/packages-and-modules.ts +++ b/src/printers/packages-and-modules.ts @@ -134,7 +134,7 @@ export default { return join(" ", parts); }, - module_body(path, print) { + module_body(path, print, options) { return printBlock( path, path.map( @@ -143,7 +143,8 @@ export default { ? [hardline, print(child)] : print(child), "namedChildren" - ) + ), + options ); }, diff --git a/test/unit-test/braces-next-line/.prettierrc.json b/test/unit-test/braces-next-line/.prettierrc.json new file mode 100644 index 00000000..5f86a3bd --- /dev/null +++ b/test/unit-test/braces-next-line/.prettierrc.json @@ -0,0 +1 @@ +{ "braceStyle": "next-line" } diff --git a/test/unit-test/braces-next-line/_input.java b/test/unit-test/braces-next-line/_input.java new file mode 100644 index 00000000..376972ea --- /dev/null +++ b/test/unit-test/braces-next-line/_input.java @@ -0,0 +1,339 @@ +import java.io.*; +import java.sql.SQLException; +import java.util.function.Consumer; + +public class BracesNextLineTest { + private String field; + + public BracesNextLineTest(String value) { + this.field = value; + } + + public void simpleMethod() { + System.out.println("Simple method"); + } + + public String methodWithReturn() { + return field; + } + + static { + System.out.println("Static initializer"); + } + + { + System.out.println("Instance initializer"); + } + + public void testIfElse(int value) { + if (value > 0) { + System.out.println("Positive"); + } + + if (value > 10) { + System.out.println("Greater than 10"); + } else { + System.out.println("10 or less"); + } + + if (value < 0) { + System.out.println("Negative"); + } else if (value == 0) { + System.out.println("Zero"); + } else { + System.out.println("Positive"); + } + + // Multiple else ifs + if (value < -10) { + System.out.println("Very negative"); + } else if (value < 0) { + System.out.println("Negative"); + } else if (value == 0) { + System.out.println("Zero"); + } else if (value > 10) { + System.out.println("Very positive"); + } else { + System.out.println("Positive"); + } + + // Else if without final else + if (value < 0) { + System.out.println("Negative"); + } else if (value == 0) { + System.out.println("Zero"); + } + + // Nested else if + if (value != 0) { + if (value < 0) { + System.out.println("Negative non-zero"); + } else if (value > 0) { + System.out.println("Positive non-zero"); + } + } + + // Without braces + if (value < 0) + System.out.println("Negative"); + else if (value == 0) + System.out.println("Zero"); + else + System.out.println("Positive"); + + // Mixed braces + if (value < 0) { + System.out.println("Negative"); + } else if (value == 0) + System.out.println("Zero"); + else { + System.out.println("Positive"); + } + } + + public void testLoops() { + for (int i = 0; i < 10; i++) { + System.out.println(i); + } + + String[] items = {"a", "b", "c"}; + for (String item : items) { + System.out.println(item); + } + + int count = 0; + while (count < 5) { + count++; + } + + do { + count--; + } while (count > 0); + } + + public void testTryCatch() { + try { + riskyOperation(); + } catch (Exception e) { + handleError(e); + } + + try { + riskyOperation(); + } catch (IOException e) { + handleIOError(e); + } catch (SQLException e) { + handleSQLError(e); + } finally { + cleanup(); + } + + try (FileInputStream fis = new FileInputStream("file.txt")) { + readFile(fis); + } catch (IOException e) { + handleError(e); + } + } + + public void testSwitch(int day) { + switch (day) { + case 1: + System.out.println("Monday"); + break; + case 2: + System.out.println("Tuesday"); + break; + default: + System.out.println("Other day"); + } + + String dayName = switch (day) { + case 1 -> "Monday"; + case 2 -> "Tuesday"; + default -> "Other"; + }; + } + + public class NestedClass { + public void nestedMethod() { + System.out.println("Nested"); + } + } + + public void testAnonymousClass() { + Runnable r = new Runnable() { + @Override + public void run() { + System.out.println("Running"); + } + }; + } + + public void testLambdas() { + Runnable r1 = () -> System.out.println("Simple lambda"); + + Runnable r2 = () -> { + System.out.println("Line 1"); + System.out.println("Line 2"); + }; + + Consumer consumer = (String s) -> { + System.out.println("Consuming: " + s); + processString(s); + }; + } + + public void testArrayInitializers() { + int[] numbers = {1, 2, 3, 4, 5}; + + String[][] matrix = { + {"a", "b"}, + {"c", "d"} + }; + } +} + + +public interface NextLineInterface { + void interfaceMethod(); + + default void defaultMethod() { + System.out.println("Default"); + } +} + + +public interface ExtendedInterface extends NextLineInterface { + void extendedMethod(); +} + + +public enum DayOfWeek { + MONDAY("Monday"), + TUESDAY("Tuesday"), + WEDNESDAY("Wednesday"); + + private final String displayName; + + DayOfWeek(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } +} + + +public class ExtendedClass extends BracesNextLineTest implements NextLineInterface { + public ExtendedClass() { + super("extended"); + } + + @Override + public void interfaceMethod() { + System.out.println("Implemented"); + } +} + + +class SynchronizedExample { + private final Object lock = new Object(); + + public void synchronizedMethod() { + synchronized (lock) { + System.out.println("Synchronized"); + } + } +} + + +class HelperMethods { + void riskyOperation() throws Exception {} + void handleError(Exception e) {} + void handleIOError(IOException e) {} + void handleSQLError(SQLException e) {} + void cleanup() {} + void readFile(FileInputStream fis) {} + void processString(String s) {} +} + + +// Test empty blocks for all constructs +class EmptyBlocksTest { + // Empty class declaration + static class EmptyClass {} + + // Empty interface declaration + interface EmptyInterface {} + + // Empty enum + enum EmptyEnum {} + + // Empty method + void emptyMethod() {} + + // Empty constructor + EmptyBlocksTest() {} + + // Empty static initializer + static {} + + // Empty instance initializer + {} + + // Empty blocks in statements + void testEmptyStatementBlocks() { + // Empty if + if (true) {} + + // Empty else + if (false) { + System.out.println("not empty"); + } else {} + + // Empty while + while (false) {} + + // Empty do-while + do {} while (false); + + // Empty for + for (int i = 0; i < 0; i++) {} + + // Empty enhanced for + for (String s : new String[0]) {} + + // Empty try + try {} catch (Exception e) { + e.printStackTrace(); + } + + // Empty catch + try { + throw new Exception(); + } catch (Exception e) {} + + // Empty finally + try { + System.out.println("try"); + } finally {} + + // Empty try-catch-finally + try {} catch (Exception e) {} finally {} + + // Empty synchronized + synchronized (this) {} + + // Empty switch + switch (1) {} + + // Empty anonymous class + Runnable r = new Runnable() { + public void run() {} + }; + + // Empty lambda block + Runnable lambda = () -> {}; + } +} diff --git a/test/unit-test/braces-next-line/_output.java b/test/unit-test/braces-next-line/_output.java new file mode 100644 index 00000000..a9b60c31 --- /dev/null +++ b/test/unit-test/braces-next-line/_output.java @@ -0,0 +1,507 @@ +import java.io.*; +import java.sql.SQLException; +import java.util.function.Consumer; + +public class BracesNextLineTest +{ + private String field; + + public BracesNextLineTest(String value) + { + this.field = value; + } + + public void simpleMethod() + { + System.out.println("Simple method"); + } + + public String methodWithReturn() + { + return field; + } + + static + { + System.out.println("Static initializer"); + } + + { + System.out.println("Instance initializer"); + } + + public void testIfElse(int value) + { + if (value > 0) + { + System.out.println("Positive"); + } + + if (value > 10) + { + System.out.println("Greater than 10"); + } + else + { + System.out.println("10 or less"); + } + + if (value < 0) + { + System.out.println("Negative"); + } + else if (value == 0) + { + System.out.println("Zero"); + } + else + { + System.out.println("Positive"); + } + + // Multiple else ifs + if (value < -10) + { + System.out.println("Very negative"); + } + else if (value < 0) + { + System.out.println("Negative"); + } + else if (value == 0) + { + System.out.println("Zero"); + } + else if (value > 10) + { + System.out.println("Very positive"); + } + else + { + System.out.println("Positive"); + } + + // Else if without final else + if (value < 0) + { + System.out.println("Negative"); + } + else if (value == 0) + { + System.out.println("Zero"); + } + + // Nested else if + if (value != 0) + { + if (value < 0) + { + System.out.println("Negative non-zero"); + } + else if (value > 0) + { + System.out.println("Positive non-zero"); + } + } + + // Without braces + if (value < 0) System.out.println("Negative"); + else if (value == 0) System.out.println("Zero"); + else System.out.println("Positive"); + + // Mixed braces + if (value < 0) + { + System.out.println("Negative"); + } + else if (value == 0) System.out.println("Zero"); + else + { + System.out.println("Positive"); + } + } + + public void testLoops() + { + for (int i = 0; i < 10; i++) + { + System.out.println(i); + } + + String[] items = + { + "a", + "b", + "c", + }; + for (String item : items) + { + System.out.println(item); + } + + int count = 0; + while (count < 5) + { + count++; + } + + do + { + count--; + } + while (count > 0); + } + + public void testTryCatch() + { + try + { + riskyOperation(); + } + catch (Exception e) + { + handleError(e); + } + + try + { + riskyOperation(); + } + catch (IOException e) + { + handleIOError(e); + } + catch (SQLException e) + { + handleSQLError(e); + } + finally + { + cleanup(); + } + + try (FileInputStream fis = new FileInputStream("file.txt")) + { + readFile(fis); + } + catch (IOException e) + { + handleError(e); + } + } + + public void testSwitch(int day) + { + switch (day) + { + case 1: + System.out.println("Monday"); + break; + case 2: + System.out.println("Tuesday"); + break; + default: + System.out.println("Other day"); + } + + String dayName = switch (day) + { + case 1 -> "Monday"; + case 2 -> "Tuesday"; + default -> "Other"; + }; + } + + public class NestedClass + { + public void nestedMethod() + { + System.out.println("Nested"); + } + } + + public void testAnonymousClass() + { + Runnable r = new Runnable() + { + @Override + public void run() + { + System.out.println("Running"); + } + }; + } + + public void testLambdas() + { + Runnable r1 = () -> System.out.println("Simple lambda"); + + Runnable r2 = () -> + { + System.out.println("Line 1"); + System.out.println("Line 2"); + }; + + Consumer consumer = (String s) -> + { + System.out.println("Consuming: " + s); + processString(s); + }; + } + + public void testArrayInitializers() + { + int[] numbers = + { + 1, + 2, + 3, + 4, + 5, + }; + + String[][] matrix = + { + { + "a", + "b", + }, + { + "c", + "d", + }, + }; + } +} + +public interface NextLineInterface +{ + void interfaceMethod(); + + default void defaultMethod() + { + System.out.println("Default"); + } +} + +public interface ExtendedInterface extends NextLineInterface +{ + void extendedMethod(); +} + +public enum DayOfWeek +{ + MONDAY("Monday"), + TUESDAY("Tuesday"), + WEDNESDAY("Wednesday"); + + private final String displayName; + + DayOfWeek(String displayName) + { + this.displayName = displayName; + } + + public String getDisplayName() + { + return displayName; + } +} + +public class ExtendedClass + extends BracesNextLineTest + implements NextLineInterface +{ + public ExtendedClass() + { + super("extended"); + } + + @Override + public void interfaceMethod() + { + System.out.println("Implemented"); + } +} + +class SynchronizedExample +{ + private final Object lock = new Object(); + + public void synchronizedMethod() + { + synchronized (lock) + { + System.out.println("Synchronized"); + } + } +} + +class HelperMethods +{ + void riskyOperation() throws Exception + { + } + + void handleError(Exception e) + { + } + + void handleIOError(IOException e) + { + } + + void handleSQLError(SQLException e) + { + } + + void cleanup() + { + } + + void readFile(FileInputStream fis) + { + } + + void processString(String s) + { + } +} + +// Test empty blocks for all constructs +class EmptyBlocksTest +{ + // Empty class declaration + static class EmptyClass + { + } + + // Empty interface declaration + interface EmptyInterface + { + } + + // Empty enum + enum EmptyEnum + { + } + + // Empty method + void emptyMethod() + { + } + + // Empty constructor + EmptyBlocksTest() + { + } + + // Empty static initializer + static + { + } + + // Empty instance initializer + { + } + + // Empty blocks in statements + void testEmptyStatementBlocks() + { + // Empty if + if (true) + { + } + + // Empty else + if (false) + { + System.out.println("not empty"); + } + else + { + } + + // Empty while + while (false) + { + } + + // Empty do-while + do + { + } + while (false); + + // Empty for + for (int i = 0; i < 0; i++) + { + } + + // Empty enhanced for + for (String s : new String[0]) + { + } + + // Empty try + try + { + } + catch (Exception e) + { + e.printStackTrace(); + } + + // Empty catch + try + { + throw new Exception(); + } + catch (Exception e) + { + } + + // Empty finally + try + { + System.out.println("try"); + } + finally + { + } + + // Empty try-catch-finally + try + { + } + catch (Exception e) + { + } + finally + { + } + + // Empty synchronized + synchronized (this) + { + } + + // Empty switch + switch (1) + { + } + + // Empty anonymous class + Runnable r = new Runnable() + { + public void run() + { + } + }; + + // Empty lambda block + Runnable lambda = () -> + { + }; + } +} diff --git a/test/unit-test/braces-next-line/braces-next-line-spec.ts b/test/unit-test/braces-next-line/braces-next-line-spec.ts new file mode 100644 index 00000000..c052d16e --- /dev/null +++ b/test/unit-test/braces-next-line/braces-next-line-spec.ts @@ -0,0 +1,11 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { testSampleWithOptions } from "../../test-utils.ts"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +describe("prettier-java", () => { + testSampleWithOptions({ + testFolder: __dirname, + }); +});