mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-06 09:26:52 +01:00
Extract 55 knowledge articles from BC review-agent prompt
Adds 55 articles (plus 76 code samples) spanning four new domains and two existing domains, extracted from the internal Business Central review-agent prompt. Content was filtered against BCQuality's remedial-knowledge premise: each article encodes BC-specific behaviour, a CodeCop rule, a platform API semantic, or an anti-false-positive guideline that a capable LLM would otherwise get wrong. New domains: - privacy (11 articles): DataClassification inheritance semantics, the StrSubstNo-defeats-Error-telemetry-classification pitfall, Privacy Notice consent for outgoing requests, anti-false-positives for pages and in-memory data. - upgrade (11 articles): upgrade-codeunit structure, upgrade-tag lifecycle and registration, protected DB reads, DataTransfer for large datasets, InitValue semantics, enum-ordinal preservation, obsolete-workflow, first-install detection. - ui (9 articles): caption capitalization by phrase type, tooltip voice, teaching-tip vs tooltip, tour-tip conventions, character limits, banned terms, ampersand handling, title punctuation. - style (11 articles): label-suffix convention, API page naming, temporary-variable prefix, label properties (Comment/Locked), named invocations, FieldCaption in user messages, OptionCaption pairing, Error-parameter passing, `this` keyword, required parentheses, file naming. Gaps in existing domains: - performance (11 articles): production-scale table catalog (no row counts, per internal-data concern), anti-false-positive for bounded tables, guard-before-Get ordering, redundant-Get-in-OnAfterGetRecord, LockTable in read-only helpers, combined ModifyAll passes, writes in OnAfterGetRecord, SetLoadFields heuristics, temporary-table regressions, FlowField source-table widening, MaintainSQLIndex disabling SIFT. - security (2 articles): environment-specific hardcoded GUIDs, ValidateTableRelation=false on user input. Intentionally excluded: specific production P95 row-count numbers (aggregated internal telemetry); rewritten as categorical guidance on which tables to treat as production-scale without publishing sizes. All articles use `bc-version: [all]` (applies to every BC version, per the new schema sentinel). Validator passes with 0 errors / 0 warnings.
This commit is contained in:
parent
9a4198eb28
commit
e570d6113f
131 changed files with 2799 additions and 0 deletions
|
|
@ -0,0 +1,17 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
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;
|
||||
}
|
||||
26
microsoft/knowledge/style/apply-approved-label-suffixes.md
Normal file
26
microsoft/knowledge/style/apply-approved-label-suffixes.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
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,22 @@
|
|||
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.") { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
page 51102 "Style Sample ApiPage Good"
|
||||
{
|
||||
PageType = API;
|
||||
APIPublisher = 'contoso';
|
||||
APIGroup = 'app1';
|
||||
APIVersion = 'v2.0';
|
||||
EntityName = 'customer';
|
||||
EntitySetName = 'customers';
|
||||
SourceTable = Customer;
|
||||
DelayedInsert = true;
|
||||
ODataKeyFields = SystemId;
|
||||
|
||||
layout
|
||||
{
|
||||
area(Content)
|
||||
{
|
||||
repeater(Group)
|
||||
{
|
||||
field(systemId; Rec.SystemId) { }
|
||||
field(number; Rec."No.") { }
|
||||
field(displayName; Rec.Name) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
microsoft/knowledge/style/follow-api-page-naming-rules.md
Normal file
26
microsoft/knowledge/style/follow-api-page-naming-rules.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
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,15 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
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,22 @@
|
|||
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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
table 51112 "Style Sample Option 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)
|
||||
{
|
||||
OptionMembers = Open,Released,Pending;
|
||||
OptionCaption = 'Open,Released,Pending';
|
||||
}
|
||||
}
|
||||
keys
|
||||
{
|
||||
key(PK; "Entry No.") { Clustered = true; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
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`.
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
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,13 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
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`.
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
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`.
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
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,12 @@
|
|||
codeunit 51111 "Style Sample FieldCaption Bad"
|
||||
{
|
||||
procedure Example(var SalesLine: Record "Sales Line")
|
||||
var
|
||||
UpdateLocationQst: Label 'Update the %1?', Comment = '%1 = field';
|
||||
begin
|
||||
// FieldName/TableName return English identifiers. User with a non-English
|
||||
// locale sees the English "Location Code" inside an otherwise translated dialog.
|
||||
if not Confirm(UpdateLocationQst, true, SalesLine.FieldName("Location Code")) then
|
||||
exit;
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
codeunit 51110 "Style Sample FieldCaption Good"
|
||||
{
|
||||
procedure Example(var SalesLine: Record "Sales Line")
|
||||
var
|
||||
UpdateLocationQst: Label 'Update the %1?', Comment = '%1 = field caption';
|
||||
TableUpdatedMsg: Label 'Updated %1.', Comment = '%1 = table caption';
|
||||
begin
|
||||
// Captions are localized for the current user's language.
|
||||
if not Confirm(UpdateLocationQst, true, SalesLine.FieldCaption("Location Code")) then
|
||||
exit;
|
||||
Message(TableUpdatedMsg, SalesLine.TableCaption());
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [fieldcaption, tablecaption, fieldname, tablename, localization, user-message]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Use FieldCaption and TableCaption in user messages, not FieldName and TableName
|
||||
|
||||
## Description
|
||||
|
||||
`FieldName` and `TableName` return the object's internal identifier in English — the name the developer typed into the declaration. `FieldCaption` and `TableCaption` return the translated caption for the current user's language. In user-facing messages, errors, confirmations, and notifications, the two pairs diverge the moment the user is running a non-English locale: `FieldName("Location Code")` reads `Location Code` in every language, while `FieldCaption("Location Code")` reads the translated equivalent. Using the wrong one leaks the English identifier into a localized UI and defeats the product's translation work.
|
||||
|
||||
## Best Practice
|
||||
|
||||
In any string the user will read, use `FieldCaption(<field>)` and `TableCaption`. Reserve `FieldName` and `TableName` for diagnostic and telemetry contexts where the stable English identifier is preferable. The same rule applies to `XmlPort`, `Query`, and other objects with a caption/name pair.
|
||||
|
||||
See sample: `use-fieldcaption-and-tablecaption-in-user-messages.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`Confirm(UpdateLocationQst, true, FieldName("Location Code"))`, `Message('Updated %1', TableName())` — both surface English identifiers to a user whose entire UI is in a different language.
|
||||
|
||||
See sample: `use-fieldcaption-and-tablecaption-in-user-messages.bad.al`.
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
codeunit 51109 "Style Sample NamedInvoke Bad"
|
||||
{
|
||||
procedure Example(var SalesShptLine: Record "Sales Shipment Line")
|
||||
begin
|
||||
// Numeric ID. The reader has to look up 525 and 206 to know what is called.
|
||||
// If either object is renumbered in a future release, this call silently retargets.
|
||||
Page.RunModal(525, SalesShptLine);
|
||||
Report.Run(206, true);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
codeunit 51108 "Style Sample NamedInvoke Good"
|
||||
{
|
||||
procedure Example(var SalesShptLine: Record "Sales Shipment Line")
|
||||
begin
|
||||
// Named invocation: reviewer sees the object, rename of 525 cannot retarget.
|
||||
Page.RunModal(Page::"Posted Sales Shipment Lines", SalesShptLine);
|
||||
Report.Run(Report::"Sales - Invoice", true);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [object-id, page-run, report-run, codeunit-run, named-invocation]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Invoke objects by name, not by numeric ID
|
||||
|
||||
## Description
|
||||
|
||||
AL supports calling `Page.RunModal(525, ...)` or `Report.Run(206, ...)` with a bare numeric ID. The platform accepts the number, but the call site loses every signal that makes the code reviewable and refactor-safe: the reader cannot tell which object is being invoked without looking up 525 in the object catalog, and the renumbering of an object in a future release (legal in AL — IDs are not a stable contract) silently retargets the call to a different object. The `Page::"..."` / `Report::"..."` syntax compiles to the same runtime call but makes the target explicit and binds by name, which is the stable identity.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Write `Page.RunModal(Page::"Posted Sales Shipment Lines", SalesShptLine)` and `Report.Run(Report::"Sales - Invoice", true)`. Apply the same rule to `Codeunit.Run`, `XmlPort.Run`, and similar runtime invocations. Reserve numeric IDs for diagnostic tooling that genuinely needs them.
|
||||
|
||||
See sample: `use-named-invocations-instead-of-object-ids.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`Page.RunModal(525, SalesShptLine);` — the reader has no idea what page 525 is without a lookup, and a future rename of page 525 or renumber of "Posted Sales Shipment Lines" produces a silent mismatch.
|
||||
|
||||
See sample: `use-named-invocations-instead-of-object-ids.bad.al`.
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
codeunit 51117 "Style Sample ThisKeyword Bad"
|
||||
{
|
||||
procedure ProcessRecord(var Customer: Record Customer)
|
||||
begin
|
||||
// Ambiguous: is ValidateCustomer a local, a global, or a method on
|
||||
// another codeunit in scope?
|
||||
ValidateCustomer(Customer);
|
||||
|
||||
// No way to pass the current codeunit without `this`.
|
||||
end;
|
||||
|
||||
local procedure ValidateCustomer(var Customer: Record Customer)
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
codeunit 51116 "Style Sample ThisKeyword Good"
|
||||
{
|
||||
procedure ProcessRecord(var Customer: Record Customer)
|
||||
var
|
||||
Other: Codeunit "Style Sample ThisKeyword Good";
|
||||
begin
|
||||
// Clearly this codeunit's method.
|
||||
this.ValidateCustomer(Customer);
|
||||
|
||||
// Only way to pass the current codeunit as an argument.
|
||||
Other.DoWith(this);
|
||||
end;
|
||||
|
||||
local procedure ValidateCustomer(var Customer: Record Customer)
|
||||
begin
|
||||
end;
|
||||
|
||||
procedure DoWith(var Helper: Codeunit "Style Sample ThisKeyword Good")
|
||||
begin
|
||||
end;
|
||||
}
|
||||
26
microsoft/knowledge/style/use-this-keyword-in-codeunits.md
Normal file
26
microsoft/knowledge/style/use-this-keyword-in-codeunits.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [this, codeunit, self-reference, aa0248, readability]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Use the `this` keyword for codeunit self-reference
|
||||
|
||||
## Description
|
||||
|
||||
CodeCop rule AA0248 recommends the `this` keyword inside codeunit procedures when referring to the codeunit's own members or passing the codeunit itself to another procedure. AL's scope resolution otherwise blurs global-variable access, local-variable access, and same-codeunit method calls into the same unqualified syntax — a reader of `ValidateCustomer(Customer)` cannot tell at the call site whether `ValidateCustomer` is a local, a global, or a method on a different codeunit in scope. `this.ValidateCustomer(Customer)` removes the ambiguity, and `OtherCodeunit.DoWork(this)` is the only way to pass the current codeunit as a parameter.
|
||||
|
||||
## Best Practice
|
||||
|
||||
In codeunits, prefix same-codeunit method calls with `this.` when the call is ambiguous or when the scope spans more than a few lines. When the current codeunit needs to be passed as an argument, write `this` — there is no alternative syntax. The rule applies to codeunits; pages, reports, and tables have their own scoping.
|
||||
|
||||
See sample: `use-this-keyword-in-codeunits.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`ValidateCustomer(Customer); SomeOtherCodeunit.DoWork(/* this codeunit? */);` — the first call has ambiguous origin, and the second cannot pass the current codeunit without `this`. The style becomes load-bearing as the codeunit grows past a few small procedures.
|
||||
|
||||
See sample: `use-this-keyword-in-codeunits.bad.al`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue