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
12 changes: 7 additions & 5 deletions content/docs/analyzers/FormattingCop/FC0007.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ linkTitle = 'FC0007'
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.
Multi-line control-flow blocks (`if`, `case`, `repeat`, `while`, `for`, `foreach`) and scope-leaving statements (`exit`, built-in `Error(...)`, `Record.FieldError(...)`, and `FieldRef.FieldError(...)`) 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`.

Expand Down Expand Up @@ -70,11 +70,13 @@ 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 follow a sibling statement in the same statement list without a whitespace-only line between them. | `ScopeLeavingMode` |
| Blank line before a **scope-leaving** statement | `exit`, a built-in `Error(...)` / `FieldError(...)` call, 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`.

Known limitation: `Error(ErrorInfo)` is currently treated as scope-leaving even when `ErrorInfo.Collectible` is `true` and the call runs in an `ErrorBehavior::Collect` scope. In that situation AL can continue after the call.

### 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:
Expand All @@ -97,7 +99,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)
- An `exit`, `Error(...)`, or `FieldError(...)` call 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
Expand All @@ -120,7 +122,7 @@ All settings live under a single `StatementBlockSpacing` object in `alcops.json`
|---|---|---|---|---|
| `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. |
| `ScopeLeavingMode` | string | `"ExitAndError"` | `"Off"`, `"ExitOnly"`, `"ErrorOnly"`, `"ExitAndError"` | `ErrorOnly` includes built-in `Error(...)` and `FieldError(...)`; `ExitAndError` includes those calls and `exit`. |
| `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. |

Expand All @@ -142,7 +144,7 @@ Turn every check off and re-enable individual ones:
}
```

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.
With this configuration the rule flags only the space before a multi-line block and before a built-in `Error(...)` or `FieldError(...)` call; `exit` statements, trailing blanks and one-liners are ignored.

#### Enforcing blank line before `else`

Expand Down
4 changes: 4 additions & 0 deletions content/docs/analyzers/LinterCop/LC0089.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ begin
end;
{{< /highlight >}}

A guard clause is an `if` whose only statement leaves the current flow: `exit`, `break`, `continue`, `CurrReport.Break()`, `CurrReport.Skip()`, `CurrReport.Quit()`, `CurrXMLport.Break()`, `CurrXMLport.Skip()`, `CurrXMLport.Quit()`, or a built-in call that never returns: `Error(...)`, `Record.FieldError(...)`, or `FieldRef.FieldError(...)`. `CurrReport` and `CurrXMLport` calls qualify only as guard exits for Cognitive Complexity; they are not general procedure terminators. A user-defined procedure named `Error` or `FieldError` is a normal call and keeps the `if` increment.

Known limitation: `Error(ErrorInfo)` is currently treated as flow-terminating even when `ErrorInfo.Collectible` is `true` and the call runs in an `ErrorBehavior::Collect` scope. In that situation AL can continue after the call, so the containing `if` may be classified as a guard clause.

For the full scoring model — how nesting penalties, logical operators, recursion, and compensating usages like `else if` are calculated — see [LC0090](../lc0090/).

### Increment diagnostics
Expand Down
24 changes: 20 additions & 4 deletions content/docs/analyzers/PlatformCop/PC0038.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ linkTitle = 'PC0038'

Procedures with a return type must produce a value on every reachable path. If one branch falls through without returning a value, AL can end up returning an implicit default value (`0`, `false`, empty text), which hides logic bugs and makes behavior harder to reason about.

This rule flags declarations where not all code paths return a value.
This rule flags declarations where not all reachable code paths return a value. A path ending with a built-in error call does not return to the procedure and therefore does not need a return value.

### Example

Expand Down Expand Up @@ -48,23 +48,39 @@ The rule is reported when all of the following are true:

Triggers are intentionally excluded from this rule, even when they declare a return type.

For named return variables, an assignment must be guaranteed on every path that reaches the end of the procedure. Assignments made through a `var` parameter or by using the return variable as a call receiver count, because those calls can write the value. The rule also follows calls in conditions, case selectors, loop conditions, `for` bounds, and `foreach` collections when the expression must execute. It remains conservative for the right side of `and` and `or`, and for either branch of a conditional expression, because those expressions can be skipped.

A `case` statement without `else` is accepted when it handles every enum or option value visible to the current compilation. Values added by an extension available at compilation time are included in that check.

### Exception

Paths that end with `Error(...)` are accepted:
Paths that end with a built-in call that never returns are accepted: `Error(...)`, `Record.FieldError(...)`, and `FieldRef.FieldError(...)`.

{{< highlight al >}}
{{< highlight al "hl_lines=6 14" >}}
procedure GetAmount(IncludeVat: Boolean): Decimal
begin
if IncludeVat then
exit(AmountInclVat)
else
Error('Prices without VAT are not supported.');
end;

procedure GetUnitPrice(Item: Record Item): Decimal
begin
if Item."Unit Price" > 0 then
exit(Item."Unit Price")
else
Item.FieldError("Unit Price", 'must be positive.');
end;
{{< /highlight >}}

Only these built-in methods qualify. A user-defined procedure named `Error` or `FieldError` is a normal call and does not terminate a path.

Known limitation: `Error(ErrorInfo)` is currently treated as terminating even when `ErrorInfo.Collectible` is `true` and the call runs in an `ErrorBehavior::Collect` scope. In that situation AL can continue after the call, but determining it requires data-flow and enclosing-call analysis beyond this rule's current invocation classification.

A named return variable passed to a `var` parameter, or used as the receiver of a call, counts as assigned:

{{< highlight al >}}
{{< highlight al "hl_lines=4 14" >}}
procedure BuildGreeting() Greeting: Text
begin
GetGreeting(Greeting);
Expand Down