AL Griller
Your AL code is about to get cooked.
AL Griller is a deterministic static analyzer for Microsoft Dynamics 365 Business Central AL code, with a humorous presentation. It is not an AI code reviewer, not a generic linter, and not a joke extension that insults developers at random. Every roast is backed by a real, detectable pattern in your code.
No AI. Local only.
AL Griller does not call OpenAI, Claude, Copilot, or any cloud or LLM API. There are no network calls of any kind. All analysis runs entirely on your machine, on the file you have open. Your source code never leaves your editor.
How it works
- Open an AL file.
- Click the AL Griller icon in the Activity Bar (or run AL Griller: Grill Current File).
- AL Griller scans the active file's text with a set of conservative, deterministic rules.
- Findings show up in the AL Griller view, grouped by category, each with a roast and a score.
- Click any finding to jump straight to the line it refers to.
Analysis is line/pattern-based, not a full AL grammar parser - it is intentionally conservative, since a false positive is worse than a missed pattern. Re-analysis happens automatically when you switch AL files or edit the current one (debounced), and on demand via AL Griller: Grill Again.
The panel
The findings panel is a webview, not a tree view - on purpose. VS Code's built-in tree items hard-truncate their label with an ellipsis (...) and can't wrap, which meant long roasts got clipped unless you dragged the sidebar uncomfortably wide. The panel now renders each finding as a real card:
- A circular score badge (color-coded green → red) plus Problem/Warning/Note chips up top
- Findings grouped into labeled, emoji-tagged category sections
- The roast is the headline of the card - large, bold, and always fully visible, wrapping naturally at any panel width
- The rule ID, severity, message, technical explanation, and suggestion are still all there - folded under a "Technical details" toggle so the roast stays the star, and the specifics are one click away
- A "Jump to line →" button on every card
Roasts
The roasts are not generic "this code could be better" filler - every one is written against the specific pattern that rule detects, not a template. They lean absurd, specific, and a little unhinged, on purpose - code personified having an existential crisis, transactions cosplaying as diplomats, database locks throwing tiny tantrums - while staying clean and never insulting the developer as a person. Each rule ships with 5 roasts; which one shows is picked deterministically per finding, so re-analyzing the same file doesn't rewrite the joke on you.
Main features
- A rich, scrollable panel in the Activity Bar - not a tree view - so every finding renders as a full card with a score badge, category grouping, and a roast that's never cut off or truncated, no matter how narrow the sidebar is
- Click "Jump to line →" on any finding to go straight to the offending line
- Every finding carries both a roast and, tucked under a collapsible "Technical details" toggle, a plain technical explanation and a suggestion where one applies - the roast stays the headline, the explanation stays one click away
- Deterministic scoring (0–100) computed directly from the findings' severities - never a made-up number
- Current-file analysis only; no workspace-wide scanning, no background indexing
Rules
AL Griller ships 26 rules, ALGRILL001–ALGRILL026, drawn from a researched, evidence-backed rule database (see the project's RULE_RESEARCH.md). This is the complete and only rule set the analyzer runs - every shipped rule went through the same selection bar: a real, detectable Business Central pattern, with explicit false-positive guards, before it was allowed in.
Because this is a single-file, line/pattern-based scanner with no cross-object symbol resolution, a few rules are intentionally narrower than their full research description - each rule's source comment in rules.ts documents exactly where it approximates and why (for example, ALGRILL001 only recognizes the single-field "No." primary-key case, since resolving an arbitrary table's full primary key would need information outside the current file).
Database
| Rule |
Severity |
What it flags |
ALGRILL001 |
Warning |
FindFirst() used when a full primary-key filter (SetRange("No.", ...)) was already applied - Get() fits better |
ALGRILL004 |
Warning |
FindSet(true)/FindSet() lock intent doesn't match whether the loop actually writes the record |
ALGRILL007 |
Info |
A FindSet + repeat/Delete/until loop with no per-row logic - DeleteAll(false) fits better |
| Rule |
Severity |
What it flags |
ALGRILL002 |
Warning |
Count() > 0 or a discarded FindFirst() used purely as an existence check - IsEmpty() fits better |
ALGRILL003 |
Warning |
CalcFields() called per row inside a loop to build a running total - CalcSums() fits better |
ALGRILL008 |
Info |
Text rebuilt with +=/string concatenation on every iteration of a loop - TextBuilder fits better |
ALGRILL013 |
Warning |
An equality condition is tested inside the loop body instead of filtered with SetRange/SetFilter before FindSet |
ALGRILL014 |
Info |
A database lookup runs before a guard that may exit the procedure without ever using it |
Transactions
| Rule |
Severity |
What it flags |
ALGRILL005 |
Warning |
Commit() called inside a loop |
ALGRILL015 |
Warning |
LockTable() called with no subsequent write in the same procedure |
ALGRILL016 |
Warning |
Confirm()/StrMenu() issued while a write-intent lock is still open |
ALGRILL017 |
Problem |
Codeunit.Run() follows a database write with no Commit() in between (this is a guaranteed platform runtime error) |
Events
| Rule |
Severity |
What it flags |
ALGRILL010 |
Warning |
An integration/business event is raised inside a per-record loop |
ALGRILL011 |
Warning |
An integration event is raised from inside a [TryFunction] body |
Error Handling
| Rule |
Severity |
What it flags |
ALGRILL018 |
Warning |
Insert()/Modify()/Delete() runs inside a [TryFunction] body - it will not be rolled back |
ALGRILL019 |
Warning |
A [TryFunction] procedure is called without consuming its Boolean return value |
Data Modeling
| Rule |
Severity |
What it flags |
ALGRILL012 |
Warning |
xRec is compared against Rec inside OnModify/OnInsert - from code, they're the same record there |
ALGRILL020 |
Info |
A field declares both TableRelation and ValidateTableRelation = false - rename propagation silently stops |
AL Objects
| Rule |
Severity |
What it flags |
ALGRILL006 |
Warning |
Modify() called inside a page's OnAfterGetRecord trigger, which fires once per displayed row |
ALGRILL021 |
Info |
Get() called inside a page trigger for the record the page already loaded into Rec |
ALGRILL023 |
Warning |
A ListPart FactBox is declared with no SubPageLink |
Query
| Rule |
Severity |
What it flags |
ALGRILL009 |
Problem |
Query.SetFilter()/SetRange() called after Open() - this silently closes the running dataset |
Security
| Rule |
Severity |
What it flags |
ALGRILL024 |
Warning |
A permission set grants access with a wildcard (tabledata * / table *) object designator |
ALGRILL025 |
Warning |
IsolatedStorage.Set() (unencrypted) is used for a key/value that looks like a credential |
ALGRILL026 |
Warning |
A credential-named variable or parameter is typed Text/Code instead of SecretText |
Code Quality
| Rule |
Severity |
What it flags |
ALGRILL022 |
Info |
FieldError() is called with a capitalized, punctuated, or field-name-restating message |
AL Griller complements the AL Language extension and Microsoft's built-in analyzers (CodeCop, AppSourceCop, PerTenantExtensionCop) rather than duplicating them - no rule in this database re-flags a pattern already covered by a Microsoft analyzer.
Scoring
Score starts at 100. Each finding subtracts a fixed penalty based on its severity: Problem −12, Warning −6, Info −2, floored at 0. It's a quick read of a file, not a substitute for looking at the findings.
Example roast
🔴 ALGRILL003 · Performance
"CalcFields() got summoned once per row like a tiny genie who can only grant
one wish before vanishing back into the lamp, forever, on repeat."
▸ Technical details
CustLedgerEntry.CalcFields("Remaining Amount") runs once per iteration to
build a running total.
CalcFields() materializes a FlowField value for one record and issues its
own SQL statement against a persistent table. Calling it inside a loop
that iterates many rows issues one query per row on top of the iteration
itself. CalcSums() answers the same aggregation question over the filtered
set as a single SQL statement.
Suggestion: Replace the per-row CalcFields() + accumulation with a single
CalcSums() call on the filtered record before or instead of the loop.
Requirements
- VS Code
^1.85.0
- No runtime npm dependencies.
typescript, @types/vscode, and @types/node are dev-time only, used to compile the extension - none of them ship or run with it.
Marketplace
AL Griller is available on the Visual Studio Marketplace. Search for AL Griller, or install it from the Extensions view in VS Code (Ctrl+Shift+X).
Contributing
Pull requests are welcome - this project benefits a lot from community input, whether that's new rules, bug fixes, or better roasts. If you're a junior AL developer, contributing here is also a good way to learn more about AL development patterns by seeing what gets flagged and why. Roast quality is subjective, so feedback on tone and jokes is just as welcome as feedback on the rules themselves. Have an idea or found a bug? Open an issue on the GitHub Issues page.
License
Released under the MIT License. You're free to use, modify, and distribute this software, subject to the license terms.
Feedback & Support
Questions or issues? Open an issue on GitHub, or reach out to the community for support and suggestions.
Made with ❤️ by Yahya Touil.
More Business Central and AL development insights on Yahya's Blog.
AL Griller - Built for Business Central AL developers.