Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions content/docs/analyzers/FormattingCop/FC0007.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions content/docs/analyzers/FormattingCop/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | | |