mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-06 07:06:54 +01:00
Merge main into AL development guidance
Reconcile the read-only plan-enrichment contracts with main's folder-review inputs and documentation structure. Record Windows alternate streams in runner evidence and clear the regression harness exit status after expected negative probes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 638b66d2-9f06-4f60-8781-808709e1485c
This commit is contained in:
commit
332947bcdb
298 changed files with 1818 additions and 780 deletions
|
|
@ -19,10 +19,10 @@ The reviewer signal is "this is a new top-level card or list page in an app whos
|
|||
|
||||
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`.
|
||||
See sample: [`abouttitle-abouttext-teaching-tips.good.al`](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`.
|
||||
See sample: [`abouttitle-abouttext-teaching-tips.bad.al`](abouttitle-abouttext-teaching-tips.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ API pages — pages declared with `PageType = API` — surface as OData/JSON end
|
|||
|
||||
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`.
|
||||
See sample: [`api-page-camelcase-properties.good.al`](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`.
|
||||
See sample: [`api-page-camelcase-properties.bad.al`](api-page-camelcase-properties.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ On a normal page, `DelayedInsert = false` is the default: the record is inserted
|
|||
|
||||
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`.
|
||||
See sample: [`api-page-delayedinsert-true.good.al`](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`.
|
||||
See sample: [`api-page-delayedinsert-true.bad.al`](api-page-delayedinsert-true.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ application-area: [all]
|
|||
|
||||
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`.
|
||||
See sample: [`api-page-entity-naming-singular-plural.good.al`](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`.
|
||||
See sample: [`api-page-entity-naming-singular-plural.bad.al`](api-page-entity-naming-singular-plural.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ The `APIVersion` property on an API page is part of the public URL path: `/api/<
|
|||
|
||||
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`.
|
||||
See sample: [`api-page-version-format.good.al`](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`.
|
||||
See sample: [`api-page-version-format.bad.al`](api-page-version-format.bad.al).
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ For targets before runtime 10.0, child controls do not inherit and must also set
|
|||
|
||||
On runtime 10.0 or later, set a suitable page-level default and override only controls that belong to a narrower area. Set `ApplicationArea` explicitly on every control or action introduced by a page or report extension.
|
||||
|
||||
See sample: `applicationarea-required-on-page-controls.good.al`.
|
||||
See sample: [`applicationarea-required-on-page-controls.good.al`](applicationarea-required-on-page-controls.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
A page object that defines neither a parent nor child value, or an extension control that assumes it inherits from the base page. The control has no effective application area and can be hidden or rejected by analyzer validation.
|
||||
|
||||
See sample: `applicationarea-required-on-page-controls.bad.al`.
|
||||
See sample: [`applicationarea-required-on-page-controls.bad.al`](applicationarea-required-on-page-controls.bad.al).
|
||||
|
||||
## Reference
|
||||
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ When a compound block follows `then`, `else`, or `do`, the `begin` keyword must
|
|||
|
||||
`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`.
|
||||
See sample: [`begin-on-same-line-as-then-else-do.good.al`](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`.
|
||||
See sample: [`begin-on-same-line-as-then-else-do.bad.al`](begin-on-same-line-as-then-else-do.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ CodeCop AA0018 requires that the block-introducing keywords `if`, `repeat`, `unt
|
|||
|
||||
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`.
|
||||
See sample: [`block-keywords-start-new-line.good.al`](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`.
|
||||
See sample: [`block-keywords-start-new-line.bad.al`](block-keywords-start-new-line.bad.al).
|
||||
|
|
|
|||
|
|
@ -9,16 +9,22 @@ page 50253 "Sample Caption Bad"
|
|||
{
|
||||
group(General)
|
||||
{
|
||||
field("Customer No."; Rec."No.")
|
||||
Caption = 'General';
|
||||
field(CustomerNoValue; CustomerNoValue)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the customer number to look up.';
|
||||
}
|
||||
field("Customer Name"; Rec.Name)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
Caption = '';
|
||||
ToolTip = 'Specifies the customer name shown on sales documents.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var
|
||||
CustomerNoValue: Code[20];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,36 @@
|
|||
// BC24 / runtime 13.0 or later for table-field tooltips.
|
||||
table 50252 "Sample Caption Source"
|
||||
{
|
||||
Caption = 'Caption Source';
|
||||
DataClassification = CustomerContent;
|
||||
|
||||
fields
|
||||
{
|
||||
field(1; "No."; Code[20])
|
||||
{
|
||||
Caption = 'No.';
|
||||
ToolTip = 'Specifies the unique number used to distinguish this customer record from other records.';
|
||||
}
|
||||
field(2; Name; Text[100])
|
||||
{
|
||||
Caption = 'Name';
|
||||
ToolTip = 'Specifies the name used to identify the customer alongside the unique customer number.';
|
||||
}
|
||||
}
|
||||
|
||||
keys
|
||||
{
|
||||
key(PK; "No.")
|
||||
{
|
||||
Clustered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
page 50252 "Sample Caption Good"
|
||||
{
|
||||
PageType = Card;
|
||||
SourceTable = Customer;
|
||||
SourceTable = "Sample Caption Source";
|
||||
|
||||
layout
|
||||
{
|
||||
|
|
@ -10,19 +39,25 @@ page 50252 "Sample Caption Good"
|
|||
group(General)
|
||||
{
|
||||
Caption = 'General';
|
||||
field("Customer No."; Rec."No.")
|
||||
field("No."; Rec."No.")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
Caption = 'Customer No.';
|
||||
ToolTip = 'Specifies the customer number.';
|
||||
}
|
||||
field("Customer Name"; Rec.Name)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
Caption = 'Customer Name';
|
||||
ToolTip = 'Specifies the customer name.';
|
||||
}
|
||||
field(DisplayValue; DisplayValue)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
Caption = 'Display Value';
|
||||
ToolTip = 'Specifies temporary text for this page; the text is not saved in the customer record.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var
|
||||
DisplayValue: Text[100];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,38 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [caption, page-field, aa0225, aa0226, codecop, captionclass]
|
||||
keywords: [caption, page-field, source-field, inheritance, aa0225, aa0226, codecop, captionclass, false-positive]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Every page field needs a `Caption` (CodeCop AA0225/AA0226)
|
||||
# Page fields can inherit their source table field's `Caption`
|
||||
|
||||
## 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.
|
||||
A page field bound to a table field inherits the source field's `Caption` unless the page overrides it. An inherited caption is valid, user-facing, and translatable; omitting a page-level `Caption` does not mean the control displays an internal identifier or loses translations. CodeCop AA0225/AA0226 concern missing or empty captions, not a requirement to duplicate a caption already supplied by the source table field.
|
||||
|
||||
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.
|
||||
Redundant page-level captions compile successfully, so compiler-error recovery does not prevent an agent from adding them. This guidance prevents that false positive rather than replacing analyzer diagnostics.
|
||||
|
||||
Controls bound to variables or expressions cannot rely on table-field caption inheritance. For user-facing fields that need a label, supply a `Caption` or a `CaptionClass` that resolves to the intended caption. API pages are not human-facing UI; do not apply this UI-label guidance to their API contract names.
|
||||
|
||||
## 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.
|
||||
Define the shared caption on the table field and let bound page fields inherit it. Add a page-level `Caption` only when there is no suitable inherited caption or the page genuinely needs different wording. Keep a valid `CaptionClass` rather than adding a redundant literal caption.
|
||||
|
||||
See sample: `caption-required-on-page-fields.good.al`.
|
||||
Before reporting a missing caption, inspect the binding and source field, including dependency symbols when needed. If the source definition is unavailable, do not treat an omitted page property as proof that the caption is missing. Caption and tooltip requirements are separate: do not add a `ToolTip` just because a caption is being reviewed; see [tooltip inheritance guidance](tooltip-required-on-page-fields.md).
|
||||
|
||||
See sample: [`caption-required-on-page-fields.good.al`](caption-required-on-page-fields.good.al). Caption inheritance applies across BC versions; the sample uses BC24/runtime 13.0 or later to also define tooltips on its table fields.
|
||||
|
||||
## 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.
|
||||
A user-facing field that needs a label but has no non-empty explicit or inherited caption and no resolving `CaptionClass` has a genuine labeling gap. This includes `Caption = '';` when no `CaptionClass` supplies the label. A variable name alone is not a translatable caption.
|
||||
|
||||
See sample: `caption-required-on-page-fields.bad.al`.
|
||||
The opposite review defect is flagging a bound field solely because it omits a page-level `Caption`, or inserting a copy of the table field's caption to satisfy AA0225/AA0226. That adds redundant text and prevents subsequent table-caption changes from flowing through to the page.
|
||||
|
||||
See sample: [`caption-required-on-page-fields.bad.al`](caption-required-on-page-fields.bad.al).
|
||||
|
||||
## References
|
||||
|
||||
[Caption property](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/properties/devenv-caption-property) and [ToolTip property remarks documenting inheritance of both properties](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/properties/devenv-tooltip-property).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ In an AL `case` statement, the action for each label is written on the line that
|
|||
|
||||
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`.
|
||||
See sample: [`case-action-on-line-after-possibility.good.al`](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`.
|
||||
See sample: [`case-action-on-line-after-possibility.bad.al`](case-action-on-line-after-possibility.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ application-area: [all]
|
|||
|
||||
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`.
|
||||
See sample: [`error-passes-parameters-directly-not-strsubstno.good.al`](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`.
|
||||
See sample: [`error-passes-parameters-directly-not-strsubstno.bad.al`](error-passes-parameters-directly-not-strsubstno.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ application-area: [all]
|
|||
|
||||
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`.
|
||||
See sample: [`fieldcaption-not-fieldname-in-user-messages.good.al`](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`.
|
||||
See sample: [`fieldcaption-not-fieldname-in-user-messages.bad.al`](fieldcaption-not-fieldname-in-user-messages.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ AL allows a parameterless procedure to be called without parentheses — `Custom
|
|||
|
||||
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`.
|
||||
See sample: [`function-call-parentheses-required.good.al`](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`.
|
||||
See sample: [`function-call-parentheses-required.bad.al`](function-call-parentheses-required.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ application-area: [all]
|
|||
|
||||
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`.
|
||||
See sample: [`label-comment-explains-placeholders.good.al`](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`.
|
||||
See sample: [`label-comment-explains-placeholders.bad.al`](label-comment-explains-placeholders.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ A `Label` is by default surfaced to translators and rewritten per locale. That i
|
|||
|
||||
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`.
|
||||
See sample: [`label-locked-for-non-translatable.good.al`](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`.
|
||||
See sample: [`label-locked-for-non-translatable.bad.al`](label-locked-for-non-translatable.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ CodeCop AA0074 flags `Label` and `TextConst` identifiers that do not end with an
|
|||
|
||||
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`.
|
||||
See sample: [`label-suffix-approved-list.good.al`](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`.
|
||||
See sample: [`label-suffix-approved-list.bad.al`](label-suffix-approved-list.bad.al).
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ Test codeunits that retain legacy uppercase forms (`OPENEDIT`, `ASSERTERROR`, `V
|
|||
|
||||
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`.
|
||||
See sample: [`lowercase-reserved-keywords.good.al`](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`.
|
||||
See sample: [`lowercase-reserved-keywords.bad.al`](lowercase-reserved-keywords.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ application-area: [all]
|
|||
|
||||
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`.
|
||||
See sample: [`named-invocations-not-object-ids.good.al`](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`.
|
||||
See sample: [`named-invocations-not-object-ids.bad.al`](named-invocations-not-object-ids.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ CodeCop AA0013 flags `begin … end` blocks that contain exactly one statement.
|
|||
|
||||
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`.
|
||||
See sample: [`no-begin-end-around-single-statement.good.al`](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`.
|
||||
See sample: [`no-begin-end-around-single-statement.bad.al`](no-begin-end-around-single-statement.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ When the `then` branch of an `if` ends in a terminating statement — `exit`, `b
|
|||
|
||||
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`.
|
||||
See sample: [`no-else-after-terminating-statement.good.al`](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`.
|
||||
See sample: [`no-else-after-terminating-statement.bad.al`](no-else-after-terminating-statement.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ CodeCop AA0002 forbids whitespace between a procedure/method name and its `(`. `
|
|||
|
||||
`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`.
|
||||
See sample: [`no-space-before-method-parenthesis.good.al`](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`.
|
||||
See sample: [`no-space-before-method-parenthesis.bad.al`](no-space-before-method-parenthesis.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ CodeCop AA0221 requires an `OptionCaption` on every option-type field that is no
|
|||
|
||||
`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`.
|
||||
See sample: [`optioncaption-required-and-matches-membercount.good.al`](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`.
|
||||
See sample: [`optioncaption-required-and-matches-membercount.bad.al`](optioncaption-required-and-matches-membercount.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ CodeCop AA0003 requires exactly one space between the `not` operator and the exp
|
|||
|
||||
`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`.
|
||||
See sample: [`single-space-after-not-operator.good.al`](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`.
|
||||
See sample: [`single-space-after-not-operator.bad.al`](single-space-after-not-operator.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ CodeCop AA0001 requires exactly one space on each side of every binary operator:
|
|||
|
||||
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`.
|
||||
See sample: [`single-space-around-binary-operators.good.al`](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`.
|
||||
See sample: [`single-space-around-binary-operators.bad.al`](single-space-around-binary-operators.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ A `Record` variable declared with the `temporary` modifier behaves nothing like
|
|||
|
||||
Every local or global variable of type `Record X temporary` must start with `Temp`. Ordinary procedure parameters follow the same convention. Event publisher parameters are owned by the events-domain rule `prefix-temporary-record-event-parameters-with-temp.md`; the style leaf must not emit a second finding for the same event parameter.
|
||||
|
||||
See sample: `temporary-variable-temp-prefix.good.al`.
|
||||
See sample: [`temporary-variable-temp-prefix.good.al`](temporary-variable-temp-prefix.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`WIPBuffer: Record "Job WIP Buffer" temporary;` as a local, global, or ordinary procedure parameter reads at the call site as if it were a database operation. Exclude event publisher parameters here so the events leaf remains their single owner.
|
||||
|
||||
See sample: `temporary-variable-temp-prefix.bad.al`.
|
||||
See sample: [`temporary-variable-temp-prefix.bad.al`](temporary-variable-temp-prefix.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ CodeCop AA0248 recommends prefixing self-references inside a codeunit with `this
|
|||
|
||||
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`.
|
||||
See sample: [`this-keyword-in-codeunits.good.al`](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`.
|
||||
See sample: [`this-keyword-in-codeunits.bad.al`](this-keyword-in-codeunits.bad.al).
|
||||
|
|
|
|||
|
|
@ -1,23 +1,29 @@
|
|||
page 50251 "Sample Tooltip Bad"
|
||||
{
|
||||
PageType = Card;
|
||||
SourceTable = Customer;
|
||||
layout
|
||||
{
|
||||
area(Content)
|
||||
{
|
||||
group(General)
|
||||
{
|
||||
field("No."; Rec."No.")
|
||||
Caption = 'General';
|
||||
field(CustomerNoValue; CustomerNoValue)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
Caption = 'Customer No.';
|
||||
}
|
||||
field(Amount; Rec."Balance (LCY)")
|
||||
field(PreviewAmount; PreviewAmount)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
Caption = 'Preview Amount';
|
||||
ToolTip = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var
|
||||
CustomerNoValue: Code[20];
|
||||
PreviewAmount: Decimal;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,62 @@
|
|||
// BC24 / runtime 13.0 or later.
|
||||
table 50250 "Sample Tooltip Source"
|
||||
{
|
||||
Caption = 'Tooltip Source';
|
||||
DataClassification = CustomerContent;
|
||||
|
||||
fields
|
||||
{
|
||||
field(1; "No."; Code[20])
|
||||
{
|
||||
Caption = 'No.';
|
||||
ToolTip = 'Specifies the unique number used to distinguish this entry from other entries.';
|
||||
}
|
||||
field(2; Amount; Decimal)
|
||||
{
|
||||
Caption = 'Amount';
|
||||
ToolTip = 'Specifies the monetary value recorded for this entry; changing it updates the saved entry.';
|
||||
}
|
||||
}
|
||||
|
||||
keys
|
||||
{
|
||||
key(PK; "No.")
|
||||
{
|
||||
Clustered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
page 50250 "Sample Tooltip Good"
|
||||
{
|
||||
PageType = Card;
|
||||
SourceTable = Customer;
|
||||
SourceTable = "Sample Tooltip Source";
|
||||
layout
|
||||
{
|
||||
area(Content)
|
||||
{
|
||||
group(General)
|
||||
{
|
||||
Caption = 'General';
|
||||
field("No."; Rec."No.")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the number that identifies the customer.';
|
||||
}
|
||||
field(Amount; Rec."Balance (LCY)")
|
||||
field(Amount; Rec.Amount)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Shows the total balance in local currency.';
|
||||
ToolTip = 'Specifies the recorded amount to compare with the temporary preview amount.';
|
||||
}
|
||||
field(PreviewAmount; PreviewAmount)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
Caption = 'Preview Amount';
|
||||
ToolTip = 'Specifies a temporary amount to compare with the recorded entry amount; this value is not saved.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var
|
||||
PreviewAmount: Decimal;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,44 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: style
|
||||
keywords: [tooltip, page-field, aa0218, codecop, accessibility, specifies]
|
||||
keywords: [tooltip, page-field, source-field, inheritance, aa0218, codecop, accessibility, specifies]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Every page field needs a `ToolTip` (CodeCop AA0218)
|
||||
# Page fields need an explicit or inherited `ToolTip` (CodeCop AA0218)
|
||||
|
||||
## Description
|
||||
|
||||
CodeCop AA0218 requires a non-empty `ToolTip` property on every field control on a page. The tooltip is what users see on hover and is what screen readers announce; an empty or missing tooltip removes a piece of UI affordance that is part of BC's accessibility baseline. AppSource technical validation rejects pages with missing tooltips. The companion rules AA0219 and AA0220 push the wording further — tooltips should describe what the field shows, conventionally starting with `'Specifies …'`, though `'Shows …'` and similar variants are acceptable when they clearly describe the field's purpose.
|
||||
User-facing page fields need tooltip text, but it does not have to be declared on each page control. Starting with BC24 (2024 release wave 1), runtime 13.0 supports `ToolTip` on table fields, and bound page fields inherit it unless they override it. A non-empty inherited tooltip satisfies the requirement; do not interpret CodeCop AA0218 as a requirement to repeat it on the page.
|
||||
|
||||
Acceptable exceptions: table fields inside `Upgrade`, `Migration`, `HybridBC14`, `HybridSL`, and `HybridGP` codeunits and tables are allowed to omit the tooltip — those types are not surfaced to users.
|
||||
For targets before runtime 13.0, table-field tooltip inheritance is not available, so user-facing page fields need page-level tooltips. Controls bound to variables or expressions also need page-level tooltips because they have no table field to inherit from. This is UI guidance, not a blanket requirement to add tooltips to every table field, including fields never exposed to users.
|
||||
|
||||
AA0218 is a compiler analyzer, but its severity is configured per app in the ruleset and is frequently downgraded to `info`/`None` or disabled entirely. PR review therefore cannot assume the compiler will surface the gap: it is the last line of defence for a missing tooltip and should flag it independently. The one case review must *not* flag is a bound field that inherits a `ToolTip` from its source table field — see `bound-page-field-inherits-source-field-tooltip`.
|
||||
AA0218's severity is configured per app and may be downgraded or disabled. Review should still report a genuinely missing tooltip, but absence of a page-level declaration alone is not evidence of a gap. See [bound page-field tooltip inheritance](../ui/bound-page-field-inherits-source-field-tooltip.md).
|
||||
|
||||
## Best Practice
|
||||
|
||||
Every field control on a regular page carries `ToolTip = 'Specifies …';` (or a clear alternative phrasing). Compose the text in the form "what this value shows" rather than "what the user does with it". In review, raise a `medium`-severity finding for a field that has neither an inline nor an inherited tooltip, independently of whether AA0218 is active in the app's ruleset.
|
||||
On runtime 13.0 or later, define shared tooltip text on the table field and omit duplicate page-level properties. Add a page-level `ToolTip` when no tooltip can be inherited or when the page needs different, context-specific help. Describe what the value shows, conventionally starting with "Specifies" or another clear phrasing.
|
||||
|
||||
See sample: `tooltip-required-on-page-fields.good.al`.
|
||||
Make the text answer a question the caption does not: what the value is used for, which values or units are expected, or what changing it affects. Do not mechanically generate "Specifies the <field name>." and consider the help complete. Use behavior established by the implementation or requirements; do not invent effects, defaults, or constraints to make a tooltip sound useful. Keep shared table-field help applicable to all pages that inherit it, and improve that shared text rather than duplicating it on each page.
|
||||
|
||||
Before raising a `medium`-severity finding, check the target runtime, the control's binding, and the source field's tooltip, including dependency symbols when needed. Report a field with neither an explicit nor an inherited tooltip independently of whether AA0218 is active. If the source definition or target runtime is unavailable, do not assume a missing page property means missing tooltip text.
|
||||
|
||||
See sample: [`tooltip-required-on-page-fields.good.al`](tooltip-required-on-page-fields.good.al) (BC24/runtime 13.0 or later).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
A field control with no `ToolTip` property at all, or `ToolTip = '';`. AA0218 flags both; the hover state is blank and the screen reader has nothing to announce.
|
||||
A user-facing control with no page-level `ToolTip` and no non-empty source tooltip it can inherit, or a page-level `ToolTip = '';` that leaves the effective tooltip empty.
|
||||
|
||||
See sample: `tooltip-required-on-page-fields.bad.al`.
|
||||
Flagging a bound field that already inherits its tooltip, or adding the same tooltip to every page, is also incorrect: duplicate overrides add maintenance and translation work and prevent source-field tooltip changes from reaching those pages.
|
||||
|
||||
Treating a non-empty tooltip that merely repeats the caption as useful help is a separate quality issue, not a missing-tooltip finding. Point out the concrete information users need rather than demanding longer wording or a page-level override for its own sake.
|
||||
|
||||
See sample: [`tooltip-required-on-page-fields.bad.al`](tooltip-required-on-page-fields.bad.al).
|
||||
|
||||
## References
|
||||
|
||||
[ToolTip property](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/properties/devenv-tooltip-property).
|
||||
|
||||
[Guidelines for tooltip text](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/user-assistance#guidelines-for-tooltip-text).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ CodeCop AA0021 requires that variable declarations inside a `var` block follow a
|
|||
|
||||
Declare all `Record` variables first, then other complex types, then primitives. A consistent order makes diffs review-friendly and matches the convention enforced by the AL formatter and CodeCop.
|
||||
|
||||
See sample: `variable-declaration-order-by-type.good.al`.
|
||||
See sample: [`variable-declaration-order-by-type.good.al`](variable-declaration-order-by-type.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
A `var` block where records and primitives are interleaved — `CustomerNo: Code[20];` between two `Record` variables, or `Amount: Decimal;` declared above the `Customer: Record Customer;` it is computed from. AA0021 flags it and the block is harder to scan; readers expect composite types at the top.
|
||||
|
||||
See sample: `variable-declaration-order-by-type.bad.al`.
|
||||
See sample: [`variable-declaration-order-by-type.bad.al`](variable-declaration-order-by-type.bad.al).
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ Three CodeCop rules — AA0198, AA0202, AA0204 — together forbid a local varia
|
|||
|
||||
Differentiate every local declaration from globals, fields, procedures, and actions on the same object. `Customer` global plus `CustomerName` local; method `GetAmount` plus local `SalesAmount`. The standard pattern is to attach a noun suffix to the local (`CustomerName`, `CustomerRec`, `CustomerNo`) rather than to the global.
|
||||
|
||||
See sample: `variable-name-must-not-shadow.good.al`.
|
||||
See sample: [`variable-name-must-not-shadow.good.al`](variable-name-must-not-shadow.good.al).
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
A procedure that declares a local `Customer: Text` inside a codeunit that already has a global `Customer: Record Customer`. The local wins and the global becomes unreachable inside the procedure. AA0198/AA0202/AA0204 flag this category of conflict whether the colliding entity is a global, a field, a method, or an action.
|
||||
|
||||
See sample: `variable-name-must-not-shadow.bad.al`.
|
||||
See sample: [`variable-name-must-not-shadow.bad.al`](variable-name-must-not-shadow.bad.al).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue