From 8a60140c9a0df7f1d05c294e6032e6e3746eda29 Mon Sep 17 00:00:00 2001 From: Carsten Scholling Date: Tue, 28 Jul 2026 09:21:17 +0200 Subject: [PATCH 1/3] feat(FC0007): add "Statement blocks should be separated by a blank line" (opt-in) Introduce FC0007, a disabled-by-default formatting rule that enforces blank-line separation around multi-line control-flow blocks (if, case, repeat, while, for, foreach), before scope-leaving statements (exit, built-in Error), and optionally before the else keyword of if/end-else constructs. Configurable via alcops.json under a new StatementBlockSpacing object with five properties: ControlFlowBefore, ControlFlowAfter, ScopeLeavingMode, ElseChainBeforeMode, and OneLinerMode. No CodeFix. --- .../docs/analyzers/FormattingCop/FC0007.md | 154 ++++++++++++++++++ .../docs/analyzers/FormattingCop/_index.md | 1 + 2 files changed, 155 insertions(+) create mode 100644 content/docs/analyzers/FormattingCop/FC0007.md diff --git a/content/docs/analyzers/FormattingCop/FC0007.md b/content/docs/analyzers/FormattingCop/FC0007.md new file mode 100644 index 0000000..b588d49 --- /dev/null +++ b/content/docs/analyzers/FormattingCop/FC0007.md @@ -0,0 +1,154 @@ ++++ +title = 'Statement blocks should be separated by a blank line' +linkTitle = 'FC0007' + +[params] + id = 'FC0007' + severity = 'Info' + category = 'Style' + codeAction = false + ignoreObsolete = true ++++ + +Multi-line control-flow blocks (`if`, `case`, `repeat`, `while`, `for`, `foreach`) and scope-leaving statements (`exit`, built-in `Error(...)`) read more clearly when they are visually separated from surrounding code by a blank line. The rule reports the exact spot where a separator is missing so it can be inserted with a single keystroke, without touching the logic. + +FC0007 is **disabled by default** and fully configurable. Enable it in your project's `.ruleset.json` (or via `AL: Configure ruleset`) and, optionally, tune the individual checks in `alcops.json`. + +### Example + +The following procedure runs several `if`-blocks and an `exit` back-to-back without visual separation: + +{{< highlight al "hl_lines=6 10 12 14" >}} +codeunit 50100 "Sales Post" +{ + procedure Post(var SalesHeader: Record "Sales Header"): Boolean + begin + SalesHeader.TestField("Sell-to Customer No."); + if SalesHeader.Status <> SalesHeader.Status::Released then begin // Insert a blank line before the block [FC0007] + SalesHeader.Validate(Status, SalesHeader.Status::Released); + SalesHeader.Modify(true); + end; + if not SalesHeader.Find() then // Insert a blank line after the block [FC0007] + exit(false); + if GuiAllowed then // Insert a blank line before the block [FC0007] + Message('Posted %1', SalesHeader."No."); + exit(true); // Insert a blank line before scope-leaving statement [FC0007] + end; +} +{{< /highlight >}} + +Add blank lines so each block stands on its own: + +{{< highlight al "linenostart=1" >}} +codeunit 50100 "Sales Post" +{ + procedure Post(var SalesHeader: Record "Sales Header"): Boolean + begin + SalesHeader.TestField("Sell-to Customer No."); + + if SalesHeader.Status <> SalesHeader.Status::Released then begin + SalesHeader.Validate(Status, SalesHeader.Status::Released); + SalesHeader.Modify(true); + end; + + if not SalesHeader.Find() then + exit(false); + + if GuiAllowed then + Message('Posted %1', SalesHeader."No."); + + exit(true); + end; +} +{{< /highlight >}} + +### Checks performed + +The rule runs four independent checks. Each check can be toggled or narrowed via `alcops.json`. + +| Check | What triggers | Setting | +|---|---|---| +| Blank line **before** a control-flow block | A multi-line `if`, `case`, `repeat`, `while`, `for`, or `foreach` immediately follows another statement in the same block. | `ControlFlowBefore` | +| Blank line **after** a control-flow block | A non-control-flow statement immediately follows the closing `end` (or `until`) of a multi-line control-flow block. | `ControlFlowAfter` | +| Blank line before a **scope-leaving** statement | `exit`, built-in `Error(...)`, or both are preceded by another statement on the immediately preceding line. | `ScopeLeavingMode` | +| Blank line before **`else`** in an `if ... end else` construct | The closing `end` and the `else` keyword sit on adjacent lines instead of being separated by a blank line. Opt-in. | `ElseChainBeforeMode` | + +Single-line control-flow statements (e.g. `if Flag then Message('x');` on one line) are excluded from the *before* / *after* checks by default. Include them with `OneLinerMode`. + +### Exception + +- The symbol is obsolete +- Adjacent case branches inside a `case` statement (the check operates on the surrounding block, not on branches) +- Any statement immediately after `begin` or immediately before `end` (the block braces already act as separators) + +### Configuration + +All settings live under a single `StatementBlockSpacing` object in `alcops.json`. Any omitted property keeps its default. + +```json +{ + "StatementBlockSpacing": { + "ControlFlowBefore": true, + "ControlFlowAfter": true, + "ScopeLeavingMode": "ExitAndError", + "ElseChainBeforeMode": "Off", + "OneLinerMode": "None" + } +} +``` + +| Property | Type | Default | Values | Purpose | +|---|---|---|---|---| +| `ControlFlowBefore` | boolean | `true` | `true` / `false` | Require a blank line before a multi-line control-flow block when it follows another statement in the same block. | +| `ControlFlowAfter` | boolean | `true` | `true` / `false` | Require a blank line after a multi-line control-flow block when a non-control-flow statement follows it directly. | +| `ScopeLeavingMode` | string | `"ExitAndError"` | `"Off"`, `"ExitOnly"`, `"ErrorOnly"`, `"ExitAndError"` | Which scope-leaving statements require a preceding blank line. | +| `ElseChainBeforeMode` | string | `"Off"` | `"Off"`, `"RequireBlank"` | Whether the `else` keyword of an `if ... end else` construct must sit on its own after a blank line. | +| `OneLinerMode` | string | `"None"` | `"None"`, `"All"` | Whether single-line control-flow statements participate in the `ControlFlowBefore` / `ControlFlowAfter` checks. | + +Enum values are matched case-insensitively (`"off"`, `"OFF"`, and `"Off"` are equivalent). Unknown values fall back to the defaults silently. + +#### Enabling only what you need + +Turn every check off and re-enable individual ones: + +```json +{ + "StatementBlockSpacing": { + "ControlFlowBefore": true, + "ControlFlowAfter": false, + "ScopeLeavingMode": "ErrorOnly", + "ElseChainBeforeMode": "Off", + "OneLinerMode": "None" + } +} +``` + +With this configuration the rule flags only the space before a multi-line block and the space before an `Error(...)` call; `exit` statements, trailing blanks and one-liners are ignored. + +#### Enforcing blank line before `else` + +Some teams prefer the `if ... end else` construct to break visually before `else`: + +```json +{ + "StatementBlockSpacing": { + "ElseChainBeforeMode": "RequireBlank" + } +} +``` + +The check runs only when the whole construct spans multiple lines (single-line `if ... else` statements are unaffected). + +#### Including one-liners + +`OneLinerMode = "All"` extends `ControlFlowBefore` / `ControlFlowAfter` to control-flow statements written entirely on one source line, so this snippet gets flagged too: + +```al +Foo(); +if Flag then Message('Bar'); // Insert a blank line before the block [FC0007] +Baz(); +``` + +### See also + +- [Ruleset files](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-rule-set-syntax-for-code-analysis-tools) on Microsoft Learn — how to enable disabled-by-default rules. diff --git a/content/docs/analyzers/FormattingCop/_index.md b/content/docs/analyzers/FormattingCop/_index.md index 4719fef..cc7124b 100644 --- a/content/docs/analyzers/FormattingCop/_index.md +++ b/content/docs/analyzers/FormattingCop/_index.md @@ -16,3 +16,4 @@ The FormattingCop enforces consistent formatting and visual structure of AL code | [FC0004](fc0004/) | Permission declarations should be ordered alphabetically | Info | ✓ | ✓ | | [FC0005](fc0005/) | Use parenthesis for method calls instead of assignment syntax | Warning | ✓ | ✓ | | [FC0006](fc0006/) | Permission values should be lowercase | Info | ✓ | ✓ | +| [FC0007](fc0007/) | Statement blocks should be separated by a blank line | Info | | | From 34ab5a7d9d1200bcd8af3b52fe1a861eac4a253c Mon Sep 17 00:00:00 2001 From: Carsten Scholling Date: Fri, 21 Aug 2026 06:16:56 +0200 Subject: [PATCH 2/3] fix(FC0007): treat only whitespace-only lines as blank --- content/docs/analyzers/FormattingCop/FC0007.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/content/docs/analyzers/FormattingCop/FC0007.md b/content/docs/analyzers/FormattingCop/FC0007.md index b588d49..61486d1 100644 --- a/content/docs/analyzers/FormattingCop/FC0007.md +++ b/content/docs/analyzers/FormattingCop/FC0007.md @@ -75,6 +75,24 @@ The rule runs four independent checks. Each check can be toggled or narrowed via Single-line control-flow statements (e.g. `if Flag then Message('x');` on one line) are excluded from the *before* / *after* checks by default. Include them with `OneLinerMode`. +### What counts as a blank line + +Only a truly whitespace-only line satisfies the separator requirement. A line containing a comment or a compiler directive (`#region`, `#pragma`, etc.) is treated as regular content: + +```al +Message('Start'); +// ---- section divider ---- +exit; // Insert a blank line before scope-leaving statement [FC0007] +``` + +A trailing comment on the same line as a statement does not turn that line into a separator, but it also does not affect a real blank line that follows or precedes it: + +```al +Message('Start'); // trailing comment + +exit; // OK: the empty line above still counts as a separator +``` + ### Exception - The symbol is obsolete From 016459fe5c897ad0a232cb650ded70a03ea811d0 Mon Sep 17 00:00:00 2001 From: Carsten Scholling Date: Fri, 21 Aug 2026 06:53:56 +0200 Subject: [PATCH 3/3] fix(FC0007): harden statement block spacing analysis --- content/docs/analyzers/FormattingCop/FC0007.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/content/docs/analyzers/FormattingCop/FC0007.md b/content/docs/analyzers/FormattingCop/FC0007.md index 61486d1..98a5dc0 100644 --- a/content/docs/analyzers/FormattingCop/FC0007.md +++ b/content/docs/analyzers/FormattingCop/FC0007.md @@ -70,7 +70,7 @@ The rule runs four independent checks. Each check can be toggled or narrowed via |---|---|---| | Blank line **before** a control-flow block | A multi-line `if`, `case`, `repeat`, `while`, `for`, or `foreach` immediately follows another statement in the same block. | `ControlFlowBefore` | | Blank line **after** a control-flow block | A non-control-flow statement immediately follows the closing `end` (or `until`) of a multi-line control-flow block. | `ControlFlowAfter` | -| Blank line before a **scope-leaving** statement | `exit`, built-in `Error(...)`, or both are preceded by another statement on the immediately preceding line. | `ScopeLeavingMode` | +| Blank line before a **scope-leaving** statement | `exit`, built-in `Error(...)`, or both follow a sibling statement in the same statement list without a whitespace-only line between them. | `ScopeLeavingMode` | | Blank line before **`else`** in an `if ... end else` construct | The closing `end` and the `else` keyword sit on adjacent lines instead of being separated by a blank line. Opt-in. | `ElseChainBeforeMode` | Single-line control-flow statements (e.g. `if Flag then Message('x');` on one line) are excluded from the *before* / *after* checks by default. Include them with `OneLinerMode`. @@ -97,6 +97,7 @@ exit; // OK: the empty line above still counts as a separa - The symbol is obsolete - Adjacent case branches inside a `case` statement (the check operates on the surrounding block, not on branches) +- An `exit` or `Error(...)` used directly as a `then` / `else` branch (the containing `if` is governed by the control-flow settings) - Any statement immediately after `begin` or immediately before `end` (the block braces already act as separators) ### Configuration