From 1b3fca1add151e64f2fe6fcaf48615941388af56 Mon Sep 17 00:00:00 2001 From: Robert burner Schadek Date: Tue, 17 Feb 2026 16:55:32 +0100 Subject: [PATCH 1/6] Add --force_curly_braces option to insert braces around single-statement bodies. When enabled, the preprocessor inserts synthetic `{` and `}` tokens around braceless bodies for all block header constructs: if, else, for, foreach, while, do, scope, synchronized, with, catch, debug, version, finally, and their combinations (static if, static foreach, etc.). Implementation uses token stream preprocessing so the formatter's existing brace-handling logic handles style and indentation naturally. Synthetic tokens use size_t.max-based indices to avoid collisions with AST info lookups. Handles compound constructs correctly: - try/catch/finally chains as single statements - Nested braceless blocks (each level gets its own braces) - Mixed braced/braceless siblings (only braceless branches get braces) - else if/else version/else static if chains (no double-wrapping) - debug/version with and without parens - Respects // dfmt off / // dfmt on regions --- src/dfmt/config.d | 3 + src/dfmt/formatter.d | 526 ++++++++++++++++++++++++++ src/dfmt/main.d | 7 +- tests/allman/force_curly_braces.d.ref | 292 ++++++++++++++ tests/force_curly_braces.args | 1 + tests/force_curly_braces.d | 186 +++++++++ tests/knr/force_curly_braces.d.ref | 237 ++++++++++++ tests/otbs/force_curly_braces.d.ref | 213 +++++++++++ 8 files changed, 1464 insertions(+), 1 deletion(-) create mode 100644 tests/allman/force_curly_braces.d.ref create mode 100644 tests/force_curly_braces.args create mode 100644 tests/force_curly_braces.d create mode 100644 tests/knr/force_curly_braces.d.ref create mode 100644 tests/otbs/force_curly_braces.d.ref diff --git a/src/dfmt/config.d b/src/dfmt/config.d index 6b41d4a..e2eabcf 100644 --- a/src/dfmt/config.d +++ b/src/dfmt/config.d @@ -69,6 +69,8 @@ struct Config OptionalBoolean dfmt_space_after_statement_keyword; /// OptionalBoolean dfmt_space_before_named_arg_colon; + /// + OptionalBoolean dfmt_force_curly_braces; mixin StandardEditorConfigFields; @@ -101,6 +103,7 @@ struct Config dfmt_single_indent = OptionalBoolean.f; dfmt_reflow_property_chains = OptionalBoolean.t; dfmt_space_before_named_arg_colon = OptionalBoolean.f; + dfmt_force_curly_braces = OptionalBoolean.f; } /** diff --git a/src/dfmt/formatter.d b/src/dfmt/formatter.d index 2545edb..1fe8941 100644 --- a/src/dfmt/formatter.d +++ b/src/dfmt/formatter.d @@ -51,6 +51,11 @@ bool format(OutputRange)(string source_desc, ubyte[] buffer, OutputRange output, auto tokens = app.data; if (!tokenRange.messages.empty) return false; + + import dfmt.editorconfig : OptionalBoolean; + if (formatterConfig.dfmt_force_curly_braces == OptionalBoolean.t) + tokens = insertForcedBraces(tokens); + auto depths = generateDepthInfo(tokens); auto tokenFormatter = TokenFormatter!OutputRange(buffer, tokens, depths, output, &astInformation, formatterConfig); @@ -2369,6 +2374,527 @@ const pure @safe @nogc: } } +/** + * Insert `{` and `}` tokens around single-statement bodies for block headers + * (if, else, for, foreach, while, do, etc.) when force_curly_braces is enabled. + * + * This preprocessing step runs on the token array before the formatter, so the + * formatter's existing brace-handling logic takes care of style and indentation. + */ +Token[] insertForcedBraces(const(Token)[] tokens) +{ + import std.array : appender, Appender; + + // Collect insertion points: (index, true=open/false=close) + // We'll insert `{` before the token at `index`, or `}` after the token at `index-1`. + struct Insertion + { + size_t position; // insert BEFORE this token index + bool isOpen; // true = `{`, false = `}` + } + + Appender!(Insertion[]) insertions; + + // Helper: is this token type a block header keyword that takes parens? + static bool isParenBlockHeader(IdType t) pure nothrow @safe @nogc + { + return t == tok!"if" || t == tok!"for" || t == tok!"foreach" + || t == tok!"foreach_reverse" || t == tok!"while" + || t == tok!"catch" || t == tok!"with" + || t == tok!"synchronized" || t == tok!"scope" + || t == tok!"version"; + } + + // Skip past comments at position i, return new position + size_t skipComments(size_t i) nothrow @safe @nogc + { + while (i < tokens.length && tokens[i].type == tok!"comment") + i++; + return i; + } + + // Skip past matched parentheses starting at open paren at position i. + // Returns position AFTER the closing paren, or tokens.length on error. + size_t skipParens(size_t i) nothrow @safe @nogc + { + if (i >= tokens.length || tokens[i].type != tok!"(") + return i; + int depth = 1; + i++; + while (i < tokens.length && depth > 0) + { + if (tokens[i].type == tok!"(") + depth++; + else if (tokens[i].type == tok!")") + depth--; + i++; + } + return i; + } + + // Find the end of a single statement starting at position `i`. + // Returns the position AFTER the statement (i.e., after the `;` or closing `}`). + // For block headers, this recurses to find the full construct including else. + size_t findStatementEnd(size_t i) + { + i = skipComments(i); + if (i >= tokens.length) + return i; + + auto t = tokens[i].type; + + // If the statement starts with '{', find matching '}' + if (t == tok!"{") + { + int depth = 1; + i++; + while (i < tokens.length && depth > 0) + { + if (tokens[i].type == tok!"{") + depth++; + else if (tokens[i].type == tok!"}") + depth--; + i++; + } + return i; + } + + // If the statement is a block header with parens (if, for, while, etc.) + if (isParenBlockHeader(t)) + { + i++; // skip keyword + i = skipComments(i); + if (i < tokens.length && tokens[i].type == tok!"(") + i = skipParens(i); + i = skipComments(i); + // Now find the body + size_t bodyEnd = findStatementEnd(i); + + // Special case for `if`: check for trailing `else` + if (t == tok!"if") + { + size_t afterBody = skipComments(bodyEnd); + if (afterBody < tokens.length && tokens[afterBody].type == tok!"else") + { + afterBody++; // skip `else` + afterBody = skipComments(afterBody); + bodyEnd = findStatementEnd(afterBody); + } + } + return bodyEnd; + } + + // `debug` can appear with or without parens + if (t == tok!"debug") + { + i++; // skip `debug` + i = skipComments(i); + if (i < tokens.length && tokens[i].type == tok!"(") + i = skipParens(i); + i = skipComments(i); + size_t bodyEnd = findStatementEnd(i); + // Check for trailing else + size_t afterBody = skipComments(bodyEnd); + if (afterBody < tokens.length && tokens[afterBody].type == tok!"else") + { + afterBody++; + afterBody = skipComments(afterBody); + bodyEnd = findStatementEnd(afterBody); + } + return bodyEnd; + } + + // `do` ... `while` construct + if (t == tok!"do") + { + i++; // skip `do` + i = skipComments(i); + size_t bodyEnd = findStatementEnd(i); + bodyEnd = skipComments(bodyEnd); + // Expect `while` + if (bodyEnd < tokens.length && tokens[bodyEnd].type == tok!"while") + { + bodyEnd++; // skip `while` + bodyEnd = skipComments(bodyEnd); + if (bodyEnd < tokens.length && tokens[bodyEnd].type == tok!"(") + bodyEnd = skipParens(bodyEnd); + // skip trailing `;` + bodyEnd = skipComments(bodyEnd); + if (bodyEnd < tokens.length && tokens[bodyEnd].type == tok!";") + bodyEnd++; + } + return bodyEnd; + } + + // `try` ... `catch` ... `finally` compound + if (t == tok!"try") + { + i++; // skip `try` + i = skipComments(i); + size_t bodyEnd = findStatementEnd(i); // try body + // Check for catch/finally chains + while (true) + { + size_t next = skipComments(bodyEnd); + if (next < tokens.length && tokens[next].type == tok!"catch") + { + next++; // skip catch + next = skipComments(next); + if (next < tokens.length && tokens[next].type == tok!"(") + next = skipParens(next); + next = skipComments(next); + bodyEnd = findStatementEnd(next); // catch body + } + else if (next < tokens.length && tokens[next].type == tok!"finally") + { + next++; // skip finally + next = skipComments(next); + bodyEnd = findStatementEnd(next); // finally body + break; // finally is always last + } + else + break; + } + return bodyEnd; + } + + // `finally` — just a body, no parens + if (t == tok!"finally") + { + i++; // skip `finally` + i = skipComments(i); + return findStatementEnd(i); + } + + // `else` (standalone, not `else if`) + if (t == tok!"else") + { + i++; // skip `else` + i = skipComments(i); + return findStatementEnd(i); + } + + // Regular statement: scan to `;` at depth 0 + int depth = 0; + while (i < tokens.length) + { + if (tokens[i].type == tok!"(" || tokens[i].type == tok!"[" + || tokens[i].type == tok!"{") + depth++; + else if (tokens[i].type == tok!")" || tokens[i].type == tok!"]" + || tokens[i].type == tok!"}") + { + depth--; + if (depth < 0) + break; // unmatched close - stop + } + else if (tokens[i].type == tok!";" && depth == 0) + { + i++; // include the semicolon + return i; + } + i++; + } + return i; + } + + // Find the end of just the "then" part of an if statement (not including else). + // This is needed because findStatementEnd for if includes the else. + size_t findThenBodyEnd(size_t i) + { + i = skipComments(i); + if (i >= tokens.length) + return i; + + auto t = tokens[i].type; + + // Braced body + if (t == tok!"{") + { + int depth = 1; + i++; + while (i < tokens.length && depth > 0) + { + if (tokens[i].type == tok!"{") + depth++; + else if (tokens[i].type == tok!"}") + depth--; + i++; + } + return i; + } + + // If the then-body is itself a block header, we need the full construct + // but NOT its else (since that else belongs to the outer if) + // Actually, for nested if: `if (a) if (b) x; else y; else z;` + // the first else belongs to the inner if. We need to include it. + // So we use findStatementEnd which handles if+else as a unit. + if (isParenBlockHeader(t) || t == tok!"debug" || t == tok!"do" || t == tok!"else" + || t == tok!"try" || t == tok!"finally") + { + return findStatementEnd(i); + } + + // Regular statement + int depth = 0; + while (i < tokens.length) + { + if (tokens[i].type == tok!"(" || tokens[i].type == tok!"[" + || tokens[i].type == tok!"{") + depth++; + else if (tokens[i].type == tok!")" || tokens[i].type == tok!"]" + || tokens[i].type == tok!"}") + { + depth--; + if (depth < 0) + break; + } + else if (tokens[i].type == tok!";" && depth == 0) + { + i++; + return i; + } + i++; + } + return i; + } + + // Process a block header body at position `i` (after the condition/parens). + // If the body is not braced, record insertion points for `{` and `}`. + // Only wraps the immediate body, not any trailing `else` — the main loop + // handles `else` and inner block headers separately. + void processBody(size_t i, IdType headerType) + { + size_t bodyStart = skipComments(i); + if (bodyStart >= tokens.length) + return; + + // Already braced or empty statement - no insertion needed + if (tokens[bodyStart].type == tok!"{" || tokens[bodyStart].type == tok!";") + return; + + // For certain keywords after function declarations, don't insert braces + if (tokens[bodyStart].type == tok!"in" || tokens[bodyStart].type == tok!"out" + || tokens[bodyStart].type == tok!"do") + return; + if (bodyStart < tokens.length && tokens[bodyStart].text == "body") + return; + + // For `if`/`debug`/`version` with `else`: wrap only the "then" body. + // The main loop will handle `else` when it encounters it. + if (headerType == tok!"if" || headerType == tok!"debug" + || headerType == tok!"version") + { + size_t thenEnd = findThenBodyEnd(bodyStart); + insertions ~= Insertion(bodyStart, true); + insertions ~= Insertion(thenEnd, false); + return; + } + + // For non-if block headers (for, foreach, while, etc.) + size_t stmtEnd = findStatementEnd(bodyStart); + insertions ~= Insertion(bodyStart, true); + insertions ~= Insertion(stmtEnd, false); + } + + // Helper: extract the meaningful text from a comment token (strip // or /* */) + static string getDfmtCommentText(string commentText) pure @safe + { + import std.string : strip; + + if (commentText.length >= 2 && commentText[0 .. 2] == "//") + return commentText[2 .. $].strip(); + else if (commentText.length > 3) + return commentText[2 .. $ - 2].strip(); + else if (commentText.length >= 2) + return commentText[2 .. $].strip(); + return commentText; + } + + // Main scan: walk through tokens and look for block headers. + // We process every token position; processBody only records insertions, + // it does NOT cause the main loop to skip tokens, so inner block headers + // (like nested if, foreach inside if body, etc.) are also processed. + size_t i = 0; + bool dfmtOff = false; + while (i < tokens.length) + { + auto t = tokens[i].type; + + // Track `// dfmt off` / `// dfmt on` comment regions. + // When dfmtOff is true, skip all block header processing. + if (t == tok!"comment") + { + auto ct = getDfmtCommentText(tokens[i].text); + if (ct == "dfmt off") + dfmtOff = true; + else if (ct == "dfmt on") + dfmtOff = false; + i++; + continue; + } + + if (dfmtOff) + { + i++; + continue; + } + + // Block header with parens + if (isParenBlockHeader(t)) + { + size_t afterKw = i + 1; + afterKw = skipComments(afterKw); + if (afterKw < tokens.length && tokens[afterKw].type == tok!"(") + { + size_t afterParens = skipParens(afterKw); + processBody(afterParens, t); + } + else if (t == tok!"synchronized") + { + // synchronized without parens: `synchronized stmt;` + processBody(afterKw, t); + } + i++; + continue; + } + + // `debug` keyword — can appear with or without parens + if (t == tok!"debug") + { + size_t afterKw = i + 1; + afterKw = skipComments(afterKw); + if (afterKw < tokens.length && tokens[afterKw].type == tok!"(") + { + size_t afterParens = skipParens(afterKw); + processBody(afterParens, t); + } + else + { + // debug without parens: `debug stmt;` + processBody(afterKw, t); + } + i++; + continue; + } + + // `do` keyword (not `do` in function body contracts) + if (t == tok!"do") + { + size_t bodyStart = skipComments(i + 1); + if (bodyStart < tokens.length && tokens[bodyStart].type != tok!"{" + && tokens[bodyStart].type != tok!";") + { + // Find end of do body (just the single statement, not the while) + size_t stmtEnd = findThenBodyEnd(bodyStart); + insertions ~= Insertion(bodyStart, true); + insertions ~= Insertion(stmtEnd, false); + } + i++; + continue; + } + + // `finally` keyword — never takes parens + if (t == tok!"finally") + { + size_t bodyStart = skipComments(i + 1); + if (bodyStart < tokens.length && tokens[bodyStart].type != tok!"{" + && tokens[bodyStart].type != tok!";") + { + size_t stmtEnd = findStatementEnd(bodyStart); + insertions ~= Insertion(bodyStart, true); + insertions ~= Insertion(stmtEnd, false); + } + i++; + continue; + } + + // `else` keyword + if (t == tok!"else") + { + size_t afterElse = skipComments(i + 1); + if (afterElse < tokens.length) + { + auto nextT = tokens[afterElse].type; + // else if / else version / else static if — skip, let inner handle it + if (nextT == tok!"if" || nextT == tok!"version" + || (nextT == tok!"static" && afterElse + 1 < tokens.length + && (tokens[afterElse + 1].type == tok!"if" + || tokens[afterElse + 1].type == tok!"foreach" + || tokens[afterElse + 1].type == tok!"foreach_reverse"))) + { + i++; + continue; + } + // else with braced body - skip + if (nextT == tok!"{") + { + i++; + continue; + } + // else with braceless body - add braces + size_t elseBodyEnd = findStatementEnd(afterElse); + insertions ~= Insertion(afterElse, true); + insertions ~= Insertion(elseBodyEnd, false); + } + i++; + continue; + } + + i++; + } + + // If no insertions needed, return the original array + if (insertions.data.length == 0) + return cast(Token[]) tokens; + + // Sort insertions by position (stable sort: opens before closes at same position) + import std.algorithm : sort; + auto ins = insertions.data; + sort!((a, b) { + if (a.position != b.position) + return a.position < b.position; + // At the same position, open brace comes before close brace + // (shouldn't normally happen, but handle gracefully) + return a.isOpen && !b.isOpen; + })(ins); + + // Build the new token array + auto result = appender!(Token[])(); + result.reserve(tokens.length + ins.length); + + size_t insIdx = 0; + for (size_t ti = 0; ti <= tokens.length; ti++) + { + // Insert any synthetic tokens at this position + while (insIdx < ins.length && ins[insIdx].position == ti) + { + Token synth; + synth.type = ins[insIdx].isOpen ? tok!"{" : tok!"}"; + // Use size_t.max minus a counter for unique synthetic indices + // that won't match any AST info lookups + synth.index = size_t.max - insIdx; + // Copy line/column from the nearest real token for reasonable error messages + if (ti < tokens.length) + { + synth.line = tokens[ti].line; + synth.column = tokens[ti].column; + } + else if (tokens.length > 0) + { + synth.line = tokens[$ - 1].line; + synth.column = tokens[$ - 1].column; + } + result ~= synth; + insIdx++; + } + if (ti < tokens.length) + result ~= tokens[ti]; + } + + return result.data; +} + bool canFindIndex(const size_t[] items, size_t index, size_t* pos = null) pure @safe @nogc { import std.range : assumeSorted; diff --git a/src/dfmt/main.d b/src/dfmt/main.d index c5a5577..027f332 100644 --- a/src/dfmt/main.d +++ b/src/dfmt/main.d @@ -107,6 +107,9 @@ else case "space_after_keywords": optConfig.dfmt_space_after_keywords = optVal; break; + case "force_curly_braces": + optConfig.dfmt_force_curly_braces = optVal; + break; default: assert(false, "Invalid command-line switch"); } @@ -141,7 +144,8 @@ else "template_constraint_style", &optConfig.dfmt_template_constraint_style, "keep_line_breaks", &handleBooleans, "single_indent", &handleBooleans, - "reflow_property_chains", &handleBooleans); + "reflow_property_chains", &handleBooleans, + "force_curly_braces", &handleBooleans); // dfmt on } catch (GetOptException e) @@ -356,6 +360,7 @@ Formatting Options: --space_before_named_arg_colon --single_indent --reflow_property_chains + --force_curly_braces `, optionsToString!(typeof(Config.dfmt_template_constraint_style))); } diff --git a/tests/allman/force_curly_braces.d.ref b/tests/allman/force_curly_braces.d.ref new file mode 100644 index 0000000..781b992 --- /dev/null +++ b/tests/allman/force_curly_braces.d.ref @@ -0,0 +1,292 @@ +// Test: force_curly_braces option +// Tests various braceless constructs that should get braces added + +// 1. Simple if without braces +int test1(int x) +{ + if (x > 0) + { + return 1; + } + return 0; +} + +// 2. if/else without braces +int test2(int x) +{ + if (x > 0) + { + return 1; + } + else + { + return 0; + } +} + +// 3. Nested if without braces +void test3(int a, int b) +{ + if (a) + { + if (b) + { + doSomething(); + } + } +} + +// 4. if/else if/else chain +int test4(int x) +{ + if (x > 0) + { + return 1; + } + else if (x < 0) + { + return -1; + } + else + { + return 0; + } +} + +// 5. for loop without braces +void test5() +{ + for (int i = 0; i < 10; i++) + { + doSomething(); + } +} + +// 6. foreach without braces +void test6(int[] arr) +{ + foreach (x; arr) + { + doSomething(); + } +} + +// 7. while without braces +void test7() +{ + while (condition()) + { + doSomething(); + } +} + +// 8. Mixed: if braced, else not +int test8(int x) +{ + if (x > 0) + { + return 1; + } + else + { + return 0; + } +} + +// 9. Mixed: if not braced, else braced +int test9(int x) +{ + if (x > 0) + { + return 1; + } + else + { + return 0; + } +} + +// 10. Already fully braced (should be unchanged) +int test10(int x) +{ + if (x > 0) + { + return 1; + } + else + { + return 0; + } +} + +// 11. Nested mixed: if with for body +void test11(int[] arr) +{ + if (arr.length > 0) + { + foreach (x; arr) + { + doSomething(); + } + } +} + +// 12. while with if body +void test12() +{ + while (condition()) + { + if (check()) + { + doSomething(); + } + } +} + +// 13. do-while without braces +void test13() +{ + do + { + doSomething(); + } + while (condition()); +} + +// 14. scope(exit) without braces +void test14() +{ + scope (exit) + { + cleanup(); + } + doWork(); +} + +// 15. synchronized without braces +void test15() +{ + synchronized + { + doSomething(); + } +} + +// 16. debug without parens +void test16() +{ + debug + { + doSomething(); + } +} + +// 17. version/else +void test17() +{ + version (Windows) + { + doWindows(); + } + else + { + doPosix(); + } +} + +// 18. with statement +void test18() +{ + with (someObj) + { + doSomething(); + } +} + +// 19. catch without braces +void test19() +{ + try + { + doSomething(); + } + catch (Exception e) + { + handleError(); + } +} + +// 20. debug with else +void test20() +{ + debug + { + doDebug(); + } + else + { + doRelease(); + } +} + +// 21. finally without braces +void test21() +{ + try + { + doSomething(); + } + catch (Exception e) + { + handleError(); + } + finally + { + cleanup(); + } +} + +// 22. try/catch/finally all braceless as body of if +void test22(bool flag) +{ + if (flag) + { + try + { + doSomething(); + } + catch (Exception e) + { + handleError(); + } + } +} + +// 23. dfmt off region — braceless code should NOT get braces +void test23() +{ + // dfmt off + if (x) + doSomething(); + // dfmt on + if (y) + { + doSomethingElse(); + } +} + +// 24. else debug without parens +void test24() +{ + version (Windows) + { + doWindows(); + } + else + { + debug + { + doDebugPosix(); + } + } +} diff --git a/tests/force_curly_braces.args b/tests/force_curly_braces.args new file mode 100644 index 0000000..b7491d7 --- /dev/null +++ b/tests/force_curly_braces.args @@ -0,0 +1 @@ +--force_curly_braces=true diff --git a/tests/force_curly_braces.d b/tests/force_curly_braces.d new file mode 100644 index 0000000..796eb23 --- /dev/null +++ b/tests/force_curly_braces.d @@ -0,0 +1,186 @@ +// Test: force_curly_braces option +// Tests various braceless constructs that should get braces added + +// 1. Simple if without braces +int test1(int x) { + if (x > 0) + return 1; + return 0; +} + +// 2. if/else without braces +int test2(int x) { + if (x > 0) + return 1; + else + return 0; +} + +// 3. Nested if without braces +void test3(int a, int b) { + if (a) + if (b) + doSomething(); +} + +// 4. if/else if/else chain +int test4(int x) { + if (x > 0) + return 1; + else if (x < 0) + return -1; + else + return 0; +} + +// 5. for loop without braces +void test5() { + for (int i = 0; i < 10; i++) + doSomething(); +} + +// 6. foreach without braces +void test6(int[] arr) { + foreach (x; arr) + doSomething(); +} + +// 7. while without braces +void test7() { + while (condition()) + doSomething(); +} + +// 8. Mixed: if braced, else not +int test8(int x) { + if (x > 0) { + return 1; + } else + return 0; +} + +// 9. Mixed: if not braced, else braced +int test9(int x) { + if (x > 0) + return 1; + else { + return 0; + } +} + +// 10. Already fully braced (should be unchanged) +int test10(int x) { + if (x > 0) { + return 1; + } else { + return 0; + } +} + +// 11. Nested mixed: if with for body +void test11(int[] arr) { + if (arr.length > 0) + foreach (x; arr) + doSomething(); +} + +// 12. while with if body +void test12() { + while (condition()) + if (check()) + doSomething(); +} + +// 13. do-while without braces +void test13() { + do + doSomething(); + while (condition()); +} + +// 14. scope(exit) without braces +void test14() { + scope(exit) + cleanup(); + doWork(); +} + +// 15. synchronized without braces +void test15() { + synchronized + doSomething(); +} + +// 16. debug without parens +void test16() { + debug + doSomething(); +} + +// 17. version/else +void test17() { + version(Windows) + doWindows(); + else + doPosix(); +} + +// 18. with statement +void test18() { + with (someObj) + doSomething(); +} + +// 19. catch without braces +void test19() { + try { + doSomething(); + } catch (Exception e) + handleError(); +} + +// 20. debug with else +void test20() { + debug + doDebug(); + else + doRelease(); +} + +// 21. finally without braces +void test21() { + try { + doSomething(); + } catch (Exception e) { + handleError(); + } finally + cleanup(); +} + +// 22. try/catch/finally all braceless as body of if +void test22(bool flag) { + if (flag) + try { + doSomething(); + } catch (Exception e) + handleError(); +} + +// 23. dfmt off region — braceless code should NOT get braces +void test23() { + // dfmt off + if (x) + doSomething(); + // dfmt on + if (y) + doSomethingElse(); +} + +// 24. else debug without parens +void test24() { + version(Windows) + doWindows(); + else + debug + doDebugPosix(); +} diff --git a/tests/knr/force_curly_braces.d.ref b/tests/knr/force_curly_braces.d.ref new file mode 100644 index 0000000..45e55ad --- /dev/null +++ b/tests/knr/force_curly_braces.d.ref @@ -0,0 +1,237 @@ +// Test: force_curly_braces option +// Tests various braceless constructs that should get braces added + +// 1. Simple if without braces +int test1(int x) +{ + if (x > 0) { + return 1; + } + return 0; +} + +// 2. if/else without braces +int test2(int x) +{ + if (x > 0) { + return 1; + } else { + return 0; + } +} + +// 3. Nested if without braces +void test3(int a, int b) +{ + if (a) { + if (b) { + doSomething(); + } + } +} + +// 4. if/else if/else chain +int test4(int x) +{ + if (x > 0) { + return 1; + } else if (x < 0) { + return -1; + } else { + return 0; + } +} + +// 5. for loop without braces +void test5() +{ + for (int i = 0; i < 10; i++) { + doSomething(); + } +} + +// 6. foreach without braces +void test6(int[] arr) +{ + foreach (x; arr) { + doSomething(); + } +} + +// 7. while without braces +void test7() +{ + while (condition()) { + doSomething(); + } +} + +// 8. Mixed: if braced, else not +int test8(int x) +{ + if (x > 0) { + return 1; + } else { + return 0; + } +} + +// 9. Mixed: if not braced, else braced +int test9(int x) +{ + if (x > 0) { + return 1; + } else { + return 0; + } +} + +// 10. Already fully braced (should be unchanged) +int test10(int x) +{ + if (x > 0) { + return 1; + } else { + return 0; + } +} + +// 11. Nested mixed: if with for body +void test11(int[] arr) +{ + if (arr.length > 0) { + foreach (x; arr) { + doSomething(); + } + } +} + +// 12. while with if body +void test12() +{ + while (condition()) { + if (check()) { + doSomething(); + } + } +} + +// 13. do-while without braces +void test13() +{ + do { + doSomething(); + } + while (condition()); +} + +// 14. scope(exit) without braces +void test14() +{ + scope (exit) { + cleanup(); + } + doWork(); +} + +// 15. synchronized without braces +void test15() +{ + synchronized { + doSomething(); + } +} + +// 16. debug without parens +void test16() +{ + debug { + doSomething(); + } +} + +// 17. version/else +void test17() +{ + version (Windows) { + doWindows(); + } else { + doPosix(); + } +} + +// 18. with statement +void test18() +{ + with (someObj) { + doSomething(); + } +} + +// 19. catch without braces +void test19() +{ + try { + doSomething(); + } catch (Exception e) { + handleError(); + } +} + +// 20. debug with else +void test20() +{ + debug { + doDebug(); + } else { + doRelease(); + } +} + +// 21. finally without braces +void test21() +{ + try { + doSomething(); + } catch (Exception e) { + handleError(); + } finally { + cleanup(); + } +} + +// 22. try/catch/finally all braceless as body of if +void test22(bool flag) +{ + if (flag) { + try { + doSomething(); + } catch (Exception e) { + handleError(); + } + } +} + +// 23. dfmt off region — braceless code should NOT get braces +void test23() +{ + // dfmt off + if (x) + doSomething(); + // dfmt on + if (y) { + doSomethingElse(); + } +} + +// 24. else debug without parens +void test24() +{ + version (Windows) { + doWindows(); + } else { + debug { + doDebugPosix(); + } + } +} diff --git a/tests/otbs/force_curly_braces.d.ref b/tests/otbs/force_curly_braces.d.ref new file mode 100644 index 0000000..927b05d --- /dev/null +++ b/tests/otbs/force_curly_braces.d.ref @@ -0,0 +1,213 @@ +// Test: force_curly_braces option +// Tests various braceless constructs that should get braces added + +// 1. Simple if without braces +int test1(int x) { + if (x > 0) { + return 1; + } + return 0; +} + +// 2. if/else without braces +int test2(int x) { + if (x > 0) { + return 1; + } else { + return 0; + } +} + +// 3. Nested if without braces +void test3(int a, int b) { + if (a) { + if (b) { + doSomething(); + } + } +} + +// 4. if/else if/else chain +int test4(int x) { + if (x > 0) { + return 1; + } else if (x < 0) { + return -1; + } else { + return 0; + } +} + +// 5. for loop without braces +void test5() { + for (int i = 0; i < 10; i++) { + doSomething(); + } +} + +// 6. foreach without braces +void test6(int[] arr) { + foreach (x; arr) { + doSomething(); + } +} + +// 7. while without braces +void test7() { + while (condition()) { + doSomething(); + } +} + +// 8. Mixed: if braced, else not +int test8(int x) { + if (x > 0) { + return 1; + } else { + return 0; + } +} + +// 9. Mixed: if not braced, else braced +int test9(int x) { + if (x > 0) { + return 1; + } else { + return 0; + } +} + +// 10. Already fully braced (should be unchanged) +int test10(int x) { + if (x > 0) { + return 1; + } else { + return 0; + } +} + +// 11. Nested mixed: if with for body +void test11(int[] arr) { + if (arr.length > 0) { + foreach (x; arr) { + doSomething(); + } + } +} + +// 12. while with if body +void test12() { + while (condition()) { + if (check()) { + doSomething(); + } + } +} + +// 13. do-while without braces +void test13() { + do { + doSomething(); + } + while (condition()); +} + +// 14. scope(exit) without braces +void test14() { + scope (exit) { + cleanup(); + } + doWork(); +} + +// 15. synchronized without braces +void test15() { + synchronized { + doSomething(); + } +} + +// 16. debug without parens +void test16() { + debug { + doSomething(); + } +} + +// 17. version/else +void test17() { + version (Windows) { + doWindows(); + } else { + doPosix(); + } +} + +// 18. with statement +void test18() { + with (someObj) { + doSomething(); + } +} + +// 19. catch without braces +void test19() { + try { + doSomething(); + } catch (Exception e) { + handleError(); + } +} + +// 20. debug with else +void test20() { + debug { + doDebug(); + } else { + doRelease(); + } +} + +// 21. finally without braces +void test21() { + try { + doSomething(); + } catch (Exception e) { + handleError(); + } finally { + cleanup(); + } +} + +// 22. try/catch/finally all braceless as body of if +void test22(bool flag) { + if (flag) { + try { + doSomething(); + } catch (Exception e) { + handleError(); + } + } +} + +// 23. dfmt off region — braceless code should NOT get braces +void test23() { + // dfmt off + if (x) + doSomething(); + // dfmt on + if (y) { + doSomethingElse(); + } +} + +// 24. else debug without parens +void test24() { + version (Windows) { + doWindows(); + } else { + debug { + doDebugPosix(); + } + } +} From d2ecb452eb693195d7459a04707c2cfaedbdc44b Mon Sep 17 00:00:00 2001 From: Robert burner Schadek Date: Wed, 25 Feb 2026 12:56:07 +0100 Subject: [PATCH 2/6] scope variables, as per requests --- tests/allman/force_curly_braces.d.ref | 3 +++ tests/force_curly_braces.d | 3 +++ tests/knr/force_curly_braces.d.ref | 3 +++ tests/otbs/force_curly_braces.d.ref | 3 +++ 4 files changed, 12 insertions(+) diff --git a/tests/allman/force_curly_braces.d.ref b/tests/allman/force_curly_braces.d.ref index 781b992..0231d7e 100644 --- a/tests/allman/force_curly_braces.d.ref +++ b/tests/allman/force_curly_braces.d.ref @@ -161,6 +161,9 @@ void test14() cleanup(); } doWork(); + + scope int[] array = new int[](127); + scope T!() x = 5; } // 15. synchronized without braces diff --git a/tests/force_curly_braces.d b/tests/force_curly_braces.d index 796eb23..67a423b 100644 --- a/tests/force_curly_braces.d +++ b/tests/force_curly_braces.d @@ -103,6 +103,9 @@ void test14() { scope(exit) cleanup(); doWork(); + + scope int[] array = new int[](127); + scope T!() x = 5; } // 15. synchronized without braces diff --git a/tests/knr/force_curly_braces.d.ref b/tests/knr/force_curly_braces.d.ref index 45e55ad..751abee 100644 --- a/tests/knr/force_curly_braces.d.ref +++ b/tests/knr/force_curly_braces.d.ref @@ -132,6 +132,9 @@ void test14() cleanup(); } doWork(); + + scope int[] array = new int[](127); + scope T!() x = 5; } // 15. synchronized without braces diff --git a/tests/otbs/force_curly_braces.d.ref b/tests/otbs/force_curly_braces.d.ref index 927b05d..bb04171 100644 --- a/tests/otbs/force_curly_braces.d.ref +++ b/tests/otbs/force_curly_braces.d.ref @@ -118,6 +118,9 @@ void test14() { cleanup(); } doWork(); + + scope int[] array = new int[](127); + scope T!() x = 5; } // 15. synchronized without braces From ad26ce5ac3bcb5dae6bfedbcc514d07fc050a3a7 Mon Sep 17 00:00:00 2001 From: Robert burner Schadek Date: Wed, 4 Mar 2026 16:41:48 +0100 Subject: [PATCH 3/6] more testing --- tests/force_curly_braces.d | 13 +++++++++++++ tests/knr/force_curly_braces.d.ref | 17 +++++++++++++++++ tests/otbs/force_curly_braces.d.ref | 16 ++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/tests/force_curly_braces.d b/tests/force_curly_braces.d index 67a423b..db09411 100644 --- a/tests/force_curly_braces.d +++ b/tests/force_curly_braces.d @@ -187,3 +187,16 @@ void test24() { debug doDebugPosix(); } + +// 25. version + unittest +void test25() { + string command; + version (Posix) command ~= " 2> /dev/null 1> /dev/null"; + + version (Posix) command ~= " 2> /dev/null 1> /dev/null"; + + unittest + { + version (Posix) command ~= " 2> /dev/null 1> /dev/null"; + } +} diff --git a/tests/knr/force_curly_braces.d.ref b/tests/knr/force_curly_braces.d.ref index 751abee..b3df10e 100644 --- a/tests/knr/force_curly_braces.d.ref +++ b/tests/knr/force_curly_braces.d.ref @@ -238,3 +238,20 @@ void test24() } } } + +// 25. version + unittest +void test25() +{ + string command; + version (Posix) { + command ~= " 2> /dev/null 1> /dev/null"; + } + version (Posix) { + command ~= " 2> /dev/null 1> /dev/null"; + } + unittest { + version (Posix) { + command ~= " 2> /dev/null 1> /dev/null"; + } + } +} diff --git a/tests/otbs/force_curly_braces.d.ref b/tests/otbs/force_curly_braces.d.ref index bb04171..b9b9614 100644 --- a/tests/otbs/force_curly_braces.d.ref +++ b/tests/otbs/force_curly_braces.d.ref @@ -214,3 +214,19 @@ void test24() { } } } + +// 25. version + unittest +void test25() { + string command; + version (Posix) { + command ~= " 2> /dev/null 1> /dev/null"; + } + version (Posix) { + command ~= " 2> /dev/null 1> /dev/null"; + } + unittest { + version (Posix) { + command ~= " 2> /dev/null 1> /dev/null"; + } + } +} From 118f688326059633398761e061a51bb46fc92052 Mon Sep 17 00:00:00 2001 From: Robert burner Schadek Date: Thu, 5 Mar 2026 16:22:03 +0100 Subject: [PATCH 4/6] fix curlys and comments --- src/dfmt/formatter.d | 315 ++++++++++++++------------ tests/allman/force_curly_braces.d.ref | 32 +++ tests/force_curly_braces.d | 162 ++++++++----- tests/knr/force_curly_braces.d.ref | 10 + tests/otbs/force_curly_braces.d.ref | 10 + 5 files changed, 328 insertions(+), 201 deletions(-) diff --git a/src/dfmt/formatter.d b/src/dfmt/formatter.d index 1fe8941..fc0ebb5 100644 --- a/src/dfmt/formatter.d +++ b/src/dfmt/formatter.d @@ -1,4 +1,3 @@ - // Copyright Brian Schott 2015. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE.txt or copy at @@ -53,6 +52,7 @@ bool format(OutputRange)(string source_desc, ubyte[] buffer, OutputRange output, return false; import dfmt.editorconfig : OptionalBoolean; + if (formatterConfig.dfmt_force_curly_braces == OptionalBoolean.t) tokens = insertForcedBraces(tokens); @@ -130,7 +130,7 @@ struct TokenFormatter(OutputRange) assert(false, "config.end_of_line was unspecified"); else { - assert (eol == eol._default); + assert(eol == eol._default); this.eolString = eolStringFromInput; } } @@ -238,10 +238,9 @@ private: if (hasCurrent) { immutable t = tokens[index].type; - if (t == tok!"identifier" || isStringLiteral(t) - || isNumberLiteral(t) || t == tok!"characterLiteral" - // a!"b" function() - || t == tok!"function" || t == tok!"delegate") + if (t == tok!"identifier" || isStringLiteral(t) || isNumberLiteral(t) + || t == tok!"characterLiteral" // a!"b" function() + || t == tok!"function" || t == tok!"delegate") write(" "); } } @@ -252,9 +251,9 @@ private: else if (currentIs(tok!"return")) { writeToken(); - if (hasCurrent && (!currentIs(tok!";") && !currentIs(tok!")") && !currentIs(tok!"{") - && !currentIs(tok!"in") && !currentIs(tok!"out") && !currentIs(tok!"do") - && tokens[index].text != "body")) + if (hasCurrent && (!currentIs(tok!";") && !currentIs(tok!")") + && !currentIs(tok!"{") && !currentIs(tok!"in") && !currentIs(tok!"out") + && !currentIs(tok!"do") && tokens[index].text != "body")) write(" "); } else if (currentIs(tok!"with")) @@ -262,13 +261,15 @@ private: if (indents.length == 0 || !indents.topIsOneOf(tok!"switch", tok!"with")) indents.push(tok!"with"); writeToken(); - if (config.dfmt_space_after_keywords) { + if (config.dfmt_space_after_keywords) + { write(" "); } if (hasCurrent && currentIs(tok!"(")) writeParens(false); - if (hasCurrent && !currentIs(tok!"switch") && !currentIs(tok!"with") - && !currentIs(tok!"{") && !(currentIs(tok!"final") && peekIs(tok!"switch"))) + if (hasCurrent && !currentIs(tok!"switch") + && !currentIs(tok!"with") && !currentIs(tok!"{") + && !(currentIs(tok!"final") && peekIs(tok!"switch"))) { newline(); } @@ -298,15 +299,16 @@ private: writeToken(); } } - else if (((isBlockHeader() || currentIs(tok!"version")) && peekIs(tok!"(")) - || (currentIs(tok!"debug") && peekIs(tok!"{"))) + else if (((isBlockHeader() || currentIs(tok!"version")) + && peekIs(tok!"(")) || (currentIs(tok!"debug") && peekIs(tok!"{"))) { if (!assumeSorted(astInformation.constraintLocations).equalRange(current.index).empty) formatConstraint(); else formatBlockHeader(); } - else if ((current.text == "body" || current == tok!"do") && peekBackIsFunctionDeclarationEnding()) + else if ((current.text == "body" || current == tok!"do") + && peekBackIsFunctionDeclarationEnding()) { formatKeyword(); } @@ -343,9 +345,8 @@ private: { const thisIndex = current.index; formatKeyword(); - if (config.dfmt_space_before_function_parameters - && (thisSpace || astInformation.constructorDestructorLocations - .canFindIndex(thisIndex))) + if (config.dfmt_space_before_function_parameters && (thisSpace + || astInformation.constructorDestructorLocations.canFindIndex(thisIndex))) { write(" "); thisSpace = false; @@ -382,7 +383,7 @@ private: || isNumberLiteral(tokens[index].type) || (inAsm && peekBack2Is(tok!";") && currentIs(tok!"[")) ))) - //dfmt on + //dfmt on { write(" "); } @@ -399,6 +400,7 @@ private: void formatConstraint() { import dfmt.editorconfig : OB = OptionalBoolean; + with (TemplateConstraintStyle) final switch (config.dfmt_template_constraint_style) { case _unspecified: @@ -417,8 +419,8 @@ private: immutable l = currentLineLength + betweenParenLength(tokens[index + 1 .. $]); if (l > config.dfmt_soft_max_line_length) { - config.dfmt_single_template_constraint_indent == OB.t ? - pushWrapIndent() : pushWrapIndent(tok!"!"); + config.dfmt_single_template_constraint_indent == OB.t + ? pushWrapIndent() : pushWrapIndent(tok!"!"); newline(); } else if (peekBackIs(tok!")") || peekBackIs(tok!"identifier")) @@ -426,8 +428,8 @@ private: break; case always_newline_indent: { - config.dfmt_single_template_constraint_indent == OB.t ? - pushWrapIndent() : pushWrapIndent(tok!"!"); + config.dfmt_single_template_constraint_indent == OB.t + ? pushWrapIndent() : pushWrapIndent(tok!"!"); newline(); } break; @@ -575,10 +577,8 @@ private: else if (currentIs(tok!"{") && config.dfmt_brace_style == BraceStyle.allman) break; else if (t == tok!"import" && !currentIs(tok!"import") - && !currentIs(tok!"}") - && !((currentIs(tok!"public") - || currentIs(tok!"private") - || currentIs(tok!"static")) + && !currentIs(tok!"}") && !((currentIs(tok!"public") + || currentIs(tok!"private") || currentIs(tok!"static")) && peekIs(tok!"import")) && !indents.topIsOneOf(tok!"if", tok!"debug", tok!"version")) { @@ -605,8 +605,7 @@ private: if (peekBack.line != current.line) { // The comment appears on its own line, keep it there. - if (!peekBackIs(tok!"comment")) - // Comments are already properly separated. + if (!peekBackIs(tok!"comment")) // Comments are already properly separated. newline(); } formatStep(); @@ -695,7 +694,8 @@ private: detail.wrap = false; detail.temp = false; - detail.breakEveryItem = astInformation.assocArrayStartLocations.canFindIndex(tokens[index - 1].index); + detail.breakEveryItem = astInformation.assocArrayStartLocations.canFindIndex( + tokens[index - 1].index); // array of (possibly associative) array, let's put each item on its own line if (!detail.breakEveryItem && currentIs(tok!"[")) detail.breakEveryItem = true; @@ -731,7 +731,8 @@ private: { import std.algorithm.searching : canFind, until; - if (tokens[index .. $].until!(tok => tok.line != current.line).canFind!(x => x.type == tok!"]")) + if (tokens[index .. $].until!(tok => tok.line != current.line) + .canFind!(x => x.type == tok!"]")) { return; } @@ -767,7 +768,7 @@ private: newline(); } if (parenDepth == 0 && (peekIs(tok!"is") || peekIs(tok!"in") - || peekIs(tok!"out") || peekIs(tok!"do") || peekIsBody)) + || peekIs(tok!"out") || peekIs(tok!"do") || peekIsBody)) { writeToken(); } @@ -825,12 +826,9 @@ private: else write(" "); } - else if (hasCurrent && (currentIs(tok!"@") - || isBasicType(tokens[index].type) - || currentIs(tok!"invariant") - || currentIs(tok!"extern") - || currentIs(tok!"identifier")) - && !currentIsIndentedTemplateConstraint()) + else if (hasCurrent && (currentIs(tok!"@") || isBasicType(tokens[index].type) + || currentIs(tok!"invariant") || currentIs(tok!"extern") + || currentIs(tok!"identifier")) && !currentIsIndentedTemplateConstraint()) { writeSpace(); } @@ -840,13 +838,15 @@ private: { import dfmt.editorconfig : OptionalBoolean; import std.algorithm : canFind, any; + immutable bool isCase = astInformation.caseEndLocations.canFindIndex(current.index); immutable bool isAttribute = astInformation.attributeDeclarationLines.canFindIndex( current.line); - immutable bool isStructInitializer = astInformation.structInfoSortedByEndLocation - .canFind!(st => st.startLocation < current.index && current.index < st.endLocation); + immutable bool isStructInitializer = astInformation.structInfoSortedByEndLocation.canFind!( + st => st.startLocation < current.index && current.index < st.endLocation); immutable bool isTernary = astInformation.ternaryColonLocations.canFindIndex(current.index); - immutable bool isNamedArg = astInformation.namedArgumentColonLocations.canFindIndex(current.index); + immutable bool isNamedArg = astInformation.namedArgumentColonLocations.canFindIndex( + current.index); if (isCase || isAttribute) { @@ -873,9 +873,9 @@ private: write(config.dfmt_space_before_named_arg_colon ? " : " : ": "); ++index; } - else if (peekBackIs(tok!"identifier") - && [tok!"{", tok!"}", tok!";", tok!":", tok!","] - .any!((ptrdiff_t token) => peekBack2Is(cast(IdType)token, true)) + else if (peekBackIs(tok!"identifier") && [ + tok!"{", tok!"}", tok!";", tok!":", tok!"," + ].any!((ptrdiff_t token) => peekBack2Is(cast(IdType) token, true)) && (!isBlockHeader(1) || peekIs(tok!"if"))) { writeToken(); @@ -939,7 +939,8 @@ private: writeToken(); indents.popWrapIndents(); linebreakHints = []; - while (indents.topIsOneOf(tok!"enum", tok!"try", tok!"catch", tok!"finally", tok!"debug")) + while (indents.topIsOneOf(tok!"enum", tok!"try", tok!"catch", + tok!"finally", tok!"debug")) indents.pop(); if (indents.topAre(tok!"static", tok!"else")) { @@ -977,11 +978,11 @@ private: { import std.algorithm.searching : find; - auto indentInfo = astInformation.indentInfoSortedByEndLocation - .find!((a,b) => a.startLocation == b)(tIndex); + auto indentInfo = astInformation.indentInfoSortedByEndLocation.find!((a, + b) => a.startLocation == b)(tIndex); assert(indentInfo.length > 0); - cast()indentInfo[0].flags |= BraceIndentInfoFlags.tempIndent; - cast()indentInfo[0].beginIndentLevel = indents.indentLevel; + cast() indentInfo[0].flags |= BraceIndentInfoFlags.tempIndent; + cast() indentInfo[0].beginIndentLevel = indents.indentLevel; indents.push(tok!"{"); newline(); @@ -1032,7 +1033,8 @@ private: newline(); else if (config.dfmt_brace_style == BraceStyle.knr && astInformation.funBodyLocations.canFindIndex(tIndex) - && (peekBackIs(tok!")") || (!peekBackIs(tok!"do") && peekBack().text != "body"))) + && (peekBackIs(tok!")") || (!peekBackIs(tok!"do") + && peekBack().text != "body"))) newline(); else if (!peekBackIsOneOf(true, tok!"{", tok!"}", tok!";")) write(" "); @@ -1049,7 +1051,7 @@ private: { void popToBeginIndent(BraceIndentInfo indentInfo) { - foreach(i; indentInfo.beginIndentLevel .. indents.indentLevel) + foreach (i; indentInfo.beginIndentLevel .. indents.indentLevel) { indents.pop(); } @@ -1068,14 +1070,16 @@ private: // Account for possible function literals in this array which offset // the previously set index (pos). Fixes issue #432. size_t newPos = pos; - while(astInformation.indentInfoSortedByEndLocation[newPos].endLocation < - tokens[index].index) + while ( + astInformation.indentInfoSortedByEndLocation[newPos].endLocation + < tokens[index].index) { newPos++; } - if (astInformation.indentInfoSortedByEndLocation[newPos].endLocation == - tokens[index].index) + if ( + astInformation.indentInfoSortedByEndLocation[newPos].endLocation + == tokens[index].index) { pos = newPos; } @@ -1117,12 +1121,12 @@ private: currentLineLength = 0; justAddedExtraNewline = true; } - if (config.dfmt_brace_style.among(BraceStyle.otbs, BraceStyle.knr) - && ((peekIs(tok!"else") - && !indents.topAre(tok!"static", tok!"if") - && !indents.topIs(tok!"foreach") && !indents.topIs(tok!"for") - && !indents.topIs(tok!"while") && !indents.topIs(tok!"do")) - || peekIs(tok!"catch") || peekIs(tok!"finally"))) + if (config.dfmt_brace_style.among(BraceStyle.otbs, + BraceStyle.knr) && ((peekIs(tok!"else") && !indents.topAre(tok!"static", + tok!"if") && !indents.topIs(tok!"foreach") + && !indents.topIs(tok!"for") + && !indents.topIs(tok!"while") && !indents.topIs(tok!"do")) + || peekIs(tok!"catch") || peekIs(tok!"finally"))) { write(" "); index++; @@ -1208,8 +1212,9 @@ private: if (!currentIs(tok!"{") && !currentIs(tok!";")) write(" "); } - else if (hasCurrent && !currentIs(tok!"{") && !currentIs(tok!";") && !currentIs(tok!"in") && - !currentIs(tok!"out") && !currentIs(tok!"do") && current.text != "body") + else if (hasCurrent && !currentIs(tok!"{") && !currentIs(tok!";") + && !currentIs(tok!"in") && !currentIs(tok!"out") + && !currentIs(tok!"do") && current.text != "body") { newline(); } @@ -1287,7 +1292,8 @@ private: writeParens(config.dfmt_space_after_cast == OptionalBoolean.t); break; case tok!"out": - if (!peekBackIsSlashSlash) { + if (!peekBackIsSlashSlash) + { if (!peekBackIs(tok!"}") && astInformation.contractLocations.canFindIndex(current.index)) newline(); @@ -1317,7 +1323,8 @@ private: break; case tok!"in": immutable isContract = astInformation.contractLocations.canFindIndex(current.index); - if (!peekBackIsSlashSlash) { + if (!peekBackIsSlashSlash) + { if (isContract) { indents.popTempIndents(); @@ -1341,7 +1348,8 @@ private: tok!"}", tok!"=", tok!"&&", tok!"||") && !peekBackIsKeyword()) write(" "); writeToken(); - if (hasCurrent && !currentIs(tok!"(") && !currentIs(tok!"{") && !currentIs(tok!"comment")) + if (hasCurrent && !currentIs(tok!"(") && !currentIs(tok!"{") + && !currentIs(tok!"comment")) write(" "); break; case tok!"case": @@ -1366,8 +1374,7 @@ private: break; case tok!"static": { - if (astInformation.staticConstructorDestructorLocations - .canFindIndex(current.index)) + if (astInformation.staticConstructorDestructorLocations.canFindIndex(current.index)) { thisSpace = true; } @@ -1375,8 +1382,8 @@ private: goto default; case tok!"shared": { - if (astInformation.sharedStaticConstructorDestructorLocations - .canFindIndex(current.index)) + if (astInformation.sharedStaticConstructorDestructorLocations.canFindIndex( + current.index)) { thisSpace = true; } @@ -1416,11 +1423,10 @@ private: bool currentIsIndentedTemplateConstraint() { - return hasCurrent - && astInformation.constraintLocations.canFindIndex(current.index) + return hasCurrent && astInformation.constraintLocations.canFindIndex(current.index) && (config.dfmt_template_constraint_style == TemplateConstraintStyle.always_newline - || config.dfmt_template_constraint_style == TemplateConstraintStyle.always_newline_indent - || currentLineLength >= config.dfmt_soft_max_line_length); + || config.dfmt_template_constraint_style == TemplateConstraintStyle.always_newline_indent + || currentLineLength >= config.dfmt_soft_max_line_length); } void formatOperator() @@ -1509,9 +1515,9 @@ private: case tok!".": regenLineBreakHintsIfNecessary(index); immutable bool ufcsWrap = config.dfmt_reflow_property_chains == OptionalBoolean.t - && astInformation.ufcsHintLocations.canFindIndex(current.index); - if (ufcsWrap || linebreakHints.canFind(index) || onNextLine - || (linebreakHints.length == 0 && currentLineLength + nextTokenLength() > config.max_line_length)) + && astInformation.ufcsHintLocations.canFindIndex(current.index); + if (ufcsWrap || linebreakHints.canFind(index) || onNextLine || (linebreakHints.length == 0 + && currentLineLength + nextTokenLength() > config.max_line_length)) { if (!indents.topIs(tok!".")) indents.push(tok!"."); @@ -1585,8 +1591,7 @@ private: { write(" "); } - if (rightOperandLine > operatorLine - && !indents.topIs(tok!"enum")) + if (rightOperandLine > operatorLine && !indents.topIs(tok!"enum")) { pushWrapIndent(); } @@ -1728,14 +1733,10 @@ private: // either the expression end index or the next mandatory line break // or a newline inside a string literal, whichever is first. auto r = assumeSorted(astInformation.ufcsHintLocations).upperBound(tokens[i].index); - immutable ufcsBreakLocation = r.empty - ? size_t.max + immutable ufcsBreakLocation = r.empty ? size_t.max : tokens[i .. $].countUntil!(t => t.index == r.front) + i; - immutable multilineStringLocation = tokens[i .. $] - .countUntil!(t => t.text.canFind('\n')); - immutable size_t j = min( - expressionEndIndex(i), - ufcsBreakLocation, + immutable multilineStringLocation = tokens[i .. $].countUntil!(t => t.text.canFind('\n')); + immutable size_t j = min(expressionEndIndex(i), ufcsBreakLocation, multilineStringLocation == -1 ? size_t.max : multilineStringLocation + i + 1); // Use magical negative value for array literals and wrap indents immutable inLvl = (indents.topIsWrap() || indents.topIs(tok!"]")) ? -indentLevel @@ -1805,8 +1806,9 @@ private: immutable l = indents.indentToMostRecent(tok!"switch"); if (l != -1 && config.dfmt_align_switch_statements == OptionalBoolean.t) indentLevel = l; - else if (astInformation.structInfoSortedByEndLocation - .canFind!(st => st.startLocation < current.index && current.index < st.endLocation)) { + else if (astInformation.structInfoSortedByEndLocation.canFind!( + st => st.startLocation < current.index && current.index < st.endLocation)) + { immutable l2 = indents.indentToMostRecent(tok!"{"); assert(l2 != -1, "Recent '{' is not found despite being in struct initializer"); indentLevel = l2 + 1; @@ -1823,13 +1825,12 @@ private: else if (currentIs(tok!"case") || currentIs(tok!"default")) { - if (peekBackIs(tok!"}", true) || peekBackIs(tok!";", true) - /** + if (peekBackIs(tok!"}", true) || peekBackIs(tok!";", true) /** * The following code is valid and should be indented flatly * case A: * case B: */ - || peekBackIs(tok!":", true)) + || peekBackIs(tok!":", true)) { indents.popTempIndents(); if (indents.topIs(tok!"case")) @@ -1869,12 +1870,13 @@ private: { indents.pop(); } - else while (sBraceDepth == 0 && indents.topIsTemp() - && ((!indents.topIsOneOf(tok!"else", tok!"if", - tok!"static", tok!"version")) || !peekIs(tok!"else"))) - { - indents.pop(); - } + else + while (sBraceDepth == 0 && indents.topIsTemp() + && ((!indents.topIsOneOf(tok!"else", tok!"if", + tok!"static", tok!"version")) || !peekIs(tok!"else"))) + { + indents.pop(); + } } else if (currentIs(tok!"]")) { @@ -1887,12 +1889,14 @@ private: // "foreach" without removing them from the stack, since they // still can be used later to indent "else". auto savedIndents = IndentStack(config); - while (indents.length >= 0 && indents.topIsTemp) { + while (indents.length >= 0 && indents.topIsTemp) + { savedIndents.push(indents.top, indents.topDetails); indents.pop; } indentLevel = indents.indentLevel; - while (savedIndents.length > 0) { + while (savedIndents.length > 0) + { indents.push(savedIndents.top, savedIndents.topDetails); savedIndents.pop; } @@ -1936,10 +1940,10 @@ private: void writeToken() { - import std.range:retro; - import std.algorithm.searching:countUntil; - import std.algorithm.iteration:joiner; - import std.string:lineSplitter; + import std.range : retro; + import std.algorithm.searching : countUntil; + import std.algorithm.iteration : joiner; + import std.string : lineSplitter; if (current.text is null) { @@ -1956,9 +1960,12 @@ private: case tok!"wstringLiteral": case tok!"dstringLiteral": immutable o = current.text.retro().countUntil('\n'); - if (o == -1) { + if (o == -1) + { currentLineLength += current.text.length; - } else { + } + else + { currentLineLength = cast(uint) o; } break; @@ -2287,8 +2294,8 @@ const pure @safe @nogc: bool peekBackIsFunctionDeclarationEnding() nothrow { return peekBackIsOneOf(false, tok!")", tok!"const", tok!"immutable", - tok!"inout", tok!"shared", tok!"@", tok!"pure", tok!"nothrow", - tok!"return", tok!"scope"); + tok!"inout", tok!"shared", tok!"@", tok!"pure", + tok!"nothrow", tok!"return", tok!"scope"); } bool peekBackIsSlashSlash() nothrow @@ -2318,7 +2325,7 @@ const pure @safe @nogc: // multi-line string), we can sum the number of the newlines in the // token and tokens[index - 1].line, the start line. const previousTokenEndLineNo = tokens[index - 1].line - + tokens[index - 1].text.representation.count('\n'); + + tokens[index - 1].text.representation.count('\n'); return previousTokenEndLineNo < tokens[index].line; } @@ -2343,9 +2350,9 @@ const pure @safe @nogc: bool isBlockHeaderToken(const IdType t) { return t == tok!"for" || t == tok!"foreach" || t == tok!"foreach_reverse" - || t == tok!"while" || t == tok!"if" || t == tok!"in"|| t == tok!"out" - || t == tok!"do" || t == tok!"catch" || t == tok!"with" - || t == tok!"synchronized" || t == tok!"scope" || t == tok!"debug"; + || t == tok!"while" || t == tok!"if" || t == tok!"in" + || t == tok!"out" || t == tok!"do" || t == tok!"catch" + || t == tok!"with" || t == tok!"synchronized" || t == tok!"scope" || t == tok!"debug"; } bool isBlockHeader(int i = 0) nothrow @@ -2357,11 +2364,11 @@ const pure @safe @nogc: if (i + index + 3 < tokens.length) { - isExpressionContract = (t == tok!"in" && peekImplementation(tok!"(", i + 1, true)) - || (t == tok!"out" && (peekImplementation(tok!"(", i + 1, true) - && (peekImplementation(tok!";", i + 2, true) + isExpressionContract = (t == tok!"in" && peekImplementation(tok!"(", i + 1, true)) || (t == tok!"out" + && (peekImplementation(tok!"(", i + 1, true) + && (peekImplementation(tok!";", i + 2, true) || (peekImplementation(tok!"identifier", i + 2, true) - && peekImplementation(tok!";", i + 3, true))))); + && peekImplementation(tok!";", i + 3, true))))); } return isBlockHeaderToken(t) && !isExpressionContract; @@ -2389,8 +2396,8 @@ Token[] insertForcedBraces(const(Token)[] tokens) // We'll insert `{` before the token at `index`, or `}` after the token at `index-1`. struct Insertion { - size_t position; // insert BEFORE this token index - bool isOpen; // true = `{`, false = `}` + size_t position; // insert BEFORE this token index + bool isOpen; // true = `{`, false = `}` } Appender!(Insertion[]) insertions; @@ -2399,10 +2406,8 @@ Token[] insertForcedBraces(const(Token)[] tokens) static bool isParenBlockHeader(IdType t) pure nothrow @safe @nogc { return t == tok!"if" || t == tok!"for" || t == tok!"foreach" - || t == tok!"foreach_reverse" || t == tok!"while" - || t == tok!"catch" || t == tok!"with" - || t == tok!"synchronized" || t == tok!"scope" - || t == tok!"version"; + || t == tok!"foreach_reverse" || t == tok!"while" || t == tok!"catch" + || t == tok!"with" || t == tok!"synchronized" || t == tok!"scope" || t == tok!"version"; } // Skip past comments at position i, return new position @@ -2413,6 +2418,25 @@ Token[] insertForcedBraces(const(Token)[] tokens) return i; } + // Include trailing comments that are on the same line as the previous token. + // Returns the position after any same-line comments. + size_t includeTrailingComments(size_t i) nothrow @safe @nogc + { + if (i == 0 || i > tokens.length) + return i; + + // Get the line number of the previous token (likely the ;) + size_t prevLine = tokens[i - 1].line; + + // Check if next token is a comment on the same line + while (i < tokens.length && tokens[i].type == tok!"comment" && tokens[i].line == prevLine) + { + i++; + } + + return i; + } + // Skip past matched parentheses starting at open paren at position i. // Returns position AFTER the closing paren, or tokens.length on error. size_t skipParens(size_t i) nothrow @safe @nogc @@ -2578,8 +2602,7 @@ Token[] insertForcedBraces(const(Token)[] tokens) int depth = 0; while (i < tokens.length) { - if (tokens[i].type == tok!"(" || tokens[i].type == tok!"[" - || tokens[i].type == tok!"{") + if (tokens[i].type == tok!"(" || tokens[i].type == tok!"[" || tokens[i].type == tok!"{") depth++; else if (tokens[i].type == tok!")" || tokens[i].type == tok!"]" || tokens[i].type == tok!"}") @@ -2591,7 +2614,7 @@ Token[] insertForcedBraces(const(Token)[] tokens) else if (tokens[i].type == tok!";" && depth == 0) { i++; // include the semicolon - return i; + return includeTrailingComments(i); } i++; } @@ -2629,8 +2652,8 @@ Token[] insertForcedBraces(const(Token)[] tokens) // Actually, for nested if: `if (a) if (b) x; else y; else z;` // the first else belongs to the inner if. We need to include it. // So we use findStatementEnd which handles if+else as a unit. - if (isParenBlockHeader(t) || t == tok!"debug" || t == tok!"do" || t == tok!"else" - || t == tok!"try" || t == tok!"finally") + if (isParenBlockHeader(t) || t == tok!"debug" || t == tok!"do" + || t == tok!"else" || t == tok!"try" || t == tok!"finally") { return findStatementEnd(i); } @@ -2639,8 +2662,7 @@ Token[] insertForcedBraces(const(Token)[] tokens) int depth = 0; while (i < tokens.length) { - if (tokens[i].type == tok!"(" || tokens[i].type == tok!"[" - || tokens[i].type == tok!"{") + if (tokens[i].type == tok!"(" || tokens[i].type == tok!"[" || tokens[i].type == tok!"{") depth++; else if (tokens[i].type == tok!")" || tokens[i].type == tok!"]" || tokens[i].type == tok!"}") @@ -2652,7 +2674,7 @@ Token[] insertForcedBraces(const(Token)[] tokens) else if (tokens[i].type == tok!";" && depth == 0) { i++; - return i; + return includeTrailingComments(i); } i++; } @@ -2674,16 +2696,15 @@ Token[] insertForcedBraces(const(Token)[] tokens) return; // For certain keywords after function declarations, don't insert braces - if (tokens[bodyStart].type == tok!"in" || tokens[bodyStart].type == tok!"out" - || tokens[bodyStart].type == tok!"do") + if (tokens[bodyStart].type == tok!"in" + || tokens[bodyStart].type == tok!"out" || tokens[bodyStart].type == tok!"do") return; if (bodyStart < tokens.length && tokens[bodyStart].text == "body") return; // For `if`/`debug`/`version` with `else`: wrap only the "then" body. // The main loop will handle `else` when it encounters it. - if (headerType == tok!"if" || headerType == tok!"debug" - || headerType == tok!"version") + if (headerType == tok!"if" || headerType == tok!"debug" || headerType == tok!"version") { size_t thenEnd = findThenBodyEnd(bodyStart); insertions ~= Insertion(bodyStart, true); @@ -2817,11 +2838,10 @@ Token[] insertForcedBraces(const(Token)[] tokens) { auto nextT = tokens[afterElse].type; // else if / else version / else static if — skip, let inner handle it - if (nextT == tok!"if" || nextT == tok!"version" - || (nextT == tok!"static" && afterElse + 1 < tokens.length - && (tokens[afterElse + 1].type == tok!"if" - || tokens[afterElse + 1].type == tok!"foreach" - || tokens[afterElse + 1].type == tok!"foreach_reverse"))) + if (nextT == tok!"if" || nextT == tok!"version" || (nextT == tok!"static" + && afterElse + 1 < tokens.length && (tokens[afterElse + 1].type == tok!"if" + || tokens[afterElse + 1].type == tok!"foreach" + || tokens[afterElse + 1].type == tok!"foreach_reverse"))) { i++; continue; @@ -2850,11 +2870,11 @@ Token[] insertForcedBraces(const(Token)[] tokens) // Sort insertions by position (stable sort: opens before closes at same position) import std.algorithm : sort; + auto ins = insertions.data; sort!((a, b) { if (a.position != b.position) - return a.position < b.position; - // At the same position, open brace comes before close brace + return a.position < b.position; // At the same position, open brace comes before close brace // (shouldn't normally happen, but handle gracefully) return a.isOpen && !b.isOpen; })(ins); @@ -2875,10 +2895,20 @@ Token[] insertForcedBraces(const(Token)[] tokens) // that won't match any AST info lookups synth.index = size_t.max - insIdx; // Copy line/column from the nearest real token for reasonable error messages + // For closing braces, use the previous token's line to avoid blank lines + // appearing before the brace (issue with double newlines) if (ti < tokens.length) { - synth.line = tokens[ti].line; - synth.column = tokens[ti].column; + if (!ins[insIdx].isOpen && ti > 0) + { + synth.line = tokens[ti - 1].line; + synth.column = tokens[ti - 1].column; + } + else + { + synth.line = tokens[ti].line; + synth.column = tokens[ti].column; + } } else if (tokens.length > 0) { @@ -2898,6 +2928,7 @@ Token[] insertForcedBraces(const(Token)[] tokens) bool canFindIndex(const size_t[] items, size_t index, size_t* pos = null) pure @safe @nogc { import std.range : assumeSorted; + if (!pos) { return !assumeSorted(items).equalRange(index).empty; diff --git a/tests/allman/force_curly_braces.d.ref b/tests/allman/force_curly_braces.d.ref index 0231d7e..dbdd587 100644 --- a/tests/allman/force_curly_braces.d.ref +++ b/tests/allman/force_curly_braces.d.ref @@ -293,3 +293,35 @@ void test24() } } } + +// 25. version + unittest +void test25() +{ + string command; + version (Posix) + { + command ~= " 2> /dev/null 1> /dev/null"; + } + + version (Posix) + { + command ~= " 2> /dev/null 1> /dev/null"; + } + + unittest + { + version (Posix) + { + command ~= " 2> /dev/null 1> /dev/null"; + } + } +} + +// 26. Trailing comments on if statement +unittest +{ + if (imin.value > 0x10FFFFUL) // One + { + imin.value = 0x10FFFFUL; // Two + } +} diff --git a/tests/force_curly_braces.d b/tests/force_curly_braces.d index db09411..c93e6bf 100644 --- a/tests/force_curly_braces.d +++ b/tests/force_curly_braces.d @@ -2,14 +2,16 @@ // Tests various braceless constructs that should get braces added // 1. Simple if without braces -int test1(int x) { +int test1(int x) +{ if (x > 0) return 1; return 0; } // 2. if/else without braces -int test2(int x) { +int test2(int x) +{ if (x > 0) return 1; else @@ -17,14 +19,16 @@ int test2(int x) { } // 3. Nested if without braces -void test3(int a, int b) { +void test3(int a, int b) +{ if (a) if (b) doSomething(); } // 4. if/else if/else chain -int test4(int x) { +int test4(int x) +{ if (x > 0) return 1; else if (x < 0) @@ -34,143 +38,172 @@ int test4(int x) { } // 5. for loop without braces -void test5() { +void test5() +{ for (int i = 0; i < 10; i++) doSomething(); } // 6. foreach without braces -void test6(int[] arr) { +void test6(int[] arr) +{ foreach (x; arr) doSomething(); } // 7. while without braces -void test7() { +void test7() +{ while (condition()) doSomething(); } // 8. Mixed: if braced, else not -int test8(int x) { - if (x > 0) { +int test8(int x) +{ + if (x > 0) + { return 1; - } else + } + else return 0; } // 9. Mixed: if not braced, else braced -int test9(int x) { +int test9(int x) +{ if (x > 0) return 1; - else { + else + { return 0; } } // 10. Already fully braced (should be unchanged) -int test10(int x) { - if (x > 0) { +int test10(int x) +{ + if (x > 0) + { return 1; - } else { + } + else + { return 0; } } // 11. Nested mixed: if with for body -void test11(int[] arr) { +void test11(int[] arr) +{ if (arr.length > 0) foreach (x; arr) doSomething(); } // 12. while with if body -void test12() { +void test12() +{ while (condition()) if (check()) doSomething(); } // 13. do-while without braces -void test13() { +void test13() +{ do doSomething(); while (condition()); } // 14. scope(exit) without braces -void test14() { - scope(exit) +void test14() +{ + scope (exit) cleanup(); doWork(); scope int[] array = new int[](127); - scope T!() x = 5; + scope T!() x = 5; } // 15. synchronized without braces -void test15() { - synchronized - doSomething(); +void test15() +{ + synchronized doSomething(); } // 16. debug without parens -void test16() { - debug - doSomething(); +void test16() +{ + debug doSomething(); } // 17. version/else -void test17() { - version(Windows) +void test17() +{ + version (Windows) doWindows(); else doPosix(); } // 18. with statement -void test18() { +void test18() +{ with (someObj) doSomething(); } // 19. catch without braces -void test19() { - try { +void test19() +{ + try + { doSomething(); - } catch (Exception e) + } + catch (Exception e) handleError(); } // 20. debug with else -void test20() { - debug - doDebug(); - else - doRelease(); +void test20() +{ + debug doDebug(); + else doRelease(); } // 21. finally without braces -void test21() { - try { +void test21() +{ + try + { doSomething(); - } catch (Exception e) { + } + catch (Exception e) + { handleError(); - } finally + } + finally cleanup(); } // 22. try/catch/finally all braceless as body of if -void test22(bool flag) { +void test22(bool flag) +{ if (flag) - try { + try + { doSomething(); - } catch (Exception e) + } + catch (Exception e) handleError(); } // 23. dfmt off region — braceless code should NOT get braces -void test23() { +void test23() +{ // dfmt off if (x) doSomething(); @@ -180,23 +213,34 @@ void test23() { } // 24. else debug without parens -void test24() { - version(Windows) +void test24() +{ + version (Windows) doWindows(); else - debug - doDebugPosix(); + debug doDebugPosix(); } // 25. version + unittest -void test25() { - string command; - version (Posix) command ~= " 2> /dev/null 1> /dev/null"; - - version (Posix) command ~= " 2> /dev/null 1> /dev/null"; +void test25() +{ + string command; + version (Posix) + command ~= " 2> /dev/null 1> /dev/null"; + + version (Posix) + command ~= " 2> /dev/null 1> /dev/null"; + + unittest + { + version (Posix) + command ~= " 2> /dev/null 1> /dev/null"; + } +} - unittest - { - version (Posix) command ~= " 2> /dev/null 1> /dev/null"; - } +// 26. Trailing comments on if statement +unittest +{ + if (imin.value > 0x10FFFFUL) // One + imin.value = 0x10FFFFUL; // Two } diff --git a/tests/knr/force_curly_braces.d.ref b/tests/knr/force_curly_braces.d.ref index b3df10e..8238e68 100644 --- a/tests/knr/force_curly_braces.d.ref +++ b/tests/knr/force_curly_braces.d.ref @@ -246,12 +246,22 @@ void test25() version (Posix) { command ~= " 2> /dev/null 1> /dev/null"; } + version (Posix) { command ~= " 2> /dev/null 1> /dev/null"; } + unittest { version (Posix) { command ~= " 2> /dev/null 1> /dev/null"; } } } + +// 26. Trailing comments on if statement +unittest { + if (imin.value > 0x10FFFFUL) // One + { + imin.value = 0x10FFFFUL; // Two + } +} diff --git a/tests/otbs/force_curly_braces.d.ref b/tests/otbs/force_curly_braces.d.ref index b9b9614..471c6e9 100644 --- a/tests/otbs/force_curly_braces.d.ref +++ b/tests/otbs/force_curly_braces.d.ref @@ -221,12 +221,22 @@ void test25() { version (Posix) { command ~= " 2> /dev/null 1> /dev/null"; } + version (Posix) { command ~= " 2> /dev/null 1> /dev/null"; } + unittest { version (Posix) { command ~= " 2> /dev/null 1> /dev/null"; } } } + +// 26. Trailing comments on if statement +unittest { + if (imin.value > 0x10FFFFUL) // One + { + imin.value = 0x10FFFFUL; // Two + } +} From 3f429d7914e9bfa2d8c265128aaac33adb0dec6b Mon Sep 17 00:00:00 2001 From: Robert burner Schadek Date: Thu, 5 Mar 2026 17:03:43 +0100 Subject: [PATCH 5/6] Fix indentation of 'else' after 'debug' conditional When a 'debug' conditional is followed by an 'else' clause, the 'else' was incorrectly indented one level too deep. This was caused by: 1. 'debug' was being popped from the indent stack too early (after closing brace) 2. 'debug' was not being preserved when there's a following 'else' 3. 'debug' was not considered when aligning 'else' with its parent construct 4. 'debug' was not popped before pushing 'else' for non-braced bodies The fix adds 'debug' to the same handling as 'if' and 'version': - Check for 'debug' when aligning 'else' indentation (line 1800) - Don't pop 'debug' after closing brace (line 943) - Keep 'debug' on stack when there's a following 'else' (line 1877) - Pop 'debug' before pushing 'else' for non-braced bodies (line 1266) Fixes issue where complex chains like: version (Windows) { ... } else { debug { ... } else { ... } } Would incorrectly format as: else { debug { ... } else { ... } } // wrong indentation Instead of: else { debug { ... } else { ... } } // correct indentation --- src/dfmt/formatter.d | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/dfmt/formatter.d b/src/dfmt/formatter.d index fc0ebb5..48e65e4 100644 --- a/src/dfmt/formatter.d +++ b/src/dfmt/formatter.d @@ -383,7 +383,7 @@ private: || isNumberLiteral(tokens[index].type) || (inAsm && peekBack2Is(tok!";") && currentIs(tok!"[")) ))) - //dfmt on + //dfmt on { write(" "); } @@ -939,8 +939,7 @@ private: writeToken(); indents.popWrapIndents(); linebreakHints = []; - while (indents.topIsOneOf(tok!"enum", tok!"try", tok!"catch", - tok!"finally", tok!"debug")) + while (indents.topIsOneOf(tok!"enum", tok!"try", tok!"catch", tok!"finally")) indents.pop(); if (indents.topAre(tok!"static", tok!"else")) { @@ -1264,7 +1263,7 @@ private: //indents.dump(); while (indents.topIsOneOf(tok!"foreach", tok!"for", tok!"while")) indents.pop(); - if (indents.topIsOneOf(tok!"if", tok!"version")) + if (indents.topIsOneOf(tok!"if", tok!"version", tok!"debug")) indents.pop(); indents.push(tok!"else"); newline(); @@ -1793,9 +1792,11 @@ private: && astInformation.namedArgumentColonLocations.canFindIndex(tokens[index + 1].index); if (currentIs(tok!"else")) { + // Align 'else' with the corresponding conditional construct (if/version/debug) immutable i = indents.indentToMostRecent(tok!"if"); immutable v = indents.indentToMostRecent(tok!"version"); - immutable mostRecent = max(i, v); + immutable d = indents.indentToMostRecent(tok!"debug"); + immutable mostRecent = max(i, v, d); if (mostRecent != -1) indentLevel = mostRecent; } @@ -1873,7 +1874,7 @@ private: else while (sBraceDepth == 0 && indents.topIsTemp() && ((!indents.topIsOneOf(tok!"else", tok!"if", - tok!"static", tok!"version")) || !peekIs(tok!"else"))) + tok!"static", tok!"version", tok!"debug")) || !peekIs(tok!"else"))) { indents.pop(); } From 61bb9e6fb9f3f4fddc4063cf7d5f0be317c704bc Mon Sep 17 00:00:00 2001 From: Robert burner Schadek Date: Thu, 5 Mar 2026 17:04:14 +0100 Subject: [PATCH 6/6] Add tests for 'else' after 'debug' conditional indentation Added comprehensive test coverage for the bug fix: 1. Test 27 in force_curly_braces.d: - Complex version/else/debug/else chain - Tests nested conditionals with multiple else clauses 2. New test file issue_else_after_debug.d with edge cases: - Simple debug with else - Nested version with debug/else - Nested debug inside if/else with another else - Static if with debug/else - Multiple nested levels - Debug with parens and else - Deep nesting: version/debug/else chain All tests verify correct indentation alignment of 'else' with its parent conditional construct (if/version/debug) across all three brace styles (allman, otbs, knr). --- tests/allman/force_curly_braces.d.ref | 24 ++++ tests/allman/issue_else_after_debug.d.ref | 132 ++++++++++++++++++++++ tests/force_curly_braces.d | 12 ++ tests/issue_else_after_debug.args | 1 + tests/issue_else_after_debug.d | 70 ++++++++++++ tests/knr/force_curly_braces.d.ref | 15 +++ tests/knr/issue_else_after_debug.d.ref | 87 ++++++++++++++ tests/otbs/force_curly_braces.d.ref | 15 +++ tests/otbs/issue_else_after_debug.d.ref | 87 ++++++++++++++ 9 files changed, 443 insertions(+) create mode 100644 tests/allman/issue_else_after_debug.d.ref create mode 100644 tests/issue_else_after_debug.args create mode 100644 tests/issue_else_after_debug.d create mode 100644 tests/knr/issue_else_after_debug.d.ref create mode 100644 tests/otbs/issue_else_after_debug.d.ref diff --git a/tests/allman/force_curly_braces.d.ref b/tests/allman/force_curly_braces.d.ref index dbdd587..520ec3c 100644 --- a/tests/allman/force_curly_braces.d.ref +++ b/tests/allman/force_curly_braces.d.ref @@ -325,3 +325,27 @@ unittest imin.value = 0x10FFFFUL; // Two } } + +// 27. Complex version/else/debug/else chain +unittest +{ + version (Windows) + { + __locale_decpoint = save; + } + else version (Foo) + { + __locale_decpoint = save; + } + else + { + debug + { + __x = 3; + } + else + { + foobar(); + } + } +} diff --git a/tests/allman/issue_else_after_debug.d.ref b/tests/allman/issue_else_after_debug.d.ref new file mode 100644 index 0000000..f06e70c --- /dev/null +++ b/tests/allman/issue_else_after_debug.d.ref @@ -0,0 +1,132 @@ +// Test: else after debug indentation +// Tests various combinations of version/debug/else chains + +// 1. Simple debug with else +unittest +{ + debug + { + x = 1; + } + else + { + y = 2; + } +} + +// 2. Nested: version with debug/else +unittest +{ + version (Windows) + { + doWindows(); + } + else + { + debug + { + doDebug(); + } + else + { + doRelease(); + } + } +} + +// 3. Nested: debug inside else with another else +unittest +{ + if (condition) + { + doSomething(); + } + else + { + debug + { + doDebug(); + } + else + { + doRelease(); + } + } +} + +// 4. Static if with debug/else +unittest +{ + static if (enabled) + { + doEnabled(); + } + else + { + debug + { + doDebug(); + } + else + { + doRelease(); + } + } +} + +// 5. Multiple nested levels +unittest +{ + version (Windows) + { + doWindows(); + } + else version (Linux) + { + debug + { + doLinuxDebug(); + } + else + { + doLinuxRelease(); + } + } +} + +// 6. Debug with parens and else +unittest +{ + debug (feature) + { + doFeature(); + } + else + { + doNormal(); + } +} + +// 7. Deep nesting: version/debug/else chain +unittest +{ + version (A) + { + doA(); + } + else version (B) + { + doB(); + } + else + { + debug + { + doDebug(); + } + else + { + doRelease(); + } + } +} diff --git a/tests/force_curly_braces.d b/tests/force_curly_braces.d index c93e6bf..faf031e 100644 --- a/tests/force_curly_braces.d +++ b/tests/force_curly_braces.d @@ -244,3 +244,15 @@ unittest if (imin.value > 0x10FFFFUL) // One imin.value = 0x10FFFFUL; // Two } + +// 27. Complex version/else/debug/else chain +unittest +{ + version (Windows) + __locale_decpoint = save; + else version (Foo) + __locale_decpoint = save; + else + debug __x = 3; + else foobar(); +} diff --git a/tests/issue_else_after_debug.args b/tests/issue_else_after_debug.args new file mode 100644 index 0000000..b7491d7 --- /dev/null +++ b/tests/issue_else_after_debug.args @@ -0,0 +1 @@ +--force_curly_braces=true diff --git a/tests/issue_else_after_debug.d b/tests/issue_else_after_debug.d new file mode 100644 index 0000000..c87e27c --- /dev/null +++ b/tests/issue_else_after_debug.d @@ -0,0 +1,70 @@ +// Test: else after debug indentation +// Tests various combinations of version/debug/else chains + +// 1. Simple debug with else +unittest +{ + debug x = 1; + else y = 2; +} + +// 2. Nested: version with debug/else +unittest +{ + version (Windows) + doWindows(); + else + debug doDebug(); + else doRelease(); +} + +// 3. Nested: debug inside else with another else +unittest +{ + if (condition) + doSomething(); + else + debug doDebug(); + else doRelease(); +} + +// 4. Static if with debug/else +unittest +{ + static if (enabled) + doEnabled(); + else + debug doDebug(); + else doRelease(); +} + +// 5. Multiple nested levels +unittest +{ + version (Windows) + doWindows(); + else version (Linux) + debug doLinuxDebug(); + else doLinuxRelease(); +} + +// 6. Debug with parens and else +unittest +{ + debug (feature) + doFeature(); + else + doNormal(); +} + +// 7. Deep nesting: version/debug/else chain +unittest +{ + version (A) + doA(); + else version (B) + doB(); + else + debug doDebug(); + else doRelease(); +} diff --git a/tests/knr/force_curly_braces.d.ref b/tests/knr/force_curly_braces.d.ref index 8238e68..1ced4bd 100644 --- a/tests/knr/force_curly_braces.d.ref +++ b/tests/knr/force_curly_braces.d.ref @@ -265,3 +265,18 @@ unittest { imin.value = 0x10FFFFUL; // Two } } + +// 27. Complex version/else/debug/else chain +unittest { + version (Windows) { + __locale_decpoint = save; + } else version (Foo) { + __locale_decpoint = save; + } else { + debug { + __x = 3; + } else { + foobar(); + } + } +} diff --git a/tests/knr/issue_else_after_debug.d.ref b/tests/knr/issue_else_after_debug.d.ref new file mode 100644 index 0000000..18a473d --- /dev/null +++ b/tests/knr/issue_else_after_debug.d.ref @@ -0,0 +1,87 @@ +// Test: else after debug indentation +// Tests various combinations of version/debug/else chains + +// 1. Simple debug with else +unittest { + debug { + x = 1; + } else { + y = 2; + } +} + +// 2. Nested: version with debug/else +unittest { + version (Windows) { + doWindows(); + } else { + debug { + doDebug(); + } else { + doRelease(); + } + } +} + +// 3. Nested: debug inside else with another else +unittest { + if (condition) { + doSomething(); + } else { + debug { + doDebug(); + } else { + doRelease(); + } + } +} + +// 4. Static if with debug/else +unittest { + static if (enabled) { + doEnabled(); + } else { + debug { + doDebug(); + } else { + doRelease(); + } + } +} + +// 5. Multiple nested levels +unittest { + version (Windows) { + doWindows(); + } else version (Linux) { + debug { + doLinuxDebug(); + } else { + doLinuxRelease(); + } + } +} + +// 6. Debug with parens and else +unittest { + debug (feature) { + doFeature(); + } else { + doNormal(); + } +} + +// 7. Deep nesting: version/debug/else chain +unittest { + version (A) { + doA(); + } else version (B) { + doB(); + } else { + debug { + doDebug(); + } else { + doRelease(); + } + } +} diff --git a/tests/otbs/force_curly_braces.d.ref b/tests/otbs/force_curly_braces.d.ref index 471c6e9..74c12c2 100644 --- a/tests/otbs/force_curly_braces.d.ref +++ b/tests/otbs/force_curly_braces.d.ref @@ -240,3 +240,18 @@ unittest { imin.value = 0x10FFFFUL; // Two } } + +// 27. Complex version/else/debug/else chain +unittest { + version (Windows) { + __locale_decpoint = save; + } else version (Foo) { + __locale_decpoint = save; + } else { + debug { + __x = 3; + } else { + foobar(); + } + } +} diff --git a/tests/otbs/issue_else_after_debug.d.ref b/tests/otbs/issue_else_after_debug.d.ref new file mode 100644 index 0000000..18a473d --- /dev/null +++ b/tests/otbs/issue_else_after_debug.d.ref @@ -0,0 +1,87 @@ +// Test: else after debug indentation +// Tests various combinations of version/debug/else chains + +// 1. Simple debug with else +unittest { + debug { + x = 1; + } else { + y = 2; + } +} + +// 2. Nested: version with debug/else +unittest { + version (Windows) { + doWindows(); + } else { + debug { + doDebug(); + } else { + doRelease(); + } + } +} + +// 3. Nested: debug inside else with another else +unittest { + if (condition) { + doSomething(); + } else { + debug { + doDebug(); + } else { + doRelease(); + } + } +} + +// 4. Static if with debug/else +unittest { + static if (enabled) { + doEnabled(); + } else { + debug { + doDebug(); + } else { + doRelease(); + } + } +} + +// 5. Multiple nested levels +unittest { + version (Windows) { + doWindows(); + } else version (Linux) { + debug { + doLinuxDebug(); + } else { + doLinuxRelease(); + } + } +} + +// 6. Debug with parens and else +unittest { + debug (feature) { + doFeature(); + } else { + doNormal(); + } +} + +// 7. Deep nesting: version/debug/else chain +unittest { + version (A) { + doA(); + } else version (B) { + doB(); + } else { + debug { + doDebug(); + } else { + doRelease(); + } + } +}