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 | | |