From ab5c8e6ff270feeafdfdfffef2d9a029fffa5626 Mon Sep 17 00:00:00 2001 From: Carsten Scholling Date: Wed, 2 Sep 2026 09:36:39 +0200 Subject: [PATCH 1/2] docs: describe shared flow-terminating built-ins - Relates to ALCops/Analyzers#463 and ALCops/Analyzers#468 - Document FieldError handling for PC0038, FC0007, and LC0089 - Explain the collectible ErrorInfo limitation --- .../docs/analyzers/FormattingCop/FC0007.md | 12 ++++++---- content/docs/analyzers/LinterCop/LC0089.md | 4 ++++ content/docs/analyzers/PlatformCop/PC0038.md | 24 +++++++++++++++---- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/content/docs/analyzers/FormattingCop/FC0007.md b/content/docs/analyzers/FormattingCop/FC0007.md index 98a5dc0..65e7fce 100644 --- a/content/docs/analyzers/FormattingCop/FC0007.md +++ b/content/docs/analyzers/FormattingCop/FC0007.md @@ -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`. @@ -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: @@ -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 @@ -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. | @@ -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` diff --git a/content/docs/analyzers/LinterCop/LC0089.md b/content/docs/analyzers/LinterCop/LC0089.md index c371fc8..fd60187 100644 --- a/content/docs/analyzers/LinterCop/LC0089.md +++ b/content/docs/analyzers/LinterCop/LC0089.md @@ -48,6 +48,10 @@ begin end; {{< /highlight >}} +A guard clause is an `if` whose only statement leaves the current flow: `exit`, `break`, `continue`, `CurrReport.Skip()` / `CurrReport.Break()` / `CurrReport.Quit()` (and the `CurrXMLport` equivalents), or a built-in call that never returns: `Error(...)`, `Record.FieldError(...)`, or `FieldRef.FieldError(...)`. 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 diff --git a/content/docs/analyzers/PlatformCop/PC0038.md b/content/docs/analyzers/PlatformCop/PC0038.md index ceaa834..43c1e29 100644 --- a/content/docs/analyzers/PlatformCop/PC0038.md +++ b/content/docs/analyzers/PlatformCop/PC0038.md @@ -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 @@ -48,11 +48,15 @@ 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 @@ -60,11 +64,23 @@ begin 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); From 15683c210cff105ab263e8bf0e888f1f25cd5a0d Mon Sep 17 00:00:00 2001 From: Carsten Scholling Date: Wed, 2 Sep 2026 10:06:36 +0200 Subject: [PATCH 2/2] docs(LC0089): clarify report guard exits - Relates to ALCops/Analyzers#463 - Document CurrReport and CurrXMLport guard-clause behavior - Distinguish metric-specific guard exits from general procedure terminators --- content/docs/analyzers/LinterCop/LC0089.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/analyzers/LinterCop/LC0089.md b/content/docs/analyzers/LinterCop/LC0089.md index fd60187..4ac2b7e 100644 --- a/content/docs/analyzers/LinterCop/LC0089.md +++ b/content/docs/analyzers/LinterCop/LC0089.md @@ -48,7 +48,7 @@ begin end; {{< /highlight >}} -A guard clause is an `if` whose only statement leaves the current flow: `exit`, `break`, `continue`, `CurrReport.Skip()` / `CurrReport.Break()` / `CurrReport.Quit()` (and the `CurrXMLport` equivalents), or a built-in call that never returns: `Error(...)`, `Record.FieldError(...)`, or `FieldRef.FieldError(...)`. A user-defined procedure named `Error` or `FieldError` is a normal call and keeps the `if` increment. +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.