mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-06 09:26:52 +01:00
Regenerate microsoft/knowledge from upstream BCApps instructions
The previous LLM-generated knowledge files contained factual
hallucinations. The most visible was the claim that `FindFirst` /
`FindLast` "forces a full-table scan" on an unfiltered record - it does
not; those APIs return a single row via the current key.
Other inaccuracies the audit found and fixed:
* `FindSet(true)` was described as "taking a LockTable". The correct
upstream phrasing is that `FindSet(true)` sets
`ReadIsolation::UpdLock` on the read. UpdLock and LockTable are
related but distinct mechanisms.
* The list of production-scale tables had been invented beyond the
upstream source (e.g. "Detailed Cust. Ledg. Entry") without a
citation. The regenerated list matches the ten tables upstream lists
with their P95 row counts.
* `SetLoadFields` guidance had been augmented with an extra mechanism
claim ("the database resolves the filter using the index without
hydrating the value") not present in upstream.
Approach: full regeneration of `microsoft/knowledge/` from the six
upstream BCApps Code Review instruction files, with Microsoft Learn /
the AL language reference as a secondary source. Every claim in every
regenerated file is anchored to a verbatim upstream quote (or a Learn
URL); the audit trail lives in artifacts/trace-<domain>.json on the
session workspace.
The PR #11 transaction/error-handling cluster is preserved verbatim:
* performance/understand-implicit-transaction-boundary.md
* performance/codeunit-run-as-atomic-sub-operation.{md,good.al,bad.al}
* performance/codeunit-run-requires-prior-commit-inside-transaction.{md,good.al,bad.al}
* performance/use-tryfunction-for-error-catching-not-rollback.{md,good.al,bad.al}
* performance/avoid-commit-inside-loops.{md,good.al,bad.al}
* security/commitbehavior-attribute-scopes-explicit-commits.{md,good.al,bad.al}
* testing/transactionmodel-attribute-governs-test-transactions.{md,good.al,bad.al}
These articles already cite Microsoft Learn and were carefully
cross-referenced; the regeneration skips their topics rather than
duplicating them.
File counts after regeneration:
performance 35 .md (5 preserved + 30 new)
privacy 17 .md
security 18 .md (1 preserved + 17 new)
style 33 .md
testing 1 .md (preserved)
ui 19 .md
upgrade 18 .md
Total 141 atomic knowledge files, each strictly one rule. All pass
.github/scripts/validate_frontmatter.py with 0 errors and 0 warnings.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
613c4b4019
commit
a9f3c50863
562 changed files with 6293 additions and 4869 deletions
|
|
@ -0,0 +1,5 @@
|
|||
page 50258 "Sample AboutTitle Bad"
|
||||
{
|
||||
PageType = List;
|
||||
SourceTable = Customer;
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
page 50256 "Sample AboutTitle Good List"
|
||||
{
|
||||
PageType = List;
|
||||
SourceTable = Customer;
|
||||
AboutTitle = 'About customers';
|
||||
AboutText = 'Manage your customer database and track customer interactions. You can create new customers, update contact information, and view customer statistics.';
|
||||
}
|
||||
|
||||
page 50257 "Sample AboutTitle Good Card"
|
||||
{
|
||||
PageType = Card;
|
||||
SourceTable = Customer;
|
||||
AboutTitle = 'About customer details';
|
||||
AboutText = 'View and edit detailed customer information including contact details, payment terms, and billing preferences.';
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [abouttitle, abouttext, teaching-tip, onboarding, page]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Use `AboutTitle` and `AboutText` to surface teaching tips on top-level pages
|
||||
|
||||
## Description
|
||||
|
||||
The `AboutTitle` and `AboutText` properties on a page render a teaching tip — an onboarding callout that appears the first time a user opens the page. They are supported on pages, individual page controls, FactBoxes, and report request pages. They are NOT supported on Role Centers or modal dialogs. The conventions: `AboutTitle` answers "what is this page about?" and uses the plural for list pages (`'About sales invoices'`) and the `[entity] details` form for card and document pages (`'About sales invoice details'`); `AboutText` answers "what can I do with this page?" in two or three short sentences. Both are translation-aware and surface to the end user verbatim.
|
||||
|
||||
The reviewer signal is "this is a new top-level card or list page in an app whose sibling pages already define teaching tips" — when the surrounding app sets the precedent, a new page without `AboutTitle`/`AboutText` is an inconsistency worth flagging.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Set `AboutTitle` and `AboutText` on every new top-level card, list, and document page in an app that already uses them. Keep `AboutText` to two or three short sentences. Describe what the page does, not the navigation steps to use it — teaching tips explain WHAT, not HOW.
|
||||
|
||||
See sample: `abouttitle-abouttext-teaching-tips.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
A new top-level page in an app whose siblings have `AboutTitle`/`AboutText`, but with no teaching tips defined. Equally wrong is filling `AboutText` with step-by-step instructions ("Click New, then enter…") — the property is for orientation, not procedural help.
|
||||
|
||||
See sample: `abouttitle-abouttext-teaching-tips.bad.al`.
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
page 50219 "Sample API Camel Bad"
|
||||
{
|
||||
PageType = API;
|
||||
APIPublisher = 'Contoso-App';
|
||||
APIGroup = 'app_1';
|
||||
APIVersion = 'v2.0';
|
||||
EntityName = 'sales_order';
|
||||
EntitySetName = 'sales_orders';
|
||||
SourceTable = "Sales Header";
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
page 51102 "Style Sample ApiPage Good"
|
||||
page 50218 "Sample API Camel Good"
|
||||
{
|
||||
PageType = API;
|
||||
APIPublisher = 'contoso';
|
||||
|
|
@ -8,7 +8,6 @@ page 51102 "Style Sample ApiPage Good"
|
|||
EntitySetName = 'customers';
|
||||
SourceTable = Customer;
|
||||
DelayedInsert = true;
|
||||
ODataKeyFields = SystemId;
|
||||
|
||||
layout
|
||||
{
|
||||
|
|
@ -16,9 +15,7 @@ page 51102 "Style Sample ApiPage Good"
|
|||
{
|
||||
repeater(Group)
|
||||
{
|
||||
field(systemId; Rec.SystemId) { }
|
||||
field(number; Rec."No.") { }
|
||||
field(displayName; Rec.Name) { }
|
||||
field(displayName; Rec.Name) { Caption = 'displayName'; }
|
||||
}
|
||||
}
|
||||
}
|
||||
26
microsoft/knowledge/style/api-page-camelcase-properties.md
Normal file
26
microsoft/knowledge/style/api-page-camelcase-properties.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [api-page, camelcase, apipublisher, apigroup, entityname, entitysetname]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# API pages use camelCase, alphanumeric-only values for API properties
|
||||
|
||||
## Description
|
||||
|
||||
API pages — pages declared with `PageType = API` — surface as OData/JSON endpoints. The strings that appear in the URL (`APIPublisher`, `APIGroup`, `EntityName`, `EntitySetName`) and the JSON payload field names follow different naming rules from the rest of AL. They must be camelCase and use only alphanumeric characters: no hyphens, no underscores, no spaces, no punctuation. `'Contoso-App'`, `'contoso_app'`, and `'contoso.app'` are all rejected. The same rule applies to page field names exposed via `Name = '…'` on API page controls — those names appear verbatim in the JSON keys.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Pick camelCase identifiers up front: `APIPublisher = 'contoso'`, `APIGroup = 'app1'`, `EntityName = 'customer'`, field `Name = 'displayName'`. Keep them short — they end up in URL paths and JSON keys that every consumer types.
|
||||
|
||||
See sample: `api-page-camelcase-properties.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`APIPublisher = 'Contoso-App'` (hyphen rejected, capitalization wrong for camelCase), `EntityName = 'sales_order'` (underscore rejected), or fields exposed with `Name = 'Display Name'` (space rejected). The compiler usually catches these, but the failure mode is opaque and the rename cost on a deployed API is high.
|
||||
|
||||
See sample: `api-page-camelcase-properties.bad.al`.
|
||||
10
microsoft/knowledge/style/api-page-delayedinsert-true.bad.al
Normal file
10
microsoft/knowledge/style/api-page-delayedinsert-true.bad.al
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
page 50227 "Sample DelayedInsert Bad"
|
||||
{
|
||||
PageType = API;
|
||||
APIPublisher = 'contoso';
|
||||
APIGroup = 'app1';
|
||||
APIVersion = 'v1.0';
|
||||
EntityName = 'customer';
|
||||
EntitySetName = 'customers';
|
||||
SourceTable = Customer;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
page 50226 "Sample DelayedInsert Good"
|
||||
{
|
||||
PageType = API;
|
||||
APIPublisher = 'contoso';
|
||||
APIGroup = 'app1';
|
||||
APIVersion = 'v1.0';
|
||||
EntityName = 'customer';
|
||||
EntitySetName = 'customers';
|
||||
SourceTable = Customer;
|
||||
DelayedInsert = true;
|
||||
}
|
||||
26
microsoft/knowledge/style/api-page-delayedinsert-true.md
Normal file
26
microsoft/knowledge/style/api-page-delayedinsert-true.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [api-page, delayedinsert, insert-trigger, validation]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Set `DelayedInsert = true` on API pages
|
||||
|
||||
## Description
|
||||
|
||||
On a normal page, `DelayedInsert = false` is the default: the record is inserted into the table as soon as the user enters the first field, and subsequent fields are written via `Modify` triggers. That model does not work for an API endpoint, where the consumer sends a complete JSON payload in a single request and expects exactly one `Insert` to fire with all fields already populated. `DelayedInsert = true` defers the insert until every field on the page has been assigned, so the `OnInsert` trigger runs once with the full record and `OnValidate` triggers on individual fields run in a predictable order. The convention is that API pages always set `DelayedInsert = true`.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Declare `DelayedInsert = true` on every page with `PageType = API`. The setting plays well with `Modify(true)` and `Insert(true)` calls inside `OnInsert` and avoids the half-populated record states that otherwise reach validation logic.
|
||||
|
||||
See sample: `api-page-delayedinsert-true.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Omitting `DelayedInsert` (which defaults to `false`) on an API page. Validation triggers fire on a partially populated record, mandatory-field errors come back to the caller for fields the JSON payload was about to supply, and the API surface produces failures that have no analogue in the UI page model.
|
||||
|
||||
See sample: `api-page-delayedinsert-true.bad.al`.
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
page 50225 "Sample API Entity Bad"
|
||||
{
|
||||
PageType = API;
|
||||
APIPublisher = 'contoso';
|
||||
APIGroup = 'app1';
|
||||
APIVersion = 'v1.0';
|
||||
EntityName = 'customers';
|
||||
EntitySetName = 'customer';
|
||||
SourceTable = Customer;
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
page 50223 "Sample API Entity Good"
|
||||
{
|
||||
PageType = API;
|
||||
APIPublisher = 'contoso';
|
||||
APIGroup = 'app1';
|
||||
APIVersion = 'v1.0';
|
||||
EntityName = 'customer';
|
||||
EntitySetName = 'customers';
|
||||
SourceTable = Customer;
|
||||
DelayedInsert = true;
|
||||
}
|
||||
|
||||
page 50224 "Sample API Compound Good"
|
||||
{
|
||||
PageType = API;
|
||||
APIPublisher = 'contoso';
|
||||
APIGroup = 'app1';
|
||||
APIVersion = 'v1.0';
|
||||
EntityName = 'salesOrder';
|
||||
EntitySetName = 'salesOrders';
|
||||
SourceTable = "Sales Header";
|
||||
DelayedInsert = true;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [api-page, entityname, entitysetname, singular, plural]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# `EntityName` is singular; `EntitySetName` is plural
|
||||
|
||||
## Description
|
||||
|
||||
`EntityName` and `EntitySetName` on an API page are the two halves of the OData naming contract. `EntityName` names a single record — `'customer'`, `'salesOrder'`, `'item'`. `EntitySetName` names the collection — `'customers'`, `'salesOrders'`, `'items'`. Swapping them — `EntityName = 'customers'`, `EntitySetName = 'customer'` — produces URLs that lie to consumers: `GET /customers` returns one row, `GET /customers('id')` returns a collection. The OData conventions consumers rely on for client-side code generation depend on the singular/plural pairing being correct.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Pick the singular noun for `EntityName` and its grammatical plural for `EntitySetName`, both in camelCase. For compound nouns, only the trailing noun is pluralized: `EntityName = 'salesOrder'`, `EntitySetName = 'salesOrders'`. For nouns whose plural is irregular, use the natural English form — `EntitySetName = 'people'` for `EntityName = 'person'`.
|
||||
|
||||
See sample: `api-page-entity-naming-singular-plural.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`EntityName = 'customers'`, `EntitySetName = 'customer'` — singular and plural swapped. Equally wrong is reusing the same form for both — `EntityName = 'customer'`, `EntitySetName = 'customer'` — which breaks OData metadata parsers and client codegen.
|
||||
|
||||
See sample: `api-page-entity-naming-singular-plural.bad.al`.
|
||||
10
microsoft/knowledge/style/api-page-version-format.bad.al
Normal file
10
microsoft/knowledge/style/api-page-version-format.bad.al
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
page 50222 "Sample API Version Bad"
|
||||
{
|
||||
PageType = API;
|
||||
APIPublisher = 'contoso';
|
||||
APIGroup = 'app1';
|
||||
APIVersion = 'v2';
|
||||
EntityName = 'customer';
|
||||
EntitySetName = 'customers';
|
||||
SourceTable = Customer;
|
||||
}
|
||||
23
microsoft/knowledge/style/api-page-version-format.good.al
Normal file
23
microsoft/knowledge/style/api-page-version-format.good.al
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
page 50220 "Sample API Version Good"
|
||||
{
|
||||
PageType = API;
|
||||
APIPublisher = 'contoso';
|
||||
APIGroup = 'app1';
|
||||
APIVersion = 'v1.0';
|
||||
EntityName = 'customer';
|
||||
EntitySetName = 'customers';
|
||||
SourceTable = Customer;
|
||||
DelayedInsert = true;
|
||||
}
|
||||
|
||||
page 50221 "Sample API Beta Good"
|
||||
{
|
||||
PageType = API;
|
||||
APIPublisher = 'contoso';
|
||||
APIGroup = 'app1';
|
||||
APIVersion = 'beta';
|
||||
EntityName = 'preview';
|
||||
EntitySetName = 'previews';
|
||||
SourceTable = Customer;
|
||||
DelayedInsert = true;
|
||||
}
|
||||
26
microsoft/knowledge/style/api-page-version-format.md
Normal file
26
microsoft/knowledge/style/api-page-version-format.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [api-page, apiversion, version, format, beta]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# `APIVersion` must follow the pattern `vX.Y` (or `beta`)
|
||||
|
||||
## Description
|
||||
|
||||
The `APIVersion` property on an API page is part of the public URL path: `/api/<publisher>/<group>/<version>/<entitySetName>`. The platform accepts only two value shapes for it: a `vMAJOR.MINOR` string such as `'v1.0'`, `'v2.0'`, or `'v2.1'`, or the literal string `'beta'` for pre-release endpoints. Anything else — `'v2'`, `'2.0'`, `'1'`, `'v2.0.0'` — is rejected. The major-minor pair lets consumers detect compatibility through URL inspection alone; the explicit `'beta'` channel signals "this contract may break without notice."
|
||||
|
||||
## Best Practice
|
||||
|
||||
Start a new public endpoint at `'v1.0'`. Bump the minor when adding fields or non-breaking changes; bump the major when changing field types, removing fields, or any breaking change. Use `'beta'` for endpoints that are still iterating and SHOULD NOT be consumed by external integrations.
|
||||
|
||||
See sample: `api-page-version-format.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`APIVersion = 'v2'` (missing minor), `APIVersion = '2.0'` (missing `v` prefix), `APIVersion = 'v2.0.0'` (extra segment). All three either fail to compile or produce a URL that consumers cannot reach.
|
||||
|
||||
See sample: `api-page-version-format.bad.al`.
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
codeunit 51101 "Style Sample LabelSuffix Bad"
|
||||
{
|
||||
procedure Example()
|
||||
var
|
||||
CannotDeleteLine: Label 'Cannot delete this line.';
|
||||
Text000: Label 'Update complete';
|
||||
UpdateLocation: Label 'Update location?';
|
||||
WrongSuffixTok: Label 'Customer %1 not found.', Comment = '%1 = Customer No.';
|
||||
CustomerNo: Code[20];
|
||||
begin
|
||||
Error(CannotDeleteLine);
|
||||
Message(Text000);
|
||||
if Confirm(UpdateLocation) then
|
||||
;
|
||||
Error(WrongSuffixTok, CustomerNo);
|
||||
end;
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
codeunit 51100 "Style Sample LabelSuffix Good"
|
||||
{
|
||||
procedure Example()
|
||||
var
|
||||
UpdateCompleteMsg: Label 'Update complete.';
|
||||
CannotDeleteLineErr: Label 'Cannot delete this line.';
|
||||
UpdateLocationQst: Label 'Update location?';
|
||||
CustomerNameLbl: Label 'Customer Name';
|
||||
HttpsMethodTok: Label 'GET', Locked = true;
|
||||
TelemetryCustomerUpdatedTxt: Label 'Customer updated.';
|
||||
begin
|
||||
Message(UpdateCompleteMsg);
|
||||
if Confirm(UpdateLocationQst) then
|
||||
;
|
||||
Session.LogMessage('0001', TelemetryCustomerUpdatedTxt,
|
||||
Verbosity::Normal, DataClassification::SystemMetadata,
|
||||
TelemetryScope::ExtensionPublisher);
|
||||
Error(CannotDeleteLineErr);
|
||||
end;
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [label, textconst, suffix, msg, err, qst, tok, lbl, txt, aa0074]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Suffix every Label and TextConst with its approved usage tag
|
||||
|
||||
## Description
|
||||
|
||||
CodeCop rule AA0074 requires every Label and TextConst to carry a suffix indicating how the value is consumed: `Msg` for Message calls, `Err` for Error calls, `Qst` for Confirm or StrMenu prompts, `Tok` for locked tokens (URLs, JSON keys, short literals with `Locked = true`), `Lbl` for captions and tooltips, and `Txt` for telemetry strings. The suffix is not decoration — it is how the compiler, linter, and reviewer detect misuse (a `Tok` value passed to `Error`, a `Msg` used as an error label). The cost of adopting the convention is one short suffix per declaration; the cost of ignoring it is that every reviewer has to inspect every call site to judge appropriateness.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Name every Label and TextConst with one of `Msg`, `Err`, `Qst`, `Tok`, `Lbl`, or `Txt` at the end. Pick the suffix that matches the consuming call, not the look of the string. When multiple suffixes are grammatically valid (`Tok` vs `Lbl` for a short caption on a locked token) the choice is a judgment call; the violation is missing a suffix or using one inconsistent with the call site.
|
||||
|
||||
See sample: `apply-approved-label-suffixes.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`CannotDeleteLine: Label 'Cannot delete this line.';` — no suffix, used with Error. `Text000: Label 'Update complete';` — generic name with no suffix at all. `WrongSuffixTok: Label 'Customer %1 not found.'` used with Error — a Tok suffix on an error label.
|
||||
|
||||
See sample: `apply-approved-label-suffixes.bad.al`.
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
codeunit 50235 "Sample Begin Own Line Bad"
|
||||
{
|
||||
procedure Run(Condition: Boolean)
|
||||
begin
|
||||
if Condition then
|
||||
begin
|
||||
DoSomething();
|
||||
DoSomethingElse();
|
||||
end;
|
||||
end;
|
||||
|
||||
local procedure DoSomething() begin end;
|
||||
local procedure DoSomethingElse() begin end;
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
codeunit 50234 "Sample Begin Same Line Good"
|
||||
{
|
||||
procedure Run(Condition: Boolean)
|
||||
var
|
||||
i: Integer;
|
||||
begin
|
||||
if Condition then begin
|
||||
DoSomething();
|
||||
DoSomethingElse();
|
||||
end else begin
|
||||
Reset();
|
||||
Notify();
|
||||
end;
|
||||
for i := 1 to 10 do begin
|
||||
DoSomething();
|
||||
DoSomethingElse();
|
||||
end;
|
||||
end;
|
||||
|
||||
local procedure DoSomething() begin end;
|
||||
local procedure DoSomethingElse() begin end;
|
||||
local procedure Reset() begin end;
|
||||
local procedure Notify() begin end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [begin, end, compound-statement, aa0005, codecop, formatting]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# `begin` goes on the same line as `then`, `else`, or `do` (CodeCop AA0005)
|
||||
|
||||
## Description
|
||||
|
||||
When a compound block follows `then`, `else`, or `do`, the `begin` keyword must sit on the same line as the preceding keyword, separated by exactly one space. `if Condition then begin` and `for i := 1 to N do begin` are correct. The form that puts `begin` on its own line — common in older AL and in languages like Pascal — is flagged by CodeCop AA0005. The rule does not change indentation of the block body; it only governs the placement of `begin` relative to `then`/`else`/`do`.
|
||||
|
||||
## Best Practice
|
||||
|
||||
`if Condition then begin … end;`, `else begin … end;`, `for i := 1 to N do begin … end;`. The block body is indented one level below the `if`/`for` line, and `end;` sits at the same indentation as the line that opened the block.
|
||||
|
||||
See sample: `begin-on-same-line-as-then-else-do.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
A line that ends with `then` (or `else`, or `do`) and is followed by a line whose only content is `begin`. The compiler accepts it but CodeCop AA0005 flags it; the visual cost is a wasted line per block and a layout that looks alien to readers used to current AL style.
|
||||
|
||||
See sample: `begin-on-same-line-as-then-else-do.bad.al`.
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
codeunit 50239 "Sample Block Kw Bad"
|
||||
{
|
||||
procedure Dispatch(IsContactName: Boolean; IsSalespersonCode: Boolean)
|
||||
var
|
||||
i: Integer;
|
||||
begin
|
||||
if IsContactName then ValidateContactName() else if IsSalespersonCode then ValidateSalespersonCode();
|
||||
for i := 1 to 10 do begin DoSomething(i); DoSomethingElse(i); end;
|
||||
end;
|
||||
|
||||
local procedure ValidateContactName() begin end;
|
||||
local procedure ValidateSalespersonCode() begin end;
|
||||
local procedure DoSomething(I: Integer) begin end;
|
||||
local procedure DoSomethingElse(I: Integer) begin end;
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
codeunit 50238 "Sample Block Kw Good"
|
||||
{
|
||||
procedure Dispatch(IsContactName: Boolean; IsSalespersonCode: Boolean)
|
||||
var
|
||||
i: Integer;
|
||||
begin
|
||||
if IsContactName then
|
||||
ValidateContactName()
|
||||
else
|
||||
if IsSalespersonCode then
|
||||
ValidateSalespersonCode();
|
||||
|
||||
for i := 1 to 10 do begin
|
||||
DoSomething(i);
|
||||
DoSomethingElse(i);
|
||||
end;
|
||||
end;
|
||||
|
||||
local procedure ValidateContactName() begin end;
|
||||
local procedure ValidateSalespersonCode() begin end;
|
||||
local procedure DoSomething(I: Integer) begin end;
|
||||
local procedure DoSomethingElse(I: Integer) begin end;
|
||||
}
|
||||
26
microsoft/knowledge/style/block-keywords-start-new-line.md
Normal file
26
microsoft/knowledge/style/block-keywords-start-new-line.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [block-keyword, end, if, repeat, until, for, while, case, aa0018]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Block keywords (`end`, `if`, `repeat`, `until`, `for`, `while`, `case`) start a new line (CodeCop AA0018)
|
||||
|
||||
## Description
|
||||
|
||||
CodeCop AA0018 requires that the block-introducing keywords `if`, `repeat`, `until`, `for`, `while`, `case`, and the block-terminating keyword `end` always start a new line. Multiple statements packed onto one line — `if A then X() else if B then Y();` written inline, or `for i := 1 to 10 do begin X(i); Y(i); end;` — defeat code review tooling that operates line-by-line and obscure the control flow. The rule does not prohibit short single-statement constructs spread across two lines (`if Cond then X();`); it prohibits packing the entire control structure onto one line.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Each `if`, `else if`, `repeat`, `for`, `while`, and `case` starts a line. Each `end;` (the closing of a `begin … end` block or a `case`) starts a line. Branch bodies are on their own line, indented.
|
||||
|
||||
See sample: `block-keywords-start-new-line.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`if IsContactName then ValidateContactName() else if IsSalespersonCode then ValidateSalespersonCode();` collapses an `if/else if` chain onto a single line; AA0018 flags both the `else` and the second `if`. The same applies to `for i := 1 to 10 do begin DoX(i); DoY(i); end;` — `end` is not at the start of its line.
|
||||
|
||||
See sample: `block-keywords-start-new-line.bad.al`.
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
table 50253 "Sample Caption Bad"
|
||||
{
|
||||
fields
|
||||
{
|
||||
field(1; "Customer No."; Code[20])
|
||||
{
|
||||
}
|
||||
field(2; "Is Active"; Boolean)
|
||||
{
|
||||
Caption = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
table 50252 "Sample Caption Good"
|
||||
{
|
||||
fields
|
||||
{
|
||||
field(1; "Customer No."; Code[20])
|
||||
{
|
||||
Caption = 'Customer No.';
|
||||
}
|
||||
field(2; "Enabled"; Boolean)
|
||||
{
|
||||
}
|
||||
field(3; Amount; Decimal)
|
||||
{
|
||||
CaptionClass = '3,5,' + 'USD';
|
||||
}
|
||||
}
|
||||
}
|
||||
28
microsoft/knowledge/style/caption-required-on-page-fields.md
Normal file
28
microsoft/knowledge/style/caption-required-on-page-fields.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [caption, page-field, aa0225, aa0226, codecop, captionclass]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Every page field needs a `Caption` (CodeCop AA0225/AA0226)
|
||||
|
||||
## Description
|
||||
|
||||
CodeCop AA0225 and AA0226 require every field control to expose a `Caption` property, separately from the field's source name. The caption is what the user sees as the column header or label; the source name is what the code uses to reference the field. Without an explicit `Caption`, AL falls back to the source field's caption — which may be wrong for the page's context — or to the field name itself in code casing, which surfaces internal naming to users and to translators.
|
||||
|
||||
Acceptable exceptions: a field whose caption is inherited via `CaptionClass = '3,5,' + CurrencyCode` (or another CaptionClass formula) does not need a literal `Caption`; the formula provides it. API pages and test pages may omit captions because their consumers are not human users. Boolean fields whose name already reads as a sentence — `Enabled`, `Posted`, `Released` — do not need a redundant Caption that repeats the name.
|
||||
|
||||
## Best Practice
|
||||
|
||||
`Caption = 'Customer No.';` paired with `ToolTip = 'Specifies …';`. Captions are short, noun-phrase, title-case for primary labels; sentence-case is allowed for descriptive labels that read as a sentence fragment.
|
||||
|
||||
See sample: `caption-required-on-page-fields.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
A field control with no `Caption` and no `CaptionClass`, or `Caption = '';`. The user sees the internal identifier as the column header and the translation pipeline has nothing to translate.
|
||||
|
||||
See sample: `caption-required-on-page-fields.bad.al`.
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
codeunit 50241 "Sample Case Format Bad"
|
||||
{
|
||||
procedure Translate(Letter: Char): Code[10]
|
||||
var
|
||||
Letter2: Code[10];
|
||||
begin
|
||||
case Letter of
|
||||
'A': Letter2 := '10';
|
||||
'B': Letter2 := '11';
|
||||
'C': begin Letter2 := '12'; DoSomething(); end;
|
||||
end;
|
||||
exit(Letter2);
|
||||
end;
|
||||
|
||||
local procedure DoSomething() begin end;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
codeunit 50240 "Sample Case Format Good"
|
||||
{
|
||||
procedure Translate(Letter: Char): Code[10]
|
||||
var
|
||||
Letter2: Code[10];
|
||||
begin
|
||||
case Letter of
|
||||
'A':
|
||||
Letter2 := '10';
|
||||
'B':
|
||||
Letter2 := '11';
|
||||
'C': begin
|
||||
Letter2 := '12';
|
||||
DoSomething();
|
||||
end;
|
||||
end;
|
||||
exit(Letter2);
|
||||
end;
|
||||
|
||||
local procedure DoSomething() begin end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [case, statement, formatting, possibility, action, line-break]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# `case` action goes on the line after the possibility
|
||||
|
||||
## Description
|
||||
|
||||
In an AL `case` statement, the action for each label is written on the line that follows the label, not on the same line. `'A': Letter2 := '10';` on a single line is the discouraged form; the convention is `'A':` on one line and `Letter2 := '10';` on the next, indented one level deeper. The exception is when the action is a `begin … end` block — there the `begin` follows the colon on the same line, consistent with the rule for `then begin` / `else begin` / `do begin`.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Each case label sits on its own line, terminated by `:`. The action below it is indented; multi-statement actions open with `begin` on the label line and close with `end;` on its own line.
|
||||
|
||||
See sample: `case-action-on-line-after-possibility.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`'A': Letter2 := '10';` (single-line label and action), and `'C': begin Letter2 := '12'; DoSomething(); end;` (everything on one line including the block body). Both defeat per-line diff review and crowd the control flow.
|
||||
|
||||
See sample: `case-action-on-line-after-possibility.bad.al`.
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
codeunit 50207 "Sample Error Params Bad"
|
||||
{
|
||||
var
|
||||
CustomerNotFoundErr: Label 'Customer %1 does not exist.';
|
||||
|
||||
procedure CheckCustomer(CustomerNo: Code[20])
|
||||
var
|
||||
Customer: Record Customer;
|
||||
begin
|
||||
if not Customer.Get(CustomerNo) then
|
||||
Error(StrSubstNo(CustomerNotFoundErr, CustomerNo));
|
||||
if not Customer.Get(CustomerNo) then
|
||||
Error('Customer ' + CustomerNo + ' not found');
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
codeunit 50206 "Sample Error Params Good"
|
||||
{
|
||||
var
|
||||
CustomerNotFoundErr: Label 'Customer %1 does not exist.';
|
||||
|
||||
procedure CheckCustomer(CustomerNo: Code[20])
|
||||
var
|
||||
Customer: Record Customer;
|
||||
begin
|
||||
if not Customer.Get(CustomerNo) then
|
||||
Error(CustomerNotFoundErr, CustomerNo);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [error, strsubstno, label, parameters, concatenation, aa0231]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Pass parameters directly to `Error()`, do not wrap with `StrSubstNo`
|
||||
|
||||
## Description
|
||||
|
||||
`Error()` accepts a format string and a variable number of arguments — `Error(SomeLabelErr, Arg1, Arg2)`. The platform performs the substitution itself, which is the path the translation pipeline understands. Wrapping the same call as `Error(StrSubstNo(SomeLabelErr, Arg1, Arg2))` hides the placeholders from the platform and removes the format-string identity from the call-site, so analyzers cannot match the call to its label and translators lose the link between the formatted message and its template. The corresponding anti-pattern for hardcoded strings — `Error('Customer ' + CustomerNo + ' not found')` — is even worse: it builds an untranslatable, unanalyzable string at runtime.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Declare a `Label` with the `Err` suffix and the appropriate `Comment` for placeholders, then call `Error(YourErr, arg1, arg2)`. The same rule applies to `Message`, `Confirm`, and other UI primitives: format string in, parameters as separate arguments, no `StrSubstNo` wrapper at the call site, no string concatenation. An `Error('')` (empty message) is acceptable when the calling code expects another layer to emit the actual diagnostic.
|
||||
|
||||
See sample: `error-passes-parameters-directly-not-strsubstno.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`Error(StrSubstNo(CustomerNotFoundErr, CustomerNo))` and `Error(CustomerNotFoundErr + ': ' + CustomerNo)` both defeat the translation and analysis machinery. Reviewers should treat `StrSubstNo` appearing as an argument to `Error`, `Message`, `Confirm`, or `StrMenu` as an unconditional signal to rewrite.
|
||||
|
||||
See sample: `error-passes-parameters-directly-not-strsubstno.bad.al`.
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [event-subscriber, parameter-name, publisher, signature, eventsubscriber]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Event subscriber parameter names must match the publisher signature
|
||||
|
||||
## Description
|
||||
|
||||
In AL, an `[EventSubscriber]` procedure is bound to its publisher by event name and parameter list. The parameter names on the subscriber are not a style choice — they must match the names the publisher declared. The compiler validates the match at build time and emits an error if the subscriber renames a parameter. This means a reviewer cannot apply a generic "use better names" pass to subscriber parameters: `Sender`, `Rec`, `xRec`, `RunTrigger`, the table-and-field-specific parameter names a publisher emits — all are dictated by the publisher and must be reproduced verbatim.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Copy the publisher signature exactly when declaring the subscriber. When in doubt, navigate to the publisher (`OnAfterValidateEvent`, `OnBeforePostSalesDoc`, etc.) and copy its parameter list. Style rules that apply to other locals — descriptive names, no spaces — do not apply to subscriber parameters.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Renaming a publisher parameter to look prettier in the subscriber. The build breaks immediately. More insidiously, a parameter name that happens to match by coincidence in one event publisher but not in a similar one will compile in some versions of BC and fail in others when the publisher signature evolves.
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
tableextension 50211 "Sample FieldCaption Bad" extends Customer
|
||||
{
|
||||
procedure ConfirmAndAnnounce(): Boolean
|
||||
var
|
||||
UpdateLocationQst: Label 'Update %1?';
|
||||
UpdatedMsg: Label 'Updated %1.';
|
||||
begin
|
||||
if not Confirm(UpdateLocationQst, true, FieldName("Location Code")) then
|
||||
exit(false);
|
||||
Message(UpdatedMsg, TableName());
|
||||
exit(true);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
tableextension 50210 "Sample FieldCaption Good" extends Customer
|
||||
{
|
||||
procedure ConfirmAndAnnounce(): Boolean
|
||||
var
|
||||
UpdateLocationQst: Label 'Update %1?';
|
||||
UpdatedMsg: Label 'Updated %1.';
|
||||
begin
|
||||
if not Confirm(UpdateLocationQst, true, FieldCaption("Location Code")) then
|
||||
exit(false);
|
||||
Message(UpdatedMsg, TableCaption());
|
||||
exit(true);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [fieldcaption, fieldname, tablecaption, tablename, translation, message, error]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Use FieldCaption/TableCaption (not FieldName/TableName) in user-facing text
|
||||
|
||||
## Description
|
||||
|
||||
`FieldName` and `TableName` return the developer-facing identifier of a field or table — a fixed English string used in metadata and in code. `FieldCaption` and `TableCaption` return the translated, user-facing label declared by the field's or table's `Caption` property. When the value is embedded in a `Message`, `Error`, `Confirm`, or any other string shown to a user, the caption is the correct source. Otherwise the user sees the English internal name regardless of locale, and any caption change must be re-applied at every call site instead of being picked up from the single point of definition.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Reach for `FieldCaption("Location Code")` and `TableCaption()` whenever the value flows into a UI primitive. The same rule applies to format parameters: `Error(SomeErr, FieldCaption("Status"), TableCaption(), "Status")` rather than `Error(SomeErr, FieldName("Status"), TableName(), "Status")`. The captions follow the user's language; the names do not.
|
||||
|
||||
See sample: `fieldcaption-not-fieldname-in-user-messages.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`Message('Updated %1', TableName())` or `Confirm(UpdateLocationQst, true, FieldName("Location Code"))`. The user sees the English internal name in every locale, and any future rename of the caption fails to reach the message.
|
||||
|
||||
See sample: `fieldcaption-not-fieldname-in-user-messages.bad.al`.
|
||||
24
microsoft/knowledge/style/file-name-object-type-pattern.md
Normal file
24
microsoft/knowledge/style/file-name-object-type-pattern.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [file-name, object-type, suffix, naming-convention]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Name AL source files `<ObjectName>.<ObjectType>.al`
|
||||
|
||||
## Description
|
||||
|
||||
Each AL source file holds a single object, and the file name is expected to be of the form `<ObjectName>.<ObjectType>.al` — `CustomerCard.Page.al`, `PostSalesInvoice.Codeunit.al`, `NoSeriesTests.Codeunit.al`, `SalesHeader.TableExt.al`. The pattern makes object types greppable from a file listing and lets tooling — symbol search, project explorers, code generators — locate objects without parsing the AL source. Snake-case, lowercase-only, or type-less file names (`customer_page.al`, `tests_noSeries.al`, `PostSalesInvoiceLogic.al`) all break that contract.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Use PascalCase for the object portion, no spaces, no underscores; the type segment is one of the AL object-type names — `Page`, `Codeunit`, `Table`, `TableExt`, `Report`, `Query`, `XmlPort`, `Enum`, `EnumExt`, `Interface`, `PermissionSet`, `PageExt`, `ReportExt`. The object portion should echo the object's name as it appears in AL.
|
||||
|
||||
See sample (file-naming pattern is structural; no AL sample shipped here).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`customer_page.al`, `PostSalesInvoiceLogic.al`, `tests_noSeries.al`. The first uses snake_case and lower-case; the second omits the type segment entirely; the third inverts the order and uses mixed casing. All three break grep, symbol search, and the implicit map between file system and AL object table.
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
page 51103 "Style Sample ApiPage Bad"
|
||||
{
|
||||
PageType = API;
|
||||
APIPublisher = 'Contoso-App'; // hyphen not allowed
|
||||
APIGroup = 'app_1'; // underscore not allowed
|
||||
APIVersion = 'v2'; // missing minor version
|
||||
EntityName = 'customers'; // should be singular
|
||||
EntitySetName = 'customer'; // should be plural
|
||||
SourceTable = Customer;
|
||||
// DelayedInsert omitted; composite-key inserts misbehave
|
||||
|
||||
layout
|
||||
{
|
||||
area(Content)
|
||||
{
|
||||
repeater(Group)
|
||||
{
|
||||
field(number; Rec."No.") { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [api-page, apiversion, entityname, entitysetname, apipublisher, apigroup, delayedinsert]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# API pages follow strict naming and property rules that differ from regular pages
|
||||
|
||||
## Description
|
||||
|
||||
Pages declared `PageType = API` are exposed through the OData API surface. The platform enforces a set of conventions that regular pages do not share: `APIPublisher`, `APIGroup`, `EntityName`, and `EntitySetName` must be camelCase alphanumeric only — no spaces, hyphens, or underscores. `APIVersion` must match the pattern `vX.Y` (for example `v2.0`) or the literal `beta`. `EntityName` is the singular form (`customer`); `EntitySetName` is the plural (`customers`). `DelayedInsert = true` is effectively required for the OData insert workflow to behave correctly on composite keys. These rules are platform-enforced and tooling-enforced; violations produce runtime errors or consumer-visible inconsistencies rather than soft warnings.
|
||||
|
||||
## Best Practice
|
||||
|
||||
For every API page: camelCase alphanumeric API properties; `APIVersion` as `vX.Y` or `beta`; singular `EntityName` and plural `EntitySetName`; `DelayedInsert = true`. Keep these properties together near the top of the page definition so reviewers can check the set at a glance.
|
||||
|
||||
See sample: `follow-api-page-naming-rules.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`APIPublisher = 'Contoso-App'` (hyphen rejected), `EntityName = 'customers'` and `EntitySetName = 'customer'` (swapped), `APIVersion = 'v2'` (missing minor version), `DelayedInsert` omitted. Each violation surfaces only when a consumer exercises the endpoint.
|
||||
|
||||
See sample: `follow-api-page-naming-rules.bad.al`.
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
codeunit 50213 "Sample Parens Bad"
|
||||
{
|
||||
procedure Run()
|
||||
var
|
||||
Customer: Record Customer;
|
||||
begin
|
||||
Customer.Init;
|
||||
if Customer.FindFirst then
|
||||
Customer.Modify;
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
codeunit 50212 "Sample Parens Good"
|
||||
{
|
||||
procedure Run()
|
||||
var
|
||||
Customer: Record Customer;
|
||||
begin
|
||||
Customer.Init();
|
||||
if Customer.FindFirst() then
|
||||
Customer.Modify();
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [parentheses, function-call, method-call, aa0008, codecop]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Always write parentheses on procedure calls (CodeCop AA0008)
|
||||
|
||||
## Description
|
||||
|
||||
AL allows a parameterless procedure to be called without parentheses — `Customer.Init` instead of `Customer.Init()` — and the result is syntactically identical at runtime. CodeCop AA0008 still flags the parenthesis-less form. The reason is twofold: written without parentheses, a procedure call is visually indistinguishable from a property read, which makes BC code harder to scan; and the same identifier may exist as both a property and a procedure on different objects, so the parentheses are the only local signal that this is a call. The rule applies to every parameterless invocation, including `Init`, `Insert`, `Modify`, `Delete`, `DeleteAll`, `FindFirst`, `FindSet`, `Next`, `Get`, `CalcFields`, and user-defined procedures.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Always write `()` on a procedure call, even when it takes no arguments: `Customer.Init();`, `TempBuffer.DeleteAll();`, `if Customer.FindFirst() then …`. The same applies inside expressions and as a condition.
|
||||
|
||||
See sample: `function-call-parentheses-required.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`Customer.Init;`, `TempBuffer.DeleteAll;`, `if Customer.FindFirst then …`. Every one of those is an AA0008 violation. Reviewers should treat a parameterless procedure name appearing without parentheses as a defect, even though the compiler accepts it.
|
||||
|
||||
See sample: `function-call-parentheses-required.bad.al`.
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
codeunit 51107 "Style Sample LabelProps Bad"
|
||||
{
|
||||
procedure Example()
|
||||
var
|
||||
// Two placeholders, no Comment. The translator has to guess which
|
||||
// identifier maps to %1 and which to %2.
|
||||
CustomerLocationErr: Label 'Customer %1 not found in %2.';
|
||||
// URL without Locked: enters the localization pipeline, may be translated.
|
||||
HttpsUrlLbl: Label 'https://example.com';
|
||||
CustomerNo: Code[20];
|
||||
LocationCode: Code[10];
|
||||
begin
|
||||
Error(CustomerLocationErr, CustomerNo, LocationCode);
|
||||
end;
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
codeunit 51106 "Style Sample LabelProps Good"
|
||||
{
|
||||
procedure Example()
|
||||
var
|
||||
CustomerNotFoundErr: Label 'Customer %1 does not exist for document %2.',
|
||||
Comment = '%1 = Customer No., %2 = Document No.';
|
||||
HttpsProtocolTok: Label 'HTTPS', Locked = true;
|
||||
ShortDescLbl: Label 'Description text', MaxLength = 50;
|
||||
CustomerNo: Code[20];
|
||||
DocumentNo: Code[20];
|
||||
begin
|
||||
Error(CustomerNotFoundErr, CustomerNo, DocumentNo);
|
||||
end;
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [label, placeholder, comment, locked, maxlength, localization]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Label placeholders need a Comment; locked strings need Locked = true
|
||||
|
||||
## Description
|
||||
|
||||
AL Labels accept optional properties — `Comment`, `Locked`, `MaxLength` — that travel with the string to localization. The Comment is the translator's only signal for what `%1` and `%2` mean; without it, `'Document %1 has errors in %2.'` translates unpredictably because the translator has to guess whether %1 is a document number, document type, or document name. `Locked = true` marks a string as non-translatable — URLs, JSON keys, short command tokens — and keeps the localization pipeline from translating literals that must stay verbatim. `MaxLength` limits how much of the label survives truncation. The Comment is required whenever placeholders are not self-evident; Locked is required on any non-text value.
|
||||
|
||||
## Best Practice
|
||||
|
||||
For placeholders, write `Comment = '%1 = Customer No., %2 = Document Type'` alongside the Label. For URLs, HTTP methods, JSON keys, and similar literals, set `Locked = true` and use the `Tok` suffix (see `apply-approved-label-suffixes`). For captions with a tight visual budget, set `MaxLength` to the enforceable length. When the placeholder meaning is obvious (`'Customer %1 not found.'`) the Comment is optional.
|
||||
|
||||
See sample: `include-comment-on-labels-with-placeholders.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`CustomerLocationErr: Label 'Customer %1 not found in %2.';` with no Comment — translators will not know which identifier maps to which placeholder. `HttpsUrl: Label 'https://example.com';` with no Locked — the URL enters the localization pipeline and may be translated into a broken address.
|
||||
|
||||
See sample: `include-comment-on-labels-with-placeholders.bad.al`.
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
codeunit 50203 "Sample Label Comment Bad"
|
||||
{
|
||||
var
|
||||
DocumentErrorErr: Label 'Document %1 has errors in %2.';
|
||||
ValidationErr: Label 'Field %1 in table %2 contains invalid value %3.';
|
||||
|
||||
procedure Validate(DocNo: Code[20]; Loc: Code[10])
|
||||
begin
|
||||
Error(DocumentErrorErr, DocNo, Loc);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
codeunit 50202 "Sample Label Comment Good"
|
||||
{
|
||||
var
|
||||
CustomerNotFoundErr: Label 'Customer %1 does not exist for sales document %2.', Comment = '%1 = Customer No., %2 = Sales Header No.';
|
||||
ValidationErr: Label 'Field %1 in table %2 contains invalid value %3.', Comment = '%1 = Field Name, %2 = Table Caption, %3 = Field Value';
|
||||
CustomerSimpleLbl: Label 'Customer %1';
|
||||
|
||||
procedure Validate(CustNo: Code[20]; DocNo: Code[20])
|
||||
begin
|
||||
Error(CustomerNotFoundErr, CustNo, DocNo);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [label, comment, placeholder, strsubstno, translation, aa0470]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Document each Label placeholder with the Comment parameter
|
||||
|
||||
## Description
|
||||
|
||||
`Label` and `TextConst` strings that contain placeholders (`%1`, `%2`, …) need a `Comment` parameter that names what each placeholder is. Translators do not see the call site, so without the Comment they cannot disambiguate `'Customer %1 not found in %2.'` — is `%2` a location code, a posting date, a company name? The pattern is `Comment = '%1 = <description>, %2 = <description>'`. The Comment is not required when the placeholder meaning is obvious from the surrounding text — `'Customer %1'` is unambiguously a Customer No. — but for any non-trivial label the Comment is a hard requirement.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Write the Comment in the form `'%1 = Customer No., %2 = Sales Header No.'` — one entry per placeholder, matched by ordinal, named in the vocabulary of the BC domain. When the label is reused across multiple call sites, the Comment names the canonical meaning all call sites must conform to.
|
||||
|
||||
See sample: `label-comment-explains-placeholders.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
A label with two or more placeholders and no Comment, leaving the translator to guess. Equally bad is a Comment that only restates the placeholders (`'%1 and %2 are values'`) without naming what they are. Both fail in translation: the localized string ends up grammatically or semantically wrong, and the bug surfaces only in a non-English tenant.
|
||||
|
||||
See sample: `label-comment-explains-placeholders.bad.al`.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
codeunit 50205 "Sample Locked Label Bad"
|
||||
{
|
||||
var
|
||||
HttpsUrl: Label 'https://example.com';
|
||||
GetVerbTok: Label 'GET';
|
||||
JsonTypeLbl: Label 'application/json';
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
codeunit 50204 "Sample Locked Label Good"
|
||||
{
|
||||
var
|
||||
GetMethodTok: Label 'GET', Locked = true;
|
||||
ContentTypeJsonTok: Label 'application/json', Locked = true;
|
||||
ApiBaseUrlTok: Label 'https://api.contoso.com/v1', Locked = true;
|
||||
TelemetryStartTxt: Label 'Operation started for %1.', Locked = true;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [label, locked, translation, token, url, json, xml]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Set `Locked = true` on Labels that must not be translated
|
||||
|
||||
## Description
|
||||
|
||||
A `Label` is by default surfaced to translators and rewritten per locale. That is wrong for strings that are not natural language: HTTP verbs (`GET`, `PUT`), URL fragments, JSON/XML snippets, content-type strings, GUIDs, application keys, and field tokens used by integrations. Translating these breaks the integration the moment a non-English tenant runs the code. The `Locked = true` parameter on the Label declaration tells the translation pipeline to keep the string verbatim, and signals to reviewers that the value is part of a wire-level contract rather than display text.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Pair `Locked = true` with the `Tok` suffix for short tokens (`GetMethodTok: Label 'GET', Locked = true;`) and with the `Txt` suffix for telemetry strings that contain format placeholders but should not be localized. The `Locked` parameter and the `Tok` / `Txt` suffix together make the intent unambiguous.
|
||||
|
||||
See sample: `label-locked-for-non-translatable.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`HttpsUrl: Label 'https://example.com';` or `ContentTypeTok: Label 'application/json';` declared without `Locked = true`. The translator localizes them, the integration fails in production for the affected tenant, and the failure is invisible in the developer's English-locale tests.
|
||||
|
||||
See sample: `label-locked-for-non-translatable.bad.al`.
|
||||
14
microsoft/knowledge/style/label-suffix-approved-list.bad.al
Normal file
14
microsoft/knowledge/style/label-suffix-approved-list.bad.al
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
codeunit 50201 "Sample Label Suffix Bad"
|
||||
{
|
||||
var
|
||||
CannotDeleteLine: Label 'Cannot delete this line.';
|
||||
Text000: Label 'Update complete';
|
||||
UpdateLocation: Label 'Update location?';
|
||||
WrongSuffixTok: Label 'Customer %1 not found.';
|
||||
|
||||
procedure ShowMessages()
|
||||
begin
|
||||
Error(WrongSuffixTok, '10000');
|
||||
Message(Text000);
|
||||
end;
|
||||
}
|
||||
15
microsoft/knowledge/style/label-suffix-approved-list.good.al
Normal file
15
microsoft/knowledge/style/label-suffix-approved-list.good.al
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
codeunit 50200 "Sample Label Suffix Good"
|
||||
{
|
||||
var
|
||||
UpdateCompleteMsg: Label 'Update complete.';
|
||||
CustomerNotFoundErr: Label 'Customer %1 does not exist.';
|
||||
DeleteRecordQst: Label 'Delete this record?';
|
||||
CustomerNameLbl: Label 'Customer Name';
|
||||
GetMethodTok: Label 'GET', Locked = true;
|
||||
TelemetryStartedTxt: Label 'Operation started for customer %1.', Locked = true;
|
||||
|
||||
procedure ShowMessage()
|
||||
begin
|
||||
Message(UpdateCompleteMsg);
|
||||
end;
|
||||
}
|
||||
26
microsoft/knowledge/style/label-suffix-approved-list.md
Normal file
26
microsoft/knowledge/style/label-suffix-approved-list.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [label, textconst, suffix, aa0074, codecop, msg, err, qst, lbl, tok]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Use approved suffixes on Label and TextConst names (CodeCop AA0074)
|
||||
|
||||
## Description
|
||||
|
||||
CodeCop AA0074 flags `Label` and `TextConst` identifiers that do not end with an approved usage suffix. The suffix signals at the call site how the text is consumed and what translation behaviour it should get. The approved suffixes and their intended usage are: `Msg` for text shown via `Message()`; `Err` for text passed to `Error()`; `Qst` for text used with `Confirm` or `StrMenu`; `Lbl` for captions and tooltips; `Tok` for short tokens such as `'GET'`, `'PUT'`, `'HTTPS'`, GUIDs, or JSON/XML snippets that are not translated (typically with `Locked = true`); and `Txt` for general text including telemetry messages. A `Label` named `Text000` or `CannotDeleteLine` without a suffix violates the rule, regardless of how readable the prose is.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Pick the suffix that matches the call where the label is consumed: `UpdateCompleteMsg` for `Message(...)`, `CustomerNotFoundErr` for `Error(...)`, `DeleteRecordQst` for `Confirm(...)`, `CustomerNameLbl` for tooltips and captions, `GetMethodTok` for locked tokens, `TelemetryDataTxt` for telemetry payloads. Suffix choices between `Tok`, `Lbl`, `Txt`, and `Msg` are judgment calls when the suffix is valid for the usage — what matters is that the suffix is on the approved list and matches the actual call.
|
||||
|
||||
See sample: `label-suffix-approved-list.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
A `Label` declared with no suffix (`CannotDeleteLine: Label '…';`), a generic name (`Text000: Label '…';`), or a suffix that contradicts the usage (`WrongSuffixTok: Label 'Customer %1 not found.'` then passed to `Error()`). All three trip AA0074 or its reviewers and obscure the call-site contract.
|
||||
|
||||
See sample: `label-suffix-approved-list.bad.al`.
|
||||
14
microsoft/knowledge/style/lowercase-reserved-keywords.bad.al
Normal file
14
microsoft/knowledge/style/lowercase-reserved-keywords.bad.al
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
codeunit 50245 "Sample Upper Keywords Bad"
|
||||
{
|
||||
procedure Walk(VAR Customer: Record Customer)
|
||||
VAR
|
||||
Found: Boolean;
|
||||
BEGIN
|
||||
IF Customer.FindSet() THEN
|
||||
REPEAT
|
||||
Found := TRUE;
|
||||
UNTIL Customer.Next() = 0;
|
||||
IF Found THEN
|
||||
EXIT;
|
||||
END;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
codeunit 50244 "Sample Lower Keywords Good"
|
||||
{
|
||||
procedure Walk(var Customer: Record Customer)
|
||||
var
|
||||
Found: Boolean;
|
||||
begin
|
||||
if Customer.FindSet() then
|
||||
repeat
|
||||
Found := true;
|
||||
until Customer.Next() = 0;
|
||||
if Found then
|
||||
exit;
|
||||
end;
|
||||
}
|
||||
28
microsoft/knowledge/style/lowercase-reserved-keywords.md
Normal file
28
microsoft/knowledge/style/lowercase-reserved-keywords.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [reserved-keyword, lowercase, aa0241, codecop, if, then, begin]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Reserved keywords are written in lowercase (CodeCop AA0241)
|
||||
|
||||
## Description
|
||||
|
||||
CodeCop AA0241 requires reserved AL keywords — `if`, `then`, `else`, `begin`, `end`, `var`, `procedure`, `local`, `internal`, `for`, `while`, `repeat`, `until`, `case`, `of`, `do`, `not`, `and`, `or`, `exit`, `break`, `skip`, `quit`, and the rest — to be lowercase. Old Navision and C/AL code used `IF…THEN…BEGIN…END` in uppercase, and that style still lingers in training data and legacy modules. New AL code is lowercase. The rule applies to keywords only — type names (`Record`, `Codeunit`, `Integer`), property names (`Caption`, `ToolTip`), and identifiers are unaffected.
|
||||
|
||||
Test codeunits that retain legacy uppercase forms (`OPENEDIT`, `ASSERTERROR`, `VALUE`) are an accepted exception: the test framework historically uses those identifiers and rewriting them brings no benefit. The rule applies to new code in modified lines, not to long-standing test patterns.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Write keywords lowercase: `if Condition then begin … end;`, `repeat … until Found;`, `for i := 1 to N do …`. The standard AL formatter normalizes casing automatically.
|
||||
|
||||
See sample: `lowercase-reserved-keywords.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`IF Condition THEN BEGIN DoSomething(); END;`, `REPEAT GetNext(); UNTIL Found;`. Uppercase keywords trip AA0241 and signal C/AL-era code that has not been modernized.
|
||||
|
||||
See sample: `lowercase-reserved-keywords.bad.al`.
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
table 51113 "Style Sample Option Bad"
|
||||
{
|
||||
fields
|
||||
{
|
||||
field(1; "Entry No."; Integer) { }
|
||||
field(10; Priority; Option)
|
||||
{
|
||||
// Four members, three captions. Critical renders with no caption.
|
||||
OptionMembers = Low,Medium,High,Critical;
|
||||
OptionCaption = 'Low,Medium,High';
|
||||
}
|
||||
field(20; Status; Option)
|
||||
{
|
||||
// Missing OptionCaption entirely.
|
||||
OptionMembers = Open,Released,Pending;
|
||||
}
|
||||
}
|
||||
keys
|
||||
{
|
||||
key(PK; "Entry No.") { Clustered = true; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [option, optionmembers, optioncaption, aa0221, aa0223, aa0224]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# OptionCaption must list exactly as many captions as OptionMembers
|
||||
|
||||
## Description
|
||||
|
||||
Option fields declare their values in `OptionMembers` and their localized display text in `OptionCaption`. The two lists are positionally paired — the Nth caption maps to the Nth member — and a mismatch either in count or in intent produces a field that renders blank for some values or shows the wrong caption for others. CodeCop rules AA0221, AA0223, and AA0224 flag the variants of this mistake: missing OptionCaption entirely on non-table-sourced option fields, OptionCaption with a different element count than OptionMembers, and OptionCaption content that does not correspond to the member names.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Whenever OptionMembers is declared, declare OptionCaption with the same number of entries in the same order. For table-sourced option fields, the base table's caption applies and a per-page override is usually unnecessary — the rule applies to option fields defined in pages, reports, and non-table sources.
|
||||
|
||||
See sample: `match-optioncaption-count-to-optionmembers.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`OptionMembers = Low,Medium,High,Critical;` paired with `OptionCaption = 'Low,Medium,High';` — three captions for four members. `Critical` rows render with the empty caption, or fall back to the member name, depending on where the option is displayed.
|
||||
|
||||
See sample: `match-optioncaption-count-to-optionmembers.bad.al`.
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [file-name, convention, object-type, al-project]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Name AL files as `<ObjectName>.<ObjectType>.al`
|
||||
|
||||
## Description
|
||||
|
||||
Business Central AL projects follow a consistent file-naming convention: the file name is the object's name, followed by a dot, followed by the object type (`Page`, `Codeunit`, `Table`, `Report`, `Enum`, etc.), followed by `.al`. `CustomerCard.Page.al`, `PostSalesInvoice.Codeunit.al`, `SalesLine.Table.al`. The convention produces an alphabetically-ordered folder that groups all of an entity's objects (`SalesLine.Table.al`, `SalesLine.TableExt.al`, `SalesLineCard.Page.al`) next to each other, and makes navigation by file name in large repos predictable.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Match the file name to the object declaration: PascalCase name, type segment, `.al`. Use `TableExt`, `PageExt`, `EnumExt` for the corresponding extension types. When multiple objects share a file (generally discouraged), name the file after the primary object.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`customer_page.al`, `PostSalesInvoiceLogic.al`, `tests_noSeries.al` — all three violate the convention. The first uses snake_case, the second adds a descriptive suffix after the object name, the third prefixes the type instead of suffixing it. Tooling that expects the convention (AL-Go scaffolding, navigation helpers, diff conventions) then misbehaves on these files.
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
codeunit 50209 "Sample Named Invocations Bad"
|
||||
{
|
||||
procedure ShowShipmentLines(var SalesShptLine: Record "Sales Shipment Line")
|
||||
begin
|
||||
Page.RunModal(525, SalesShptLine);
|
||||
end;
|
||||
|
||||
procedure RunInvoiceReport()
|
||||
begin
|
||||
Report.Run(206, true);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
codeunit 50208 "Sample Named Invocations Good"
|
||||
{
|
||||
procedure ShowShipmentLines(var SalesShptLine: Record "Sales Shipment Line")
|
||||
begin
|
||||
Page.RunModal(Page::"Posted Sales Shipment Lines", SalesShptLine);
|
||||
end;
|
||||
|
||||
procedure RunInvoiceReport()
|
||||
begin
|
||||
Report.Run(Report::"Sales - Invoice", true);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [page, report, codeunit, runmodal, run, object-id, named-invocation]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Call objects by name, not by numeric ID
|
||||
|
||||
## Description
|
||||
|
||||
`Page.RunModal`, `Report.Run`, `Codeunit.Run`, and the `Page::`, `Report::`, `Codeunit::`, `Table::`, `XmlPort::` selectors accept either a numeric ID or a named alias. The named form — `Page::"Posted Sales Shipment Lines"`, `Report::"Sales - Invoice"` — is the one to use. Numeric IDs are an implementation detail that change with renumbering, do not survive a rename, and carry no signal to a reader about what the call actually does. The compiler resolves named aliases at build time, so the named form is no slower than the numeric form.
|
||||
|
||||
## Best Practice
|
||||
|
||||
When invoking an object whose named alias is available in the same app (or in a dependency the current app already references), use the named form: `Page.RunModal(Page::"Posted Sales Shipment Lines", SalesShptLine)`, `Report.Run(Report::"Sales - Invoice", true)`. The same applies to `Codeunit.Run`, `XmlPort.Run`, `Query.Open`, and any platform method that takes an object reference. The named form makes diffs reviewable — a rename is visible — and makes log output and stack traces interpretable.
|
||||
|
||||
See sample: `named-invocations-not-object-ids.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`Page.RunModal(525, …)` or `Report.Run(206, true)`. The numeric form is unreadable, fragile across renumbering, and breaks every search that looks for callers of a named object.
|
||||
|
||||
See sample: `named-invocations-not-object-ids.bad.al`.
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
codeunit 50237 "Sample Single Stmt Bad"
|
||||
{
|
||||
procedure Validate(IsAssemblyOutputLine: Boolean)
|
||||
var
|
||||
SalesLine: Record "Sales Line";
|
||||
begin
|
||||
if IsAssemblyOutputLine then begin
|
||||
SalesLine.TestField("Order Line No.", 0);
|
||||
end;
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
codeunit 50236 "Sample Single Stmt Good"
|
||||
{
|
||||
procedure Validate(IsAssemblyOutputLine: Boolean)
|
||||
var
|
||||
SalesLine: Record "Sales Line";
|
||||
begin
|
||||
if IsAssemblyOutputLine then
|
||||
SalesLine.TestField("Order Line No.", 0);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [begin, end, single-statement, aa0013, codecop, compound]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Do not wrap a single statement in `begin … end` (CodeCop AA0013)
|
||||
|
||||
## Description
|
||||
|
||||
CodeCop AA0013 flags `begin … end` blocks that contain exactly one statement. The compound-block syntax exists to group multiple statements as a unit; using it for a single statement adds two lines and a level of nesting without adding meaning. `if IsAssemblyOutputLine then begin TestField("Order Line No.", 0); end;` should be `if IsAssemblyOutputLine then TestField("Order Line No.", 0);` — one statement, no block. The same logic applies after `else`, `for`, `while`, and `repeat`.
|
||||
|
||||
## Best Practice
|
||||
|
||||
A single statement following `then`, `else`, `do`, or a case label is written on its own line, indented one level, with no `begin … end`. Use `begin … end` only when there are two or more statements to group.
|
||||
|
||||
See sample: `no-begin-end-around-single-statement.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`if Cond then begin OneCall(); end;` — single statement wrapped in a block. AA0013 flags it. The reviewer signal is "a `begin` followed by exactly one statement before its `end`."
|
||||
|
||||
See sample: `no-begin-end-around-single-statement.bad.al`.
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
codeunit 50243 "Sample Redundant Else Bad"
|
||||
{
|
||||
procedure Validate(IsAdjmtBinCodeChanged: Boolean)
|
||||
var
|
||||
AdjmtBinErr: Label 'Adjustment bin code change not allowed.';
|
||||
BinCodeErr: Label 'Bin code change not allowed.';
|
||||
begin
|
||||
if IsAdjmtBinCodeChanged then
|
||||
Error(AdjmtBinErr)
|
||||
else
|
||||
Error(BinCodeErr);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
codeunit 50242 "Sample No Else Good"
|
||||
{
|
||||
procedure Validate(IsAdjmtBinCodeChanged: Boolean)
|
||||
var
|
||||
AdjmtBinErr: Label 'Adjustment bin code change not allowed.';
|
||||
BinCodeErr: Label 'Bin code change not allowed.';
|
||||
begin
|
||||
if IsAdjmtBinCodeChanged then
|
||||
Error(AdjmtBinErr);
|
||||
Error(BinCodeErr);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [else, exit, break, skip, quit, error, terminating, control-flow]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Omit `else` when the `then` branch ends with `exit`, `break`, `skip`, `quit`, or `error`
|
||||
|
||||
## Description
|
||||
|
||||
When the `then` branch of an `if` ends in a terminating statement — `exit`, `break`, `skip`, `quit`, or `error` — the `else` branch becomes the natural fall-through. `if Cond then exit; DoX();` and `if Cond then exit else DoX();` are equivalent, and the second form adds a layer of nesting that the reader has to mentally flatten. The same applies to `Error(...)`: `if IsAdjmtBinCodeChanged() then Error(AdjmtErr) else Error(BinErr);` is better written as `if IsAdjmtBinCodeChanged() then Error(AdjmtErr); Error(BinErr);` — the second `Error` is always reached when the first branch is not taken.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Drop the `else` when the `then` branch unconditionally exits the procedure or the enclosing loop. The body that would have been inside `else` becomes the unindented continuation.
|
||||
|
||||
See sample: `no-else-after-terminating-statement.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
An `if … then Error(…) else Error(…)` pair where both branches terminate. The `else` is structural noise — the reader cannot tell at a glance whether it exists to handle an actual continuation or simply mirrors the `then`. The fix is to drop `else` and let the second `Error` fall through naturally.
|
||||
|
||||
See sample: `no-else-after-terminating-statement.bad.al`.
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
codeunit 50231 "Sample No Space Paren Bad"
|
||||
{
|
||||
procedure Lookup(CustomerNo: Code[20])
|
||||
var
|
||||
Customer: Record Customer;
|
||||
GreetingMsg: Label 'Hello %1';
|
||||
begin
|
||||
if Customer.Get ( CustomerNo ) then
|
||||
Message ( GreetingMsg, Customer.Name );
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
codeunit 50230 "Sample No Space Paren Good"
|
||||
{
|
||||
procedure Lookup(CustomerNo: Code[20])
|
||||
var
|
||||
Customer: Record Customer;
|
||||
GreetingMsg: Label 'Hello %1';
|
||||
begin
|
||||
if Customer.Get(CustomerNo) then
|
||||
Message(GreetingMsg, Customer.Name);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [spacing, parenthesis, method-call, aa0002, codecop]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# No space between a method name and its opening parenthesis (CodeCop AA0002)
|
||||
|
||||
## Description
|
||||
|
||||
CodeCop AA0002 forbids whitespace between a procedure/method name and its `(`. `Customer.Get(CustomerNo)` is correct; `Customer.Get (CustomerNo)` is not. The rule applies to user-defined procedures, system methods (`Insert`, `FindFirst`, `CalcFields`), trigger-style invocations, and the parenthesised cast/conversion forms (`Format(Value)`, `CopyStr(Source, 1, 10)`). The whitespace between `(` and the first argument, and between the last argument and `)`, is also forbidden by the same rule.
|
||||
|
||||
## Best Practice
|
||||
|
||||
`Customer.Get(CustomerNo)`, `Customer.SetFilter("No.", '%1', '*A*')`, `Message(GreetingMsg, UserName)`. The standard AL formatter enforces this automatically.
|
||||
|
||||
See sample: `no-space-before-method-parenthesis.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`Customer.Get ( CustomerNo )`, `Message ( GreetingMsg, UserName )`. Both trip AA0002 and read as if the call had an extra unnamed parameter — a small but persistent friction every reader pays.
|
||||
|
||||
See sample: `no-space-before-method-parenthesis.bad.al`.
|
||||
22
microsoft/knowledge/style/object-name-30-char-limit.md
Normal file
22
microsoft/knowledge/style/object-name-30-char-limit.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [object-name, length, prefix, affix, 30-characters, appsource]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Keep object names within the 30-character platform limit
|
||||
|
||||
## Description
|
||||
|
||||
Business Central object names — for tables, pages, codeunits, reports, queries, XML ports, enums, and permission sets — are limited to 30 characters in total. AppSource and per-tenant extensions also have to carry a mandatory prefix or affix (typically 3–4 characters), which leaves roughly 26 characters for the descriptive part of the name. Names hitting the 30-character ceiling are routinely rejected at publish time, and over-aggressive abbreviation to fit (`CustLE`, `SIPoster`, `SalesInv`) makes the object name opaque to reviewers and to anyone reading dependency lists. The right move is to plan name length around the budget — descriptive base + prefix — not to discover the limit during AppSource validation.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Choose a clear, descriptive name in the 20–26-character range and reserve the remaining characters for the mandatory app prefix. `"Customer Ledger Entry"`, `"Sales Invoice Posting"`, `"Sales Invoice"` are descriptive and well under the budget. When you genuinely need to abbreviate, prefer abbreviations that are already established in BC (`Cust.`, `Vend.`, `Gen. Jnl.`, `WHSE`) over ad-hoc shortenings.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Names like `"CustLE"` or `"SIPoster"` that abbreviate beyond comprehensibility, or names like `"Customer Ledger Entry Posting Helper Codeunit"` that breach 30 characters and force a rename during publish.
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
table 50255 "Sample OptionCaption Bad"
|
||||
{
|
||||
fields
|
||||
{
|
||||
field(1; Status; Option)
|
||||
{
|
||||
Caption = 'Status';
|
||||
OptionMembers = Open,Released,Pending;
|
||||
}
|
||||
field(2; Priority; Option)
|
||||
{
|
||||
Caption = 'Priority';
|
||||
OptionMembers = Low,Medium,High,Critical;
|
||||
OptionCaption = 'Low,Medium,High';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,18 @@
|
|||
table 51112 "Style Sample Option Good"
|
||||
table 50254 "Sample OptionCaption Good"
|
||||
{
|
||||
fields
|
||||
{
|
||||
field(1; "Entry No."; Integer) { }
|
||||
field(10; Priority; Option)
|
||||
{
|
||||
OptionMembers = Low,Medium,High,Critical;
|
||||
OptionCaption = 'Low,Medium,High,Critical';
|
||||
}
|
||||
field(20; Status; Option)
|
||||
field(1; Status; Option)
|
||||
{
|
||||
Caption = 'Status';
|
||||
OptionMembers = Open,Released,Pending;
|
||||
OptionCaption = 'Open,Released,Pending';
|
||||
}
|
||||
}
|
||||
keys
|
||||
{
|
||||
key(PK; "Entry No.") { Clustered = true; }
|
||||
field(2; Priority; Option)
|
||||
{
|
||||
Caption = 'Priority';
|
||||
OptionMembers = Low,Medium,High,Critical;
|
||||
OptionCaption = 'Low,Medium,High,Critical';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [optioncaption, option, member-count, aa0221, aa0223, aa0224]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Option fields need `OptionCaption`, and its element count must match `OptionMembers` (CodeCop AA0221/AA0223/AA0224)
|
||||
|
||||
## Description
|
||||
|
||||
CodeCop AA0221 requires an `OptionCaption` on every option-type field that is not sourced from a table column (table-sourced option fields inherit the captions of the underlying field). AA0223 and AA0224 add two integrity checks: the number of comma-separated entries in `OptionCaption` must equal the number of entries in `OptionMembers`, and each caption must align by position with its member. The position alignment is what the platform uses to translate option values — the `OptionMembers` list never changes per locale, the `OptionCaption` list does. A mismatch in count or order produces silent corruption: the option `Released` shows the caption that belongs to `Pending`, and the bug is locale-dependent.
|
||||
|
||||
## Best Practice
|
||||
|
||||
`OptionMembers = Open,Released,Pending;` and `OptionCaption = 'Open,Released,Pending';` — same count, same order. When adding a new member, update both lines in the same commit.
|
||||
|
||||
See sample: `optioncaption-required-and-matches-membercount.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`OptionMembers = Open,Released,Pending;` with no `OptionCaption` at all (the user sees the raw English members and translation is impossible), or `OptionMembers = Low,Medium,High,Critical;` paired with `OptionCaption = 'Low,Medium,High';` — count mismatch, `Critical` displays as blank or carries the wrong caption depending on platform version.
|
||||
|
||||
See sample: `optioncaption-required-and-matches-membercount.bad.al`.
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [page-name, source-table, misleading, naming]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# A page or view name must describe the table it shows
|
||||
|
||||
## Description
|
||||
|
||||
A page (or filtered page View) whose name references one entity but whose `SourceTable` is a different entity misleads every consumer of the object's metadata. A page named `"Items with Negative Inventory"` that sources `"Stockkeeping Unit"` looks like a list of items in the search bar and in role explorer, but presents stockkeeping-unit fields and behaviour. The fix is either to rename the page to match the source table — `"Stockkeeping Units with Negative Inventory"` — or to change the source table to the entity the name promises. The choice depends on which the actual users are asking for; the constraint is that the two MUST agree.
|
||||
|
||||
The rule extends to filtered Views declared inside a page: the `View` name should describe the filter applied to the page's existing source, not introduce a different entity.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Read the page name out loud and ask: "If a user typed this into the search bar, would they expect to see rows from `<SourceTable>`?" If the answer is no, rename one side or the other. The same check applies whenever the source table changes — the name has to follow.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`page "Items with Negative Inventory" { SourceTable = "Stockkeeping Unit"; … }`. The Tell-Don't-Ask name asserts items; the source contradicts it. Reviewers should flag every mismatch they spot, even when both sides "make sense individually" — they have to agree.
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
codeunit 51115 "Style Sample ErrorParams Bad"
|
||||
{
|
||||
procedure Fail(CustomerNo: Code[20])
|
||||
var
|
||||
CustomerNotFoundErr: Label 'Customer %1 does not exist.', Comment = '%1 = Customer No.';
|
||||
begin
|
||||
// Pre-built Text to Error: translation skipped, telemetry opaque.
|
||||
Error(StrSubstNo(CustomerNotFoundErr, CustomerNo));
|
||||
|
||||
// Concatenation: translation skipped, hard-coded delimiters baked in.
|
||||
Error('Customer ' + CustomerNo + ' not found');
|
||||
end;
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
codeunit 51114 "Style Sample ErrorParams Good"
|
||||
{
|
||||
procedure Fail(CustomerNo: Code[20]; DocumentNo: Code[20])
|
||||
var
|
||||
CustomerNotFoundErr: Label 'Customer %1 does not exist for document %2.',
|
||||
Comment = '%1 = Customer No., %2 = Document No.';
|
||||
begin
|
||||
// Label + arguments passed directly. Translations apply; telemetry classifies per field.
|
||||
Error(CustomerNotFoundErr, CustomerNo, DocumentNo);
|
||||
end;
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [error, label, strsubstno, concatenation, telemetry, aa0216, aa0217]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Pass Error parameters directly to the Label; do not pre-build with StrSubstNo or concatenation
|
||||
|
||||
## Description
|
||||
|
||||
`Error` accepts a Label and its substitution parameters directly (`Error(CustomerNotFoundErr, CustomerNo, DocumentNo)`). Pre-building the message via `StrSubstNo` and passing the resulting Text, or concatenating parts with `+` and passing the result, compiles but produces two distinct regressions. The localization pipeline can only translate the Label; a pre-built Text is passed through untouched, so non-English users see the English template. Platform telemetry inspects the Label's placeholder arguments for DataClassification; a pre-built Text is opaque, so PII in the arguments is logged verbatim (see `strsubstno-prebuild-breaks-error-telemetry-classification` in the privacy domain).
|
||||
|
||||
## Best Practice
|
||||
|
||||
Declare the Label with placeholders and pass arguments directly to Error: `Error(CustomerNotFoundErr, CustomerNo, DocumentNo)`. Use `Comment` on the Label to document each placeholder (see `include-comment-on-labels-with-placeholders`). `Error('')` is acceptable when the caller is responsible for the surfaced error.
|
||||
|
||||
See sample: `pass-parameters-directly-to-error-no-strsubstno.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`Error(StrSubstNo(CustomerNotFoundErr, CustomerNo))` — loses translation. `Error(CustomerNotFoundErr + ': ' + CustomerNo)` — loses translation, concatenates hard-coded delimiters. `Error('Customer ' + CustomerNo + ' not found')` — uses no Label at all.
|
||||
|
||||
See sample: `pass-parameters-directly-to-error-no-strsubstno.bad.al`.
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
codeunit 51105 "Style Sample TempPrefix Bad"
|
||||
{
|
||||
procedure BuildWorkingSet()
|
||||
var
|
||||
WIPBuffer: Record "Job WIP Buffer" temporary;
|
||||
Customer: Record Customer;
|
||||
begin
|
||||
// Call sites read as persistent. A reviewer cannot tell at a glance
|
||||
// whether DeleteAll hits the database or the in-memory buffer.
|
||||
WIPBuffer.DeleteAll();
|
||||
if Customer.FindSet() then
|
||||
repeat
|
||||
WIPBuffer.Init();
|
||||
WIPBuffer.Insert();
|
||||
until Customer.Next() = 0;
|
||||
end;
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
codeunit 51104 "Style Sample TempPrefix Good"
|
||||
{
|
||||
procedure BuildWorkingSet()
|
||||
var
|
||||
TempJobWIPBuffer: Record "Job WIP Buffer" temporary;
|
||||
Customer: Record Customer;
|
||||
begin
|
||||
// Every read site shows whether the variable is temporary.
|
||||
TempJobWIPBuffer.DeleteAll();
|
||||
if Customer.FindSet() then
|
||||
repeat
|
||||
TempJobWIPBuffer.Init();
|
||||
TempJobWIPBuffer.Insert();
|
||||
until Customer.Next() = 0;
|
||||
end;
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [temporary, record, variable, prefix, naming, temp]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Prefix temporary record variables with "Temp"
|
||||
|
||||
## Description
|
||||
|
||||
A `Record X temporary` variable behaves differently from a persistent Record variable of the same type: Insert/Modify/Delete mutate an in-memory buffer, not the underlying table. Code that mixes persistent and temporary variables of the same type is a recurring source of data-loss bugs — a helper that does `DeleteAll` on what the caller believed was a temporary buffer wipes the real table. The convention across Business Central is to prefix every temporary record variable with `Temp` (`TempJobWIPBuffer`, `TempSalesLine`, `TempCustomer`) so the distinction is visible at every read site, not only at the declaration.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Prefix every temporary-record variable with `Temp`. The prefix goes on the variable name, not the type; the `temporary` keyword remains on the declaration. Matching the prefix against the declaration makes it a one-line check in code review: if the name starts with `Temp`, the declaration ends in `temporary`, and vice versa.
|
||||
|
||||
See sample: `prefix-temporary-record-variables-with-temp.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`WIPBuffer: Record "Job WIP Buffer" temporary` — the variable reads like a persistent record in every call site below the declaration. A reviewer scanning a mutation call (`WIPBuffer.DeleteAll()`) cannot tell from the call site whether the effect is in-memory or production.
|
||||
|
||||
See sample: `prefix-temporary-record-variables-with-temp.bad.al`.
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
codeunit 51119 "Style Sample Parentheses Bad"
|
||||
{
|
||||
procedure Example(var Customer: Record Customer)
|
||||
var
|
||||
TempBuffer: Record "Integer" temporary;
|
||||
begin
|
||||
// Parentheses omitted. The call site reads like a field access.
|
||||
Customer.Init;
|
||||
TempBuffer.DeleteAll;
|
||||
if Customer.FindFirst then
|
||||
;
|
||||
end;
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
codeunit 51118 "Style Sample Parentheses Good"
|
||||
{
|
||||
procedure Example(var Customer: Record Customer)
|
||||
var
|
||||
TempBuffer: Record "Integer" temporary;
|
||||
begin
|
||||
Customer.Init();
|
||||
TempBuffer.DeleteAll();
|
||||
if Customer.FindFirst() then
|
||||
;
|
||||
end;
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [parentheses, function-call, aa0008, invocation]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Every function call carries parentheses, even with no arguments
|
||||
|
||||
## Description
|
||||
|
||||
AL allows `Customer.Init`, `TempBuffer.DeleteAll`, and `Customer.FindFirst` without trailing parentheses when the method takes no parameters. CodeCop rule AA0008 requires the parentheses anyway. The reason is readability: without `()`, the reader has to know the member is a method and not a property — an ambiguity that resolves differently for the platform's own APIs (FindFirst is a method; `Name` is a field). With `()`, the call site is visibly a method invocation and a simple grep for `Init(` or `DeleteAll(` finds every usage.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Always write parentheses on method calls, even when empty: `Customer.Init()`, `TempBuffer.DeleteAll()`, `if Customer.FindFirst() then`. Apply the rule to platform methods and to user-defined procedures alike.
|
||||
|
||||
See sample: `require-parentheses-on-function-calls.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`Customer.Init;`, `TempBuffer.DeleteAll;`, `if Customer.FindFirst then` — all three compile but obscure what is a call and what is a field access. The inconsistency compounds when the same codebase has both conventions.
|
||||
|
||||
See sample: `require-parentheses-on-function-calls.bad.al`.
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
codeunit 50233 "Sample Not Spacing Bad"
|
||||
{
|
||||
procedure Check(): Boolean
|
||||
var
|
||||
Customer: Record Customer;
|
||||
begin
|
||||
if NOT Customer.IsEmpty() then
|
||||
exit(true);
|
||||
exit(false);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
codeunit 50232 "Sample Not Spacing Good"
|
||||
{
|
||||
procedure Check(): Boolean
|
||||
var
|
||||
Customer: Record Customer;
|
||||
begin
|
||||
if not Customer.IsEmpty() then
|
||||
exit(true);
|
||||
exit(false);
|
||||
end;
|
||||
}
|
||||
26
microsoft/knowledge/style/single-space-after-not-operator.md
Normal file
26
microsoft/knowledge/style/single-space-after-not-operator.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [spacing, not, operator, aa0003, codecop]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Exactly one space between `not` and its argument (CodeCop AA0003)
|
||||
|
||||
## Description
|
||||
|
||||
CodeCop AA0003 requires exactly one space between the `not` operator and the expression it negates. `if not Customer.FindFirst() then …` is correct; `if not Customer.FindFirst() then …` (two spaces) and `if notCustomer.FindFirst() then …` (zero — which fails parsing anyway) are not. The rule is also the place where uppercase `NOT` is flagged in combination with CodeCop AA0241 (reserved keywords must be lowercase): `if NOT Condition then` is doubly wrong.
|
||||
|
||||
## Best Practice
|
||||
|
||||
`if not Condition then`, `if not Customer.IsEmpty() then`, `exit(not Result)`. One space, lowercase keyword, no parentheses around the bare boolean.
|
||||
|
||||
See sample: `single-space-after-not-operator.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`if NOT condition then`, `if not condition then`, `if !condition then` (which is not even AL — `!` is not a negation operator in AL). All three either trip AA0003 / AA0241 or fail to compile.
|
||||
|
||||
See sample: `single-space-after-not-operator.bad.al`.
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
codeunit 50229 "Sample Spaces Op Bad"
|
||||
{
|
||||
procedure Compute(Amount: Decimal; Quantity: Decimal): Decimal
|
||||
var
|
||||
Price: Decimal;
|
||||
begin
|
||||
Price:=Amount*Quantity;
|
||||
if (Amount>0)and(Quantity>0) then
|
||||
exit(Price);
|
||||
exit(0);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
codeunit 50228 "Sample Spaces Op Good"
|
||||
{
|
||||
procedure Compute(Amount: Decimal; Quantity: Decimal): Decimal
|
||||
var
|
||||
Price: Decimal;
|
||||
begin
|
||||
Price := Amount * Quantity;
|
||||
if (Amount > 0) and (Quantity > 0) then
|
||||
exit(Price);
|
||||
exit(0);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [spacing, binary-operator, aa0001, codecop, formatting]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# One space on each side of every binary operator (CodeCop AA0001)
|
||||
|
||||
## Description
|
||||
|
||||
CodeCop AA0001 requires exactly one space on each side of every binary operator: assignment (`:=`), arithmetic (`+`, `-`, `*`, `/`, `mod`, `div`), comparison (`=`, `<>`, `<`, `<=`, `>`, `>=`), logical (`and`, `or`, `xor`), and string concatenation. `x:=1+2`, `Price:=Amount*Quantity`, `if a=b then`, and `if a and b then` all violate the rule. The rule applies to the binary use of `-` (subtraction); the unary minus (`-Profit`) takes no leading space.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Write `x := 1 + 2`, `Price := Amount * Quantity`, `if a = b then`, `if a and b then`. The standard AL formatter inserts these spaces automatically; running `Alt+Shift+F` (Format Document) in the AL extension is the simplest way to bring an entire file into compliance.
|
||||
|
||||
See sample: `single-space-around-binary-operators.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`x:=1+2;`, `Price:=Amount*Quantity;`, `if a=b then`, `if a and b then`. All trip AA0001.
|
||||
|
||||
See sample: `single-space-around-binary-operators.bad.al`.
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
codeunit 50217 "Sample Temp Prefix Bad"
|
||||
{
|
||||
procedure BuildBuffer(var SalesLine: Record "Sales Line" temporary)
|
||||
var
|
||||
WIPBuffer: Record "Job WIP Buffer" temporary;
|
||||
begin
|
||||
WIPBuffer.Init();
|
||||
WIPBuffer.Insert();
|
||||
SalesLine.Init();
|
||||
SalesLine.Insert();
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
codeunit 50216 "Sample Temp Prefix Good"
|
||||
{
|
||||
procedure BuildBuffer(var TempSalesLine: Record "Sales Line" temporary)
|
||||
var
|
||||
TempJobWIPBuffer: Record "Job WIP Buffer" temporary;
|
||||
begin
|
||||
TempJobWIPBuffer.Init();
|
||||
TempJobWIPBuffer.Insert();
|
||||
TempSalesLine.Init();
|
||||
TempSalesLine.Insert();
|
||||
end;
|
||||
}
|
||||
26
microsoft/knowledge/style/temporary-variable-temp-prefix.md
Normal file
26
microsoft/knowledge/style/temporary-variable-temp-prefix.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [temporary, temp, prefix, record-variable, naming]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Prefix temporary record variables with `Temp`
|
||||
|
||||
## Description
|
||||
|
||||
A `Record` variable declared with the `temporary` modifier behaves nothing like a normal record variable: it never touches the database, holds rows only for the lifetime of the variable, and is not visible to filters or queries on the underlying table. The BC convention is to make that difference visible at every call site by prefixing the variable name with `Temp` — `TempJobWIPBuffer`, `TempSalesLine`, `TempIntegerBuffer`. The convention is load-bearing for code review: when a reader sees `SalesLine.Insert()`, they expect a database write; when they see `TempSalesLine.Insert()`, they know it is an in-memory buffer.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Every variable of type `Record X temporary` must start with `Temp`. The same applies to parameters: a procedure that receives a temporary record as a buffer names the parameter `TempBuffer`, `TempSalesLine`, and so on. The convention extends naturally to derived names — `TempJobWIPBufferCopy`, `TempSourceSalesLine` — anything that starts with `Temp` is in-memory.
|
||||
|
||||
See sample: `temporary-variable-temp-prefix.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`WIPBuffer: Record "Job WIP Buffer" temporary;` reads at the call site as if it were a database operation: `WIPBuffer.Insert()` looks identical to a write to the underlying table. The reader has to scroll back to the declaration to discover that this is in-memory, every time.
|
||||
|
||||
See sample: `temporary-variable-temp-prefix.bad.al`.
|
||||
14
microsoft/knowledge/style/this-keyword-in-codeunits.bad.al
Normal file
14
microsoft/knowledge/style/this-keyword-in-codeunits.bad.al
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
codeunit 50215 "Sample This Bad"
|
||||
{
|
||||
procedure ProcessRecord(Customer: Record Customer)
|
||||
var
|
||||
Helper: Codeunit "Sample This Helper";
|
||||
begin
|
||||
ValidateCustomer(Customer);
|
||||
Helper.DoWork();
|
||||
end;
|
||||
|
||||
local procedure ValidateCustomer(Customer: Record Customer)
|
||||
begin
|
||||
end;
|
||||
}
|
||||
14
microsoft/knowledge/style/this-keyword-in-codeunits.good.al
Normal file
14
microsoft/knowledge/style/this-keyword-in-codeunits.good.al
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
codeunit 50214 "Sample This Good"
|
||||
{
|
||||
procedure ProcessRecord(Customer: Record Customer)
|
||||
var
|
||||
Helper: Codeunit "Sample This Helper";
|
||||
begin
|
||||
this.ValidateCustomer(Customer);
|
||||
Helper.DoWork(this);
|
||||
end;
|
||||
|
||||
local procedure ValidateCustomer(Customer: Record Customer)
|
||||
begin
|
||||
end;
|
||||
}
|
||||
26
microsoft/knowledge/style/this-keyword-in-codeunits.md
Normal file
26
microsoft/knowledge/style/this-keyword-in-codeunits.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [this, codeunit, self-reference, aa0248, scope]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Use the `this` keyword for self-reference inside codeunits (CodeCop AA0248)
|
||||
|
||||
## Description
|
||||
|
||||
CodeCop AA0248 recommends prefixing self-references inside a codeunit with `this`. `this.ValidateCustomer(Customer)` is unambiguous: the call resolves to a procedure on the current codeunit, not to a local variable or a procedure on a passed-in object. Without the prefix, a reader of a 200-line procedure has to scan the whole codeunit to confirm whether `ValidateCustomer` is local. `this` also makes it possible to pass the current codeunit as an argument — `SomeOtherCodeunit.DoWork(this)` — which is the only way to expose the running codeunit instance to a collaborator. The rule applies only to codeunits, not to pages, reports, queries, or tables — those object types do not have a `this` reference in AL.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Inside a codeunit, prefix calls to procedures and accesses to global variables on the same codeunit with `this.`, and pass `this` when an external codeunit needs a reference to the running instance.
|
||||
|
||||
See sample: `this-keyword-in-codeunits.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Calling a codeunit-local procedure as a bare identifier (`ValidateCustomer(Customer)`) when other readings are possible. The ambiguity costs reading time on every encounter and grows with codeunit size.
|
||||
|
||||
See sample: `this-keyword-in-codeunits.bad.al`.
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
page 50251 "Sample Tooltip Bad"
|
||||
{
|
||||
PageType = Card;
|
||||
SourceTable = Customer;
|
||||
layout
|
||||
{
|
||||
area(Content)
|
||||
{
|
||||
group(General)
|
||||
{
|
||||
field("No."; Rec."No.")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
}
|
||||
field(Amount; Rec."Balance (LCY)")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue