This code analyzer is meant to check your code for all sorts of problems. Be it code that technically compiles but will generate errors during runtime or more a kind of guideline check to achieve cleaner code. Some rule even are disabled by default as they may not go along the main coding guidelines but are maybe helpful in certain projects. In general all rule ideas are welcome, even if they should be and maybe will be covered by Microsoft at some point but could be part of the linter in the meantime.
If you are not happy with some rules or only feel like you need one rule of this analyzer, you can always control the rules with a Custom.ruleset.json and disable all rules you don't need.
The Linter is not finished yet (and probably never will be :D ) If you have any rule on mind that would be nice to be covered, please start a new discussion! then we can maybe sharpen the rule a bit if necessary. This way we can build value for all of us. If you want to write the rule yourself you can of course also submit a pull request ;)
You can also contribute to the collection of unit tests. If you want to add a new test case, please create a new test class in the BusinessCentral.LinterCop.Test folder. The test class name should match the rule name. Use one of the existing test classes as an example.
Test class:
namespace BusinessCentral.LinterCop.Test;
public class RuleXXXX // Id of the rule that is being tested
{
private string _testCaseDir = "";
[SetUp]
public void Setup()
{
_testCaseDir = Path.Combine(Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName,
"TestCases", "RuleXXXX"); // Set path to subfolder with test cases
}
[Test]
[TestCase("1")] // Test case 1.al
[TestCase("2")] // Test case 2.al
...
[TestCase("n")]
public async Task HasDiagnostic(string testCase) // Positive test
{
// Load code file for test case
var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al"))
.ConfigureAwait(false);
// Create fixture for rule
var fixture = RoslynFixtureFactory.Create<RuleXXXXMyCodeRule>();
// Check for reported diagnostic
fixture.HasDiagnostic(code, DiagnosticDescriptors.RuleXXXXMyCodeRule.Id);
}
[Test]
[TestCase("1")] // Test case 1.al
[TestCase("2")] // Test case 2.al
...
[TestCase("n")]
public async Task NoDiagnostic(string testCase) // Negative test
{
// Load code file for test case
var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al"))
.ConfigureAwait(false);
// Create fixture for rule
var fixture = RoslynFixtureFactory.Create<RuleXXXXMyCodeRule>();
// Check for no reported diagnostic
fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.RuleXXXXMyCodeRule.Id);
}
}
Test cases can be positive (a diagnostic is expected) or negative (no diagnostic is expected) and are contained in the BusinessCentral.LinterCop.Test/TestCases/ folder, with a subfolder per rule and a sub-subfolder for positive HasDiagnostic and negative NoDiagnostic test cases. The individual test cases are numbered (if you have an idea for a better naming scheme, please let me know) and must be compilable AL code.
Basic folder structure:
├───BusinessCentral.LinterCop.Test
│ ├───RoslynTestKit
│ │ ├───CodeActionLocators
│ │ └───Utils
│ └───TestCases
│ ├───Rule0001
│ │ ├───HasDiagnostic
│ │ │ ├───1.al
│ │ │ ├───2.al
│ │ │ └───X.al
│ │ └───NoDiagnostic
│ │ │ ├───1.al
│ │ │ ├───2.al
│ │ │ └───X.al
│ ├───Rule0002
│ │ ├───HasDiagnostic
│ │ │ ├───X.al
│ │ └───NoDiagnostic
│ │ │ ├───X.al
│ ├───RuleXXXX
│ │ ├───HasDiagnostic
│ │ │ ├───X.al
│ │ └───NoDiagnostic
│ │ │ ├───X.al
├───Rule0001.cs
├───Rule0002.cs
└───RuleXXXX.cs
You surround the range in the code you want to check for the diagnostic with [|
and|]
.
Test case:
codeunit 50100 MyCodeunit
{
procedure MyProcedure()
var
MyVariable: Integer;
begin
// We want to check for a diagnostic between the [| and |] markers
[|MyVariable := 1;|]
end;
}
The LinterCop is compatible with various approaches and solutions for the AL Language extension for Microsoft Dynamics 365 Business Central.
I recommend you use GitHub Codespaces for getting started, as the ContainerPrep script will take care about everything that needs to be set up. If you prefer to run it locally, just know that you will need to have dotnet installed. The Prep Script might need a few tweaks as well.
Once the Codespace is ready, use F1
to open the command pallete and search for Run Task
, choose the Prep Codespace
task from the list.
At the end of this file, it will try to open new tab of vs code within the codespace, and there might be a popup asking you if you want to continue, confirm that. This will open the test AL project for you so you can debug your analyzer.
Also note, that the app create does not have any dependencies, also not against Mircosoft Application, that way we do not need any symbols.
For debugging you will first need to rebuild the project to have it reflect the latest changes, if you did not do so already, clone the second tab with the AL Project. Otherwise the AL Lanauage server, which is responsible for the diagnosics/warnings in vs code, might block the .dll and prevent the Compiler from replacing it.
Now once you have the changes in the Analyzer code itsself, run the tasks again, this time choose Build
it will run build dotnet and open vs code at once. So you will get a new compiled version and it will ask you again to open the AL Project after it finished compiling.
I recommend you wait a few moments until the first diagnostics are coming up, that way you can be sure that everything is loaded and up and running.
Now go back to the Analyzer project, set your breakpoint and press F5
. This will open up a menu where you need to select the process to attach to. For this project this will be the Microsoft.Dynamics.Nav.EditorServices.Host
, but you should find it if you just start to type Dynamics
. Sometime there are multiple process running, just select the one with the highest process id.
Keep an eye on the break point you have set, before debugging it should have been red, right during attaching it will turn into a grey circle. If it does not turn red again after a few seconds, it means that the process you attached to, does not run the same code you are looking it. The reason could be that the compile did not work or that you did not attach to the right process. If that happens just repeat the build and attach.
After the debugger is attached correclty, you can switch to the AL Project again and resave the file you want to debug in, that will retrigger the analyzer.
And thats it, that should get you started.
Some rules can be configured by adding a file named LinterCop.json
in the root of your project.
Important: The file will only be read on startup of the linter, meaning if you make any changes you need to reload VS Code once.
For an example and the default values see: LinterCop.json
If you want to use the LinterCop.json
file in a pipeline, using BcContainerHelper, you need to copy the file to C:\build\vsix\extension\bin\win32\LinterCop.json
inside the container before calling Compile-AppInBcContainer
. See this issue for details on how to accomplish that.
Since the linter integrates with the AL compiler directly, you can use the custom rule sets like you are used to from the other code cops. https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-rule-set-syntax-for-code-analysis-tools
Of course you can also use pragmas for disabling a rule just for a certain place in code. https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/directives/devenv-directive-pragma-warning
For an example and the default values see: LinterCop.ruleset.json
Id | Title | Default Severity | AL version |
---|---|---|---|
LC0000 | An error occurred in a given rule. Please create an issue on GitHub | Info | |
LC0001 | FlowFields should not be editable. | Warning | |
LC0002 | Commit() needs a comment to justify its existence. Either a leading or a trailing comment. |
Warning | |
LC0003 | Do not use an Object ID for properties or variable declarations. | Warning | |
LC0004 | DrillDownPageId and LookupPageId must be filled in table when table is used in list page. |
Warning | |
LC0005 | The casing of variable/method usage must align with the definition. | Warning | |
LC0006 | Fields with property AutoIncrement cannot be used in temporary table (TableType = Temporary ). |
Error | |
LC0007 | Every table needs to specify a value for the DataPerCompany property. Either true or false . |
Disabled | |
LC0008 | Filter operators should not be used in SetRange . |
Warning | |
LC0009 | Show info message about code metrics for each function or trigger. | Disabled | |
LC0010 | Show warning about code metrics for each function or trigger if either cyclomatic complexity is 8 or greater or maintainability index 20 or lower. | Warning | |
LC0011 | Every object needs to specify a value for the Access property. Either true or false . Optionally this can also be activated for table fields with the setting enableRule0011ForTableFields . |
Disabled | |
LC0012 | Using hardcoded IDs in functions like Codeunit.Run() is not allowed. |
Warning | |
LC0013 | Any table with a single field in the PK of type code or text, should have set NotBlank on the PK field. |
Warning | |
LC0014 | The Caption of permissionset objects should not exceed the maximum length. | Warning | |
LC0015 | All application objects should be covered by at least one permission set in the extension. | Warning | |
LC0016 | Caption is missing. Optionally this can also be activated for fields on API objects with the setting enableRule0016ForApiObjects . |
Warning | |
LC0017 | Writing to a FlowField is not common. Add a comment to explain this. | Warning | |
LC0018 | Events in internal codeunits are not accessible to extensions and should therefore be avoided. | Info | |
LC0019 | If Data Classification is set on the Table. Fields do not need the same classification. | Info | |
LC0020 | If Application Area is set on the TablePage. Controls do not need the same classification. | Info | |
LC0021 | Confirm() must be implemented through the Confirm Management codeunit from the System Application. |
Info | |
LC0022 | GlobalLanguage() must be implemented through the Translation Helper codeunit from the Base Application. |
Info | |
LC0023 | Always provide fieldsgroups DropDown and Brick on tables. |
Info | |
LC0024 | Procedure or Trigger declaration should not end with semicolon. | Info | |
LC0025 | Procedure must be either local, internal or define a documentation comment. | Info | |
LC0026 | ToolTip must end with a dot. | Info | |
LC0027 | Utilize the Page Management codeunit for launching page. |
Info | |
LC0028 | Event subscriber arguments now use identifier syntax instead of string literals. | Info | |
LC0029 | Use CompareDateTime method in Type Helper codeunit for DateTime variable comparisons. |
Info | |
LC0030 | Set Access property to Internal for Install/Upgrade codeunits. | Info | |
LC0031 | Use ReadIsolation instead of LockTable. | Info | |
LC0032 | Clear(All) does not affect or change values for global variables in single instance codeunits. | Warning | |
LC0033 | The specified runtime version in app.json is falling behind. | Info | |
LC0034 | The property Extensible should be explicitly set for public objects. |
Disabled | |
LC0035 | Explicitly set AllowInCustomizations for fields omitted on pages. |
Info | 12.1 |
LC0036 | ToolTip must start with the verb "Specifies". | Info | |
LC0037 | Do not use line breaks in ToolTip. | Info | |
LC0038 | Try to not exceed 200 characters (including spaces). | Info | |
LC0039 | The given argument has a different type from the one expected. | Warning | |
LC0040 | Explicitly set the RunTrigger parameter on build-in methods. |
Info | |
LC0041 | Empty Captions should be Locked . |
Info | |
LC0042 | AutoCalcFields should only be used for FlowFields or Blob fields. |
Warning | |
LC0043 | Use SecretText type to protect credentials and sensitive textual values from being revealed. |
Info | 14.0 |
LC0044 | Tables coupled with TransferFields must have matching fields. |
Warning | |
LC0045 | Zero (0) Enum value should be reserved for Empty Value. |
Info | |
LC0046 | Label with suffix Tok must be locked. |
Info | |
LC0047 | Locked Label must have a suffix Tok. |
None | |
LC0048 | Use Error with a ErrorInfo or Label variable to improve telemetry details. |
Info | |
LC0049 | SourceTable property not defined on Page. |
Info | |
LC0050 | SetFilter with unsupported operator in filter expression. |
Info | |
LC0051 | Do not assign a text to a target with smaller size. | Warning | 12.1 |
LC0052 | The internal procedure is declared but never used. | Info | |
LC0053 | The internal procedure is only used in the object in which it is declared. Consider making the procedure local. | Info | |
LC0054 | Interface name must start with the capital 'I' without any spaces following it. | Info | |
LC0055 | The suffix Tok is meant to be used when the value of the label matches the name. |
Info | |
LC0056 | Empty Enum values should not have a specified Caption property. |
Info | |
LC0057 | Enum values must have non-empty a Caption to be selectable in the client |
Info | |
LC0058 | PageVariable.SetRecord(): You cannot use a temporary record for the Record parameter. | Warning | |
LC0059 | Single quote escaping issue detected. | Warning | |
LC0060 | The ApplicationArea property is not applicable to API pages. |
Info | |
LC0061 | Pages of type API must have the ODataKeyFields property set to the SystemId field. |
Info | |
LC0062 | Mandatory field is missing on API page. | Info | |
LC0063 | Consider naming field with a more descriptive name. | Info | |
LC0064 | Missing ToolTip property on table field. | Info | |
LC0065 | Event subscriber var keyword mismatch. | Info | |
LC0066 | Duplicate ToolTip between page and table field. | Info | |
LC0067 | Set NotBlank property to false when 'No. Series' TableRelation exists. |
Warning | |
LC0068 | Informs the user that there are missing permission to access tabledata. | Info | |
LC0069 | Empty statements should be avoided or should have a leading or trailing comment explaining their use. | Info | |
LC0070 | Zero index access on 1-based List objects. | Warning | |
LC0071 | Incorrect 'IsHandled' parameter assignment. | Info | |
LC0072 | The documentation comment must match the procedure syntax. | Info | |
LC0073 | Handled parameters in event signatures should be passed by var. | Warning | |
LC0074 | Set values for FlowFilter fields using filtering methods. | Warning | |
LC0075 | Incorrect number or type of arguments in .Get() method on Record object. |
Warning | 13.0 |
LC0076 | The field with table relation should have at least the same length as the referenced field. | Warning | |
LC0077 | Methods should always be called with parenthesis. | Info | |
LC0078 | Temporary records should not trigger table triggers. | Info | |
LC0079 | Event publishers should not be public. | Info | |
LC0080 | Replace double quotes in JPath expressions with two single quotes. | Warning | |
LC0081 | Use Rec.IsEmpty() for checking record existence. |
Info | |
LC0082 | Use Rec.Find('-') with Rec.Next() for checking exactly one record. |
Info | |
LC0083 | Use new Date/Time/DateTime methods for extracting parts. | Info | |
LC0084 | Use return value for better error handling. | Info | |
LC0085 | Use the (CR)LFSeparator from the "Type Helper" codeunit. | Info | |
LC0086 | Use the new PageStyle datatype instead string literals. |
Info | 14.0 |
LC0087 | Use IsNullGuid() to check for empty GUID values. |
Warning |