From 0528ed9db523c5323536becd840ae62e517e7785 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:40:26 +0100 Subject: [PATCH 01/28] New Rule0075: Record Get Procedure Arguments (#826) * New Rule0075: Record Get Procedure Arguments * Exclude AL version below 13.0 * Move GetTypeSymbol() to ArgumentExtension * Remove conversion Integer to Enum * Fix test --- BusinessCentral.LinterCop.Test/Rule0075.cs | 62 ++++++++ .../ImplicitConversiontCodeToEnum.al | 25 +++ .../ImplicitConversiontEnumToAnotherEnum.al | 27 ++++ .../HasDiagnostic/RecordGetGlobalVariable.al | 23 +++ .../HasDiagnostic/RecordGetLocalVariable.al | 22 +++ .../Rule0075/HasDiagnostic/RecordGetMethod.al | 24 +++ .../HasDiagnostic/RecordGetParameter.al | 20 +++ .../HasDiagnostic/RecordGetReportDataItem.al | 25 +++ .../HasDiagnostic/RecordGetReturnValue.al | 20 +++ ...GetSetupTableIncorrectArgumentsProvided.al | 22 +++ .../RecordGetSetupTableNoArgumentsProvided.al | 22 +++ .../RecordGetXmlPortTableElement.al | 25 +++ .../ImplicitConversiontIntegerToEnum.al | 26 +++ .../RecordGetBuiltInMethodRecordId.al | 22 +++ .../NoDiagnostic/RecordGetFieldRecordId.al | 23 +++ .../NoDiagnostic/RecordGetGlobalVariable.al | 23 +++ .../NoDiagnostic/RecordGetLocalVariable.al | 22 +++ .../RecordGetLocalVariableRecordId.al | 23 +++ .../Rule0075/NoDiagnostic/RecordGetMethod.al | 24 +++ .../NoDiagnostic/RecordGetMethodRecordId.al | 26 +++ .../NoDiagnostic/RecordGetParameter.al | 20 +++ .../RecordGetParameterRecordId.al | 22 +++ .../NoDiagnostic/RecordGetReportDataItem.al | 25 +++ .../NoDiagnostic/RecordGetReturnValue.al | 20 +++ .../RecordGetReturnValueRecordId.al | 22 +++ ...rdGetSetupTableCorrectArgumentsProvided.al | 22 +++ .../RecordGetSetupTableNoArgumentsProvided.al | 22 +++ .../RecordGetXmlPortTableElement.al | 25 +++ .../Rule0075RecordGetProcedureArguments.cs | 150 ++++++++++++++++++ .../Helpers/ArgumentHelper.cs | 19 +++ .../LinterCop.ruleset.json | 5 + .../LinterCopAnalyzers.resx | 9 ++ README.md | 3 +- 33 files changed, 869 insertions(+), 1 deletion(-) create mode 100644 BusinessCentral.LinterCop.Test/Rule0075.cs create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/ImplicitConversiontCodeToEnum.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/ImplicitConversiontEnumToAnotherEnum.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetGlobalVariable.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetLocalVariable.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetMethod.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetParameter.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetReportDataItem.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetReturnValue.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetSetupTableIncorrectArgumentsProvided.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetSetupTableNoArgumentsProvided.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetXmlPortTableElement.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/ImplicitConversiontIntegerToEnum.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetBuiltInMethodRecordId.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetFieldRecordId.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetGlobalVariable.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetLocalVariable.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetLocalVariableRecordId.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetMethod.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetMethodRecordId.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetParameter.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetParameterRecordId.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetReportDataItem.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetReturnValue.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetReturnValueRecordId.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetSetupTableCorrectArgumentsProvided.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetSetupTableNoArgumentsProvided.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetXmlPortTableElement.al create mode 100644 BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs create mode 100644 BusinessCentral.LinterCop/Helpers/ArgumentHelper.cs diff --git a/BusinessCentral.LinterCop.Test/Rule0075.cs b/BusinessCentral.LinterCop.Test/Rule0075.cs new file mode 100644 index 00000000..5667efb3 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/Rule0075.cs @@ -0,0 +1,62 @@ +#if !LessThenSpring2024 +namespace BusinessCentral.LinterCop.Test; + +public class Rule0075 +{ + private string _testCaseDir = ""; + + [SetUp] + public void Setup() + { + _testCaseDir = Path.Combine(Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + "TestCases", "Rule0075"); + } + + [Test] + [TestCase("ImplicitConversiontCodeToEnum")] + [TestCase("ImplicitConversiontEnumToAnotherEnum")] + [TestCase("RecordGetGlobalVariable")] + [TestCase("RecordGetLocalVariable")] + [TestCase("RecordGetMethod")] + [TestCase("RecordGetParameter")] + [TestCase("RecordGetReportDataItem")] + [TestCase("RecordGetReturnValue")] + [TestCase("RecordGetSetupTableIncorrectArgumentsProvided")] + [TestCase("RecordGetSetupTableNoArgumentsProvided")] + [TestCase("RecordGetXmlPortTableElement")] + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.HasDiagnostic(code, Rule0075RecordGetProcedureArguments.DiagnosticDescriptors.Rule0075RecordGetProcedureArguments.Id); + } + + [Test] + [TestCase("ImplicitConversiontIntegerToEnum")] + [TestCase("RecordGetBuiltInMethodRecordId")] + [TestCase("RecordGetFieldRecordId")] + [TestCase("RecordGetGlobalVariable")] + [TestCase("RecordGetLocalVariable")] + [TestCase("RecordGetLocalVariableRecordId")] + [TestCase("RecordGetMethod")] + [TestCase("RecordGetMethodRecordId")] + [TestCase("RecordGetParameter")] + [TestCase("RecordGetParameterRecordId")] + [TestCase("RecordGetReportDataItem")] + [TestCase("RecordGetReturnValue")] + [TestCase("RecordGetReturnValueRecordId")] + [TestCase("RecordGetSetupTableCorrectArgumentsProvided")] + [TestCase("RecordGetSetupTableNoArgumentsProvided")] + [TestCase("RecordGetXmlPortTableElement")] + public async Task NoDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.NoDiagnosticAtMarker(code, Rule0075RecordGetProcedureArguments.DiagnosticDescriptors.Rule0075RecordGetProcedureArguments.Id); + } +} +#endif \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/ImplicitConversiontCodeToEnum.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/ImplicitConversiontCodeToEnum.al new file mode 100644 index 00000000..cb5a97d6 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/ImplicitConversiontCodeToEnum.al @@ -0,0 +1,25 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + SalesHeader: Record "Sales Header"; + DocumentNo: Code[20]; + begin + [|SalesHeader.Get(DocumentNo, DocumentNo)|]; + end; +} + +table 50100 "Sales Header" +{ + fields + { + field(1; "Document Type"; Enum "Sales Document Type") { } + field(2; "No."; Code[20]) { } + } + keys + { + key(Key1; "Document Type", "No.") { } + } +} + +enum 50100 "Sales Document Type" { } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/ImplicitConversiontEnumToAnotherEnum.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/ImplicitConversiontEnumToAnotherEnum.al new file mode 100644 index 00000000..4ad0faed --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/ImplicitConversiontEnumToAnotherEnum.al @@ -0,0 +1,27 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + SalesHeader: Record "Sales Header"; + PurchaseDocumentType: Enum "Purchase Document Type"; + DocumentNo: Code[20]; + begin + [|SalesHeader.Get(PurchaseDocumentType, DocumentNo)|]; + end; +} + +table 50100 "Sales Header" +{ + fields + { + field(1; "Document Type"; Enum "Sales Document Type") { } + field(2; "No."; Code[20]) { } + } + keys + { + key(Key1; "Document Type", "No.") { } + } +} + +enum 50100 "Sales Document Type" { } +enum 50101 "Purchase Document Type" { } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetGlobalVariable.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetGlobalVariable.al new file mode 100644 index 00000000..41c53a73 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetGlobalVariable.al @@ -0,0 +1,23 @@ +codeunit 50100 MyCodeunit +{ + var + ItemVariant: Record "Item Variant"; + + procedure MyProcedure() + begin + [|ItemVariant.Get('10000')|]; + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetLocalVariable.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetLocalVariable.al new file mode 100644 index 00000000..01123731 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetLocalVariable.al @@ -0,0 +1,22 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + ItemVariant: Record "Item Variant"; + begin + [|ItemVariant.Get('10000')|]; + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetMethod.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetMethod.al new file mode 100644 index 00000000..4da2488c --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetMethod.al @@ -0,0 +1,24 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + begin + [|RecordReturnValue().Get('10000')|]; + end; + + procedure RecordReturnValue() ItemVariant: Record "Item Variant" + begin + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetParameter.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetParameter.al new file mode 100644 index 00000000..3d598578 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetParameter.al @@ -0,0 +1,20 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure(var ItemVariant: Record "Item Variant") + begin + [|ItemVariant.Get('10000')|]; + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetReportDataItem.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetReportDataItem.al new file mode 100644 index 00000000..33b86b8a --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetReportDataItem.al @@ -0,0 +1,25 @@ +report 50100 MyReport +{ + dataset + { + dataitem(ItemVariant; "Item Variant") { } + } + + trigger OnPreReport() + begin + [|ItemVariant.Get('10000')|]; + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetReturnValue.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetReturnValue.al new file mode 100644 index 00000000..04cdd3c1 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetReturnValue.al @@ -0,0 +1,20 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() ItemVariant: Record "Item Variant" + begin + [|ItemVariant.Get('10000')|]; + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetSetupTableIncorrectArgumentsProvided.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetSetupTableIncorrectArgumentsProvided.al new file mode 100644 index 00000000..0ab08d38 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetSetupTableIncorrectArgumentsProvided.al @@ -0,0 +1,22 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + CompanyInformation: Record "Company Information"; + begin + [|CompanyInformation.Get('', 12345)|]; + end; +} + +table 79 "Company Information" +{ + fields + { + field(1; "Primary Key"; Code[10]) { } + } + + keys + { + key(Key1; "Primary Key") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetSetupTableNoArgumentsProvided.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetSetupTableNoArgumentsProvided.al new file mode 100644 index 00000000..ba58289c --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetSetupTableNoArgumentsProvided.al @@ -0,0 +1,22 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + ItemVariant: Record "Item Variant"; + begin + [|ItemVariant.Get()|]; + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetXmlPortTableElement.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetXmlPortTableElement.al new file mode 100644 index 00000000..e5f02979 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetXmlPortTableElement.al @@ -0,0 +1,25 @@ +xmlport 50100 MyXmlport +{ + schema + { + tableelement(ItemVariant; "Item Variant") { } + } + + trigger OnPreXmlPort() + begin + [|ItemVariant.Get('10000')|]; + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/ImplicitConversiontIntegerToEnum.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/ImplicitConversiontIntegerToEnum.al new file mode 100644 index 00000000..83fe11d6 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/ImplicitConversiontIntegerToEnum.al @@ -0,0 +1,26 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + SalesHeader: Record "Sales Header"; + myInteger: Integer; + DocumentNo: Code[20]; + begin + [|SalesHeader.Get("Sales Document Type".FromInteger(myInteger), DocumentNo)|]; + end; +} + +table 50100 "Sales Header" +{ + fields + { + field(1; "Document Type"; Enum "Sales Document Type") { } + field(2; "No."; Code[20]) { } + } + keys + { + key(Key1; "Document Type", "No.") { } + } +} + +enum 50100 "Sales Document Type" { } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetBuiltInMethodRecordId.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetBuiltInMethodRecordId.al new file mode 100644 index 00000000..85486bca --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetBuiltInMethodRecordId.al @@ -0,0 +1,22 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + ItemVariant: Record "Item Variant"; + begin + [|ItemVariant.Get(ItemVariant.RecordId())|]; + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetFieldRecordId.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetFieldRecordId.al new file mode 100644 index 00000000..fd7d52d4 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetFieldRecordId.al @@ -0,0 +1,23 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + ItemVariant: Record "Item Variant"; + begin + [|ItemVariant.Get(ItemVariant."Record ID")|]; + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + field(3; "Record ID"; RecordId) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetGlobalVariable.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetGlobalVariable.al new file mode 100644 index 00000000..a5d1116a --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetGlobalVariable.al @@ -0,0 +1,23 @@ +codeunit 50100 MyCodeunit +{ + var + ItemVariant: Record "Item Variant"; + + procedure MyProcedure() + begin + [|ItemVariant.Get('10000', 'VARIANTCODE')|]; + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetLocalVariable.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetLocalVariable.al new file mode 100644 index 00000000..be75a3ac --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetLocalVariable.al @@ -0,0 +1,22 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + ItemVariant: Record "Item Variant"; + begin + [|ItemVariant.Get('10000', 'VARIANTCODE')|]; + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetLocalVariableRecordId.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetLocalVariableRecordId.al new file mode 100644 index 00000000..8deff8dc --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetLocalVariableRecordId.al @@ -0,0 +1,23 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + ItemVariant: Record "Item Variant"; + MyRecordId: RecordId; + begin + [|ItemVariant.Get(MyRecordId)|]; + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetMethod.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetMethod.al new file mode 100644 index 00000000..da09c0f3 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetMethod.al @@ -0,0 +1,24 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + begin + [|RecordReturnValue().Get('10000', 'VARIANTCODE')|]; + end; + + procedure RecordReturnValue() ItemVariant: Record "Item Variant" + begin + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetMethodRecordId.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetMethodRecordId.al new file mode 100644 index 00000000..b6412e2b --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetMethodRecordId.al @@ -0,0 +1,26 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + ItemVariant: Record "Item Variant"; + begin + [|ItemVariant.Get(ReturnValueRecordId())|]; + end; + + procedure ReturnValueRecordId() MyRecordId: RecordId + begin + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetParameter.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetParameter.al new file mode 100644 index 00000000..a2b1994a --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetParameter.al @@ -0,0 +1,20 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure(var ItemVariant: Record "Item Variant") + begin + [|ItemVariant.Get('10000', 'VARIANTCODE')|]; + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetParameterRecordId.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetParameterRecordId.al new file mode 100644 index 00000000..b2bd1586 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetParameterRecordId.al @@ -0,0 +1,22 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure(MyRecordId: RecordId) + var + ItemVariant: Record "Item Variant"; + begin + [|ItemVariant.Get(MyRecordId)|]; + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetReportDataItem.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetReportDataItem.al new file mode 100644 index 00000000..4ea91a17 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetReportDataItem.al @@ -0,0 +1,25 @@ +report 50100 MyReport +{ + dataset + { + dataitem(ItemVariant; "Item Variant") { } + } + + trigger OnPreReport() + begin + [|ItemVariant.Get('10000', 'VARIANTCODE')|]; + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetReturnValue.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetReturnValue.al new file mode 100644 index 00000000..ddf3855e --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetReturnValue.al @@ -0,0 +1,20 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() ItemVariant: Record "Item Variant" + begin + [|ItemVariant.Get('10000', 'VARIANTCODE')|]; + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetReturnValueRecordId.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetReturnValueRecordId.al new file mode 100644 index 00000000..35aae164 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetReturnValueRecordId.al @@ -0,0 +1,22 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() MyRecordId: RecordId + var + ItemVariant: Record "Item Variant"; + begin + [|ItemVariant.Get(MyRecordId)|]; + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetSetupTableCorrectArgumentsProvided.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetSetupTableCorrectArgumentsProvided.al new file mode 100644 index 00000000..79109f7f --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetSetupTableCorrectArgumentsProvided.al @@ -0,0 +1,22 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + CompanyInformation: Record "Company Information"; + begin + [|CompanyInformation.Get('')|]; + end; +} + +table 79 "Company Information" +{ + fields + { + field(1; "Primary Key"; Code[10]) { } + } + + keys + { + key(Key1; "Primary Key") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetSetupTableNoArgumentsProvided.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetSetupTableNoArgumentsProvided.al new file mode 100644 index 00000000..ddb0fddb --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetSetupTableNoArgumentsProvided.al @@ -0,0 +1,22 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + CompanyInformation: Record "Company Information"; + begin + [|CompanyInformation.Get()|]; + end; +} + +table 79 "Company Information" +{ + fields + { + field(1; "Primary Key"; Code[10]) { } + } + + keys + { + key(Key1; "Primary Key") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetXmlPortTableElement.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetXmlPortTableElement.al new file mode 100644 index 00000000..bf04a5ee --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetXmlPortTableElement.al @@ -0,0 +1,25 @@ +xmlport 50100 MyXmlport +{ + schema + { + tableelement(ItemVariant; "Item Variant") { } + } + + trigger OnPreXmlPort() + begin + [|ItemVariant.Get('10000', 'VARIANTCODE')|]; + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs b/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs new file mode 100644 index 00000000..0c18380c --- /dev/null +++ b/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs @@ -0,0 +1,150 @@ +#if !LessThenSpring2024 +using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.ArgumentExtension; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; +using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; +using System.Collections.Immutable; + +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0075RecordGetProcedureArguments : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0075RecordGetProcedureArguments); + + private static readonly Dictionary> ImplicitConversions = new() + { + // Integer can be converted to Option and/or BigInteger + { NavTypeKind.Integer, new HashSet { NavTypeKind.Option, NavTypeKind.BigInteger } }, + + // BigInteger can be converted to Duration + { NavTypeKind.BigInteger, new HashSet { NavTypeKind.Duration } }, + + // Code can be converted to Text + { NavTypeKind.Code, new HashSet { NavTypeKind.Text } }, + + // Text can be converted to Code + { NavTypeKind.Text, new HashSet { NavTypeKind.Code } }, + + // String(literal) can be converted to Text and/or Code + { NavTypeKind.String, new HashSet { NavTypeKind.Text, NavTypeKind.Code } } + }; + + public override void Initialize(AnalysisContext context) + { + context.RegisterOperationAction(AnalyzeAssignmentStatement, OperationKind.InvocationExpression); + } + + private void AnalyzeAssignmentStatement(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) + return; + + if (ctx.Operation is not IInvocationExpression operation) + return; + + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + !SemanticFacts.IsSameName(operation.TargetMethod.Name, "Get")) + return; + + // Skip unsupported single argument scenarios, like Record.Get(RecordId) + if (operation.Arguments.Length == 1 && + operation.Arguments[0].Value is IConversionExpression { Operand.Type: { } type } && + type.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.RecordId) + { + return; + } + + if (operation.Instance?.Type.GetTypeSymbol()?.OriginalDefinition is not ITableTypeSymbol table) + return; + + if (IsSingletonTable(table)) + { + if (operation.Arguments.Length == 0) + return; + + if (operation.Arguments.Length == 1 && + operation.Arguments[0].Value is IConversionExpression { Operand.ConstantValue: { HasValue: true } constant } && + constant.Value?.ToString() == "") + { + return; + } + } + + if (operation.Arguments.Length != table.PrimaryKey.Fields.Length) + { + string expectedArgs = operation.Arguments.Length < table.PrimaryKey.Fields.Length + ? $"Insufficient arguments provided; expected {table.PrimaryKey.Fields.Length} arguments" + : $"Too many arguments provided; expected {table.PrimaryKey.Fields.Length} arguments"; + + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0075RecordGetProcedureArguments, + ctx.Operation.Syntax.GetLocation(), new object[] { table.Name.QuoteIdentifierIfNeeded(), expectedArgs })); + return; + } + + for (int i = 0; i < operation.Arguments.Length; i++) + { + if (!AreFieldCompatible(operation.Arguments[i], table.PrimaryKey.Fields[i])) + { + var argumentType = operation.Arguments[i].GetTypeSymbol(); + var fieldType = table.PrimaryKey.Fields[i].Type; + + string expectedArgs = $"Argument at position {i + 1} has an invalid type; expected '{fieldType}', found '{argumentType}'"; + + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0075RecordGetProcedureArguments, + ctx.Operation.Syntax.GetLocation(), new object[] { table.Name.QuoteIdentifierIfNeeded(), expectedArgs })); + return; + } + } + } + + private bool AreFieldCompatible(IArgument argument, IFieldSymbol field) + { + var argumentType = argument.GetTypeSymbol(); + var fieldType = field.Type; + + if (argumentType == null || fieldType is null) + return true; + + var argumentNavType = argumentType.GetNavTypeKindSafe(); + var fieldNavType = fieldType.GetNavTypeKindSafe(); + + if (argumentNavType == NavTypeKind.Enum && fieldNavType == NavTypeKind.Enum) + return argumentType.OriginalDefinition == fieldType.OriginalDefinition; + + if (argumentNavType == fieldNavType || + argumentNavType == NavTypeKind.None || + argumentNavType == NavTypeKind.Joker) + return true; + + if (ImplicitConversions.TryGetValue(argumentNavType, out var compatibleTypes) && !compatibleTypes.Contains(fieldNavType)) + return false; + + return true; + } + + private static bool IsSingletonTable(ITableTypeSymbol table) + { + return table.PrimaryKey.Fields.Length == 1 && + table.PrimaryKey.Fields[0].OriginalDefinition.GetTypeSymbol() is { } typeSymbol && + typeSymbol.GetNavTypeKindSafe() == NavTypeKind.Code; + } + + public static class DiagnosticDescriptors + { + public static readonly DiagnosticDescriptor Rule0075RecordGetProcedureArguments = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0075", + title: LinterCopAnalyzers.GetLocalizableString("Rule0075RecordGetProcedureArgumentsTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0075RecordGetProcedureArgumentsFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0075RecordGetProcedureArgumentsDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0075"); + } +} +#endif \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Helpers/ArgumentHelper.cs b/BusinessCentral.LinterCop/Helpers/ArgumentHelper.cs new file mode 100644 index 00000000..d91fceb7 --- /dev/null +++ b/BusinessCentral.LinterCop/Helpers/ArgumentHelper.cs @@ -0,0 +1,19 @@ +using Microsoft.Dynamics.Nav.CodeAnalysis; + +namespace BusinessCentral.LinterCop.ArgumentExtension +{ + public static class ArgumentExtensions + { + public static ITypeSymbol? GetTypeSymbol(this IArgument argument) + { + switch (argument.Value.Kind) + { + case OperationKind.ConversionExpression: + return ((IConversionExpression)argument.Value).Operand.Type; + case OperationKind.InvocationExpression: + return ((IInvocationExpression)argument.Value).TargetMethod.ReturnValueSymbol.ReturnType; + } + return null; + } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCop.ruleset.json b/BusinessCentral.LinterCop/LinterCop.ruleset.json index 7ac1a827..eaed96c5 100644 --- a/BusinessCentral.LinterCop/LinterCop.ruleset.json +++ b/BusinessCentral.LinterCop/LinterCop.ruleset.json @@ -371,6 +371,11 @@ "id": "LC0074", "action": "Warning", "justification": "Set values for FlowFilter fields using filtering methods." + }, + { + "id": "LC0075", + "action": "Warning", + "justification": "Incorrect number or type of arguments in .Get() method on Record object." } ] } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx index 8d0facfb..57310dca 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx @@ -786,4 +786,13 @@ Directly assigning values to FlowFilter fields bypasses their purpose and invalidates the filtering logic, resulting in incorrect or unintended calculations. Instead, use the .SetFilter() or .SetRange() methods to define the appropriate filters. + + Incorrect number or type of arguments in .Get() method on Record object. + + + Invalid arguments in .Get() method for record {0}: {1}. + + + This rule ensures that calls to the built-in .Get() procedure on Record objects have the correct number and types of arguments matching the primary key (PK) of the record in question. + \ No newline at end of file diff --git a/README.md b/README.md index ee16600e..eff1f103 100644 --- a/README.md +++ b/README.md @@ -227,4 +227,5 @@ For an example and the default values see: [LinterCop.ruleset.json](./BusinessCe |[LC0071](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0071)|Incorrect 'IsHandled' parameter assignment.|Info| |[LC0072](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0072)|The documentation comment must match the procedure syntax.|Info| |[LC0073](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0073)|Handled parameters in event signatures should be passed by var.|Warning| -|[LC0074](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0074)|Set values for FlowFilter fields using filtering methods.|Warning| \ No newline at end of file +|[LC0074](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0074)|Set values for FlowFilter fields using filtering methods.|Warning| +|[LC0075](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0075)|Incorrect number or type of arguments in `.Get()` method on Record object.|Warning|13.0 \ No newline at end of file From 9853b94b2fcbcb15c4c0dd4ac98f8359be789bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tine=20Stari=C4=8D?= <42935028+tinestaric@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:49:50 +0000 Subject: [PATCH 02/28] Add Too Long Table Relation Check (#829) * Add Too Long Table Relation Check * Move Diagnostic Discriptor within class 0076 * Switch the rule to RegisterSymbolAction * Add Tests for Table Relation * Fix test case naming * Fix Table Ext Tests * Fix type cast safety on currentField Co-authored-by: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> * Exclude TableExt tests on <13 versions Co-authored-by: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> * Exclude TableExt tests on <13 versions Co-authored-by: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> * Remove redundant property type check * Join two Linq itterations * Remove duplicate testcase * Add ruleset entry --------- Co-authored-by: Tine Staric Co-authored-by: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Co-authored-by: Arthur van de Vondervoort --- BusinessCentral.LinterCop.Test/Rule0076.cs | 43 +++++++ .../HasDiagnostic/TableExtRelationLonger.al | 49 ++++++++ .../HasDiagnostic/TableRelationLonger.al | 25 ++++ .../NoDiagnostic/TableExtRelationEqual.al | 49 ++++++++ .../NoDiagnostic/TableExtRelationShorter.al | 48 ++++++++ .../NoDiagnostic/TableRelationEqual.al | 25 ++++ .../NoDiagnostic/TableRelationShorter.al | 25 ++++ .../Design/Rule0076TableRelationTooLong.cs | 111 ++++++++++++++++++ .../LinterCop.ruleset.json | 5 + .../LinterCopAnalyzers.resx | 9 ++ README.md | 9 +- 11 files changed, 394 insertions(+), 4 deletions(-) create mode 100644 BusinessCentral.LinterCop.Test/Rule0076.cs create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0076/HasDiagnostic/TableExtRelationLonger.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0076/HasDiagnostic/TableRelationLonger.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0076/NoDiagnostic/TableExtRelationEqual.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0076/NoDiagnostic/TableExtRelationShorter.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0076/NoDiagnostic/TableRelationEqual.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0076/NoDiagnostic/TableRelationShorter.al create mode 100644 BusinessCentral.LinterCop/Design/Rule0076TableRelationTooLong.cs diff --git a/BusinessCentral.LinterCop.Test/Rule0076.cs b/BusinessCentral.LinterCop.Test/Rule0076.cs new file mode 100644 index 00000000..b176b15b --- /dev/null +++ b/BusinessCentral.LinterCop.Test/Rule0076.cs @@ -0,0 +1,43 @@ +namespace BusinessCentral.LinterCop.Test; + +public class Rule0076 +{ + private string _testCaseDir = ""; + + [SetUp] + public void Setup() + { + _testCaseDir = Path.Combine(Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + "TestCases", "Rule0076"); + } + + [Test] + [TestCase("TableRelationLonger")] +#if !LessThenSpring2024 + [TestCase("TableExtRelationLonger")] +#endif + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.HasDiagnostic(code, Rule0076TableRelationTooLong.DiagnosticDescriptors.Rule0076TableRelationTooLong.Id); + } + + [Test] + [TestCase("TableRelationEqual")] + [TestCase("TableRelationShorter")] +#if !LessThenSpring2024 + [TestCase("TableExtRelationEqual")] + [TestCase("TableExtRelationShorter")] +#endif + public async Task NoDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.NoDiagnosticAtMarker(code, Rule0076TableRelationTooLong.DiagnosticDescriptors.Rule0076TableRelationTooLong.Id); + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0076/HasDiagnostic/TableExtRelationLonger.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0076/HasDiagnostic/TableExtRelationLonger.al new file mode 100644 index 00000000..2d85e6b0 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0076/HasDiagnostic/TableExtRelationLonger.al @@ -0,0 +1,49 @@ +table 50108 MyTable +{ + fields + { + field(1; [|MyText|]; Text[1]) + { + TableRelation = MyTable2.MyTextExt; + } + + field(2; [|MyCode|]; Text[1]) + { + TableRelation = if (MyCode = const('const')) MyTable2.MyCodeExt + else + MyTable2.MyTextExt; + } + } + + keys + { + key(PK; MyText) { Clustered = true; } + } +} + + +table 50107 MyTable2 +{ + fields + { + field(1; MyText; Text[1]) { } + + field(2; MyCode; Text[1]) { } + } + + keys + { + key(PK; MyText) { Clustered = true; } + } +} + + +tableextension 50110 MyExtension extends MyTable2 +{ + fields + { + field(3; MyTextExt; Text[100]) { } + + field(4; MyCodeExt; Text[100]) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0076/HasDiagnostic/TableRelationLonger.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0076/HasDiagnostic/TableRelationLonger.al new file mode 100644 index 00000000..0ab5ef4d --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0076/HasDiagnostic/TableRelationLonger.al @@ -0,0 +1,25 @@ +table 50108 MyTable +{ + fields + { + field(1; [|MyText|]; Text[1]) + { + TableRelation = MyTable.MyTextExt; + } + + field(2; [|MyCode|]; Text[1]) + { + TableRelation = if (MyCode = const('const')) MyTable.MyCodeExt + else + MyTable.MyTextExt; + } + field(3; MyTextExt; Text[100]) { } + + field(4; MyCodeExt; Text[100]) { } + } + + keys + { + key(PK; MyText) { Clustered = true; } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0076/NoDiagnostic/TableExtRelationEqual.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0076/NoDiagnostic/TableExtRelationEqual.al new file mode 100644 index 00000000..f4f2ca93 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0076/NoDiagnostic/TableExtRelationEqual.al @@ -0,0 +1,49 @@ +table 50108 MyTable +{ + fields + { + field(1; [|MyText|]; Text[100]) + { + TableRelation = MyTable2.MyTextExt; + } + + field(2; [|MyCode|]; Text[100]) + { + TableRelation = if (MyCode = const('const')) MyTable2.MyCodeExt + else + MyTable2.MyTextExt; + } + } + + keys + { + key(PK; MyText) { Clustered = true; } + } +} + + +table 50107 MyTable2 +{ + fields + { + field(1; MyText; Text[1]) { } + + field(2; MyCode; Text[1]) { } + } + + keys + { + key(PK; MyText) { Clustered = true; } + } +} + + +tableextension 50110 MyExtension extends MyTable2 +{ + fields + { + field(3; MyTextExt; Text[100]) { } + + field(4; MyCodeExt; Text[100]) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0076/NoDiagnostic/TableExtRelationShorter.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0076/NoDiagnostic/TableExtRelationShorter.al new file mode 100644 index 00000000..72e5531d --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0076/NoDiagnostic/TableExtRelationShorter.al @@ -0,0 +1,48 @@ +table 50108 MyTable +{ + fields + { + field(1; [|MyText|]; Text[200]) + { + TableRelation = MyTable2.MyTextExt; + } + + field(2; [|MyCode|]; Text[200]) + { + TableRelation = if (MyCode = const('const')) MyTable2.MyCodeExt + else + MyTable2.MyTextExt; + } + } + + keys + { + key(PK; MyText) { Clustered = true; } + } +} + +table 50107 MyTable2 +{ + fields + { + field(1; MyText; Text[1]) { } + + field(2; MyCode; Text[1]) { } + } + + keys + { + key(PK; MyText) { Clustered = true; } + } +} + + +tableextension 50110 MyExtension extends MyTable2 +{ + fields + { + field(3; MyTextExt; Text[100]) { } + + field(4; MyCodeExt; Text[100]) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0076/NoDiagnostic/TableRelationEqual.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0076/NoDiagnostic/TableRelationEqual.al new file mode 100644 index 00000000..13033378 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0076/NoDiagnostic/TableRelationEqual.al @@ -0,0 +1,25 @@ +table 50108 MyTable +{ + fields + { + field(1; [|MyText|]; Text[100]) + { + TableRelation = MyTable.MyTextExt; + } + + field(2; [|MyCode|]; Text[100]) + { + TableRelation = if (MyCode = const('const')) MyTable.MyCodeExt + else + MyTable.MyTextExt; + } + field(3; MyTextExt; Text[100]) { } + + field(4; MyCodeExt; Text[100]) { } + } + + keys + { + key(PK; MyText) { Clustered = true; } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0076/NoDiagnostic/TableRelationShorter.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0076/NoDiagnostic/TableRelationShorter.al new file mode 100644 index 00000000..225aa18f --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0076/NoDiagnostic/TableRelationShorter.al @@ -0,0 +1,25 @@ +table 50108 MyTable +{ + fields + { + field(1; [|MyText|]; Text[200]) + { + TableRelation = MyTable.MyTextExt; + } + + field(2; [|MyCode|]; Text[200]) + { + TableRelation = if (MyCode = const('const')) MyTable.MyCodeExt + else + MyTable.MyTextExt; + } + field(3; MyTextExt; Text[100]) { } + + field(4; MyCodeExt; Text[100]) { } + } + + keys + { + key(PK; MyText) { Clustered = true; } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0076TableRelationTooLong.cs b/BusinessCentral.LinterCop/Design/Rule0076TableRelationTooLong.cs new file mode 100644 index 00000000..bcc70611 --- /dev/null +++ b/BusinessCentral.LinterCop/Design/Rule0076TableRelationTooLong.cs @@ -0,0 +1,111 @@ +using BusinessCentral.LinterCop.AnalysisContextExtension; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; +using System.Collections.Immutable; + +namespace BusinessCentral.LinterCop.Design; +[DiagnosticAnalyzer] +public class Rule0076TableRelationTooLong : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0076TableRelationTooLong); + + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Field); + + private void AnalyzeSymbol(SymbolAnalysisContext context) + { + if (context.IsObsoletePendingOrRemoved()) + return; + + if (context.Symbol is not IFieldSymbol currentField) + return; + + var tableRelation = currentField + .GetProperty(PropertyKind.TableRelation) + ?.GetPropertyValueSyntax(); + + if (tableRelation is null) + return; + + AnalyzeTableRelations(context, currentField, tableRelation); + } + + private void AnalyzeTableRelations(SymbolAnalysisContext context, IFieldSymbol currentField, TableRelationPropertyValueSyntax? tableRelation) + { + while (tableRelation is not null) + { + if (tableRelation.RelatedTableField is QualifiedNameSyntax relatedField) + { + var relatedFieldSymbol = GetRelatedFieldSymbol( + relatedField.Left as IdentifierNameSyntax, + relatedField.Right as IdentifierNameSyntax, + context.Compilation); + + if (relatedFieldSymbol is not null && ShouldReportDiagnostic(currentField, relatedFieldSymbol)) + { + ReportLengthMismatch(context, currentField, relatedFieldSymbol, relatedField); + } + } + + tableRelation = tableRelation.ElseExpression?.ElseTableRelationCondition; + } + } + + private static bool ShouldReportDiagnostic(IFieldSymbol currentField, IFieldSymbol? relatedField) => + relatedField?.HasLength == true && + currentField.HasLength && + currentField.Length < relatedField.Length; + + private static void ReportLengthMismatch(SymbolAnalysisContext context, IFieldSymbol currentField, + IFieldSymbol relatedField, QualifiedNameSyntax relatedFieldSyntax) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0076TableRelationTooLong, + currentField.GetLocation(), + relatedField.Length, + relatedFieldSyntax.ToString(), + currentField.Length, + currentField.Name)); + } + + private IFieldSymbol? GetRelatedFieldSymbol(IdentifierNameSyntax? table, IdentifierNameSyntax? field, Compilation compilation) + { + if (table?.GetIdentifierOrLiteralValue() is not string tableName || + field?.GetIdentifierOrLiteralValue() is not string fieldName) + return null; + + return GetFieldFromTable(tableName, fieldName, compilation) ?? + GetFieldFromTableExtension(tableName, fieldName, compilation); + } + + private static IFieldSymbol? GetFieldFromTable(string tableName, string fieldName, Compilation compilation) + { + var tables = compilation.GetApplicationObjectTypeSymbolsByNameAcrossModules(SymbolKind.Table, tableName); + return tables.FirstOrDefault() is ITableTypeSymbol tableSymbol + ? tableSymbol.Fields.FirstOrDefault(x => x.Name == fieldName) + : null; + } + + private static IFieldSymbol? GetFieldFromTableExtension(string tableName, string fieldName, Compilation compilation) + { + return compilation.GetDeclaredApplicationObjectSymbols() + .OfType() + .Where(ext => ext.Target?.Name == tableName) + .SelectMany(ext => ext.AddedFields) + .FirstOrDefault(field => field.Name == fieldName); + } + + public static class DiagnosticDescriptors + { + public static readonly DiagnosticDescriptor Rule0076TableRelationTooLong = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0076", + title: LinterCopAnalyzers.GetLocalizableString("Rule0076TableRelationTooLongTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0076TableRelationTooLongFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0076TableRelationTooLongDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0076"); + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCop.ruleset.json b/BusinessCentral.LinterCop/LinterCop.ruleset.json index eaed96c5..d9fc3d40 100644 --- a/BusinessCentral.LinterCop/LinterCop.ruleset.json +++ b/BusinessCentral.LinterCop/LinterCop.ruleset.json @@ -376,6 +376,11 @@ "id": "LC0075", "action": "Warning", "justification": "Incorrect number or type of arguments in .Get() method on Record object." + }, + { + "id": "LC0076", + "action": "Warning", + "justification": "The field with table relation should have at least the same length as the referenced field." } ] } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx index 57310dca..d57a862c 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx @@ -795,4 +795,13 @@ This rule ensures that calls to the built-in .Get() procedure on Record objects have the correct number and types of arguments matching the primary key (PK) of the record in question. + + Table relation field length mismatch + + + The related field has length {0} ({1}) which is longer than the current field length {2} ({3}) + + + The field with table relation should have at least the same length as the referenced field. + \ No newline at end of file diff --git a/README.md b/README.md index eff1f103..f536b6de 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ [![Github All Releases](https://img.shields.io/github/v/release/StefanMaron/BusinessCentral.LinterCop?label=latest%20version)]() [![Github All Releases](https://img.shields.io/github/downloads/StefanMaron/BusinessCentral.LinterCop/latest/total?label=downloads%20latest%20version)]() -This code analyzer is meant to check your code for all sorts of problems. Be it code that tecnically 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. +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](LinterCop.ruleset.json) and disable all rules you dont need. +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](LinterCop.ruleset.json) and disable all rules you don't need. ## Please Contribute! @@ -153,7 +153,7 @@ For an example and the default values see: [LinterCop.ruleset.json](./BusinessCe |Id| Title|Default Severity|AL version| |---|---|---|---| -|[LC0000](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0000)|An error ocurred in a given rule. Please create an issue on GitHub|Info| +|[LC0000](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0000)|An error occurred in a given rule. Please create an issue on GitHub|Info| |[LC0001](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0001)|FlowFields should not be editable.|Warning| |[LC0002](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0002)|`Commit()` needs a comment to justify its existence. Either a leading or a trailing comment.|Warning| |[LC0003](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0003)|Do not use an Object ID for properties or variable declarations.|Warning| @@ -228,4 +228,5 @@ For an example and the default values see: [LinterCop.ruleset.json](./BusinessCe |[LC0072](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0072)|The documentation comment must match the procedure syntax.|Info| |[LC0073](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0073)|Handled parameters in event signatures should be passed by var.|Warning| |[LC0074](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0074)|Set values for FlowFilter fields using filtering methods.|Warning| -|[LC0075](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0075)|Incorrect number or type of arguments in `.Get()` method on Record object.|Warning|13.0 \ No newline at end of file +|[LC0075](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0075)|Incorrect number or type of arguments in `.Get()` method on Record object.|Warning|13.0 +|[LC0076](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0076)|The field with table relation should have at least the same length as the referenced field.|Warning| From f56d4cd82c2e357c86b1e28de5a12b98d3d180f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tine=20Stari=C4=8D?= <42935028+tinestaric@users.noreply.github.com> Date: Fri, 13 Dec 2024 07:16:42 +0000 Subject: [PATCH 03/28] New rules: Missing brackets - Triggers on Temporary records - Non-Public Event Publishers (#830) * Add three rules WIP * Add tests and readme * Fix 0078 if underlying table is temp * Add ruleset entries * Fix ResX reference * Sort on Title/Format/Description * Remove whitespaces * Move diagnostic location to method identifier Co-authored-by: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> * Add Safe Type casting Co-authored-by: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> * Lower rule severity to Info * Move warning location checks in tests * Change diagnostic location * Fix lazy evaluation --------- Co-authored-by: Tine Staric Co-authored-by: Arthur van de Vondervoort Co-authored-by: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> --- BusinessCentral.LinterCop.Test/Rule0077.cs | 35 ++++++++++ BusinessCentral.LinterCop.Test/Rule0078.cs | 38 +++++++++++ BusinessCentral.LinterCop.Test/Rule0079.cs | 36 ++++++++++ .../Rule0077/HasDiagnostic/NoBrackets.al | 23 +++++++ .../Rule0077/NoDiagnostic/Brackets.al | 24 +++++++ .../Rule0078/HasDiagnostic/TempVar.al | 22 +++++++ .../Rule0078/NoDiagnostic/TempTable.al | 24 +++++++ .../NoDiagnostic/TempTableExplicitTemp.al | 24 +++++++ .../Rule0078/NoDiagnostic/TempVarExplicit.al | 22 +++++++ .../Rule0078/NoDiagnostic/TempVarImplicit.al | 22 +++++++ .../Rule0079/HasDiagnostic/PublicEvent.al | 22 +++++++ .../Rule0079/NoDiagnostic/InternalEvent.al | 22 +++++++ .../Rule0079/NoDiagnostic/LocalEvent.al | 22 +++++++ .../Design/Rule0077MissingParenthesis.cs | 56 ++++++++++++++++ .../Design/Rule0078TempRecRunTrigger.cs | 65 +++++++++++++++++++ .../Design/Rule0079NonPublicEventPublisher.cs | 39 +++++++++++ .../LinterCop.ruleset.json | 17 ++++- .../LinterCopAnalyzers.resx | 27 ++++++++ README.md | 3 + 19 files changed, 542 insertions(+), 1 deletion(-) create mode 100644 BusinessCentral.LinterCop.Test/Rule0077.cs create mode 100644 BusinessCentral.LinterCop.Test/Rule0078.cs create mode 100644 BusinessCentral.LinterCop.Test/Rule0079.cs create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0077/HasDiagnostic/NoBrackets.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0077/NoDiagnostic/Brackets.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0078/HasDiagnostic/TempVar.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0078/NoDiagnostic/TempTable.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0078/NoDiagnostic/TempTableExplicitTemp.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0078/NoDiagnostic/TempVarExplicit.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0078/NoDiagnostic/TempVarImplicit.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0079/HasDiagnostic/PublicEvent.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0079/NoDiagnostic/InternalEvent.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0079/NoDiagnostic/LocalEvent.al create mode 100644 BusinessCentral.LinterCop/Design/Rule0077MissingParenthesis.cs create mode 100644 BusinessCentral.LinterCop/Design/Rule0078TempRecRunTrigger.cs create mode 100644 BusinessCentral.LinterCop/Design/Rule0079NonPublicEventPublisher.cs diff --git a/BusinessCentral.LinterCop.Test/Rule0077.cs b/BusinessCentral.LinterCop.Test/Rule0077.cs new file mode 100644 index 00000000..97ef29a6 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/Rule0077.cs @@ -0,0 +1,35 @@ +namespace BusinessCentral.LinterCop.Test; + +public class Rule0077 +{ + private string _testCaseDir = ""; + + [SetUp] + public void Setup() + { + _testCaseDir = Path.Combine(Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + "TestCases", "Rule0077"); + } + + [Test] + [TestCase("NoBrackets")] + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.HasDiagnostic(code, Rule0077MissingParenthesis.DiagnosticDescriptors.Rule0077MissingParenthesis.Id); + } + + [Test] + [TestCase("Brackets")] + public async Task NoDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.NoDiagnosticAtMarker(code, Rule0077MissingParenthesis.DiagnosticDescriptors.Rule0077MissingParenthesis.Id); + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/Rule0078.cs b/BusinessCentral.LinterCop.Test/Rule0078.cs new file mode 100644 index 00000000..52b311a8 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/Rule0078.cs @@ -0,0 +1,38 @@ +namespace BusinessCentral.LinterCop.Test; + +public class Rule0078 +{ + private string _testCaseDir = ""; + + [SetUp] + public void Setup() + { + _testCaseDir = Path.Combine(Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + "TestCases", "Rule0078"); + } + + [Test] + [TestCase("TempVar")] + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.HasDiagnostic(code, Rule0078TemporaryRecordsShouldNotTriggerTableTriggers.DiagnosticDescriptors.Rule0078TemporaryRecordsShouldNotTriggerTableTriggers.Id); + } + + [Test] + [TestCase("TempVarImplicit")] + [TestCase("TempVarExplicit")] + [TestCase("TempTable")] + [TestCase("TempTableExplicitTemp")] + public async Task NoDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.NoDiagnosticAtMarker(code, Rule0078TemporaryRecordsShouldNotTriggerTableTriggers.DiagnosticDescriptors.Rule0078TemporaryRecordsShouldNotTriggerTableTriggers.Id); + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/Rule0079.cs b/BusinessCentral.LinterCop.Test/Rule0079.cs new file mode 100644 index 00000000..a49a20c1 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/Rule0079.cs @@ -0,0 +1,36 @@ +namespace BusinessCentral.LinterCop.Test; + +public class Rule0079 +{ + private string _testCaseDir = ""; + + [SetUp] + public void Setup() + { + _testCaseDir = Path.Combine(Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + "TestCases", "Rule0079"); + } + + [Test] + [TestCase("PublicEvent")] + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.HasDiagnostic(code, Rule0079NonPublicEventPublisher.DiagnosticDescriptors.Rule0079NonPublicEventPublisher.Id); + } + + [Test] + [TestCase("LocalEvent")] + [TestCase("InternalEvent")] + public async Task NoDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.NoDiagnosticAtMarker(code, Rule0079NonPublicEventPublisher.DiagnosticDescriptors.Rule0079NonPublicEventPublisher.Id); + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0077/HasDiagnostic/NoBrackets.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0077/HasDiagnostic/NoBrackets.al new file mode 100644 index 00000000..b2410339 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0077/HasDiagnostic/NoBrackets.al @@ -0,0 +1,23 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + i: Integer; + b: Boolean; + begin + [|Today|]; + [|WorkDate|]; + [|GuiAllowed|]; + i := MyTable.[|Count|]; + b := MyTable.[|IsEmpty|]; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0077/NoDiagnostic/Brackets.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0077/NoDiagnostic/Brackets.al new file mode 100644 index 00000000..5cbbdc39 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0077/NoDiagnostic/Brackets.al @@ -0,0 +1,24 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + i: Integer; + b: Boolean; + d: Date; + begin + d := [|Today()|]; + d := [|WorkDate()|]; + b := [|GuiAllowed()|]; + i := MyTable.[|Count()|]; + b := MyTable.[|IsEmpty()|]; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0078/HasDiagnostic/TempVar.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0078/HasDiagnostic/TempVar.al new file mode 100644 index 00000000..7fa4c88e --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0078/HasDiagnostic/TempVar.al @@ -0,0 +1,22 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable temporary; + begin + [|MyTable.Validate(MyField, 1)|]; + [|MyTable.Insert(true)|]; + [|MyTable.Modify(true)|]; + [|MyTable.Delete(true)|]; + [|MyTable.DeleteAll(true)|]; + [|MyTable.ModifyAll(MyField, 1, true)|]; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0078/NoDiagnostic/TempTable.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0078/NoDiagnostic/TempTable.al new file mode 100644 index 00000000..5e0e7693 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0078/NoDiagnostic/TempTable.al @@ -0,0 +1,24 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + [|MyTable.Validate(MyField, 1)|]; + [|MyTable.Insert(true)|]; + [|MyTable.Modify(true)|]; + [|MyTable.Delete(true)|]; + [|MyTable.DeleteAll(true)|]; + [|MyTable.ModifyAll(MyField, 1, true)|]; + end; +} + +table 50100 MyTable +{ + TableType = Temporary; + + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0078/NoDiagnostic/TempTableExplicitTemp.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0078/NoDiagnostic/TempTableExplicitTemp.al new file mode 100644 index 00000000..aba3538f --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0078/NoDiagnostic/TempTableExplicitTemp.al @@ -0,0 +1,24 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable temporary; + begin + [|MyTable.Validate(MyField, 1)|]; + [|MyTable.Insert(true)|]; + [|MyTable.Modify(true)|]; + [|MyTable.Delete(true)|]; + [|MyTable.DeleteAll(true)|]; + [|MyTable.ModifyAll(MyField, 1, true)|]; + end; +} + +table 50100 MyTable +{ + TableType = Temporary; + + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0078/NoDiagnostic/TempVarExplicit.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0078/NoDiagnostic/TempVarExplicit.al new file mode 100644 index 00000000..8f536833 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0078/NoDiagnostic/TempVarExplicit.al @@ -0,0 +1,22 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable temporary; + begin + [|MyTable.MyField := 1|]; + [|MyTable.Insert(false)|]; + [|MyTable.Modify(false)|]; + [|MyTable.Delete(false)|]; + [|MyTable.DeleteAll(false)|]; + [|MyTable.ModifyAll(MyField, 1, false)|]; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0078/NoDiagnostic/TempVarImplicit.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0078/NoDiagnostic/TempVarImplicit.al new file mode 100644 index 00000000..8bfc09da --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0078/NoDiagnostic/TempVarImplicit.al @@ -0,0 +1,22 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable temporary; + begin + [|MyTable.MyField := 1|]; + [|MyTable.Insert()|]; + [|MyTable.Modify()|]; + [|MyTable.Delete()|]; + [|MyTable.DeleteAll()|]; + [|MyTable.ModifyAll(MyField, 1)|]; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0079/HasDiagnostic/PublicEvent.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0079/HasDiagnostic/PublicEvent.al new file mode 100644 index 00000000..7c484f22 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0079/HasDiagnostic/PublicEvent.al @@ -0,0 +1,22 @@ +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } + + [BusinessEvent(true)] + procedure [|MyProcedure|]() + begin + end; + + [IntegrationEvent(true, false)] + procedure [|MyProcedure2|]() + begin + end; + + [InternalEvent(false)] + procedure [|MyProcedure3|]() + begin + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0079/NoDiagnostic/InternalEvent.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0079/NoDiagnostic/InternalEvent.al new file mode 100644 index 00000000..ecffb66e --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0079/NoDiagnostic/InternalEvent.al @@ -0,0 +1,22 @@ +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } + + [BusinessEvent(true)] + internal procedure [|MyProcedure|]() + begin + end; + + [IntegrationEvent(true, false)] + internal procedure [|MyProcedure2|]() + begin + end; + + [InternalEvent(false)] + internal procedure [|MyProcedure3|]() + begin + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0079/NoDiagnostic/LocalEvent.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0079/NoDiagnostic/LocalEvent.al new file mode 100644 index 00000000..c3f8dd54 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0079/NoDiagnostic/LocalEvent.al @@ -0,0 +1,22 @@ +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } + + [BusinessEvent(true)] + local procedure [|MyProcedure|]() + begin + end; + + [IntegrationEvent(true, false)] + local procedure [|MyProcedure2|]() + begin + end; + + [InternalEvent(false)] + local procedure [|MyProcedure3|]() + begin + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0077MissingParenthesis.cs b/BusinessCentral.LinterCop/Design/Rule0077MissingParenthesis.cs new file mode 100644 index 00000000..53a7510e --- /dev/null +++ b/BusinessCentral.LinterCop/Design/Rule0077MissingParenthesis.cs @@ -0,0 +1,56 @@ +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; +using System.Collections.Immutable; +using BusinessCentral.LinterCop.AnalysisContextExtension; + +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0077MissingParenthesis : DiagnosticAnalyzer +{ + private static readonly ImmutableHashSet MethodsRequiringParenthesis = ImmutableHashSet.Create( + "Count", + "IsEmpty", + "Today", + "WorkDate", + "GuiAllowed" + ); + + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0077MissingParenthesis); + + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(AnalyzeParenthesis, OperationKind.InvocationExpression); + + private void AnalyzeParenthesis(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) + return; + + if (ctx.Operation is not IInvocationExpression { Arguments.Length: 0 } operation || + operation.TargetMethod is not IMethodSymbol { MethodKind: MethodKind.BuiltInMethod } method) + return; + + if (MethodsRequiringParenthesis.Contains(method.Name) && + !operation.Syntax.GetLastToken().IsKind(SyntaxKind.CloseParenToken)) + { + var location = operation.Syntax.GetIdentifierNameSyntax()?.GetLocation() ?? operation.Syntax.GetLocation(); + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0077MissingParenthesis, + location, + method.Name)); + } + } + + public static class DiagnosticDescriptors + { + public static readonly DiagnosticDescriptor Rule0077MissingParenthesis = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0077", + title: LinterCopAnalyzers.GetLocalizableString("Rule0077MissingParenthesisTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0077MissingParenthesisFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0077MissingParenthesisDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0077"); + } +} diff --git a/BusinessCentral.LinterCop/Design/Rule0078TempRecRunTrigger.cs b/BusinessCentral.LinterCop/Design/Rule0078TempRecRunTrigger.cs new file mode 100644 index 00000000..7ec2e362 --- /dev/null +++ b/BusinessCentral.LinterCop/Design/Rule0078TempRecRunTrigger.cs @@ -0,0 +1,65 @@ +using BusinessCentral.LinterCop.AnalysisContextExtension; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; + +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0078TemporaryRecordsShouldNotTriggerTableTriggers : DiagnosticAnalyzer +{ + private static readonly HashSet methodsToCheck = new() { "Insert", "Modify", "Delete", "DeleteAll", "Validate", "ModifyAll" }; + + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0078TemporaryRecordsShouldNotTriggerTableTriggers); + + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(AnalyzeTemporaryRecords, OperationKind.InvocationExpression); + + private void AnalyzeTemporaryRecords(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) + return; + + if (ctx.Operation is not IInvocationExpression invocationExpression) + return; + if (!methodsToCheck.Contains(invocationExpression.TargetMethod.Name)) + return; + + if (invocationExpression.Instance?.Type is not IRecordTypeSymbol record || + !record.Temporary || + record.BaseTable.TableType == TableTypeKind.Temporary) + return; + + bool isExecutingTriggersOrValidation = invocationExpression.TargetMethod.Name switch + { + "Validate" => true, + "ModifyAll" => invocationExpression.Arguments.Length == 3 && + IsRunTriggerEnabled(invocationExpression.Arguments[2]), + _ => invocationExpression.Arguments.Length == 1 && + IsRunTriggerEnabled(invocationExpression.Arguments[0]) + }; + + if (isExecutingTriggersOrValidation) + { + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0078TemporaryRecordsShouldNotTriggerTableTriggers, + ctx.Operation.Syntax.GetLocation())); + } + } + + private static bool IsRunTriggerEnabled(IArgument argument) => + argument.Value.ConstantValue.HasValue && + argument.Value.ConstantValue.Value is bool isEnabled && + isEnabled; + + public static class DiagnosticDescriptors + { + public static readonly DiagnosticDescriptor Rule0078TemporaryRecordsShouldNotTriggerTableTriggers = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0078", + title: LinterCopAnalyzers.GetLocalizableString("Rule0078TemporaryRecordsTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0078TemporaryRecordsFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0078TemporaryRecordsDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0078"); + } +} diff --git a/BusinessCentral.LinterCop/Design/Rule0079NonPublicEventPublisher.cs b/BusinessCentral.LinterCop/Design/Rule0079NonPublicEventPublisher.cs new file mode 100644 index 00000000..d7e0316c --- /dev/null +++ b/BusinessCentral.LinterCop/Design/Rule0079NonPublicEventPublisher.cs @@ -0,0 +1,39 @@ +using BusinessCentral.LinterCop.AnalysisContextExtension; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; + +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0079NonPublicEventPublisher : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0079NonPublicEventPublisher); + + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(AnalyzeEventPublisher, SymbolKind.Method); + + private void AnalyzeEventPublisher(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) + return; + + if (ctx.Symbol is not IMethodSymbol symbol || !symbol.IsEvent) + return; + + if (!symbol.IsLocal && !symbol.IsInternal) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0079NonPublicEventPublisher, symbol.GetLocation())); + } + + public static class DiagnosticDescriptors + { + public static readonly DiagnosticDescriptor Rule0079NonPublicEventPublisher = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0079", + title: LinterCopAnalyzers.GetLocalizableString("Rule0079NonPublicEventPublisherTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0079NonPublicEventPublisherFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0079NonPublicEventPublisherDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0079"); + } +} diff --git a/BusinessCentral.LinterCop/LinterCop.ruleset.json b/BusinessCentral.LinterCop/LinterCop.ruleset.json index d9fc3d40..2ef22e83 100644 --- a/BusinessCentral.LinterCop/LinterCop.ruleset.json +++ b/BusinessCentral.LinterCop/LinterCop.ruleset.json @@ -381,6 +381,21 @@ "id": "LC0076", "action": "Warning", "justification": "The field with table relation should have at least the same length as the referenced field." - } + }, + { + "id": "LC0077", + "action": "Info", + "justification": "Methods that require parenthesis should always be called with parenthesis." + }, + { + "id": "LC0078", + "action": "Info", + "justification": "Temporary records should not trigger table triggers." + }, + { + "id": "LC0079", + "action": "Info", + "justification": "Event publishers should not be public." + } ] } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx index d57a862c..3ae7453e 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx @@ -803,5 +803,32 @@ The field with table relation should have at least the same length as the referenced field. + + + Function calls should have parenthesis even if they do not have any parameters. + + + You must specify open and close parenthesis after '{0}' + + + You must specify open and close parenthesis after '{0}' + + + Temporary records should not trigger the table triggers. + + + Temporary records should not trigger the table triggers. + + + Temporary records should not trigger the table triggers. + + + Event Publishers should be local or internal to allow for future parameter extensions. + + + Event Publishers should be local or internal to allow for future parameter extensions. + + + Event Publishers should be local or internal to allow for future parameter extensions. \ No newline at end of file diff --git a/README.md b/README.md index f536b6de..22f09e56 100644 --- a/README.md +++ b/README.md @@ -230,3 +230,6 @@ For an example and the default values see: [LinterCop.ruleset.json](./BusinessCe |[LC0074](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0074)|Set values for FlowFilter fields using filtering methods.|Warning| |[LC0075](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0075)|Incorrect number or type of arguments in `.Get()` method on Record object.|Warning|13.0 |[LC0076](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0076)|The field with table relation should have at least the same length as the referenced field.|Warning| +|[LC0077](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0077)|Methods should always be called with parenthesis.|Info| +|[LC0078](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0078)|Temporary records should not trigger table triggers.|Info| +|[LC0079](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0079)|Event publishers should not be public.|Info| \ No newline at end of file From 9650306b609dbc48721409e2a357618bd1f79c2d Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Fri, 13 Dec 2024 19:00:00 +0100 Subject: [PATCH 04/28] Add new Rule0080: Analyze JsonToken JPath (#832) --- BusinessCentral.LinterCop.Test/Rule0080.cs | 35 +++++++++++ .../Rule0080/HasDiagnostic/SelectToken.al | 10 ++++ .../Rule0080/NoDiagnostic/SelectToken.al | 10 ++++ .../Design/Rule0080AnalyzeJsonTokenJPath.cs | 58 +++++++++++++++++++ .../LinterCop.ruleset.json | 7 ++- .../LinterCopAnalyzers.resx | 9 +++ README.md | 3 +- 7 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 BusinessCentral.LinterCop.Test/Rule0080.cs create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0080/HasDiagnostic/SelectToken.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0080/NoDiagnostic/SelectToken.al create mode 100644 BusinessCentral.LinterCop/Design/Rule0080AnalyzeJsonTokenJPath.cs diff --git a/BusinessCentral.LinterCop.Test/Rule0080.cs b/BusinessCentral.LinterCop.Test/Rule0080.cs new file mode 100644 index 00000000..3ede1696 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/Rule0080.cs @@ -0,0 +1,35 @@ +namespace BusinessCentral.LinterCop.Test; + +public class Rule0080 +{ + private string _testCaseDir = ""; + + [SetUp] + public void Setup() + { + _testCaseDir = Path.Combine(Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + "TestCases", "Rule0080"); + } + + [Test] + [TestCase("SelectToken")] + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.HasDiagnostic(code, Rule0080AnalyzeJsonTokenJPath.DiagnosticDescriptors.Rule0080AnalyzeJsonTokenJPath.Id); + } + + [Test] + [TestCase("SelectToken")] + public async Task NoDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.NoDiagnosticAtMarker(code, Rule0080AnalyzeJsonTokenJPath.DiagnosticDescriptors.Rule0080AnalyzeJsonTokenJPath.Id); + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0080/HasDiagnostic/SelectToken.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0080/HasDiagnostic/SelectToken.al new file mode 100644 index 00000000..882da16f --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0080/HasDiagnostic/SelectToken.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyJsonToken: JsonToken; + Result: JsonToken; + begin + MyJsonToken.SelectToken([|'$.custom_attributes[?(@.attribute_code == "activation_status")].value'|], Result); + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0080/NoDiagnostic/SelectToken.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0080/NoDiagnostic/SelectToken.al new file mode 100644 index 00000000..842d95b8 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0080/NoDiagnostic/SelectToken.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyJsonToken: JsonToken; + Result: JsonToken; + begin + MyJsonToken.SelectToken([|'$.custom_attributes[?(@.attribute_code == ''activation_status'')].value'|], Result); + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0080AnalyzeJsonTokenJPath.cs b/BusinessCentral.LinterCop/Design/Rule0080AnalyzeJsonTokenJPath.cs new file mode 100644 index 00000000..16cec88a --- /dev/null +++ b/BusinessCentral.LinterCop/Design/Rule0080AnalyzeJsonTokenJPath.cs @@ -0,0 +1,58 @@ +using BusinessCentral.LinterCop.AnalysisContextExtension; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; +using System.Collections.Immutable; + +namespace BusinessCentral.LinterCop.Design +{ + [DiagnosticAnalyzer] + public class Rule0080AnalyzeJsonTokenJPath : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0080AnalyzeJsonTokenJPath); + + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(new Action(this.SelectSingleNode), OperationKind.InvocationExpression); + + private void SelectSingleNode(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) + return; + + if (ctx.Operation is not IInvocationExpression operation) + return; + + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + operation.TargetMethod.Name != "SelectToken" || + operation.TargetMethod.ContainingSymbol?.Name != "JsonToken") + return; + + var stringLiterals = operation.Arguments + .Where(p => p.Parameter?.Name == "Path") + .Select(p => p.Syntax) + .OfType() + .Select(l => l.Literal) + .OfType() + .Where(l => l.Value.ValueText?.Contains('"') ?? false); + + foreach (var stringLiteral in stringLiterals) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0080AnalyzeJsonTokenJPath, + stringLiteral.GetLocation())); + } + } + + public static class DiagnosticDescriptors + { + public static readonly DiagnosticDescriptor Rule0080AnalyzeJsonTokenJPath = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0080", + title: LinterCopAnalyzers.GetLocalizableString("Rule0080AnalyzeJsonTokenJPathTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0080AnalyzePathOnJsonTokenFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0080AnalyzePathOnJsonTokenDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0080"); + } + } +} diff --git a/BusinessCentral.LinterCop/LinterCop.ruleset.json b/BusinessCentral.LinterCop/LinterCop.ruleset.json index 2ef22e83..c504c31f 100644 --- a/BusinessCentral.LinterCop/LinterCop.ruleset.json +++ b/BusinessCentral.LinterCop/LinterCop.ruleset.json @@ -396,6 +396,11 @@ "id": "LC0079", "action": "Info", "justification": "Event publishers should not be public." - } + }, + { + "id": "LC0080", + "action": "Warning", + "justification": "Replace double quotes in JPath expressions with two single quotes." + } ] } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx index 3ae7453e..052e45d2 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx @@ -831,4 +831,13 @@ Event Publishers should be local or internal to allow for future parameter extensions. + + Replace double quotes in JPath expressions with two single quotes. + + + Double quote character detected in JPath expression. Replace all double quotes (") with two single quotes (''). + + + Detects and warns against the use of double-quote (") characters in JPath expressions. + \ No newline at end of file diff --git a/README.md b/README.md index 22f09e56..a9a33b8f 100644 --- a/README.md +++ b/README.md @@ -232,4 +232,5 @@ For an example and the default values see: [LinterCop.ruleset.json](./BusinessCe |[LC0076](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0076)|The field with table relation should have at least the same length as the referenced field.|Warning| |[LC0077](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0077)|Methods should always be called with parenthesis.|Info| |[LC0078](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0078)|Temporary records should not trigger table triggers.|Info| -|[LC0079](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0079)|Event publishers should not be public.|Info| \ No newline at end of file +|[LC0079](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0079)|Event publishers should not be public.|Info| +|[LC0080](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0080)|Replace double quotes in JPath expressions with two single quotes.|Warning| \ No newline at end of file From f0a7234bf80a782a623cc08dea6a8e05c0f6ad2c Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Sat, 14 Dec 2024 15:36:44 +0100 Subject: [PATCH 05/28] New Rule0081: Use Rec.IsEmpty() for checking record existence (#833) --- BusinessCentral.LinterCop.Test/Rule0081.cs | 43 ++++++++++ .../OneGreaterThanRecordCount.al | 17 ++++ .../HasDiagnostic/RecordCountEqualsZero.al | 17 ++++ .../RecordCountGreaterThanOrEqualZero.al | 17 ++++ .../RecordCountGreaterThanZero.al | 17 ++++ .../HasDiagnostic/RecordCountLessThanOne.al | 17 ++++ .../RecordCountLessThanOrEqualZero.al | 17 ++++ .../HasDiagnostic/RecordCountLessThanZero.al | 17 ++++ .../HasDiagnostic/RecordCountNotEqualsZero.al | 17 ++++ .../NoDiagnostic/RecordCountEqualsOne.al | 17 ++++ .../RecordTemporaryCountEqualsZero.al | 17 ++++ .../Design/Rule0081UseIsEmptyMethod.cs | 83 +++++++++++++++++++ .../LinterCop.ruleset.json | 5 ++ .../LinterCopAnalyzers.resx | 9 ++ README.md | 3 +- 15 files changed, 312 insertions(+), 1 deletion(-) create mode 100644 BusinessCentral.LinterCop.Test/Rule0081.cs create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/OneGreaterThanRecordCount.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountEqualsZero.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountGreaterThanOrEqualZero.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountGreaterThanZero.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountLessThanOne.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountLessThanOrEqualZero.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountLessThanZero.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountNotEqualsZero.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0081/NoDiagnostic/RecordCountEqualsOne.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0081/NoDiagnostic/RecordTemporaryCountEqualsZero.al create mode 100644 BusinessCentral.LinterCop/Design/Rule0081UseIsEmptyMethod.cs diff --git a/BusinessCentral.LinterCop.Test/Rule0081.cs b/BusinessCentral.LinterCop.Test/Rule0081.cs new file mode 100644 index 00000000..c072910d --- /dev/null +++ b/BusinessCentral.LinterCop.Test/Rule0081.cs @@ -0,0 +1,43 @@ +namespace BusinessCentral.LinterCop.Test; + +public class Rule0081 +{ + private string _testCaseDir = ""; + + [SetUp] + public void Setup() + { + _testCaseDir = Path.Combine(Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + "TestCases", "Rule0081"); + } + + [Test] + [TestCase("OneGreaterThanRecordCount")] + [TestCase("RecordCountEqualsZero")] + [TestCase("RecordCountGreaterThanOrEqualZero")] + [TestCase("RecordCountGreaterThanZero")] + [TestCase("RecordCountLessThanOne")] + [TestCase("RecordCountLessThanOrEqualZero")] + [TestCase("RecordCountLessThanZero")] + [TestCase("RecordCountNotEqualsZero")] + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.HasDiagnostic(code, Rule0081UseIsEmptyMethod.DiagnosticDescriptors.Rule0081UseIsEmptyMethod.Id); + } + + [Test] + [TestCase("RecordCountEqualsOne")] + [TestCase("RecordTemporaryCountEqualsZero")] + public async Task NoDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.NoDiagnosticAtMarker(code, Rule0081UseIsEmptyMethod.DiagnosticDescriptors.Rule0081UseIsEmptyMethod.Id); + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/OneGreaterThanRecordCount.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/OneGreaterThanRecordCount.al new file mode 100644 index 00000000..1f3b7564 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/OneGreaterThanRecordCount.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + if [|1 > MyTable.Count()|] then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountEqualsZero.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountEqualsZero.al new file mode 100644 index 00000000..83ced9a1 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountEqualsZero.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + if [|MyTable.Count() = 0|] then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountGreaterThanOrEqualZero.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountGreaterThanOrEqualZero.al new file mode 100644 index 00000000..db9eae73 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountGreaterThanOrEqualZero.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + if [|MyTable.Count() >= 0|] then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountGreaterThanZero.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountGreaterThanZero.al new file mode 100644 index 00000000..0ae86b04 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountGreaterThanZero.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + if [|MyTable.Count() > 0|] then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountLessThanOne.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountLessThanOne.al new file mode 100644 index 00000000..7c5f2caa --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountLessThanOne.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + if [|MyTable.Count() < 1|] then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountLessThanOrEqualZero.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountLessThanOrEqualZero.al new file mode 100644 index 00000000..0ce2216f --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountLessThanOrEqualZero.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + if [|MyTable.Count() <= 0|] then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountLessThanZero.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountLessThanZero.al new file mode 100644 index 00000000..0526fca9 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountLessThanZero.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + if [|MyTable.Count() < 0|] then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountNotEqualsZero.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountNotEqualsZero.al new file mode 100644 index 00000000..a3fb3cbd --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/HasDiagnostic/RecordCountNotEqualsZero.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + if [|MyTable.Count() <> 0|] then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0081/NoDiagnostic/RecordCountEqualsOne.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/NoDiagnostic/RecordCountEqualsOne.al new file mode 100644 index 00000000..b84e45c3 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/NoDiagnostic/RecordCountEqualsOne.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + if [|MyTable.Count() = 1|] then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0081/NoDiagnostic/RecordTemporaryCountEqualsZero.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/NoDiagnostic/RecordTemporaryCountEqualsZero.al new file mode 100644 index 00000000..02350841 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0081/NoDiagnostic/RecordTemporaryCountEqualsZero.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + TempMyTable: Record MyTable temporary; + begin + if [|TempMyTable.Count() = 0|] then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0081UseIsEmptyMethod.cs b/BusinessCentral.LinterCop/Design/Rule0081UseIsEmptyMethod.cs new file mode 100644 index 00000000..8a5eca8d --- /dev/null +++ b/BusinessCentral.LinterCop/Design/Rule0081UseIsEmptyMethod.cs @@ -0,0 +1,83 @@ +using BusinessCentral.LinterCop.AnalysisContextExtension; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; +using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; +using System.Collections.Immutable; + +namespace BusinessCentral.LinterCop.Design +{ + [DiagnosticAnalyzer] + public class Rule0081UseIsEmptyMethod : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0081UseIsEmptyMethod); + + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(new Action(this.AnalyzeCountMethod), OperationKind.InvocationExpression); + + private void AnalyzeCountMethod(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) + return; + + if (ctx.Operation is not IInvocationExpression operation) + return; + + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + operation.TargetMethod.Name != "Count" || + operation.TargetMethod.ContainingSymbol?.Name != "Table") + return; + + if (operation.Instance?.GetSymbol() is not IVariableSymbol { Type: IRecordTypeSymbol recordTypeSymbol } || recordTypeSymbol.Temporary) + return; + + if (operation.Syntax.Parent is not BinaryExpressionSyntax binaryExpression) + return; + + if (IsLiteralExpressionValue(binaryExpression.Left, 0) || + IsLiteralExpressionValue(binaryExpression.Right, 0)) + { + ReportDiagnostic(ctx, operation); + return; + } + + if (binaryExpression.OperatorToken.Kind == SyntaxKind.LessThanToken && + IsLiteralExpressionValue(binaryExpression.Right, 1)) + { + ReportDiagnostic(ctx, operation); + return; + } + + if (binaryExpression.OperatorToken.Kind == SyntaxKind.GreaterThanToken && + IsLiteralExpressionValue(binaryExpression.Left, 1)) + { + ReportDiagnostic(ctx, operation); + } + } + + private static bool IsLiteralExpressionValue(CodeExpressionSyntax codeExpression, int value) => + codeExpression is LiteralExpressionSyntax { Literal: { Kind: SyntaxKind.Int32SignedLiteralValue } literal } + && literal.GetLiteralValue() is int literalvalue && literalvalue == value; + + private static void ReportDiagnostic(OperationAnalysisContext ctx, IInvocationExpression operation) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0081UseIsEmptyMethod, + operation.Syntax.Parent.GetLocation(), + new object[] { operation.Instance?.GetSymbol()?.Name.QuoteIdentifierIfNeeded() ?? string.Empty })); + } + + public static class DiagnosticDescriptors + { + public static readonly DiagnosticDescriptor Rule0081UseIsEmptyMethod = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0081", + title: LinterCopAnalyzers.GetLocalizableString("Rule0081UseIsEmptyMethodTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0081UseIsEmptyMethodFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0081UseIsEmptyMethodDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0081"); + } + } +} diff --git a/BusinessCentral.LinterCop/LinterCop.ruleset.json b/BusinessCentral.LinterCop/LinterCop.ruleset.json index c504c31f..70d4bafd 100644 --- a/BusinessCentral.LinterCop/LinterCop.ruleset.json +++ b/BusinessCentral.LinterCop/LinterCop.ruleset.json @@ -401,6 +401,11 @@ "id": "LC0080", "action": "Warning", "justification": "Replace double quotes in JPath expressions with two single quotes." + }, + { + "id": "LC0081", + "action": "Info", + "justification": "Use Rec.IsEmpty() for checking record existence." } ] } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx index 052e45d2..ea5f90c0 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx @@ -840,4 +840,13 @@ Detects and warns against the use of double-quote (") characters in JPath expressions. + + Use Rec.IsEmpty() for checking record existence. + + + Avoid using {0}.Count() for checking record existence. Use {0}.IsEmpty() instead for better performance. + + + To check for the existence of records, use the more efficient Rec.IsEmpty() function instead of Rec.Count(). + \ No newline at end of file diff --git a/README.md b/README.md index a9a33b8f..3cccb45a 100644 --- a/README.md +++ b/README.md @@ -233,4 +233,5 @@ For an example and the default values see: [LinterCop.ruleset.json](./BusinessCe |[LC0077](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0077)|Methods should always be called with parenthesis.|Info| |[LC0078](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0078)|Temporary records should not trigger table triggers.|Info| |[LC0079](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0079)|Event publishers should not be public.|Info| -|[LC0080](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0080)|Replace double quotes in JPath expressions with two single quotes.|Warning| \ No newline at end of file +|[LC0080](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0080)|Replace double quotes in JPath expressions with two single quotes.|Warning| +|[LC0081](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0081)|Use `Rec.IsEmpty()` for checking record existence.|Info| \ No newline at end of file From df392a21600e71cda38dd040bce7573cd82e6bbc Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Sat, 14 Dec 2024 15:42:55 +0100 Subject: [PATCH 06/28] Improve naming of method --- .../Design/Rule0080AnalyzeJsonTokenJPath.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BusinessCentral.LinterCop/Design/Rule0080AnalyzeJsonTokenJPath.cs b/BusinessCentral.LinterCop/Design/Rule0080AnalyzeJsonTokenJPath.cs index 16cec88a..0f71b551 100644 --- a/BusinessCentral.LinterCop/Design/Rule0080AnalyzeJsonTokenJPath.cs +++ b/BusinessCentral.LinterCop/Design/Rule0080AnalyzeJsonTokenJPath.cs @@ -12,9 +12,9 @@ public class Rule0080AnalyzeJsonTokenJPath : DiagnosticAnalyzer public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0080AnalyzeJsonTokenJPath); public override void Initialize(AnalysisContext context) => - context.RegisterOperationAction(new Action(this.SelectSingleNode), OperationKind.InvocationExpression); + context.RegisterOperationAction(new Action(this.AnalyzeSelectToken), OperationKind.InvocationExpression); - private void SelectSingleNode(OperationAnalysisContext ctx) + private void AnalyzeSelectToken(OperationAnalysisContext ctx) { if (ctx.IsObsoletePendingOrRemoved()) return; From 033678e4b317df01b7c09af204164c21ab925eb9 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Sat, 14 Dec 2024 21:16:33 +0100 Subject: [PATCH 07/28] New Rule0082: Use Rec.Find('-') with Rec.Next() for checking exactly one record. (#834) --- BusinessCentral.LinterCop.Test/Rule0081.cs | 8 +- BusinessCentral.LinterCop.Test/Rule0082.cs | 42 +++++ .../HasDiagnostic/RecordCountEqualsOne.al | 17 ++ .../RecordCountGreaterThanOne.al | 17 ++ .../RecordCountGreaterThanOrEqualOne.al | 17 ++ .../RecordCountLessThanOrEqualZero.al | 17 ++ .../HasDiagnostic/RecordCountLessThanTwo.al | 17 ++ .../HasDiagnostic/RecordCountNotEqualsOne.al | 17 ++ .../TwoGreaterThanRecordCount.al | 17 ++ .../NoDiagnostic/RecordCountEqualsTwo.al | 17 ++ .../RecordTemporaryCountEqualsOne.al | 17 ++ .../Design/Rule0081AnalyzeCountMethod.cs | 146 ++++++++++++++++++ .../Design/Rule0081UseIsEmptyMethod.cs | 83 ---------- .../LinterCop.ruleset.json | 5 + .../LinterCopAnalyzers.resx | 9 ++ README.md | 3 +- 16 files changed, 361 insertions(+), 88 deletions(-) create mode 100644 BusinessCentral.LinterCop.Test/Rule0082.cs create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountEqualsOne.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountGreaterThanOne.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountGreaterThanOrEqualOne.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountLessThanOrEqualZero.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountLessThanTwo.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountNotEqualsOne.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/TwoGreaterThanRecordCount.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0082/NoDiagnostic/RecordCountEqualsTwo.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0082/NoDiagnostic/RecordTemporaryCountEqualsOne.al create mode 100644 BusinessCentral.LinterCop/Design/Rule0081AnalyzeCountMethod.cs delete mode 100644 BusinessCentral.LinterCop/Design/Rule0081UseIsEmptyMethod.cs diff --git a/BusinessCentral.LinterCop.Test/Rule0081.cs b/BusinessCentral.LinterCop.Test/Rule0081.cs index c072910d..ccea2f59 100644 --- a/BusinessCentral.LinterCop.Test/Rule0081.cs +++ b/BusinessCentral.LinterCop.Test/Rule0081.cs @@ -25,8 +25,8 @@ public async Task HasDiagnostic(string testCase) var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) .ConfigureAwait(false); - var fixture = RoslynFixtureFactory.Create(); - fixture.HasDiagnostic(code, Rule0081UseIsEmptyMethod.DiagnosticDescriptors.Rule0081UseIsEmptyMethod.Id); + var fixture = RoslynFixtureFactory.Create(); + fixture.HasDiagnostic(code, Rule0081AnalyzeCountMethod.DiagnosticDescriptors.Rule0081UseIsEmptyMethod.Id); } [Test] @@ -37,7 +37,7 @@ public async Task NoDiagnostic(string testCase) var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) .ConfigureAwait(false); - var fixture = RoslynFixtureFactory.Create(); - fixture.NoDiagnosticAtMarker(code, Rule0081UseIsEmptyMethod.DiagnosticDescriptors.Rule0081UseIsEmptyMethod.Id); + var fixture = RoslynFixtureFactory.Create(); + fixture.NoDiagnosticAtMarker(code, Rule0081AnalyzeCountMethod.DiagnosticDescriptors.Rule0081UseIsEmptyMethod.Id); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/Rule0082.cs b/BusinessCentral.LinterCop.Test/Rule0082.cs new file mode 100644 index 00000000..6f62822b --- /dev/null +++ b/BusinessCentral.LinterCop.Test/Rule0082.cs @@ -0,0 +1,42 @@ +namespace BusinessCentral.LinterCop.Test; + +public class Rule0082 +{ + private string _testCaseDir = ""; + + [SetUp] + public void Setup() + { + _testCaseDir = Path.Combine(Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + "TestCases", "Rule0082"); + } + + [Test] + [TestCase("RecordCountEqualsOne")] + [TestCase("RecordCountGreaterThanOne")] + [TestCase("RecordCountGreaterThanOrEqualOne")] + [TestCase("RecordCountLessThanOrEqualZero")] + [TestCase("RecordCountLessThanTwo")] + [TestCase("RecordCountNotEqualsOne")] + [TestCase("TwoGreaterThanRecordCount")] + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.HasDiagnostic(code, Rule0081AnalyzeCountMethod.DiagnosticDescriptors.Rule0082UseFindWithNext.Id); + } + + [Test] + [TestCase("RecordCountEqualsTwo")] + [TestCase("RecordTemporaryCountEqualsOne")] + public async Task NoDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.NoDiagnosticAtMarker(code, Rule0081AnalyzeCountMethod.DiagnosticDescriptors.Rule0082UseFindWithNext.Id); + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountEqualsOne.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountEqualsOne.al new file mode 100644 index 00000000..b84e45c3 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountEqualsOne.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + if [|MyTable.Count() = 1|] then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountGreaterThanOne.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountGreaterThanOne.al new file mode 100644 index 00000000..de64b1cb --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountGreaterThanOne.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + if [|MyTable.Count() > 1|] then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountGreaterThanOrEqualOne.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountGreaterThanOrEqualOne.al new file mode 100644 index 00000000..0dcb095c --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountGreaterThanOrEqualOne.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + if [|MyTable.Count() >= 1|] then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountLessThanOrEqualZero.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountLessThanOrEqualZero.al new file mode 100644 index 00000000..a03f8532 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountLessThanOrEqualZero.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + if [|MyTable.Count() <= 1|] then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountLessThanTwo.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountLessThanTwo.al new file mode 100644 index 00000000..9e744a1e --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountLessThanTwo.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + if [|MyTable.Count() < 2|] then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountNotEqualsOne.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountNotEqualsOne.al new file mode 100644 index 00000000..46d6c417 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/RecordCountNotEqualsOne.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + if [|MyTable.Count() <> 1|] then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/TwoGreaterThanRecordCount.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/TwoGreaterThanRecordCount.al new file mode 100644 index 00000000..74f5811c --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0082/HasDiagnostic/TwoGreaterThanRecordCount.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + if [|2 > MyTable.Count()|] then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0082/NoDiagnostic/RecordCountEqualsTwo.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0082/NoDiagnostic/RecordCountEqualsTwo.al new file mode 100644 index 00000000..b14dc62d --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0082/NoDiagnostic/RecordCountEqualsTwo.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + if [|MyTable.Count() = 2|] then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0082/NoDiagnostic/RecordTemporaryCountEqualsOne.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0082/NoDiagnostic/RecordTemporaryCountEqualsOne.al new file mode 100644 index 00000000..3555ba2e --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0082/NoDiagnostic/RecordTemporaryCountEqualsOne.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + TempMyTable: Record MyTable temporary; + begin + if [|TempMyTable.Count() = 1|] then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0081AnalyzeCountMethod.cs b/BusinessCentral.LinterCop/Design/Rule0081AnalyzeCountMethod.cs new file mode 100644 index 00000000..2963501b --- /dev/null +++ b/BusinessCentral.LinterCop/Design/Rule0081AnalyzeCountMethod.cs @@ -0,0 +1,146 @@ +using BusinessCentral.LinterCop.AnalysisContextExtension; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; +using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; +using System.Collections.Immutable; + +namespace BusinessCentral.LinterCop.Design +{ + [DiagnosticAnalyzer] + public class Rule0081AnalyzeCountMethod : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0081UseIsEmptyMethod, DiagnosticDescriptors.Rule0082UseFindWithNext); + + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(new Action(this.AnalyzeCountMethod), OperationKind.InvocationExpression); + + private void AnalyzeCountMethod(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) + return; + + if (ctx.Operation is not IInvocationExpression operation) + return; + + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + operation.TargetMethod.Name != "Count" || + operation.TargetMethod.ContainingSymbol?.Name != "Table") + return; + + if (operation.Instance?.GetSymbol() is not IVariableSymbol { Type: IRecordTypeSymbol recordTypeSymbol } || recordTypeSymbol.Temporary) + return; + + if (operation.Syntax.Parent is not BinaryExpressionSyntax binaryExpression) + return; + + int rightValue = GetLiteralExpressionValue(binaryExpression.Right); + if (rightValue > Literals.MaxRelevantValue) + return; + + int leftValue = GetLiteralExpressionValue(binaryExpression.Left); + if (leftValue > Literals.MaxRelevantValue) + return; + + if (IsZeroComparison(leftValue, rightValue)) + { + ReportUseIsEmptyDiagnostic(ctx, operation); + return; + } + + if (IsLessThanOneComparison(binaryExpression, rightValue) || IsGreaterThanOneComparison(binaryExpression, leftValue)) + { + ReportUseIsEmptyDiagnostic(ctx, operation); + return; + } + + if (IsOneComparison(leftValue, rightValue)) + { + ReportUseFindWithNextDiagnostic(ctx, operation, GetOperatorKind(binaryExpression.OperatorToken.Kind)); + return; + } + + if (IsLessThanTwoComparison(binaryExpression, rightValue) || IsGreaterThanTwoComparison(binaryExpression, leftValue)) + { + ReportUseFindWithNextDiagnostic(ctx, operation, SyntaxKind.EqualsToken); + return; + } + } + + private static int GetLiteralExpressionValue(CodeExpressionSyntax codeExpression) => + codeExpression is LiteralExpressionSyntax { Literal.Kind: SyntaxKind.Int32SignedLiteralValue } literalExpression && + literalExpression.Literal.GetLiteralValue() is int value ? value : -1; + + private static SyntaxKind GetOperatorKind(SyntaxKind tokenKind) => + tokenKind == SyntaxKind.EqualsToken ? SyntaxKind.EqualsToken : SyntaxKind.NotEqualsToken; + + private static bool IsZeroComparison(int left, int right) + => left == Literals.Zero || right == Literals.Zero; + + private static bool IsLessThanOneComparison(BinaryExpressionSyntax expr, int right) => + expr.OperatorToken.Kind == SyntaxKind.LessThanToken && right == Literals.One; + + private static bool IsGreaterThanOneComparison(BinaryExpressionSyntax expr, int left) => + expr.OperatorToken.Kind == SyntaxKind.GreaterThanToken && left == Literals.One; + + private static bool IsOneComparison(int left, int right) => + left == Literals.One || right == Literals.One; + + private static bool IsLessThanTwoComparison(BinaryExpressionSyntax expr, int right) => + expr.OperatorToken.Kind == SyntaxKind.LessThanToken && right == Literals.Two; + + private static bool IsGreaterThanTwoComparison(BinaryExpressionSyntax expr, int left) => + expr.OperatorToken.Kind == SyntaxKind.GreaterThanToken && left == Literals.Two; + + private static class Literals + { + public const int Zero = 0; + public const int One = 1; + public const int Two = 2; + public const int MaxRelevantValue = 2; + } + + private static void ReportUseIsEmptyDiagnostic(OperationAnalysisContext ctx, IInvocationExpression operation) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0081UseIsEmptyMethod, + operation.Syntax.Parent.GetLocation(), + new object[] { GetSymbolName(operation) })); + } + + private static void ReportUseFindWithNextDiagnostic(OperationAnalysisContext ctx, IInvocationExpression operation, SyntaxKind operatorToken) + { + string operatorSign = operatorToken == SyntaxKind.EqualsToken ? "=" : "<>"; + + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0082UseFindWithNext, + operation.Syntax.Parent.GetLocation(), + new object[] { GetSymbolName(operation), operatorSign })); + } + + private static string GetSymbolName(IInvocationExpression operation) => + operation.Instance?.GetSymbol()?.Name.QuoteIdentifierIfNeeded() ?? string.Empty; + + public static class DiagnosticDescriptors + { + public static readonly DiagnosticDescriptor Rule0081UseIsEmptyMethod = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0081", + title: LinterCopAnalyzers.GetLocalizableString("Rule0081UseIsEmptyMethodTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0081UseIsEmptyMethodFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0081UseIsEmptyMethodDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0081"); + + public static readonly DiagnosticDescriptor Rule0082UseFindWithNext = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0082", + title: LinterCopAnalyzers.GetLocalizableString("Rule0082UseFindWithNextTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0082UseFindWithNextFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0082UseFindWithNextDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0082"); + } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0081UseIsEmptyMethod.cs b/BusinessCentral.LinterCop/Design/Rule0081UseIsEmptyMethod.cs deleted file mode 100644 index 8a5eca8d..00000000 --- a/BusinessCentral.LinterCop/Design/Rule0081UseIsEmptyMethod.cs +++ /dev/null @@ -1,83 +0,0 @@ -using BusinessCentral.LinterCop.AnalysisContextExtension; -using Microsoft.Dynamics.Nav.CodeAnalysis; -using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; -using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; -using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; -using System.Collections.Immutable; - -namespace BusinessCentral.LinterCop.Design -{ - [DiagnosticAnalyzer] - public class Rule0081UseIsEmptyMethod : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(DiagnosticDescriptors.Rule0081UseIsEmptyMethod); - - public override void Initialize(AnalysisContext context) => - context.RegisterOperationAction(new Action(this.AnalyzeCountMethod), OperationKind.InvocationExpression); - - private void AnalyzeCountMethod(OperationAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) - return; - - if (ctx.Operation is not IInvocationExpression operation) - return; - - if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || - operation.TargetMethod.Name != "Count" || - operation.TargetMethod.ContainingSymbol?.Name != "Table") - return; - - if (operation.Instance?.GetSymbol() is not IVariableSymbol { Type: IRecordTypeSymbol recordTypeSymbol } || recordTypeSymbol.Temporary) - return; - - if (operation.Syntax.Parent is not BinaryExpressionSyntax binaryExpression) - return; - - if (IsLiteralExpressionValue(binaryExpression.Left, 0) || - IsLiteralExpressionValue(binaryExpression.Right, 0)) - { - ReportDiagnostic(ctx, operation); - return; - } - - if (binaryExpression.OperatorToken.Kind == SyntaxKind.LessThanToken && - IsLiteralExpressionValue(binaryExpression.Right, 1)) - { - ReportDiagnostic(ctx, operation); - return; - } - - if (binaryExpression.OperatorToken.Kind == SyntaxKind.GreaterThanToken && - IsLiteralExpressionValue(binaryExpression.Left, 1)) - { - ReportDiagnostic(ctx, operation); - } - } - - private static bool IsLiteralExpressionValue(CodeExpressionSyntax codeExpression, int value) => - codeExpression is LiteralExpressionSyntax { Literal: { Kind: SyntaxKind.Int32SignedLiteralValue } literal } - && literal.GetLiteralValue() is int literalvalue && literalvalue == value; - - private static void ReportDiagnostic(OperationAnalysisContext ctx, IInvocationExpression operation) - { - ctx.ReportDiagnostic(Diagnostic.Create( - DiagnosticDescriptors.Rule0081UseIsEmptyMethod, - operation.Syntax.Parent.GetLocation(), - new object[] { operation.Instance?.GetSymbol()?.Name.QuoteIdentifierIfNeeded() ?? string.Empty })); - } - - public static class DiagnosticDescriptors - { - public static readonly DiagnosticDescriptor Rule0081UseIsEmptyMethod = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0081", - title: LinterCopAnalyzers.GetLocalizableString("Rule0081UseIsEmptyMethodTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0081UseIsEmptyMethodFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0081UseIsEmptyMethodDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0081"); - } - } -} diff --git a/BusinessCentral.LinterCop/LinterCop.ruleset.json b/BusinessCentral.LinterCop/LinterCop.ruleset.json index 70d4bafd..d5442965 100644 --- a/BusinessCentral.LinterCop/LinterCop.ruleset.json +++ b/BusinessCentral.LinterCop/LinterCop.ruleset.json @@ -406,6 +406,11 @@ "id": "LC0081", "action": "Info", "justification": "Use Rec.IsEmpty() for checking record existence." + }, + { + "id": "LC0082", + "action": "Info", + "justification": "Use Rec.Find('-') with Rec.Next() for checking exactly one record." } ] } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx index ea5f90c0..8dcd2ddd 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx @@ -849,4 +849,13 @@ To check for the existence of records, use the more efficient Rec.IsEmpty() function instead of Rec.Count(). + + Use Rec.Find('-') with Rec.Next() for checking exactly one record. + + + Use {0}.Find('-') together with {0}.Next() instead of {0}.Count() for performance optimization. Replace {0}.Count() with: {0}.Find('-') and (Rec.Next() {1} 0). + + + Instead of relying on Rec.Count(), you should use a combination of Rec.Find('-') and Rec.Next() for faster and more efficient record checks. + \ No newline at end of file diff --git a/README.md b/README.md index 3cccb45a..908cb634 100644 --- a/README.md +++ b/README.md @@ -234,4 +234,5 @@ For an example and the default values see: [LinterCop.ruleset.json](./BusinessCe |[LC0078](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0078)|Temporary records should not trigger table triggers.|Info| |[LC0079](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0079)|Event publishers should not be public.|Info| |[LC0080](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0080)|Replace double quotes in JPath expressions with two single quotes.|Warning| -|[LC0081](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0081)|Use `Rec.IsEmpty()` for checking record existence.|Info| \ No newline at end of file +|[LC0081](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0081)|Use `Rec.IsEmpty()` for checking record existence.|Info| +|[LC0082](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0082)|Use `Rec.Find('-')` with `Rec.Next()` for checking exactly one record.|Info| \ No newline at end of file From 9766aa25169926e9167853e254cd3506938c5ee4 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Sat, 14 Dec 2024 21:27:02 +0100 Subject: [PATCH 08/28] Fix typo --- .../Design/Rule0080AnalyzeJsonTokenJPath.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BusinessCentral.LinterCop/Design/Rule0080AnalyzeJsonTokenJPath.cs b/BusinessCentral.LinterCop/Design/Rule0080AnalyzeJsonTokenJPath.cs index 0f71b551..b55e753b 100644 --- a/BusinessCentral.LinterCop/Design/Rule0080AnalyzeJsonTokenJPath.cs +++ b/BusinessCentral.LinterCop/Design/Rule0080AnalyzeJsonTokenJPath.cs @@ -48,10 +48,10 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor Rule0080AnalyzeJsonTokenJPath = new( id: LinterCopAnalyzers.AnalyzerPrefix + "0080", title: LinterCopAnalyzers.GetLocalizableString("Rule0080AnalyzeJsonTokenJPathTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0080AnalyzePathOnJsonTokenFormat"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0080AnalyzeJsonTokenJPathFormat"), category: "Design", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0080AnalyzePathOnJsonTokenDescription"), + description: LinterCopAnalyzers.GetLocalizableString("Rule0080AnalyzeJsonTokenJPathDescription"), helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0080"); } } From c7d8adcab4b613e10ffc8020196a2ff5fb5637f2 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:56:30 +0100 Subject: [PATCH 09/28] Improve Rule0017 to mitigate InvalidCastException (#836) --- BusinessCentral.LinterCop.Test/Rule0017.cs | 6 +- .../TestCases/Rule0017/HasDiagnostic/1.al | 30 ---- .../Rule0017/HasDiagnostic/Assignment.al | 16 ++ .../Rule0017/HasDiagnostic/Validate.al | 16 ++ .../TestCases/Rule0017/NoDiagnostic/1.al | 31 ---- .../Rule0017/NoDiagnostic/Assignment.al | 17 ++ .../Rule0017/NoDiagnostic/Validate.al | 17 ++ .../Design/Rule0017WriteToFlowField.cs | 160 ++++++++---------- 8 files changed, 136 insertions(+), 157 deletions(-) delete mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0017/HasDiagnostic/1.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0017/HasDiagnostic/Assignment.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0017/HasDiagnostic/Validate.al delete mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/1.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/Assignment.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/Validate.al diff --git a/BusinessCentral.LinterCop.Test/Rule0017.cs b/BusinessCentral.LinterCop.Test/Rule0017.cs index 996c38f9..3004fa3c 100644 --- a/BusinessCentral.LinterCop.Test/Rule0017.cs +++ b/BusinessCentral.LinterCop.Test/Rule0017.cs @@ -12,7 +12,8 @@ public void Setup() } [Test] - [TestCase("1")] + [TestCase("Assignment")] + [TestCase("Validate")] public async Task HasDiagnostic(string testCase) { var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) @@ -23,7 +24,8 @@ public async Task HasDiagnostic(string testCase) } [Test] - [TestCase("1")] + [TestCase("Assignment")] + [TestCase("Validate")] public async Task NoDiagnostic(string testCase) { var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0017/HasDiagnostic/1.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0017/HasDiagnostic/1.al deleted file mode 100644 index eed0daf8..00000000 --- a/BusinessCentral.LinterCop.Test/TestCases/Rule0017/HasDiagnostic/1.al +++ /dev/null @@ -1,30 +0,0 @@ -table 50100 MyTable -{ - DataClassification = ToBeClassified; - - fields - { - field(1; MyField; Integer) - { - DataClassification = ToBeClassified; - } - field(2; MyField2; Boolean) - { - FieldClass = FlowField; - CalcFormula = exist(MyTable where (MyField = field(MyField))); - } - } - - keys - { - key(Key1; MyField) - { - Clustered = true; - } - } - - procedure MyProcedure(); - begin - [|Rec.MyField2 := false;|] - end; -} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0017/HasDiagnostic/Assignment.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0017/HasDiagnostic/Assignment.al new file mode 100644 index 00000000..60430aa9 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0017/HasDiagnostic/Assignment.al @@ -0,0 +1,16 @@ +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + field(2; MyField2; Boolean) + { + FieldClass = FlowField; + } + } + + procedure MyProcedure(); + begin + [|Rec.MyField2|] := false; + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0017/HasDiagnostic/Validate.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0017/HasDiagnostic/Validate.al new file mode 100644 index 00000000..ff339fdd --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0017/HasDiagnostic/Validate.al @@ -0,0 +1,16 @@ +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + field(2; MyField2; Boolean) + { + FieldClass = FlowField; + } + } + + procedure MyProcedure(); + begin + Rec.Validate([|MyField2|], false); + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/1.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/1.al deleted file mode 100644 index 83c6843c..00000000 --- a/BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/1.al +++ /dev/null @@ -1,31 +0,0 @@ -table 50100 MyTable -{ - DataClassification = ToBeClassified; - - fields - { - field(1; MyField; Integer) - { - DataClassification = ToBeClassified; - } - field(2; MyField2; Boolean) - { - FieldClass = FlowField; - CalcFormula = exist(MyTable where (MyField = field(MyField))); - } - } - - keys - { - key(Key1; MyField) - { - Clustered = true; - } - } - - procedure MyProcedure(); - begin - //Comment - [|Rec.MyField2 := false;|] - end; -} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/Assignment.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/Assignment.al new file mode 100644 index 00000000..aa996ab8 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/Assignment.al @@ -0,0 +1,17 @@ +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + field(2; MyField2; Boolean) + { + FieldClass = FlowField; + } + } + + procedure MyProcedure(); + begin + // Comment + [|Rec.MyField2|] := false; + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/Validate.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/Validate.al new file mode 100644 index 00000000..4d4072e5 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/Validate.al @@ -0,0 +1,17 @@ +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + field(2; MyField2; Boolean) + { + FieldClass = FlowField; + } + } + + procedure MyProcedure(); + begin + // Comment + Rec.Validate([|MyField2|], false); + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0017WriteToFlowField.cs b/BusinessCentral.LinterCop/Design/Rule0017WriteToFlowField.cs index 48dc5d51..9e36d01b 100644 --- a/BusinessCentral.LinterCop/Design/Rule0017WriteToFlowField.cs +++ b/BusinessCentral.LinterCop/Design/Rule0017WriteToFlowField.cs @@ -1,108 +1,80 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.AnalysisContextExtension; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Semantics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0017WriteToFlowField : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0017WriteToFlowField : DiagnosticAnalyzer + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0017WriteToFlowField, DiagnosticDescriptors.Rule0000ErrorInRule); + + public override void Initialize(AnalysisContext context) { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0017WriteToFlowField, DiagnosticDescriptors.Rule0000ErrorInRule); + context.RegisterOperationAction(new Action(this.AnalyzeAssignmentStatement), OperationKind.AssignmentStatement); + context.RegisterOperationAction(new Action(this.AnalyzeInvocationExpression), OperationKind.InvocationExpression); + } - public override void Initialize(AnalysisContext context) - => context.RegisterOperationAction(new Action(this.CheckForWriteToFlowField), - OperationKind.AssignmentStatement, - OperationKind.InvocationExpression - ); + private void AnalyzeInvocationExpression(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) + return; - private void CheckForWriteToFlowField(OperationAnalysisContext context) - { - if (context.IsObsoletePendingOrRemoved()) return; - - if (context.Operation.Kind == OperationKind.InvocationExpression) - { - try - { - IInvocationExpression operation = (IInvocationExpression)context.Operation; - if (operation.TargetMethod.Name == "Validate" && operation.TargetMethod.ContainingType.ToString() == "Table") - { - IFieldSymbol field = null; - if (operation.Arguments[0].Value.GetType().GetProperty("Operand") != null) - field = ((IFieldAccess)((IConversionExpression)operation.Arguments[0].Value).Operand).FieldSymbol; - else - field = ((IFieldAccess)operation.Arguments[0].Value).FieldSymbol; - - var FieldClass = field.FieldClass; - if (FieldClass == FieldClassKind.FlowField) - if (!HasExplainingComment(context.Operation)) - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0017WriteToFlowField, context.Operation.Syntax.GetLocation())); - } - } - catch (InvalidCastException) - { - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0000ErrorInRule, context.Operation.Syntax.GetLocation(), new Object[] { "Rule0017", "InvalidCastException", "at Line 41" })); - } - catch (ArgumentException) - { - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0000ErrorInRule, context.Operation.Syntax.GetLocation(), new Object[] { "Rule0017", "ArgumentException", "at Line 45" })); - } - } - else - { - IAssignmentStatement operation = (IAssignmentStatement)context.Operation; - if (operation.Target.Kind == OperationKind.FieldAccess) - { - try - { - var FieldClass = FieldClassKind.Normal; - - if (operation.Target.Syntax.Kind == SyntaxKind.ArrayIndexExpression) - { - if (((ITextIndexAccess)operation.Target).TextExpression.Kind != OperationKind.FieldAccess) - return; - - FieldClass = ((IFieldAccess)((ITextIndexAccess)operation.Target).TextExpression).FieldSymbol.FieldClass; - } - else - FieldClass = ((IFieldAccess)operation.Target).FieldSymbol.FieldClass; - - if (FieldClass == FieldClassKind.FlowField) - if (!HasExplainingComment(context.Operation)) - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0017WriteToFlowField, context.Operation.Syntax.GetLocation())); - } - catch (InvalidCastException) - { - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0000ErrorInRule, context.Operation.Syntax.GetLocation(), new Object[] { "Rule0017", "InvalidCastException", "at Line 62" })); - } - catch (ArgumentException) - { - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0000ErrorInRule, context.Operation.Syntax.GetLocation(), new Object[] { "Rule0017", "ArgumentException", "at Line 66" })); - } - } - } - } + if (ctx.Operation is not IInvocationExpression operation) + return; + + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + operation.TargetMethod.Name != "Validate" || + operation.TargetMethod.ContainingSymbol?.Name != "Table") + return; + + if (operation.Arguments.Length < 1 || + operation.Arguments[0].Value is not IConversionExpression conversionExpression || + conversionExpression.Operand is not IFieldAccess fieldAccess || + fieldAccess.FieldSymbol.FieldClass != FieldClassKind.FlowField || + HasExplainingComment(operation)) + return; + + ctx.ReportDiagnostic( + Diagnostic.Create(DiagnosticDescriptors.Rule0017WriteToFlowField, + operation.Arguments[0].Value.Syntax.GetLocation())); + } - private bool HasExplainingComment(IOperation operation) + private void AnalyzeAssignmentStatement(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) + return; + + if (ctx.Operation is not IAssignmentStatement operation) + return; + + if (operation.Target.Kind != OperationKind.FieldAccess) + return; + + var fieldSymbol = ExtractFieldSymbolFromAssignment(operation); + if (fieldSymbol?.FieldClass != FieldClassKind.FlowField || HasExplainingComment(operation)) + return; + + ctx.ReportDiagnostic( + Diagnostic.Create(DiagnosticDescriptors.Rule0017WriteToFlowField, + operation.Target.Syntax.GetLocation())); + } + + private IFieldSymbol? ExtractFieldSymbolFromAssignment(IAssignmentStatement operation) + { + if (operation.Target.Syntax.Kind == SyntaxKind.ArrayIndexExpression && + operation.Target is ITextIndexAccess textIndexAccess && + textIndexAccess.TextExpression is IFieldAccess fieldAccess) { - foreach (SyntaxTrivia trivia in operation.Syntax.GetLeadingTrivia()) - { - if (trivia.IsKind(SyntaxKind.LineCommentTrivia)) - { - return true; - } - } - foreach (SyntaxTrivia trivia in operation.Syntax.GetTrailingTrivia()) - { - if (trivia.IsKind(SyntaxKind.LineCommentTrivia)) - { - return true; - } - } - return false; + return fieldAccess.FieldSymbol; } + + return operation.Target is IFieldAccess directFieldAccess ? directFieldAccess.FieldSymbol : null; } -} + + private bool HasExplainingComment(IOperation operation) => + operation.Syntax.GetLeadingTrivia().Any(trivia => trivia.IsKind(SyntaxKind.LineCommentTrivia)); +} \ No newline at end of file From 6c0d8112fbd8c7a8b044f252836f9a329dc8f2d9 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Wed, 18 Dec 2024 09:04:30 +0100 Subject: [PATCH 10/28] Add Length validation to Rule0075 (#839) --- BusinessCentral.LinterCop.Test/Rule0075.cs | 1 + .../RecordGetCodeFieldLengthTooLong.al | 22 +++++++++++++++++++ .../Rule0075RecordGetProcedureArguments.cs | 10 ++++++--- 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetCodeFieldLengthTooLong.al diff --git a/BusinessCentral.LinterCop.Test/Rule0075.cs b/BusinessCentral.LinterCop.Test/Rule0075.cs index 5667efb3..c9915449 100644 --- a/BusinessCentral.LinterCop.Test/Rule0075.cs +++ b/BusinessCentral.LinterCop.Test/Rule0075.cs @@ -15,6 +15,7 @@ public void Setup() [Test] [TestCase("ImplicitConversiontCodeToEnum")] [TestCase("ImplicitConversiontEnumToAnotherEnum")] + [TestCase("RecordGetCodeFieldLengthTooLong")] [TestCase("RecordGetGlobalVariable")] [TestCase("RecordGetLocalVariable")] [TestCase("RecordGetMethod")] diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetCodeFieldLengthTooLong.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetCodeFieldLengthTooLong.al new file mode 100644 index 00000000..2d7a6c9e --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/HasDiagnostic/RecordGetCodeFieldLengthTooLong.al @@ -0,0 +1,22 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure(ItemNo: Code[20]; VariantCode: Code[20]) + var + ItemVariant: Record "Item Variant"; + begin + [|ItemVariant.Get(ItemNo, VariantCode)|]; + end; +} + +table 50100 "Item Variant" +{ + fields + { + field(1; "Code"; Code[10]) { } + field(2; "Item No."; Code[20]) { } + } + keys + { + key(Key1; "Item No.", "Code") { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs b/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs index 0c18380c..bf7c30f5 100644 --- a/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs +++ b/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs @@ -117,14 +117,18 @@ private bool AreFieldCompatible(IArgument argument, IFieldSymbol field) if (argumentNavType == NavTypeKind.Enum && fieldNavType == NavTypeKind.Enum) return argumentType.OriginalDefinition == fieldType.OriginalDefinition; - if (argumentNavType == fieldNavType || - argumentNavType == NavTypeKind.None || - argumentNavType == NavTypeKind.Joker) + if ((argumentNavType == fieldNavType && argumentType.Length == fieldType.Length) || + argumentNavType == NavTypeKind.None || + argumentNavType == NavTypeKind.Joker) return true; if (ImplicitConversions.TryGetValue(argumentNavType, out var compatibleTypes) && !compatibleTypes.Contains(fieldNavType)) return false; + if (argumentType.Length > 0 && fieldType.Length > 0 && + argumentType.Length > fieldType.Length) + return false; + return true; } From d71c7b8769b651230d4a29771ecde705b3618c08 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Wed, 18 Dec 2024 09:48:20 +0100 Subject: [PATCH 11/28] Extend Rule0051 to analyze parameters of .Get() method (#840) * Extend Rule0051 to analyze parameters of .Get() method * Exclude AL version 12.x * Exclude tests vor AL version 12.x * Safe type casting * Fix copy/past typo * consistency --- BusinessCentral.LinterCop.Test/Rule0051.cs | 16 +++- .../HasDiagnostic/GetMethodStrSubstNo.al | 18 ++++ .../HasDiagnostic/GetMethodStringLiteral.al | 17 ++++ .../NoDiagnostic/GetMethodStrSubstNo.al | 18 ++++ .../NoDiagnostic/GetMethodStringLiteral.al | 17 ++++ ...s => Rule0051PossibleOverflowAssigning.cs} | 92 +++++++++++++++---- .../LinterCopAnalyzers.Generated.cs | 6 +- .../LinterCopAnalyzers.resx | 6 +- 8 files changed, 163 insertions(+), 27 deletions(-) create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0051/HasDiagnostic/GetMethodStrSubstNo.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0051/HasDiagnostic/GetMethodStringLiteral.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0051/NoDiagnostic/GetMethodStrSubstNo.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0051/NoDiagnostic/GetMethodStringLiteral.al rename BusinessCentral.LinterCop/Design/{Rule0051SetFilterPossibleOverflow.cs => Rule0051PossibleOverflowAssigning.cs} (76%) diff --git a/BusinessCentral.LinterCop.Test/Rule0051.cs b/BusinessCentral.LinterCop.Test/Rule0051.cs index f5f99a28..c394ef8d 100644 --- a/BusinessCentral.LinterCop.Test/Rule0051.cs +++ b/BusinessCentral.LinterCop.Test/Rule0051.cs @@ -13,25 +13,33 @@ public void Setup() } [Test] +#if !LessThenSpring2024 + [TestCase("GetMethodStringLiteral")] + [TestCase("GetMethodStrSubstNo")] +#endif [TestCase("SetFilterFieldCode")] public async Task HasDiagnostic(string testCase) { var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) .ConfigureAwait(false); - var fixture = RoslynFixtureFactory.Create(); - fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0051SetFilterPossibleOverflow.Id); + var fixture = RoslynFixtureFactory.Create(); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0051PossibleOverflowAssigning.Id); } [Test] +#if !LessThenSpring2024 + [TestCase("GetMethodStringLiteral")] + [TestCase("GetMethodStrSubstNo")] +#endif [TestCase("SetFilterFieldRef")] public async Task NoDiagnostic(string testCase) { var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) .ConfigureAwait(false); - var fixture = RoslynFixtureFactory.Create(); - fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0051SetFilterPossibleOverflow.Id); + var fixture = RoslynFixtureFactory.Create(); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0051PossibleOverflowAssigning.Id); } } #endif \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0051/HasDiagnostic/GetMethodStrSubstNo.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0051/HasDiagnostic/GetMethodStrSubstNo.al new file mode 100644 index 00000000..bcf61f61 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0051/HasDiagnostic/GetMethodStrSubstNo.al @@ -0,0 +1,18 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + MyCodeFieldA, MyCodeFieldB : Code[20]; + begin + MyTable.Get([|StrSubstNo('%1%2', MyCodeFieldA, MyCodeFieldB)|]); + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Code[20]) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0051/HasDiagnostic/GetMethodStringLiteral.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0051/HasDiagnostic/GetMethodStringLiteral.al new file mode 100644 index 00000000..d6a178bf --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0051/HasDiagnostic/GetMethodStringLiteral.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + MyTable.Get([|'ABCDEFGHIJKLMNOPQRSTU'|]); + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Code[20]) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0051/NoDiagnostic/GetMethodStrSubstNo.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0051/NoDiagnostic/GetMethodStrSubstNo.al new file mode 100644 index 00000000..f3f2715d --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0051/NoDiagnostic/GetMethodStrSubstNo.al @@ -0,0 +1,18 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + MyCodeFieldA, MyCodeFieldB : Code[10]; + begin + MyTable.Get([|StrSubstNo('%1%2', MyCodeFieldA, MyCodeFieldB)|]); + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Code[20]) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0051/NoDiagnostic/GetMethodStringLiteral.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0051/NoDiagnostic/GetMethodStringLiteral.al new file mode 100644 index 00000000..5466aac3 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0051/NoDiagnostic/GetMethodStringLiteral.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + MyTable.Get([|'ABCDEFGHIJKLMNOPQRST'|]); + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Code[20]) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0051SetFilterPossibleOverflow.cs b/BusinessCentral.LinterCop/Design/Rule0051PossibleOverflowAssigning.cs similarity index 76% rename from BusinessCentral.LinterCop/Design/Rule0051SetFilterPossibleOverflow.cs rename to BusinessCentral.LinterCop/Design/Rule0051PossibleOverflowAssigning.cs index e2e4d308..2c23c098 100644 --- a/BusinessCentral.LinterCop/Design/Rule0051SetFilterPossibleOverflow.cs +++ b/BusinessCentral.LinterCop/Design/Rule0051PossibleOverflowAssigning.cs @@ -1,5 +1,6 @@ #if !LessThenFall2023RV1 using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.ArgumentExtension; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; @@ -9,34 +10,40 @@ namespace BusinessCentral.LinterCop.Design { [DiagnosticAnalyzer] - public class Rule0051SetFilterPossibleOverflow : DiagnosticAnalyzer + public class Rule0051PossibleOverflowAssigning : DiagnosticAnalyzer { private readonly Lazy strSubstNoPatternLazy = new Lazy((Func)(() => new Regex("[#%](\\d+)", RegexOptions.Compiled))); - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0051SetFilterPossibleOverflow, DiagnosticDescriptors.Rule0000ErrorInRule); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0051PossibleOverflowAssigning); private Regex StrSubstNoPattern => this.strSubstNoPatternLazy.Value; - public override void Initialize(AnalysisContext context) => context.RegisterOperationAction(new Action(this.AnalyzeInvocation), OperationKind.InvocationExpression); - - private void AnalyzeInvocation(OperationAnalysisContext ctx) + public override void Initialize(AnalysisContext context) + { + context.RegisterOperationAction(new Action(this.AnalyzeSetFilter), OperationKind.InvocationExpression); +#if !LessThenSpring2024 + context.RegisterOperationAction(new Action(this.AnalyzeGetMethod), OperationKind.InvocationExpression); +#endif + } + private void AnalyzeSetFilter(OperationAnalysisContext ctx) { if (ctx.IsObsoletePendingOrRemoved()) return; - if ((ctx.Operation is not IInvocationExpression operation) - || operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod - || operation.TargetMethod == null - || !SemanticFacts.IsSameName(operation.TargetMethod.Name, "SetFilter") - || operation.Arguments.Count() < 3 - || operation.Arguments[0].Value.Kind != OperationKind.ConversionExpression) + if ((ctx.Operation is not IInvocationExpression operation) || + operation.TargetMethod is null || + operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + !SemanticFacts.IsSameName(operation.TargetMethod.Name, "SetFilter") || + operation.Arguments.Count() < 3 || + operation.Arguments[0].Value.Kind != OperationKind.ConversionExpression) return; var fieldOperand = ((IConversionExpression)operation.Arguments[0].Value).Operand; if (fieldOperand.Type is not ITypeSymbol fieldType) return; - if (fieldType.GetNavTypeKindSafe() == NavTypeKind.Text) return; + if (fieldType.GetNavTypeKindSafe() == NavTypeKind.Text) + return; bool isError = false; int typeLength = GetTypeLength(fieldType, ref isError); @@ -46,16 +53,67 @@ private void AnalyzeInvocation(OperationAnalysisContext ctx) foreach (int argIndex in GetArgumentIndexes(operation.Arguments[1].Value)) { int index = argIndex + 1; // The placeholders are defines as %1, %2, %3, where in case of %1 we need the second (zero based) index of the arguments of the SetFilter method - if ((index < 2) - || (index >= operation.Arguments.Count()) - || (operation.Arguments[index].Value.Kind != OperationKind.ConversionExpression)) + if ((index < 2) || + (index >= operation.Arguments.Count()) || + (operation.Arguments[index].Value.Kind != OperationKind.ConversionExpression)) continue; - int expressionLength = this.CalculateMaxExpressionLength(((IConversionExpression)operation.Arguments[index].Value).Operand, ref isError); + if (operation.Arguments[index].Value is not IConversionExpression argValue) + continue; + + int expressionLength = this.CalculateMaxExpressionLength(argValue.Operand, ref isError); if (!isError && expressionLength > typeLength) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0051SetFilterPossibleOverflow, operation.Syntax.GetLocation(), GetDisplayString(operation.Arguments[index], operation), GetDisplayString(operation.Arguments[0], operation))); + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0051PossibleOverflowAssigning, operation.Syntax.GetLocation(), GetDisplayString(operation.Arguments[index], operation), GetDisplayString(operation.Arguments[0], operation))); + } + } + +#if !LessThenSpring2024 + private void AnalyzeGetMethod(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) + return; + + if ((ctx.Operation is not IInvocationExpression operation) || + operation.TargetMethod is null || + operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + !SemanticFacts.IsSameName(operation.TargetMethod.Name, "Get") || + operation.Arguments.Count() < 1) + return; + + if (operation.Instance?.Type.GetTypeSymbol()?.OriginalDefinition is not ITableTypeSymbol table) + return; + + if (operation.Arguments.Length < table.PrimaryKey.Fields.Length) + return; + + for (int index = 0; index < operation.Arguments.Length; index++) + { + var fieldType = table.PrimaryKey.Fields[index].Type; + var argumentType = operation.Arguments[index].GetTypeSymbol(); + + if (fieldType is null || argumentType is null || argumentType.HasLength) + continue; + + bool isError = false; + int fieldLength = GetTypeLength(fieldType, ref isError); + if (isError || fieldLength == 0) + continue; + + if (operation.Arguments[index].Value is not IConversionExpression argValue) + continue; + + int expressionLength = this.CalculateMaxExpressionLength(argValue.Operand, ref isError); + if (!isError && expressionLength > fieldLength) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0051PossibleOverflowAssigning, + operation.Arguments[index].Syntax.GetLocation(), + $"{argumentType.ToDisplayString()}[{expressionLength}]", + fieldType.ToDisplayString())); + } } } +#endif private static int GetTypeLength(ITypeSymbol type, ref bool isError) { diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs b/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs index 87b8b853..2dc6935f 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs @@ -54,7 +54,7 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor Rule0048ErrorWithTextConstant = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0048", (LocalizableString)new LocalizableResourceString("Rule0048ErrorWithTextConstantTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0048ErrorWithTextConstantFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0048ErrorWithTextConstantDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0048"); public static readonly DiagnosticDescriptor Rule0049PageWithoutSourceTable = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0049", (LocalizableString)new LocalizableResourceString("Rule0049PageWithoutSourceTableTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0049PageWithoutSourceTableFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0049PageWithoutSourceTableDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0049"); public static readonly DiagnosticDescriptor Rule0050OperatorAndPlaceholderInFilterExpression = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0050", (LocalizableString)new LocalizableResourceString("Rule0050OperatorAndPlaceholderInFilterExpressionTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0050OperatorAndPlaceholderInFilterExpressionFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0050OperatorAndPlaceholderInFilterExpressionDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0050"); - public static readonly DiagnosticDescriptor Rule0051SetFilterPossibleOverflow = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0051", (LocalizableString)new LocalizableResourceString("Rule0051SetFilterPossibleOverflowTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0051SetFilterPossibleOverflowFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0051SetFilterPossibleOverflowDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0051"); + public static readonly DiagnosticDescriptor Rule0051PossibleOverflowAssigning = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0051", (LocalizableString)new LocalizableResourceString("Rule0051PossibleOverflowAssigningTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0051PossibleOverflowAssigningFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0051PossibleOverflowAssigningDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0051"); public static readonly DiagnosticDescriptor Rule0052InternalProceduresNotReferencedAnalyzerDescriptor = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0052", (LocalizableString)new LocalizableResourceString("Rule0052InternalProceduresNotReferencedAnalyzer", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0052InternalProceduresNotReferencedAnalyzerFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, isEnabledByDefault: true, (LocalizableString)new LocalizableResourceString("Rule0052InternalProceduresNotReferencedAnalyzerDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0052"); public static readonly DiagnosticDescriptor Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzerDescriptor = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0053", (LocalizableString)new LocalizableResourceString("Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzer", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzerFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, isEnabledByDefault: true, (LocalizableString)new LocalizableResourceString("Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzerDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0053"); public static readonly DiagnosticDescriptor Rule0054FollowInterfaceObjectNameGuide = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0054", (LocalizableString)new LocalizableResourceString("Rule0054FollowInterfaceObjectNameGuideTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0054FollowInterfaceObjectNameGuideFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0054FollowInterfaceObjectNameGuideDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0054"); @@ -67,7 +67,7 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor Rule0061SetODataKeyFieldsWithSystemIdField = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0061", (LocalizableString)new LocalizableResourceString("Rule0061SetODataKeyFieldsWithSystemIdFieldTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0061SetODataKeyFieldsWithSystemIdFieldFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0061SetODataKeyFieldsWithSystemIdFieldDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0061"); public static readonly DiagnosticDescriptor Rule0062MandatoryFieldMissingOnApiPage = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0062", (LocalizableString)new LocalizableResourceString("Rule0062MandatoryFieldMissingOnApiPageTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0062MandatoryFieldMissingOnApiPageFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0062MandatoryFieldMissingOnApiPageDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0062"); public static readonly DiagnosticDescriptor Rule0069EmptyStatements = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0069", (LocalizableString)new LocalizableResourceString("Rule0069EmptyStatementsTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0069EmptyStatementsFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0069EmptyStatementsDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0069"); - public static readonly DiagnosticDescriptor Rule0071DoNotSetIsHandledToFalse = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0071", (LocalizableString)new LocalizableResourceString("Rule0071DoNotSetIsHandledToFalseTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0071DoNotSetIsHandledToFalseFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0071DoNotSetIsHandledToFalseDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0071"); - public static readonly DiagnosticDescriptor Rule0072CheckProcedureDocumentationComment = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0072", (LocalizableString)new LocalizableResourceString("Rule0072CheckProcedureDocumentationCommentTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0072CheckProcedureDocumentationCommentFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0072CheckProcedureDocumentationCommentDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0072"); + public static readonly DiagnosticDescriptor Rule0071DoNotSetIsHandledToFalse = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0071", (LocalizableString)new LocalizableResourceString("Rule0071DoNotSetIsHandledToFalseTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0071DoNotSetIsHandledToFalseFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0071DoNotSetIsHandledToFalseDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0071"); + public static readonly DiagnosticDescriptor Rule0072CheckProcedureDocumentationComment = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0072", (LocalizableString)new LocalizableResourceString("Rule0072CheckProcedureDocumentationCommentTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0072CheckProcedureDocumentationCommentFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0072CheckProcedureDocumentationCommentDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0072"); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx index 8dcd2ddd..dcc7876d 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx @@ -570,13 +570,13 @@ Found operator '{0}' together with placeholder '{1}' in filter expression, which results in unexpected behavior. Use the StrSubstNo() method to circumvent this. - + Do not assign a text to a target with smaller size. - + Possible overflow assigning '{0}' to '{1}'. - + Do not assign a text to a target with smaller size. From c3faddb45baeecc6cf255910f4532eb4df714ac3 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Thu, 19 Dec 2024 16:34:35 +0100 Subject: [PATCH 12/28] New Rule0083: Use new Date/Time/DateTime methods for extracting parts (#841) * New Rule0083: Use new Date/Time/DateTime methods for extracting parts * Exclude for AL Version 13.x (and lower) --- BusinessCentral.LinterCop.Test/Rule0083.cs | 43 +++++++ .../Rule0083/HasDiagnostic/DT2Date.al | 10 ++ .../Rule0083/HasDiagnostic/DT2Time.al | 10 ++ .../Rule0083/HasDiagnostic/Date2DMY.al | 10 ++ .../Rule0083/HasDiagnostic/Date2DWY.al | 10 ++ .../Rule0083/HasDiagnostic/FormatHour.al | 10 ++ .../HasDiagnostic/FormatMillisecond.al | 10 ++ .../Rule0083/HasDiagnostic/FormatMinute.al | 10 ++ .../Rule0083/HasDiagnostic/FormatSecond.al | 10 ++ .../Design/Rule0083BuiltInDateTimeMethod.cs | 112 ++++++++++++++++++ .../LinterCop.ruleset.json | 5 + .../LinterCopAnalyzers.resx | 9 ++ README.md | 5 +- 13 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 BusinessCentral.LinterCop.Test/Rule0083.cs create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/DT2Date.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/DT2Time.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/Date2DMY.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/Date2DWY.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/FormatHour.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/FormatMillisecond.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/FormatMinute.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/FormatSecond.al create mode 100644 BusinessCentral.LinterCop/Design/Rule0083BuiltInDateTimeMethod.cs diff --git a/BusinessCentral.LinterCop.Test/Rule0083.cs b/BusinessCentral.LinterCop.Test/Rule0083.cs new file mode 100644 index 00000000..74659859 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/Rule0083.cs @@ -0,0 +1,43 @@ +namespace BusinessCentral.LinterCop.Test; + +#if !LessThenFall2024 +public class Rule0083 +{ + private string _testCaseDir = ""; + + [SetUp] + public void Setup() + { + _testCaseDir = Path.Combine(Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + "TestCases", "Rule0083"); + } + + [Test] + [TestCase("Date2DMY")] + [TestCase("Date2DWY")] + [TestCase("DT2Date")] + [TestCase("DT2Time")] + [TestCase("FormatHour")] + [TestCase("FormatMillisecond")] + [TestCase("FormatMinute")] + [TestCase("FormatSecond")] + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.HasDiagnostic(code, Rule0083BuiltInDateTimeMethod.DiagnosticDescriptors.Rule0083BuiltInDateTimeMethod.Id); + } + + // [Test] + // public async Task NoDiagnostic(string testCase) + // { + // var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) + // .ConfigureAwait(false); + + // var fixture = RoslynFixtureFactory.Create(); + // fixture.NoDiagnosticAtMarker(code, Rule0083BuiltInDateTimeMethod.DiagnosticDescriptors.Rule0083BuiltInDateTimeMethod.Id); + // } +} +#endif \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/DT2Date.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/DT2Date.al new file mode 100644 index 00000000..dea0970e --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/DT2Date.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDateTime: DateTime; + MyDate: Date; + begin + MyDate := [|DT2Date(MyDateTime)|]; + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/DT2Time.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/DT2Time.al new file mode 100644 index 00000000..cc7cbd9e --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/DT2Time.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDateTime: DateTime; + MyTime: Time; + begin + MyTime := [|DT2Time(MyDateTime)|]; + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/Date2DMY.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/Date2DMY.al new file mode 100644 index 00000000..e96b3d3e --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/Date2DMY.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDate: Date; + MyInteger: Integer; + begin + MyInteger := [|Date2DMY(MyDate, 1)|]; + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/Date2DWY.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/Date2DWY.al new file mode 100644 index 00000000..e96b3d3e --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/Date2DWY.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDate: Date; + MyInteger: Integer; + begin + MyInteger := [|Date2DMY(MyDate, 1)|]; + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/FormatHour.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/FormatHour.al new file mode 100644 index 00000000..52e8ff71 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/FormatHour.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTime: Time; + i: Integer; + begin + Evaluate(i, [|Format(MyTime, 2, '')|]); + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/FormatMillisecond.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/FormatMillisecond.al new file mode 100644 index 00000000..99ff68e4 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/FormatMillisecond.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTime: Time; + i: Integer; + begin + Evaluate(i, [|Format(MyTime, 2, '')|]); + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/FormatMinute.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/FormatMinute.al new file mode 100644 index 00000000..143e3574 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/FormatMinute.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTime: Time; + i: Integer; + begin + Evaluate(i, [|Format(MyTime, 2, '')|]); + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/FormatSecond.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/FormatSecond.al new file mode 100644 index 00000000..bc3062e5 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0083/HasDiagnostic/FormatSecond.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTime: Time; + i: Integer; + begin + Evaluate(i, [|Format(MyTime, 2, '')|]); + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0083BuiltInDateTimeMethod.cs b/BusinessCentral.LinterCop/Design/Rule0083BuiltInDateTimeMethod.cs new file mode 100644 index 00000000..990ee7ca --- /dev/null +++ b/BusinessCentral.LinterCop/Design/Rule0083BuiltInDateTimeMethod.cs @@ -0,0 +1,112 @@ +#if !LessThenFall2024 +using BusinessCentral.LinterCop.AnalysisContextExtension; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; +using System.Collections.Immutable; + +namespace BusinessCentral.LinterCop.Design +{ + [DiagnosticAnalyzer] + public class Rule0083BuiltInDateTimeMethod : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0083BuiltInDateTimeMethod); + + public override VersionCompatibility SupportedVersions => VersionCompatibility.Fall2024OrGreater; + + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(new Action(this.AnalyzeInvocation), OperationKind.InvocationExpression); + + private void AnalyzeInvocation(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) + return; + + if ((ctx.Operation is not IInvocationExpression operation) || + operation.TargetMethod is null || + operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + operation.Arguments.Count() < 1) + return; + + string? recommendedMethod = operation.TargetMethod.Name switch + { + "Date2DMY" => GetDate2DMYReplacement(operation), + "Date2DWY" => GetDate2DWYReplacement(operation), + "DT2Date" => "Date", + "DT2Time" => "Time", + "Format" => GetFormatReplacement(operation), + _ => null + }; + + if (string.IsNullOrEmpty(recommendedMethod)) + return; + + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0083BuiltInDateTimeMethod, + ctx.Operation.Syntax.GetLocation(), + new object[] { operation.Arguments[0].Value.Syntax.ToString().QuoteIdentifierIfNeeded(), recommendedMethod })); + } + + private string? GetDate2DMYReplacement(IInvocationExpression operation) + { + if (operation.Arguments.Length < 2) + return null; + + return operation.Arguments[1].Value.ConstantValue.Value switch + { + 1 => "Day", + 2 => "Month", + 3 => "Year", + _ => "" + }; + } + + private string? GetDate2DWYReplacement(IInvocationExpression operation) + { + int formatSpecifier = -1; + + if (operation.Arguments.Length >= 2 && + operation.Arguments[1].Value.ConstantValue.Value is int extractedValue) + { + formatSpecifier = extractedValue; + } + + return formatSpecifier switch + { + 1 => "DayOfWeek", + 2 => "Year", + _ => "" + }; + } + + private string? GetFormatReplacement(IInvocationExpression operation) + { + string? formatSpecifier = String.Empty; + + if (operation.Arguments.Length >= 3) + formatSpecifier = operation.Arguments[2].Value.ConstantValue.Value?.ToString(); + + return formatSpecifier switch + { + "" => "Hour", + "" => "Minute", + "" => "Second", + "" => "Millisecond", + _ => "" + }; + } + + public static class DiagnosticDescriptors + { + public static readonly DiagnosticDescriptor Rule0083BuiltInDateTimeMethod = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0083", + title: LinterCopAnalyzers.GetLocalizableString("Rule0083BuiltInDateTimeMethodTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0083BuiltInDateTimeMethodFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0083BuiltInDateTimeMethodDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0082"); + } + } +} +#endif \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCop.ruleset.json b/BusinessCentral.LinterCop/LinterCop.ruleset.json index d5442965..ed389782 100644 --- a/BusinessCentral.LinterCop/LinterCop.ruleset.json +++ b/BusinessCentral.LinterCop/LinterCop.ruleset.json @@ -411,6 +411,11 @@ "id": "LC0082", "action": "Info", "justification": "Use Rec.Find('-') with Rec.Next() for checking exactly one record." + }, + { + "id": "LC0083", + "action": "Info", + "justification": "Use new Date/Time/DateTime methods for extracting parts." } ] } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx index dcc7876d..603d32a1 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx @@ -858,4 +858,13 @@ Instead of relying on Rec.Count(), you should use a combination of Rec.Find('-') and Rec.Next() for faster and more efficient record checks. + + Use new Date/Time/DateTime methods for extracting parts. + + + Use the new method '{0}.{1}' to extract specific parts of date/time values. + + + Replace outdated functions for extracting specific parts of Date, Time, and DateTime types (such as day, month, hour, or second) with the new, modernized methods. + \ No newline at end of file diff --git a/README.md b/README.md index 908cb634..188d0c01 100644 --- a/README.md +++ b/README.md @@ -235,4 +235,7 @@ For an example and the default values see: [LinterCop.ruleset.json](./BusinessCe |[LC0079](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0079)|Event publishers should not be public.|Info| |[LC0080](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0080)|Replace double quotes in JPath expressions with two single quotes.|Warning| |[LC0081](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0081)|Use `Rec.IsEmpty()` for checking record existence.|Info| -|[LC0082](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0082)|Use `Rec.Find('-')` with `Rec.Next()` for checking exactly one record.|Info| \ No newline at end of file +|[LC0082](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0082)|Use `Rec.Find('-')` with `Rec.Next()` for checking exactly one record.|Info| +|[LC0083](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0083)|Use new Date/Time/DateTime methods for extracting parts.|Info| + + From aa91a1df2a279e63a9b015d4cf722fde362638cf Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Fri, 20 Dec 2024 08:49:53 +0100 Subject: [PATCH 13/28] Add Build-in methods with indirect return length on Rule0051 (#842) * Add static Build-in methods with indirect return length * Fix typo * Use MethodKind instead of casting --- BusinessCentral.LinterCop.Test/Rule0051.cs | 2 ++ .../NoDiagnostic/GetMethodCompanyName.al | 19 +++++++++++++ .../Rule0051/NoDiagnostic/GetMethodUserId.al | 17 ++++++++++++ .../Rule0051PossibleOverflowAssigning.cs | 27 ++++++++++++++++++- 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0051/NoDiagnostic/GetMethodCompanyName.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0051/NoDiagnostic/GetMethodUserId.al diff --git a/BusinessCentral.LinterCop.Test/Rule0051.cs b/BusinessCentral.LinterCop.Test/Rule0051.cs index c394ef8d..562ef694 100644 --- a/BusinessCentral.LinterCop.Test/Rule0051.cs +++ b/BusinessCentral.LinterCop.Test/Rule0051.cs @@ -29,8 +29,10 @@ public async Task HasDiagnostic(string testCase) [Test] #if !LessThenSpring2024 + [TestCase("GetMethodCompanyName")] [TestCase("GetMethodStringLiteral")] [TestCase("GetMethodStrSubstNo")] + [TestCase("GetMethodUserId")] #endif [TestCase("SetFilterFieldRef")] public async Task NoDiagnostic(string testCase) diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0051/NoDiagnostic/GetMethodCompanyName.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0051/NoDiagnostic/GetMethodCompanyName.al new file mode 100644 index 00000000..8006e279 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0051/NoDiagnostic/GetMethodCompanyName.al @@ -0,0 +1,19 @@ +codeunit 50100 MyCodeunit +{ + procedure GetCompanySystemId(): Guid + var + Company: Record Company; + begin + Company.Get([|Database.CompanyName()|]); + exit(Company.Id); + end; +} + +table 50100 Company +{ + fields + { + field(1; Name; Text[30]) { } + field(2; Id; Guid) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0051/NoDiagnostic/GetMethodUserId.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0051/NoDiagnostic/GetMethodUserId.al new file mode 100644 index 00000000..836898bc --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0051/NoDiagnostic/GetMethodUserId.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + MyTable.Get([|Database.UserId()|]); + end; +} + +table 50100 MyTable +{ + fields + { + field(1; UserName; Code[50]) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0051PossibleOverflowAssigning.cs b/BusinessCentral.LinterCop/Design/Rule0051PossibleOverflowAssigning.cs index 2c23c098..a77558c4 100644 --- a/BusinessCentral.LinterCop/Design/Rule0051PossibleOverflowAssigning.cs +++ b/BusinessCentral.LinterCop/Design/Rule0051PossibleOverflowAssigning.cs @@ -4,6 +4,7 @@ using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; +using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; using System.Text.RegularExpressions; @@ -14,6 +15,13 @@ public class Rule0051PossibleOverflowAssigning : DiagnosticAnalyzer { private readonly Lazy strSubstNoPatternLazy = new Lazy((Func)(() => new Regex("[#%](\\d+)", RegexOptions.Compiled))); + // Build-in methods like Database.CompanyName() and Database.UserId() have indirectly a return length + private static readonly Dictionary BuiltInMethodNameWithReturnLength = new() + { + { "CompanyName", 30 }, + { "UserId", 50 } + }; + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0051PossibleOverflowAssigning); private Regex StrSubstNoPattern => this.strSubstNoPatternLazy.Value; @@ -105,10 +113,14 @@ operation.TargetMethod is null || int expressionLength = this.CalculateMaxExpressionLength(argValue.Operand, ref isError); if (!isError && expressionLength > fieldLength) { + string lengthSuffix = expressionLength < int.MaxValue + ? $"[{expressionLength}]" + : string.Empty; + ctx.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.Rule0051PossibleOverflowAssigning, operation.Arguments[index].Syntax.GetLocation(), - $"{argumentType.ToDisplayString()}[{expressionLength}]", + $"{argumentType.ToDisplayString()}{lengthSuffix}", fieldType.ToDisplayString())); } } @@ -160,6 +172,9 @@ private int CalculateMaxExpressionLength(IOperation expression, ref bool isError IMethodSymbol targetMethod = invocation.TargetMethod; if (targetMethod != null && targetMethod.ContainingSymbol?.Kind == SymbolKind.Class) { + if (IsBuiltInMethodWithReturnLength(targetMethod, out int length)) + return length; + switch (targetMethod.Name.ToLowerInvariant()) { case "convertstr": @@ -316,6 +331,16 @@ private List GetArgumentIndexes(IOperation operand) return results; } + + private static bool IsBuiltInMethodWithReturnLength(IMethodSymbol targetMethod, out int length) + { + length = 0; + + if (targetMethod.MethodKind != MethodKind.BuiltInMethod) + return false; + + return BuiltInMethodNameWithReturnLength.TryGetValue(targetMethod.Name, out length); + } } } #endif \ No newline at end of file From a4cdeda29b0a6c37f6ca347f31a7a4aa93a25d5a Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Fri, 20 Dec 2024 09:41:36 +0100 Subject: [PATCH 14/28] Allow implicit conversion from Label argument to Code/Text field (#843) --- BusinessCentral.LinterCop.Test/Rule0075.cs | 1 + .../ImplicitConversiontLabelToCode.al | 18 ++++++++++++++++++ .../Rule0075RecordGetProcedureArguments.cs | 4 ++-- 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/ImplicitConversiontLabelToCode.al diff --git a/BusinessCentral.LinterCop.Test/Rule0075.cs b/BusinessCentral.LinterCop.Test/Rule0075.cs index c9915449..2c0f0383 100644 --- a/BusinessCentral.LinterCop.Test/Rule0075.cs +++ b/BusinessCentral.LinterCop.Test/Rule0075.cs @@ -36,6 +36,7 @@ public async Task HasDiagnostic(string testCase) [Test] [TestCase("ImplicitConversiontIntegerToEnum")] + [TestCase("ImplicitConversiontLabelToCode")] [TestCase("RecordGetBuiltInMethodRecordId")] [TestCase("RecordGetFieldRecordId")] [TestCase("RecordGetGlobalVariable")] diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/ImplicitConversiontLabelToCode.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/ImplicitConversiontLabelToCode.al new file mode 100644 index 00000000..be42a66d --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/ImplicitConversiontLabelToCode.al @@ -0,0 +1,18 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + JobQueueCategory: Record "Job Queue Category"; + JobQueueCategoryCodeLbl: Label 'MyCategory', Locked = true; + begin + [|JobQueueCategory.Get(JobQueueCategoryCodeLbl)|]; + end; +} + +table 50100 "Job Queue Category" +{ + fields + { + field(1; "Code"; Code[10]) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs b/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs index bf7c30f5..dc0b9495 100644 --- a/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs +++ b/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs @@ -125,8 +125,8 @@ private bool AreFieldCompatible(IArgument argument, IFieldSymbol field) if (ImplicitConversions.TryGetValue(argumentNavType, out var compatibleTypes) && !compatibleTypes.Contains(fieldNavType)) return false; - if (argumentType.Length > 0 && fieldType.Length > 0 && - argumentType.Length > fieldType.Length) + if (argumentType.HasLength && fieldType.HasLength && + argumentType.Length > fieldType.Length) return false; return true; From 0e66f2a091548d2c7b28ffb0d9b9f33f1f2d2ea9 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Sun, 22 Dec 2024 15:42:05 +0100 Subject: [PATCH 15/28] Review rules and enable nullable (#844) --- BusinessCentral.LinterCop.Test/Rule0004.cs | 4 +- BusinessCentral.LinterCop.Test/Rule0013.cs | 14 +- BusinessCentral.LinterCop.Test/Rule0027.cs | 2 +- BusinessCentral.LinterCop.Test/Rule0065.cs | 4 +- BusinessCentral.LinterCop.Test/Rule0067.cs | 4 +- BusinessCentral.LinterCop.Test/Rule0068.cs | 4 +- BusinessCentral.LinterCop.Test/Rule0070.cs | 4 +- BusinessCentral.LinterCop.Test/Rule0073.cs | 4 +- BusinessCentral.LinterCop.Test/Rule0074.cs | 4 +- BusinessCentral.LinterCop.Test/Rule0075.cs | 4 +- BusinessCentral.LinterCop.Test/Rule0076.cs | 4 +- BusinessCentral.LinterCop.Test/Rule0077.cs | 4 +- BusinessCentral.LinterCop.Test/Rule0078.cs | 4 +- BusinessCentral.LinterCop.Test/Rule0079.cs | 4 +- BusinessCentral.LinterCop.Test/Rule0080.cs | 4 +- BusinessCentral.LinterCop.Test/Rule0081.cs | 4 +- BusinessCentral.LinterCop.Test/Rule0082.cs | 4 +- BusinessCentral.LinterCop.Test/Rule0083.cs | 4 +- .../TestCases/Rule0013/HasDiagnostic/1.al | 20 - .../HasDiagnostic/PrimaryKeyCodeField.al | 15 + .../TestCases/Rule0013/NoDiagnostic/1.al | 21 - .../TestCases/Rule0013/NoDiagnostic/2.al | 21 - .../TestCases/Rule0013/NoDiagnostic/3.al | 20 - .../TestCases/Rule0013/NoDiagnostic/4.al | 24 - .../PrimaryKeyCodeFieldNotBlankFalse.al | 15 + .../PrimaryKeyCodeFieldNotBlankTrue.al | 15 + .../NoDiagnostic/PrimaryKeyIntegerField.al | 12 + .../NoDiagnostic/PrimaryKeyMultipleFields.al | 13 + .../TestCases/Rule0028/HasDiagnostic/1.al | 4 +- .../TestCases/Rule0028/NoDiagnostic/1.al | 4 +- .../Rule0001FlowFieldsShouldNotBeEditable.cs | 33 +- .../Rule0002CommitMustBeExplainedByComment.cs | 58 +- ...oNotUseObjectIDsInVariablesOrProperties.cs | 160 +-- .../Rule0004LookupPageIdAndDrillDownPageId.cs | 58 +- ...bleCasingShouldNotDifferFromDeclaration.cs | 679 +++++++------ ...06FieldNotAutoIncrementInTemporaryTable.cs | 72 +- ...Rule0007DataPerCompanyShouldAlwaysBeSet.cs | 55 +- .../Rule0008NoFilterOperatorsInSetRange.cs | 95 +- .../Design/Rule0009CodeMetrics.cs | 208 ++-- ...Rule0011AccessPropertyShouldAlwaysBeSet.cs | 68 +- ...le0012DoNotUseObjectIdInSystemFunctions.cs | 27 +- ...heckForNotBlankOnSingleFieldPrimaryKeys.cs | 76 +- .../Rule0014PermissionSetCaptionLength.cs | 100 +- .../Design/Rule0015PermissionSetCoverage.cs | 232 +++-- .../Design/Rule0016CheckForMissingCaptions.cs | 299 +++--- .../Design/Rule0017WriteToFlowField.cs | 2 +- .../Rule0018NoEventsInInternalCodeunits.cs | 88 +- ...e0019DataClassificationFieldEqualsTable.cs | 55 +- .../Rule0020ApplicationAreaEqualsToPage.cs | 57 +- ...21BuiltInMethodImplementThroughCodeunit.cs | 67 +- .../Rule0023AlwaysSpecifyFieldgroups.cs | 85 +- ...e0024SemicolonAfterProcedureDeclaration.cs | 38 +- .../Rule0025InternalProcedureModifier.cs | 58 +- .../Design/Rule0026ToolTipPunctuation.cs | 113 ++- .../Rule0027RunPageImplementPageManagement.cs | 174 ++-- .../Rule0028IdentifiersInEventSubscribers.cs | 54 +- .../Rule0029CompareDateTimeThroughCodeunit.cs | 57 +- ...essInternalForInstallOrUpgradeCodeunits.cs | 38 +- .../Rule0031RecordInstanceIsolationLevel.cs | 37 +- .../Rule0032ClearCodeunitSingleInstance.cs | 182 ++-- .../Rule0033AppManifestRuntimeBehind.cs | 141 +-- ...0034ExtensiblePropertyShouldAlwaysBeSet.cs | 45 +- ...ule0035ExplicitSetAllowInCustomizations.cs | 235 ++--- ...le0039ArgumentDifferentTypeThenExpected.cs | 293 +++--- .../Design/Rule0040ExplicitlySetRunTrigger.cs | 55 +- .../Design/Rule0041EmptyCaptionLocked.cs | 106 +- .../Rule0042AutoCalcFieldsOnNormalFields.cs | 39 +- .../Design/Rule0043SecretText.cs | 171 ++-- .../Design/Rule0044AnalyzeTransferField.cs | 134 +-- .../Rule0045ZeroEnumValueReservedForEmpty.cs | 52 +- .../Design/Rule0046LockedTokLabels.cs | 51 +- .../Design/Rule0048ErrorWithTextConstant.cs | 61 +- ...peratorAndPlaceholderInFilterExpression.cs | 88 +- .../Rule0051PossibleOverflowAssigning.cs | 533 +++++----- ...2and0053InternalProceduresNotReferenced.cs | 325 +++--- .../Rule0054FollowInterfaceObjectNameGuide.cs | 166 ++-- .../Design/Rule0055TokSuffixForTokenLabels.cs | 81 +- ...le0056AccessibilityEnumValueWithCaption.cs | 51 +- .../Design/Rule0059SingleQuoteEscaping.cs | 88 +- ...0RemovePropertyApplicationAreaOnApiPage.cs | 52 +- ...e0061SetODataKeyFieldsWithSystemIdField.cs | 54 +- .../Rule0062MandatoryFieldMissingOnApiPage.cs | 27 +- .../Rule0063GiveFieldMoreDescriptiveName.cs | 124 ++- .../Design/Rule0064UseTableFieldToolTip.cs | 120 ++- .../Rule0065CheckEventSubscriberVarKeyword.cs | 73 +- .../Design/Rule0068CheckObjectPermission.cs | 400 ++++---- .../Design/Rule0069EmptyStatements.cs | 40 +- .../Design/Rule0070ListObjectsAreOneBased.cs | 69 +- .../Rule0071DoNotSetIsHandledToFalse.cs | 195 ++-- ...e0072CheckProcedureDocumentationComment.cs | 101 +- .../Rule0073EventPublisherIsHandledByVar.cs | 16 +- .../Design/Rule0074FlowFilterAssignment.cs | 25 +- .../Rule0075RecordGetProcedureArguments.cs | 30 +- .../Design/Rule0076TableRelationTooLong.cs | 17 +- .../Design/Rule0077MissingParenthesis.cs | 33 +- .../Design/Rule0078TempRecRunTrigger.cs | 43 +- .../Design/Rule0079NonPublicEventPublisher.cs | 26 +- .../Design/Rule0080AnalyzeJsonTokenJPath.cs | 16 - .../Design/Rule0081AnalyzeCountMethod.cs | 211 ++-- .../Design/Rule0083BuiltInDateTimeMethod.cs | 174 ++-- BusinessCentral.LinterCop/Extensions.cs | 72 -- .../Helpers/AnalysisContextHelper.cs | 89 +- .../Helpers/ArgumentHelper.cs | 21 +- .../Helpers/HelperFunctions.cs | 78 +- .../Helpers/LinterSettings.cs | 12 +- .../Helpers/TypeSymbolHelper.cs | 27 + .../LinterCopAnalyzers.Designer.cs | 6 +- .../LinterCopAnalyzers.Generated.cs | 922 ++++++++++++++++-- 108 files changed, 5018 insertions(+), 4359 deletions(-) delete mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0013/HasDiagnostic/1.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0013/HasDiagnostic/PrimaryKeyCodeField.al delete mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/1.al delete mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/2.al delete mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/3.al delete mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/4.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/PrimaryKeyCodeFieldNotBlankFalse.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/PrimaryKeyCodeFieldNotBlankTrue.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/PrimaryKeyIntegerField.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/PrimaryKeyMultipleFields.al delete mode 100644 BusinessCentral.LinterCop/Extensions.cs create mode 100644 BusinessCentral.LinterCop/Helpers/TypeSymbolHelper.cs diff --git a/BusinessCentral.LinterCop.Test/Rule0004.cs b/BusinessCentral.LinterCop.Test/Rule0004.cs index 9676ea4f..b35f2094 100644 --- a/BusinessCentral.LinterCop.Test/Rule0004.cs +++ b/BusinessCentral.LinterCop.Test/Rule0004.cs @@ -19,7 +19,7 @@ public async Task HasDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.HasDiagnostic(code, Rule0004LookupPageIdAndDrillDownPageId.DiagnosticDescriptors.Rule0004LookupPageIdAndDrillDownPageId.Id); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0004LookupPageIdAndDrillDownPageId.Id); } [Test] @@ -31,6 +31,6 @@ public async Task NoDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.NoDiagnosticAtMarker(code, Rule0004LookupPageIdAndDrillDownPageId.DiagnosticDescriptors.Rule0004LookupPageIdAndDrillDownPageId.Id); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0004LookupPageIdAndDrillDownPageId.Id); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/Rule0013.cs b/BusinessCentral.LinterCop.Test/Rule0013.cs index ea46f045..1701b46c 100644 --- a/BusinessCentral.LinterCop.Test/Rule0013.cs +++ b/BusinessCentral.LinterCop.Test/Rule0013.cs @@ -12,27 +12,27 @@ public void Setup() } [Test] - [TestCase("1")] + [TestCase("PrimaryKeyCodeField")] public async Task HasDiagnostic(string testCase) { var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.HasDiagnostic(code, Rule0013CheckForNotBlankOnSingleFieldPrimaryKeys.DiagnosticDescriptors.Rule0013CheckForNotBlankOnSingleFieldPrimaryKeys.Id); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0013CheckForNotBlankOnSingleFieldPrimaryKeys.Id); } [Test] - [TestCase("1")] - [TestCase("2")] - [TestCase("3")] - [TestCase("4")] + [TestCase("PrimaryKeyCodeFieldNotBlankFalse")] + [TestCase("PrimaryKeyCodeFieldNotBlankTrue")] + [TestCase("PrimaryKeyIntegerField")] + [TestCase("PrimaryKeyMultipleFields")] public async Task NoDiagnostic(string testCase) { var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.NoDiagnosticAtMarker(code, Rule0013CheckForNotBlankOnSingleFieldPrimaryKeys.DiagnosticDescriptors.Rule0013CheckForNotBlankOnSingleFieldPrimaryKeys.Id); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0013CheckForNotBlankOnSingleFieldPrimaryKeys.Id); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/Rule0027.cs b/BusinessCentral.LinterCop.Test/Rule0027.cs index 65922afc..25e31bc2 100644 --- a/BusinessCentral.LinterCop.Test/Rule0027.cs +++ b/BusinessCentral.LinterCop.Test/Rule0027.cs @@ -31,6 +31,6 @@ public async Task NoDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.NoDiagnosticAtMarker(code, Rule0027RunPageImplementPageManagement.DiagnosticDescriptors.Rule0027RunPageImplementPageManagement.Id); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0027RunPageImplementPageManagement.Id); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/Rule0065.cs b/BusinessCentral.LinterCop.Test/Rule0065.cs index 4c2ec266..d84facc6 100644 --- a/BusinessCentral.LinterCop.Test/Rule0065.cs +++ b/BusinessCentral.LinterCop.Test/Rule0065.cs @@ -19,7 +19,7 @@ public async Task HasDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.HasDiagnostic(code, Rule0065CheckEventSubscriberVarKeyword.DiagnosticDescriptors.Rule0065EventSubscriberVarCheck.Id); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0065EventSubscriberVarCheck.Id); } [Test] @@ -30,6 +30,6 @@ public async Task NoDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.NoDiagnosticAtMarker(code, Rule0065CheckEventSubscriberVarKeyword.DiagnosticDescriptors.Rule0065EventSubscriberVarCheck.Id); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0065EventSubscriberVarCheck.Id); } } diff --git a/BusinessCentral.LinterCop.Test/Rule0067.cs b/BusinessCentral.LinterCop.Test/Rule0067.cs index 21543588..7b0f5cc4 100644 --- a/BusinessCentral.LinterCop.Test/Rule0067.cs +++ b/BusinessCentral.LinterCop.Test/Rule0067.cs @@ -21,7 +21,7 @@ public async Task HasDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.HasDiagnostic(code, Rule0013CheckForNotBlankOnSingleFieldPrimaryKeys.DiagnosticDescriptors.Rule0067DisableNotBlankOnSingleFieldPrimaryKey.Id); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0067DisableNotBlankOnSingleFieldPrimaryKey.Id); } [Test] @@ -35,6 +35,6 @@ public async Task NoDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.NoDiagnosticAtMarker(code, Rule0013CheckForNotBlankOnSingleFieldPrimaryKeys.DiagnosticDescriptors.Rule0067DisableNotBlankOnSingleFieldPrimaryKey.Id); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0067DisableNotBlankOnSingleFieldPrimaryKey.Id); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/Rule0068.cs b/BusinessCentral.LinterCop.Test/Rule0068.cs index b6e71e6b..ef2e2ea9 100644 --- a/BusinessCentral.LinterCop.Test/Rule0068.cs +++ b/BusinessCentral.LinterCop.Test/Rule0068.cs @@ -22,7 +22,7 @@ public async Task HasDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.HasDiagnostic(code, Rule0068CheckObjectPermission.DiagnosticDescriptors.Rule0068CheckObjectPermission.Id); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0068CheckObjectPermission.Id); } [Test] @@ -47,6 +47,6 @@ public async Task NoDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.NoDiagnosticAtMarker(code, Rule0068CheckObjectPermission.DiagnosticDescriptors.Rule0068CheckObjectPermission.Id); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0068CheckObjectPermission.Id); } } diff --git a/BusinessCentral.LinterCop.Test/Rule0070.cs b/BusinessCentral.LinterCop.Test/Rule0070.cs index 4a161c92..559e33e9 100644 --- a/BusinessCentral.LinterCop.Test/Rule0070.cs +++ b/BusinessCentral.LinterCop.Test/Rule0070.cs @@ -20,7 +20,7 @@ public async Task HasDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.HasDiagnostic(code, Rule0070ListObjectsAreOneBased.DiagnosticDescriptors.Rule0070ListObjectsAreOneBased.Id); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0070ListObjectsAreOneBased.Id); } [Test] @@ -32,6 +32,6 @@ public async Task NoDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.NoDiagnosticAtMarker(code, Rule0070ListObjectsAreOneBased.DiagnosticDescriptors.Rule0070ListObjectsAreOneBased.Id); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0070ListObjectsAreOneBased.Id); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/Rule0073.cs b/BusinessCentral.LinterCop.Test/Rule0073.cs index aef62600..dff3c634 100644 --- a/BusinessCentral.LinterCop.Test/Rule0073.cs +++ b/BusinessCentral.LinterCop.Test/Rule0073.cs @@ -20,7 +20,7 @@ public async Task HasDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.HasDiagnostic(code, Rule0073EventPublisherIsHandledByVar.DiagnosticDescriptors.Rule0073EventPublisherIsHandledByVar.Id); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0073EventPublisherIsHandledByVar.Id); } [Test] @@ -32,6 +32,6 @@ public async Task NoDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.NoDiagnosticAtMarker(code, Rule0073EventPublisherIsHandledByVar.DiagnosticDescriptors.Rule0073EventPublisherIsHandledByVar.Id); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0073EventPublisherIsHandledByVar.Id); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/Rule0074.cs b/BusinessCentral.LinterCop.Test/Rule0074.cs index 1cf5f42e..e1f4aa5a 100644 --- a/BusinessCentral.LinterCop.Test/Rule0074.cs +++ b/BusinessCentral.LinterCop.Test/Rule0074.cs @@ -20,7 +20,7 @@ public async Task HasDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.HasDiagnostic(code, Rule0074FlowFilterAssignment.DiagnosticDescriptors.Rule0074FlowFilterAssignment.Id); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0074FlowFilterAssignment.Id); } [Test] @@ -31,6 +31,6 @@ public async Task NoDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.NoDiagnosticAtMarker(code, Rule0074FlowFilterAssignment.DiagnosticDescriptors.Rule0074FlowFilterAssignment.Id); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0074FlowFilterAssignment.Id); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/Rule0075.cs b/BusinessCentral.LinterCop.Test/Rule0075.cs index 2c0f0383..fcf11e52 100644 --- a/BusinessCentral.LinterCop.Test/Rule0075.cs +++ b/BusinessCentral.LinterCop.Test/Rule0075.cs @@ -31,7 +31,7 @@ public async Task HasDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.HasDiagnostic(code, Rule0075RecordGetProcedureArguments.DiagnosticDescriptors.Rule0075RecordGetProcedureArguments.Id); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0075RecordGetProcedureArguments.Id); } [Test] @@ -58,7 +58,7 @@ public async Task NoDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.NoDiagnosticAtMarker(code, Rule0075RecordGetProcedureArguments.DiagnosticDescriptors.Rule0075RecordGetProcedureArguments.Id); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0075RecordGetProcedureArguments.Id); } } #endif \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/Rule0076.cs b/BusinessCentral.LinterCop.Test/Rule0076.cs index b176b15b..29149e69 100644 --- a/BusinessCentral.LinterCop.Test/Rule0076.cs +++ b/BusinessCentral.LinterCop.Test/Rule0076.cs @@ -22,7 +22,7 @@ public async Task HasDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.HasDiagnostic(code, Rule0076TableRelationTooLong.DiagnosticDescriptors.Rule0076TableRelationTooLong.Id); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0076TableRelationTooLong.Id); } [Test] @@ -38,6 +38,6 @@ public async Task NoDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.NoDiagnosticAtMarker(code, Rule0076TableRelationTooLong.DiagnosticDescriptors.Rule0076TableRelationTooLong.Id); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0076TableRelationTooLong.Id); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/Rule0077.cs b/BusinessCentral.LinterCop.Test/Rule0077.cs index 97ef29a6..347feb75 100644 --- a/BusinessCentral.LinterCop.Test/Rule0077.cs +++ b/BusinessCentral.LinterCop.Test/Rule0077.cs @@ -19,7 +19,7 @@ public async Task HasDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.HasDiagnostic(code, Rule0077MissingParenthesis.DiagnosticDescriptors.Rule0077MissingParenthesis.Id); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0077MissingParenthesis.Id); } [Test] @@ -30,6 +30,6 @@ public async Task NoDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.NoDiagnosticAtMarker(code, Rule0077MissingParenthesis.DiagnosticDescriptors.Rule0077MissingParenthesis.Id); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0077MissingParenthesis.Id); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/Rule0078.cs b/BusinessCentral.LinterCop.Test/Rule0078.cs index 52b311a8..b3edd669 100644 --- a/BusinessCentral.LinterCop.Test/Rule0078.cs +++ b/BusinessCentral.LinterCop.Test/Rule0078.cs @@ -19,7 +19,7 @@ public async Task HasDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.HasDiagnostic(code, Rule0078TemporaryRecordsShouldNotTriggerTableTriggers.DiagnosticDescriptors.Rule0078TemporaryRecordsShouldNotTriggerTableTriggers.Id); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0078TemporaryRecordsShouldNotTriggerTableTriggers.Id); } [Test] @@ -33,6 +33,6 @@ public async Task NoDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.NoDiagnosticAtMarker(code, Rule0078TemporaryRecordsShouldNotTriggerTableTriggers.DiagnosticDescriptors.Rule0078TemporaryRecordsShouldNotTriggerTableTriggers.Id); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0078TemporaryRecordsShouldNotTriggerTableTriggers.Id); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/Rule0079.cs b/BusinessCentral.LinterCop.Test/Rule0079.cs index a49a20c1..7f764d11 100644 --- a/BusinessCentral.LinterCop.Test/Rule0079.cs +++ b/BusinessCentral.LinterCop.Test/Rule0079.cs @@ -19,7 +19,7 @@ public async Task HasDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.HasDiagnostic(code, Rule0079NonPublicEventPublisher.DiagnosticDescriptors.Rule0079NonPublicEventPublisher.Id); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0079NonPublicEventPublisher.Id); } [Test] @@ -31,6 +31,6 @@ public async Task NoDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.NoDiagnosticAtMarker(code, Rule0079NonPublicEventPublisher.DiagnosticDescriptors.Rule0079NonPublicEventPublisher.Id); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0079NonPublicEventPublisher.Id); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/Rule0080.cs b/BusinessCentral.LinterCop.Test/Rule0080.cs index 3ede1696..6d0d3005 100644 --- a/BusinessCentral.LinterCop.Test/Rule0080.cs +++ b/BusinessCentral.LinterCop.Test/Rule0080.cs @@ -19,7 +19,7 @@ public async Task HasDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.HasDiagnostic(code, Rule0080AnalyzeJsonTokenJPath.DiagnosticDescriptors.Rule0080AnalyzeJsonTokenJPath.Id); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0080AnalyzeJsonTokenJPath.Id); } [Test] @@ -30,6 +30,6 @@ public async Task NoDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.NoDiagnosticAtMarker(code, Rule0080AnalyzeJsonTokenJPath.DiagnosticDescriptors.Rule0080AnalyzeJsonTokenJPath.Id); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0080AnalyzeJsonTokenJPath.Id); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/Rule0081.cs b/BusinessCentral.LinterCop.Test/Rule0081.cs index ccea2f59..00add5b9 100644 --- a/BusinessCentral.LinterCop.Test/Rule0081.cs +++ b/BusinessCentral.LinterCop.Test/Rule0081.cs @@ -26,7 +26,7 @@ public async Task HasDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.HasDiagnostic(code, Rule0081AnalyzeCountMethod.DiagnosticDescriptors.Rule0081UseIsEmptyMethod.Id); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0081UseIsEmptyMethod.Id); } [Test] @@ -38,6 +38,6 @@ public async Task NoDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.NoDiagnosticAtMarker(code, Rule0081AnalyzeCountMethod.DiagnosticDescriptors.Rule0081UseIsEmptyMethod.Id); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0081UseIsEmptyMethod.Id); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/Rule0082.cs b/BusinessCentral.LinterCop.Test/Rule0082.cs index 6f62822b..c593dee4 100644 --- a/BusinessCentral.LinterCop.Test/Rule0082.cs +++ b/BusinessCentral.LinterCop.Test/Rule0082.cs @@ -25,7 +25,7 @@ public async Task HasDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.HasDiagnostic(code, Rule0081AnalyzeCountMethod.DiagnosticDescriptors.Rule0082UseFindWithNext.Id); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0082UseFindWithNext.Id); } [Test] @@ -37,6 +37,6 @@ public async Task NoDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.NoDiagnosticAtMarker(code, Rule0081AnalyzeCountMethod.DiagnosticDescriptors.Rule0082UseFindWithNext.Id); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0082UseFindWithNext.Id); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/Rule0083.cs b/BusinessCentral.LinterCop.Test/Rule0083.cs index 74659859..c9de9f81 100644 --- a/BusinessCentral.LinterCop.Test/Rule0083.cs +++ b/BusinessCentral.LinterCop.Test/Rule0083.cs @@ -27,7 +27,7 @@ public async Task HasDiagnostic(string testCase) .ConfigureAwait(false); var fixture = RoslynFixtureFactory.Create(); - fixture.HasDiagnostic(code, Rule0083BuiltInDateTimeMethod.DiagnosticDescriptors.Rule0083BuiltInDateTimeMethod.Id); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0083BuiltInDateTimeMethod.Id); } // [Test] @@ -37,7 +37,7 @@ public async Task HasDiagnostic(string testCase) // .ConfigureAwait(false); // var fixture = RoslynFixtureFactory.Create(); - // fixture.NoDiagnosticAtMarker(code, Rule0083BuiltInDateTimeMethod.DiagnosticDescriptors.Rule0083BuiltInDateTimeMethod.Id); + // fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0083BuiltInDateTimeMethod.Id); // } } #endif \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0013/HasDiagnostic/1.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0013/HasDiagnostic/1.al deleted file mode 100644 index 0237f97a..00000000 --- a/BusinessCentral.LinterCop.Test/TestCases/Rule0013/HasDiagnostic/1.al +++ /dev/null @@ -1,20 +0,0 @@ -table 50100 MyTable -{ - DataClassification = ToBeClassified; - - fields - { - [|field(1; MyField; Code[10])|] - { - DataClassification = ToBeClassified; - } - } - - keys - { - key(Key1; MyField) - { - Clustered = true; - } - } -} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0013/HasDiagnostic/PrimaryKeyCodeField.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0013/HasDiagnostic/PrimaryKeyCodeField.al new file mode 100644 index 00000000..303c8d16 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0013/HasDiagnostic/PrimaryKeyCodeField.al @@ -0,0 +1,15 @@ +table 50100 MyTable +{ + fields + { + [|field(1; MyField; Code[10])|] { } + } + + keys + { + key(Key1; MyField) + { + Clustered = true; + } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/1.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/1.al deleted file mode 100644 index 1837d3f9..00000000 --- a/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/1.al +++ /dev/null @@ -1,21 +0,0 @@ -table 50100 MyTable -{ - DataClassification = ToBeClassified; - - fields - { - [|field(1; MyField; Code[10])|] - { - NotBlank = true; - DataClassification = ToBeClassified; - } - } - - keys - { - key(Key1; MyField) - { - Clustered = true; - } - } -} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/2.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/2.al deleted file mode 100644 index c25d69dd..00000000 --- a/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/2.al +++ /dev/null @@ -1,21 +0,0 @@ -table 50100 MyTable -{ - DataClassification = ToBeClassified; - - fields - { - [|field(1; MyField; Code[10])|] - { - NotBlank = false; - DataClassification = ToBeClassified; - } - } - - keys - { - key(Key1; MyField) - { - Clustered = true; - } - } -} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/3.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/3.al deleted file mode 100644 index f3f011d7..00000000 --- a/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/3.al +++ /dev/null @@ -1,20 +0,0 @@ -table 50100 MyTable -{ - DataClassification = ToBeClassified; - - fields - { - [|field(1; MyField; Integer)|] - { - DataClassification = ToBeClassified; - } - } - - keys - { - key(Key1; MyField) - { - Clustered = true; - } - } -} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/4.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/4.al deleted file mode 100644 index a21463ec..00000000 --- a/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/4.al +++ /dev/null @@ -1,24 +0,0 @@ -table 50100 MyTable -{ - DataClassification = ToBeClassified; - - fields - { - [|field(1; MyField; Code[10])|] - { - DataClassification = ToBeClassified; - } - field(2; MyField2; Integer) - { - DataClassification = ToBeClassified; - } - } - - keys - { - key(Key1; MyField, MyField2) - { - Clustered = true; - } - } -} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/PrimaryKeyCodeFieldNotBlankFalse.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/PrimaryKeyCodeFieldNotBlankFalse.al new file mode 100644 index 00000000..3d7753fa --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/PrimaryKeyCodeFieldNotBlankFalse.al @@ -0,0 +1,15 @@ +table 50100 MyTable +{ + fields + { + [|field(1; MyField; Code[10])|] + { + NotBlank = false; + } + } + + keys + { + key(Key1; MyField) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/PrimaryKeyCodeFieldNotBlankTrue.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/PrimaryKeyCodeFieldNotBlankTrue.al new file mode 100644 index 00000000..772705f7 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/PrimaryKeyCodeFieldNotBlankTrue.al @@ -0,0 +1,15 @@ +table 50100 MyTable +{ + fields + { + [|field(1; MyField; Code[10])|] + { + NotBlank = true; + } + } + + keys + { + key(Key1; MyField) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/PrimaryKeyIntegerField.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/PrimaryKeyIntegerField.al new file mode 100644 index 00000000..de0c3115 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/PrimaryKeyIntegerField.al @@ -0,0 +1,12 @@ +table 50100 MyTable +{ + fields + { + [|field(1; MyField; Integer)|] { } + } + + keys + { + key(Key1; MyField) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/PrimaryKeyMultipleFields.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/PrimaryKeyMultipleFields.al new file mode 100644 index 00000000..6a1851ed --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0013/NoDiagnostic/PrimaryKeyMultipleFields.al @@ -0,0 +1,13 @@ +table 50100 MyTable +{ + fields + { + [|field(1; MyField; Code[10])|] { } + field(2; MyField2; Integer) { } + } + + keys + { + key(Key1; MyField, MyField2) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0028/HasDiagnostic/1.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0028/HasDiagnostic/1.al index e42ee70d..06885067 100644 --- a/BusinessCentral.LinterCop.Test/TestCases/Rule0028/HasDiagnostic/1.al +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0028/HasDiagnostic/1.al @@ -16,8 +16,8 @@ table 50100 MyTable codeunit 50100 MyCodeunit { - [EventSubscriber(ObjectType::Table, Database::MyTable, 'OnAfterDeleteEvent', '', false, false)] - [|local procedure OnAfterDeleteEvent(var Rec: Record MyTable; RunTrigger: Boolean)|] + [|[EventSubscriber(ObjectType::Table, Database::MyTable, 'OnAfterDeleteEvent', '', false, false)]|] + local procedure OnAfterDeleteEvent(var Rec: Record MyTable; RunTrigger: Boolean) begin end; diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0028/NoDiagnostic/1.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0028/NoDiagnostic/1.al index 4bf0d05f..4baa93b5 100644 --- a/BusinessCentral.LinterCop.Test/TestCases/Rule0028/NoDiagnostic/1.al +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0028/NoDiagnostic/1.al @@ -16,8 +16,8 @@ table 50100 MyTable codeunit 50100 MyCodeunit { - [EventSubscriber(ObjectType::Table, Database::MyTable, OnAfterDeleteEvent, '', false, false)] - [|local procedure OnAfterDeleteEvent(var Rec: Record MyTable; RunTrigger: Boolean)|] + [|[EventSubscriber(ObjectType::Table, Database::MyTable, OnAfterDeleteEvent, '', false, false)]|] + local procedure OnAfterDeleteEvent(var Rec: Record MyTable; RunTrigger: Boolean) begin end; diff --git a/BusinessCentral.LinterCop/Design/Rule0001FlowFieldsShouldNotBeEditable.cs b/BusinessCentral.LinterCop/Design/Rule0001FlowFieldsShouldNotBeEditable.cs index e56391d9..7d957b54 100644 --- a/BusinessCentral.LinterCop/Design/Rule0001FlowFieldsShouldNotBeEditable.cs +++ b/BusinessCentral.LinterCop/Design/Rule0001FlowFieldsShouldNotBeEditable.cs @@ -1,25 +1,30 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0001FlowFieldsShouldNotBeEditable : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0001FlowFieldsShouldNotBeEditable : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0001FlowFieldsShouldNotBeEditable); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0001FlowFieldsShouldNotBeEditable); - public override void Initialize(AnalysisContext context) => context.RegisterSymbolAction(new Action(this.AnalyzeFlowFieldEditable), SymbolKind.Field); + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(new Action(this.AnalyzeFlowFieldEditable), SymbolKind.Field); - private void AnalyzeFlowFieldEditable(SymbolAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; + private void AnalyzeFlowFieldEditable(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Symbol is not IFieldSymbol field) + return; - IFieldSymbol field = (IFieldSymbol)ctx.Symbol; - if (field.FieldClass == FieldClassKind.FlowField && field.GetBooleanPropertyValue(PropertyKind.Editable).Value) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0001FlowFieldsShouldNotBeEditable, field.Location)); + if (field.FieldClass == FieldClassKind.FlowField && + field.GetBooleanPropertyValue(PropertyKind.Editable).GetValueOrDefault()) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0001FlowFieldsShouldNotBeEditable, + field.GetLocation())); } } } diff --git a/BusinessCentral.LinterCop/Design/Rule0002CommitMustBeExplainedByComment.cs b/BusinessCentral.LinterCop/Design/Rule0002CommitMustBeExplainedByComment.cs index aa54c772..e02a4193 100644 --- a/BusinessCentral.LinterCop/Design/Rule0002CommitMustBeExplainedByComment.cs +++ b/BusinessCentral.LinterCop/Design/Rule0002CommitMustBeExplainedByComment.cs @@ -1,43 +1,39 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0002CommitMustBeExplainedByComment : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0002CommitMustBeExplainedByComment : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0002CommitMustBeExplainedByComment); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0002CommitMustBeExplainedByComment); + + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(new Action(this.CheckCommitForExplainingComment), OperationKind.InvocationExpression); - public override void Initialize(AnalysisContext context) => context.RegisterOperationAction(new Action(this.CheckCommitForExplainingComment), OperationKind.InvocationExpression); + private void CheckCommitForExplainingComment(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) + return; - private void CheckCommitForExplainingComment(OperationAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + operation.TargetMethod.Name != "Commit") + return; - IInvocationExpression operation = (IInvocationExpression)ctx.Operation; - if (operation.TargetMethod.Name.ToUpper() == "COMMIT" && operation.TargetMethod.MethodKind == MethodKind.BuiltInMethod) - { - foreach (SyntaxTrivia trivia in operation.Syntax.Parent.GetLeadingTrivia()) - { - if (trivia.IsKind(SyntaxKind.LineCommentTrivia)) - { - return; - } - } - foreach (SyntaxTrivia trivia in operation.Syntax.Parent.GetTrailingTrivia()) - { - if (trivia.IsKind(SyntaxKind.LineCommentTrivia)) - { - return; - } - } + var parentSyntax = operation.Syntax.Parent; + if (HasLineComment(parentSyntax.GetLeadingTrivia()) || HasLineComment(parentSyntax.GetTrailingTrivia())) + return; - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0002CommitMustBeExplainedByComment, ctx.Operation.Syntax.GetLocation())); - } - } + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0002CommitMustBeExplainedByComment, + ctx.Operation.Syntax.GetLocation())); + } + private static bool HasLineComment(SyntaxTriviaList triviaList) + { + return triviaList.Any(trivia => trivia.IsKind(SyntaxKind.LineCommentTrivia)); } } diff --git a/BusinessCentral.LinterCop/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs b/BusinessCentral.LinterCop/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs index cd7cb9ca..7335ec31 100644 --- a/BusinessCentral.LinterCop/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs +++ b/BusinessCentral.LinterCop/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs @@ -1,109 +1,98 @@ #nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0003DoNotUseObjectIDsInVariablesOrProperties : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0003DoNotUseObjectIDsInVariablesOrProperties : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create( + DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, + DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration); - public override void Initialize(AnalysisContext context) - { - context.RegisterSyntaxNodeAction(new Action(this.CheckForObjectIDsInVariablesOrProperties), new SyntaxKind[] { + public override void Initialize(AnalysisContext context) => + context.RegisterSyntaxNodeAction(new Action(this.CheckForObjectIDsInVariablesOrProperties), new SyntaxKind[] { SyntaxKind.ObjectReference, SyntaxKind.PermissionValue - }); - } + }); - private void CheckForObjectIDsInVariablesOrProperties(SyntaxNodeAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; + private void CheckForObjectIDsInVariablesOrProperties(SyntaxNodeAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) + return; - string correctName; - if (ctx.ContainingSymbol.Kind == SymbolKind.LocalVariable || ctx.ContainingSymbol.Kind == SymbolKind.GlobalVariable) - { - IVariableSymbol variable = (IVariableSymbol)ctx.ContainingSymbol; - if (variable.Type.IsErrorType() || variable.Type.GetNavTypeKindSafe() == NavTypeKind.DotNet) return; + string correctName; + if (ctx.ContainingSymbol.Kind == SymbolKind.LocalVariable || ctx.ContainingSymbol.Kind == SymbolKind.GlobalVariable) + { + IVariableSymbol variable = (IVariableSymbol)ctx.ContainingSymbol; + if (variable.Type.IsErrorType() || variable.Type.GetNavTypeKindSafe() == NavTypeKind.DotNet) + return; - if (variable.Type.GetNavTypeKindSafe() == NavTypeKind.Array) - correctName = ((IArrayTypeSymbol)variable.Type).ElementType.Name.ToString(); - else - correctName = variable.Type.Name; + if (variable.Type.GetNavTypeKindSafe() == NavTypeKind.Array) + correctName = ((IArrayTypeSymbol)variable.Type).ElementType.Name.ToString(); + else + correctName = variable.Type.Name; - if (ctx.Node.GetLastToken().ToString().Trim('"').ToUpper() != correctName.ToUpper()) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, ctx.Node.GetLocation(), new object[] { ctx.Node.ToString().Trim('"'), correctName })); + if (ctx.Node.GetLastToken().ToString().Trim('"').ToUpper() != correctName.ToUpper()) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, ctx.Node.GetLocation(), new object[] { ctx.Node.ToString().Trim('"'), correctName })); - if (ctx.Node.GetLastToken().ToString().Trim('"') != correctName) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Node.GetLocation(), new object[] { correctName, "" })); - } - if (ctx.ContainingSymbol.Kind == SymbolKind.Property) + if (ctx.Node.GetLastToken().ToString().Trim('"') != correctName) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Node.GetLocation(), new object[] { correctName, "" })); + } + if (ctx.ContainingSymbol.Kind == SymbolKind.Property) + { + IPropertySymbol property = (IPropertySymbol)ctx.ContainingSymbol; + if (ctx.Node.Kind == SyntaxKind.PermissionValue) { - IPropertySymbol property = (IPropertySymbol)ctx.ContainingSymbol; - if (ctx.Node.Kind == SyntaxKind.PermissionValue) - { - var nodes = ctx.Node.ChildNodesAndTokens().GetEnumerator(); + var nodes = ctx.Node.ChildNodesAndTokens().GetEnumerator(); - while (nodes.MoveNext()) - { - if (nodes.Current.IsNode) - { - var subnodes = nodes.Current.ChildNodesAndTokens().GetEnumerator(); - - while (subnodes.MoveNext()) - { - if (subnodes.Current.Kind == SyntaxKind.ObjectId) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, nodes.Current.GetLocation(), new object[] { "", "the object name" })); - }; - } - }; - } - - if (property.PropertyKind != PropertyKind.Permissions && property.PropertyKind != PropertyKind.AccessByPermission) + while (nodes.MoveNext()) { - if (ctx.Node.GetLastToken().ToString().Trim('"').ToUpper() != property.ValueText.ToUpper()) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, ctx.Node.GetLocation(), new object[] { ctx.Node.ToString().Trim('"'), property.ValueText })); + if (nodes.Current.IsNode) + { + var subnodes = nodes.Current.ChildNodesAndTokens().GetEnumerator(); - if (ctx.Node.GetLastToken().ToString().Trim('"') != property.ValueText) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Node.GetLocation(), new object[] { property.ValueText, "" })); - } + while (subnodes.MoveNext()) + { + if (subnodes.Current.Kind == SyntaxKind.ObjectId) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, nodes.Current.GetLocation(), new object[] { "", "the object name" })); + }; + } + }; } - if (ctx.ContainingSymbol.Kind == SymbolKind.Method) + if (property.PropertyKind != PropertyKind.Permissions && property.PropertyKind != PropertyKind.AccessByPermission) { - IMethodSymbol method = (IMethodSymbol)ctx.ContainingSymbol; - - foreach (IParameterSymbol parameter in method.Parameters) - { - if (parameter.ParameterType.GetNavTypeKindSafe() == NavTypeKind.DotNet) continue; - - if (ctx.Node.GetLocation().SourceSpan.End == parameter.DeclaringSyntaxReference.GetSyntax(ctx.CancellationToken).Span.End) - { - if (parameter.ParameterType.GetNavTypeKindSafe() == NavTypeKind.Array) - correctName = ((IArrayTypeSymbol)parameter.ParameterType).ElementType.Name.ToString(); - else - correctName = parameter.ParameterType.Name; + if (ctx.Node.GetLastToken().ToString().Trim('"').ToUpper() != property.ValueText.ToUpper()) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, ctx.Node.GetLocation(), new object[] { ctx.Node.ToString().Trim('"'), property.ValueText })); - if (string.IsNullOrEmpty(correctName)) - continue; + if (ctx.Node.GetLastToken().ToString().Trim('"') != property.ValueText) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Node.GetLocation(), new object[] { property.ValueText, "" })); + } + } - if (ctx.Node.GetLastToken().ToString().Trim('"').ToUpper() != correctName.ToUpper()) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, ctx.Node.GetLocation(), new object[] { ctx.Node.ToString().Trim('"'), correctName })); + if (ctx.ContainingSymbol.Kind == SymbolKind.Method) + { + IMethodSymbol method = (IMethodSymbol)ctx.ContainingSymbol; - if (ctx.Node.GetLastToken().ToString().Trim('"') != correctName) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Node.GetLocation(), new object[] { correctName, "" })); - } - } - IReturnValueSymbol returnValue = method.ReturnValueSymbol; - if (returnValue?.DeclaringSyntaxReference == null || returnValue.ReturnType.GetNavTypeKindSafe() == NavTypeKind.DotNet) return; + foreach (IParameterSymbol parameter in method.Parameters) + { + if (parameter.ParameterType.GetNavTypeKindSafe() == NavTypeKind.DotNet) continue; - if (ctx.Node.GetLocation().SourceSpan.End == returnValue.DeclaringSyntaxReference.GetSyntax(ctx.CancellationToken).Span.End) + if (ctx.Node.GetLocation().SourceSpan.End == parameter.DeclaringSyntaxReference.GetSyntax(ctx.CancellationToken).Span.End) { - correctName = returnValue.ReturnType.Name; + if (parameter.ParameterType.GetNavTypeKindSafe() == NavTypeKind.Array) + correctName = ((IArrayTypeSymbol)parameter.ParameterType).ElementType.Name.ToString(); + else + correctName = parameter.ParameterType.Name; + + if (string.IsNullOrEmpty(correctName)) + continue; if (ctx.Node.GetLastToken().ToString().Trim('"').ToUpper() != correctName.ToUpper()) ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, ctx.Node.GetLocation(), new object[] { ctx.Node.ToString().Trim('"'), correctName })); @@ -112,6 +101,19 @@ private void CheckForObjectIDsInVariablesOrProperties(SyntaxNodeAnalysisContext ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Node.GetLocation(), new object[] { correctName, "" })); } } + IReturnValueSymbol returnValue = method.ReturnValueSymbol; + if (returnValue?.DeclaringSyntaxReference is null || returnValue.ReturnType.GetNavTypeKindSafe() == NavTypeKind.DotNet) return; + + if (ctx.Node.GetLocation().SourceSpan.End == returnValue.DeclaringSyntaxReference.GetSyntax(ctx.CancellationToken).Span.End) + { + correctName = returnValue.ReturnType.Name; + + if (ctx.Node.GetLastToken().ToString().Trim('"').ToUpper() != correctName.ToUpper()) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, ctx.Node.GetLocation(), new object[] { ctx.Node.ToString().Trim('"'), correctName })); + + if (ctx.Node.GetLastToken().ToString().Trim('"') != correctName) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Node.GetLocation(), new object[] { correctName, "" })); + } } } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0004LookupPageIdAndDrillDownPageId.cs b/BusinessCentral.LinterCop/Design/Rule0004LookupPageIdAndDrillDownPageId.cs index e7f43312..97d5ff8e 100644 --- a/BusinessCentral.LinterCop/Design/Rule0004LookupPageIdAndDrillDownPageId.cs +++ b/BusinessCentral.LinterCop/Design/Rule0004LookupPageIdAndDrillDownPageId.cs @@ -1,5 +1,4 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; @@ -10,48 +9,43 @@ namespace BusinessCentral.LinterCop.Design; [DiagnosticAnalyzer] public class Rule0004LookupPageIdAndDrillDownPageId : DiagnosticAnalyzer { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0004LookupPageIdAndDrillDownPageId); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0004LookupPageIdAndDrillDownPageId); - public override void Initialize(AnalysisContext context) - => context.RegisterSymbolAction(new Action(this.CheckForLookupPageIdAndDrillDownPageId), SymbolKind.Page); + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(new Action(this.CheckForLookupPageIdAndDrillDownPageId), SymbolKind.Page); private void CheckForLookupPageIdAndDrillDownPageId(SymbolAnalysisContext context) { - if (context.IsObsoletePendingOrRemoved()) return; + if (context.IsObsoletePendingOrRemoved() || context.Symbol is not IPageTypeSymbol pageTypeSymbol) + return; - IPageTypeSymbol pageTypeSymbol = (IPageTypeSymbol)context.Symbol; - if (pageTypeSymbol.PageType != PageTypeKind.List || pageTypeSymbol.RelatedTable == null) return; - if (pageTypeSymbol.GetBooleanPropertyValue(PropertyKind.SourceTableTemporary).GetValueOrDefault()) return; - if (pageTypeSymbol.RelatedTable.ContainingModule != context.Symbol.ContainingModule) return; - CheckTable(pageTypeSymbol.RelatedTable, context); + if (pageTypeSymbol.PageType != PageTypeKind.List || + pageTypeSymbol.RelatedTable is null || + pageTypeSymbol.GetBooleanPropertyValue(PropertyKind.SourceTableTemporary).GetValueOrDefault() || + pageTypeSymbol.RelatedTable.ContainingModule != context.Symbol.ContainingModule) + return; + + AnalyzeRelatedTable(pageTypeSymbol.RelatedTable, context); } - private void CheckTable(ITableTypeSymbol table, SymbolAnalysisContext context) + private void AnalyzeRelatedTable(ITableTypeSymbol table, SymbolAnalysisContext context) { - if (table.IsObsoletePendingOrRemoved()) return; + if (table.TableType == TableTypeKind.Temporary || + !table.GetLocation().IsInSource || + table.IsObsoletePendingOrRemoved()) + return; - if (!table.GetLocation().IsInSource) return; - if (table.TableType == TableTypeKind.Temporary) return; + bool hasRequiredProperties = table.Properties.Count(property => + property.PropertyKind is PropertyKind.DrillDownPageId or PropertyKind.LookupPageId) == 2; - bool exists = table.Properties.Where(e => e.PropertyKind == PropertyKind.DrillDownPageId || e.PropertyKind == PropertyKind.LookupPageId).Count() == 2; - if (exists) return; + if (hasRequiredProperties) + return; - context.ReportDiagnostic( - Diagnostic.Create( + context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.Rule0004LookupPageIdAndDrillDownPageId, table.GetLocation(), - new object[] { table.Name.ToString().QuoteIdentifierIfNeeded(), context.Symbol.Name.ToString().QuoteIdentifierIfNeeded() })); - } - - public static class DiagnosticDescriptors - { - public static readonly DiagnosticDescriptor Rule0004LookupPageIdAndDrillDownPageId = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0004", - title: LinterCopAnalyzers.GetLocalizableString("Rule0004LookupPageIdAndDrillDownPageIdTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0004LookupPageIdAndDrillDownPageIdFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0004LookupPageIdAndDrillDownPageIdDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0004"); + table.Name.ToString().QuoteIdentifierIfNeeded(), + context.Symbol.Name.ToString().QuoteIdentifierIfNeeded())); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs b/BusinessCentral.LinterCop/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs index 92dfa761..803c88b6 100644 --- a/BusinessCentral.LinterCop/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs +++ b/BusinessCentral.LinterCop/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs @@ -1,4 +1,5 @@ -using BusinessCentral.LinterCop.AnalysisContextExtension; +#nullable disable // TODO: Enable nullable and review rule +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; @@ -6,39 +7,38 @@ using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0005VariableCasingShouldNotDifferFromDeclaration : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0005VariableCasingShouldNotDifferFromDeclaration : DiagnosticAnalyzer + public override ImmutableArray SupportedDiagnostics { get; } + = ImmutableArray.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration); + + private static readonly HashSet _dataTypeSyntaxKinds = Enum.GetValues(typeof(SyntaxKind)).Cast().Where(x => x.ToString().AsSpan().EndsWith("DataType")).ToHashSet(); + private static readonly string[] _areaKinds = Enum.GetValues(typeof(AreaKind)).Cast().Select(x => x.ToString()).ToArray(); + private static readonly string[] _actionAreaKinds = Enum.GetValues(typeof(ActionAreaKind)).Cast().Select(x => x.ToString()).ToArray(); + private static readonly string[] _labelPropertyString = LabelPropertyHelper.GetAllLabelProperties(); + private static readonly string[] _navTypeKindStrings = GenerateNavTypeKindArray(); + private static readonly string[] _propertyKindStrings = Enum.GetValues(typeof(PropertyKind)).Cast().Select(x => x.ToString()).ToArray(); + private static readonly string[] _symbolKinds = GenerateSymbolKindArray(); + private static readonly Dictionary _triggerTypeKinds = GenerateNTriggerTypeKindMappings(); + + public override void Initialize(AnalysisContext context) { - public override ImmutableArray SupportedDiagnostics { get; } - = ImmutableArray.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration); - - private static readonly HashSet _dataTypeSyntaxKinds = Enum.GetValues(typeof(SyntaxKind)).Cast().Where(x => x.ToString().AsSpan().EndsWith("DataType")).ToHashSet(); - private static readonly string[] _areaKinds = Enum.GetValues(typeof(AreaKind)).Cast().Select(x => x.ToString()).ToArray(); - private static readonly string[] _actionAreaKinds = Enum.GetValues(typeof(ActionAreaKind)).Cast().Select(x => x.ToString()).ToArray(); - private static readonly string[] _labelPropertyString = LabelPropertyHelper.GetAllLabelProperties(); - private static readonly string[] _navTypeKindStrings = GenerateNavTypeKindArray(); - private static readonly string[] _propertyKindStrings = Enum.GetValues(typeof(PropertyKind)).Cast().Select(x => x.ToString()).ToArray(); - private static readonly string[] _symbolKinds = GenerateSymbolKindArray(); - private static readonly Dictionary _triggerTypeKinds = GenerateNTriggerTypeKindMappings(); - - - public override void Initialize(AnalysisContext context) - { - context.RegisterSyntaxNodeAction(new Action(this.AnalyzeLabel), SyntaxKind.Label); - context.RegisterSyntaxNodeAction(new Action(this.AnalyzePropertyName), SyntaxKind.PropertyName); - context.RegisterSyntaxNodeAction(new Action(this.AnalyzeMemberAccessExpression), SyntaxKind.MemberAccessExpression); - context.RegisterSyntaxNodeAction(new Action(this.AnalyzeAreaSectionName), SyntaxKind.PageArea); - context.RegisterSyntaxNodeAction(new Action(this.AnalyzeActionAreaSectionName), SyntaxKind.PageActionArea); - context.RegisterSyntaxNodeAction(new Action(this.AnalyzeTriggerDeclaration), SyntaxKind.TriggerDeclaration); - context.RegisterSyntaxNodeAction(new Action(this.AnalyzeIdentifierName), SyntaxKind.IdentifierName); - context.RegisterSyntaxNodeAction(new Action(this.AnalyzeQualifiedName), SyntaxKind.QualifiedName); - context.RegisterSyntaxNodeAction(new Action(this.AnalyzeQualifiedNameWithoutNamespace), SyntaxKind.QualifiedName); - context.RegisterSyntaxNodeAction(new Action(this.AnalyzeLengthDataType), SyntaxKind.LengthDataType); - context.RegisterSyntaxNodeAction(new Action(this.AnalyzeOptionAccessExpression), SyntaxKind.OptionAccessExpression); - - context.RegisterOperationAction(new Action(this.CheckForBuiltInMethodsWithCasingMismatch), new OperationKind[] { + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeLabel), SyntaxKind.Label); + context.RegisterSyntaxNodeAction(new Action(this.AnalyzePropertyName), SyntaxKind.PropertyName); + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeMemberAccessExpression), SyntaxKind.MemberAccessExpression); + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeAreaSectionName), SyntaxKind.PageArea); + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeActionAreaSectionName), SyntaxKind.PageActionArea); + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeTriggerDeclaration), SyntaxKind.TriggerDeclaration); + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeIdentifierName), SyntaxKind.IdentifierName); + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeQualifiedName), SyntaxKind.QualifiedName); + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeQualifiedNameWithoutNamespace), SyntaxKind.QualifiedName); + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeLengthDataType), SyntaxKind.LengthDataType); + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeOptionAccessExpression), SyntaxKind.OptionAccessExpression); + + context.RegisterOperationAction(new Action(this.CheckForBuiltInMethodsWithCasingMismatch), new OperationKind[] { OperationKind.InvocationExpression, OperationKind.FieldAccess, OperationKind.GlobalReferenceExpression, @@ -48,7 +48,7 @@ public override void Initialize(AnalysisContext context) OperationKind.XmlPortDataItemAccess }); - context.RegisterSymbolAction(new Action(this.CheckForBuiltInTypeCasingMismatch), new SymbolKind[] { + context.RegisterSymbolAction(new Action(this.CheckForBuiltInTypeCasingMismatch), new SymbolKind[] { SymbolKind.Codeunit, SymbolKind.Entitlement, SymbolKind.Enum, @@ -69,419 +69,418 @@ public override void Initialize(AnalysisContext context) SymbolKind.TableExtension, SymbolKind.XmlPort }); - } + } - private static string[] GenerateNavTypeKindArray() - { - var navTypeKinds = Enum.GetValues(typeof(NavTypeKind)) - .Cast() - .Select(s => s.ToString()) - .ToList(); + private static string[] GenerateNavTypeKindArray() + { + var navTypeKinds = Enum.GetValues(typeof(NavTypeKind)) + .Cast() + .Select(s => s.ToString()) + .ToList(); + + navTypeKinds.Add("Database"); // for Database::"G/L Entry" (there is no NavTypeKind for this) + return navTypeKinds.ToArray(); + } - navTypeKinds.Add("Database"); // for Database::"G/L Entry" (there is no NavTypeKind for this) - return navTypeKinds.ToArray(); + private static string[] GenerateSymbolKindArray() + { + var symbolKinds = Enum.GetValues(typeof(SymbolKind)) + .Cast() + .Select(x => x.ToString()) + .ToList(); + + // Find the index of "XmlPort" and update it to "Xmlport" + int index = symbolKinds.FindIndex(s => s == "XmlPort"); + if (index != -1) + { + symbolKinds[index] = "Xmlport"; } - private static string[] GenerateSymbolKindArray() + symbolKinds.Add("Database"); // for Database::"G/L Entry" (there is no SymbolKind for this) + return symbolKinds.ToArray(); + } + + private static Dictionary GenerateNTriggerTypeKindMappings() + { + var mappings = new Dictionary(); + + foreach (TriggerTypeKind type in Enum.GetValues(typeof(TriggerTypeKind))) { - var symbolKinds = Enum.GetValues(typeof(SymbolKind)) - .Cast() - .Select(x => x.ToString()) - .ToList(); - - // Find the index of "XmlPort" and update it to "Xmlport" - int index = symbolKinds.FindIndex(s => s == "XmlPort"); - if (index != -1) + string typeName = type.ToString(); + int index = typeName.IndexOf("On"); + if (index > 0) { - symbolKinds[index] = "Xmlport"; + mappings[type] = typeName.Substring(index); ; } - - symbolKinds.Add("Database"); // for Database::"G/L Entry" (there is no SymbolKind for this) - return symbolKinds.ToArray(); } - private static Dictionary GenerateNTriggerTypeKindMappings() + return mappings; + } + + private void AnalyzeLabel(SyntaxNodeAnalysisContext ctx) + { + IEnumerable nodes = ctx.Node.DescendantNodes() + .Where(n => n.Kind == SyntaxKind.IdentifierEqualsLiteral); + + foreach (SyntaxNode node in nodes) { - var mappings = new Dictionary(); + ctx.CancellationToken.ThrowIfCancellationRequested(); - foreach (TriggerTypeKind type in Enum.GetValues(typeof(TriggerTypeKind))) - { - string typeName = type.ToString(); - int index = typeName.IndexOf("On"); - if (index > 0) - { - mappings[type] = typeName.Substring(index); ; - } - } + SyntaxToken syntaxToken = ((IdentifierEqualsLiteralSyntax)node).Identifier; + int result = Array.FindIndex(_labelPropertyString, t => t.Equals(syntaxToken.ValueText, StringComparison.OrdinalIgnoreCase)); + if (result == -1) + continue; - return mappings; + if (!syntaxToken.ValueText.AsSpan().Equals(_labelPropertyString[result].ToString().AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, syntaxToken.GetLocation(), new object[] { _labelPropertyString[result].ToString(), "" })); } + } - private void AnalyzeLabel(SyntaxNodeAnalysisContext ctx) - { - IEnumerable nodes = ctx.Node.DescendantNodes() - .Where(n => n.Kind == SyntaxKind.IdentifierEqualsLiteral); + private void AnalyzePropertyName(SyntaxNodeAnalysisContext ctx) + { + SyntaxToken syntaxToken = ((PropertyNameSyntax)ctx.Node).Identifier; + int result = Array.FindIndex(_propertyKindStrings, t => t.Equals(syntaxToken.ValueText, StringComparison.OrdinalIgnoreCase)); + if (result == -1) + return; - foreach (SyntaxNode node in nodes) - { - ctx.CancellationToken.ThrowIfCancellationRequested(); + if (!syntaxToken.ValueText.AsSpan().Equals(_propertyKindStrings[result].ToString().AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, syntaxToken.GetLocation(), new object[] { _propertyKindStrings[result].ToString(), "" })); + } - SyntaxToken syntaxToken = ((IdentifierEqualsLiteralSyntax)node).Identifier; - int result = Array.FindIndex(_labelPropertyString, t => t.Equals(syntaxToken.ValueText, StringComparison.OrdinalIgnoreCase)); - if (result == -1) - continue; + private void AnalyzeMemberAccessExpression(SyntaxNodeAnalysisContext ctx) + { + SyntaxNode childNode = ctx.Node.ChildNodes().Where(n => n.Kind == SyntaxKind.IdentifierName).FirstOrDefault(); + if (childNode is null) return; - if (!syntaxToken.ValueText.AsSpan().Equals(_labelPropertyString[result].ToString().AsSpan(), StringComparison.Ordinal)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, syntaxToken.GetLocation(), new object[] { _labelPropertyString[result].ToString(), "" })); - } - } + SyntaxToken syntaxToken = ((IdentifierNameSyntax)childNode).Identifier; + int result = Array.FindIndex(_symbolKinds, t => t.Equals(syntaxToken.ValueText, StringComparison.OrdinalIgnoreCase)); + if (result == -1) + return; - private void AnalyzePropertyName(SyntaxNodeAnalysisContext ctx) - { - SyntaxToken syntaxToken = ((PropertyNameSyntax)ctx.Node).Identifier; - int result = Array.FindIndex(_propertyKindStrings, t => t.Equals(syntaxToken.ValueText, StringComparison.OrdinalIgnoreCase)); - if (result == -1) - return; + if (!syntaxToken.ValueText.AsSpan().Equals(_symbolKinds[result].ToString().AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, syntaxToken.GetLocation(), new object[] { _symbolKinds[result].ToString(), "" })); + } - if (!syntaxToken.ValueText.AsSpan().Equals(_propertyKindStrings[result].ToString().AsSpan(), StringComparison.Ordinal)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, syntaxToken.GetLocation(), new object[] { _propertyKindStrings[result].ToString(), "" })); - } + private void AnalyzeAreaSectionName(SyntaxNodeAnalysisContext ctx) + { + IEnumerable childNodes = ctx.Node.ChildNodes().Where(n => n.Kind == SyntaxKind.IdentifierName); - private void AnalyzeMemberAccessExpression(SyntaxNodeAnalysisContext ctx) + foreach (SyntaxNode childNode in childNodes) { - SyntaxNode childNode = ctx.Node.ChildNodes().Where(n => n.Kind == SyntaxKind.IdentifierName).FirstOrDefault(); - if (childNode == null) return; + ctx.CancellationToken.ThrowIfCancellationRequested(); SyntaxToken syntaxToken = ((IdentifierNameSyntax)childNode).Identifier; - int result = Array.FindIndex(_symbolKinds, t => t.Equals(syntaxToken.ValueText, StringComparison.OrdinalIgnoreCase)); + int result = Array.FindIndex(_areaKinds, t => t.Equals(syntaxToken.ValueText, StringComparison.OrdinalIgnoreCase)); if (result == -1) - return; + continue; - if (!syntaxToken.ValueText.AsSpan().Equals(_symbolKinds[result].ToString().AsSpan(), StringComparison.Ordinal)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, syntaxToken.GetLocation(), new object[] { _symbolKinds[result].ToString(), "" })); + if (!syntaxToken.ValueText.AsSpan().Equals(_areaKinds[result].ToString().AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, childNode.GetLocation(), new object[] { _areaKinds[result].ToString(), "" })); } + } - private void AnalyzeAreaSectionName(SyntaxNodeAnalysisContext ctx) - { - IEnumerable childNodes = ctx.Node.ChildNodes().Where(n => n.Kind == SyntaxKind.IdentifierName); + private void AnalyzeActionAreaSectionName(SyntaxNodeAnalysisContext ctx) + { + IEnumerable childNodes = ctx.Node.ChildNodes().Where(n => n.Kind == SyntaxKind.IdentifierName); - foreach (SyntaxNode childNode in childNodes) - { - ctx.CancellationToken.ThrowIfCancellationRequested(); + foreach (SyntaxNode childNode in childNodes) + { + ctx.CancellationToken.ThrowIfCancellationRequested(); - SyntaxToken syntaxToken = ((IdentifierNameSyntax)childNode).Identifier; - int result = Array.FindIndex(_areaKinds, t => t.Equals(syntaxToken.ValueText, StringComparison.OrdinalIgnoreCase)); - if (result == -1) - continue; + SyntaxToken syntaxToken = ((IdentifierNameSyntax)childNode).Identifier; + int result = Array.FindIndex(_actionAreaKinds, t => t.Equals(syntaxToken.ValueText, StringComparison.OrdinalIgnoreCase)); + if (result == -1) + continue; - if (!syntaxToken.ValueText.AsSpan().Equals(_areaKinds[result].ToString().AsSpan(), StringComparison.Ordinal)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, childNode.GetLocation(), new object[] { _areaKinds[result].ToString(), "" })); - } + if (!syntaxToken.ValueText.AsSpan().Equals(_actionAreaKinds[result].ToString().AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, childNode.GetLocation(), new object[] { _actionAreaKinds[result].ToString(), "" })); } + } - private void AnalyzeActionAreaSectionName(SyntaxNodeAnalysisContext ctx) - { - IEnumerable childNodes = ctx.Node.ChildNodes().Where(n => n.Kind == SyntaxKind.IdentifierName); + private void AnalyzeTriggerDeclaration(SyntaxNodeAnalysisContext ctx) + { + if (ctx.Node is not TriggerDeclarationSyntax syntax) + return; - foreach (SyntaxNode childNode in childNodes) - { - ctx.CancellationToken.ThrowIfCancellationRequested(); + if (ctx.ContainingSymbol.ContainingSymbol is not ISymbolWithTriggers symbolWithTriggers) + return; - SyntaxToken syntaxToken = ((IdentifierNameSyntax)childNode).Identifier; - int result = Array.FindIndex(_actionAreaKinds, t => t.Equals(syntaxToken.ValueText, StringComparison.OrdinalIgnoreCase)); - if (result == -1) - continue; + if (symbolWithTriggers.GetTriggerTypeInfo(syntax.Name.Identifier.ValueText) is not TriggerTypeInfo triggerTypeInfo) + return; - if (!syntaxToken.ValueText.AsSpan().Equals(_actionAreaKinds[result].ToString().AsSpan(), StringComparison.Ordinal)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, childNode.GetLocation(), new object[] { _actionAreaKinds[result].ToString(), "" })); - } - } + if (!_triggerTypeKinds.TryGetValue(triggerTypeInfo.Kind, out string targetName)) + return; - private void AnalyzeTriggerDeclaration(SyntaxNodeAnalysisContext ctx) - { - if (ctx.Node is not TriggerDeclarationSyntax syntax) - return; + if (!syntax.Name.Identifier.ValueText.AsSpan().Equals(targetName.AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, syntax.Name.GetLocation(), new object[] { targetName, "" })); + } - if (ctx.ContainingSymbol.ContainingSymbol is not ISymbolWithTriggers symbolWithTriggers) - return; + private void AnalyzeIdentifierName(SyntaxNodeAnalysisContext ctx) + { + if (ctx.Node is not IdentifierNameSyntax node) + return; - if (symbolWithTriggers.GetTriggerTypeInfo(syntax.Name.Identifier.ValueText) is not TriggerTypeInfo triggerTypeInfo) - return; + if (node.Parent.Kind == SyntaxKind.PragmaWarningDirectiveTrivia) + return; - if (!_triggerTypeKinds.TryGetValue(triggerTypeInfo.Kind, out string targetName)) - return; + if (ctx.SemanticModel.GetSymbolInfo(ctx.Node, ctx.CancellationToken).Symbol is not ISymbol fieldSymbol) + return; - if (!syntax.Name.Identifier.ValueText.AsSpan().Equals(targetName.AsSpan(), StringComparison.Ordinal)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, syntax.Name.GetLocation(), new object[] { targetName, "" })); - } + // TODO: Support more SymbolKinds + if (fieldSymbol.Kind != SymbolKind.Field) + return; - private void AnalyzeIdentifierName(SyntaxNodeAnalysisContext ctx) - { - if (ctx.Node is not IdentifierNameSyntax node) - return; + string identifierName = StringExtensions.UnquoteIdentifier(node.Identifier.ValueText); - if (node.Parent.Kind == SyntaxKind.PragmaWarningDirectiveTrivia) - return; + if (!identifierName.AsSpan().Equals(fieldSymbol.Name.AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, node.GetLocation(), new object[] { fieldSymbol.Name.QuoteIdentifierIfNeeded(), "" })); + } - if (ctx.SemanticModel.GetSymbolInfo(ctx.Node, ctx.CancellationToken).Symbol is not ISymbol fieldSymbol) - return; + private void AnalyzeQualifiedName(SyntaxNodeAnalysisContext ctx) + { + if (ctx.Node is not QualifiedNameSyntax node) + return; - // TODO: Support more SymbolKinds - if (fieldSymbol.Kind != SymbolKind.Field) - return; + if (ctx.SemanticModel.GetSymbolInfo(ctx.Node, ctx.CancellationToken).Symbol is not ISymbol fieldSymbol) + return; - string identifierName = StringExtensions.UnquoteIdentifier(node.Identifier.ValueText); + string identifierName = StringExtensions.UnquoteIdentifier(node.Right.Identifier.ValueText); - if (!identifierName.AsSpan().Equals(fieldSymbol.Name.AsSpan(), StringComparison.Ordinal)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, node.GetLocation(), new object[] { fieldSymbol.Name.QuoteIdentifierIfNeeded(), "" })); - } + if (!identifierName.AsSpan().Equals(fieldSymbol.Name.AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, node.Right.GetLocation(), new object[] { fieldSymbol.Name.QuoteIdentifierIfNeeded(), "" })); + } - private void AnalyzeQualifiedName(SyntaxNodeAnalysisContext ctx) - { - if (ctx.Node is not QualifiedNameSyntax node) - return; + private void AnalyzeQualifiedNameWithoutNamespace(SyntaxNodeAnalysisContext ctx) + { + if (ctx.Node is not QualifiedNameSyntax node) + return; - if (ctx.SemanticModel.GetSymbolInfo(ctx.Node, ctx.CancellationToken).Symbol is not ISymbol fieldSymbol) - return; + if (node.Left.Kind != SyntaxKind.IdentifierName) + return; - string identifierName = StringExtensions.UnquoteIdentifier(node.Right.Identifier.ValueText); + if (ctx.SemanticModel.GetSymbolInfo(ctx.Node, ctx.CancellationToken).Symbol is not ISymbol fieldSymbol) + return; - if (!identifierName.AsSpan().Equals(fieldSymbol.Name.AsSpan(), StringComparison.Ordinal)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, node.Right.GetLocation(), new object[] { fieldSymbol.Name.QuoteIdentifierIfNeeded(), "" })); - } + if (fieldSymbol.ContainingSymbol is not IObjectTypeSymbol objectTypeSymbol) + return; - private void AnalyzeQualifiedNameWithoutNamespace(SyntaxNodeAnalysisContext ctx) + if (fieldSymbol.ContainingSymbol.Kind == SymbolKind.TableExtension) { - if (ctx.Node is not QualifiedNameSyntax node) + ITableExtensionTypeSymbol tableExtension = (ITableExtensionTypeSymbol)fieldSymbol.ContainingSymbol; + if (tableExtension.Target is not IObjectTypeSymbol tableExtensionTypeSymbol) + { return; + } + objectTypeSymbol = tableExtensionTypeSymbol; + } - if (node.Left.Kind != SyntaxKind.IdentifierName) - return; + IdentifierNameSyntax identifierNameSyntax = (IdentifierNameSyntax)node.Left; + SyntaxToken identifier = identifierNameSyntax.Identifier; + if (identifier == null) + return; - if (ctx.SemanticModel.GetSymbolInfo(ctx.Node, ctx.CancellationToken).Symbol is not ISymbol fieldSymbol) - return; + string identifierName = StringExtensions.UnquoteIdentifier(identifier.ValueText); - if (fieldSymbol.ContainingSymbol is not IObjectTypeSymbol objectTypeSymbol) - return; + if (!identifierName.AsSpan().Equals(objectTypeSymbol.Name.AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, identifierNameSyntax.GetLocation(), new object[] { objectTypeSymbol.Name.QuoteIdentifierIfNeeded(), "" })); + } - if (fieldSymbol.ContainingSymbol.Kind == SymbolKind.TableExtension) - { - ITableExtensionTypeSymbol tableExtension = (ITableExtensionTypeSymbol)fieldSymbol.ContainingSymbol; - if (tableExtension.Target is not IObjectTypeSymbol tableExtensionTypeSymbol) - { - return; - } - objectTypeSymbol = tableExtensionTypeSymbol; - } + private void AnalyzeLengthDataType(SyntaxNodeAnalysisContext ctx) + { + if (ctx.Node is not LengthDataTypeSyntax node) + return; - IdentifierNameSyntax identifierNameSyntax = (IdentifierNameSyntax)node.Left; - SyntaxToken identifier = identifierNameSyntax.Identifier; - if (identifier == null) - return; + SyntaxToken identifierToken = node.GetFirstToken(); + if (!IsNavTypeKindWithDifferentCasing(identifierToken.ValueText, out string targetName)) + return; - string identifierName = StringExtensions.UnquoteIdentifier(identifier.ValueText); + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, identifierToken.GetLocation(), new object[] { targetName, "" })); + } - if (!identifierName.AsSpan().Equals(objectTypeSymbol.Name.AsSpan(), StringComparison.Ordinal)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, identifierNameSyntax.GetLocation(), new object[] { objectTypeSymbol.Name.QuoteIdentifierIfNeeded(), "" })); - } + private void AnalyzeOptionAccessExpression(SyntaxNodeAnalysisContext ctx) + { + if (ctx.Node is not OptionAccessExpressionSyntax node) + return; - private void AnalyzeLengthDataType(SyntaxNodeAnalysisContext ctx) + switch (node.Expression) { - if (ctx.Node is not LengthDataTypeSyntax node) - return; + case IdentifierNameSyntax identifierNameSyntax: + int result = Array.FindIndex(_symbolKinds, t => t.Equals(identifierNameSyntax.Identifier.ValueText, StringComparison.OrdinalIgnoreCase)); + if (result == -1) + return; - SyntaxToken identifierToken = node.GetFirstToken(); - if (!IsNavTypeKindWithDifferentCasing(identifierToken.ValueText, out string targetName)) - return; + if (!identifierNameSyntax.Identifier.ValueText.AsSpan().Equals(_symbolKinds[result].ToString().AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, identifierNameSyntax.Identifier.GetLocation(), new object[] { _symbolKinds[result].ToString(), "" })); - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, identifierToken.GetLocation(), new object[] { targetName, "" })); + break; } + } - private void AnalyzeOptionAccessExpression(SyntaxNodeAnalysisContext ctx) - { - if (ctx.Node is not OptionAccessExpressionSyntax node) - return; + private void CheckForBuiltInTypeCasingMismatch(SymbolAnalysisContext ctx) + { + AnalyseTokens(ctx); + AnalyseNodes(ctx); + } - switch (node.Expression) - { - case IdentifierNameSyntax identifierNameSyntax: - int result = Array.FindIndex(_symbolKinds, t => t.Equals(identifierNameSyntax.Identifier.ValueText, StringComparison.OrdinalIgnoreCase)); - if (result == -1) - return; + private void AnalyseTokens(SymbolAnalysisContext ctx) + { + IEnumerable descendantTokens = ctx.Symbol.DeclaringSyntaxReference?.GetSyntax().DescendantTokens() + .Where(t => t.Kind.IsKeyword()) + .Where(t => !_dataTypeSyntaxKinds.Contains(t.Parent.Kind)) + .Where(t => !string.IsNullOrEmpty(t.ToString())); - if (!identifierNameSyntax.Identifier.ValueText.AsSpan().Equals(_symbolKinds[result].ToString().AsSpan(), StringComparison.Ordinal)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, identifierNameSyntax.Identifier.GetLocation(), new object[] { _symbolKinds[result].ToString(), "" })); + foreach (SyntaxToken descendantToken in descendantTokens ?? Enumerable.Empty()) + { + ctx.CancellationToken.ThrowIfCancellationRequested(); - break; - } - } + SyntaxToken syntaxToken = SyntaxFactory.Token(descendantToken.Kind); + if (syntaxToken.Kind == SyntaxKind.None) + continue; - private void CheckForBuiltInTypeCasingMismatch(SymbolAnalysisContext ctx) - { - AnalyseTokens(ctx); - AnalyseNodes(ctx); + if (!syntaxToken.ToString().AsSpan().Equals(descendantToken.ToString().AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, descendantToken.GetLocation(), new object[] { syntaxToken, "" })); } + } + + private void AnalyseNodes(SymbolAnalysisContext ctx) + { + IEnumerable descendantNodes = ctx.Symbol.DeclaringSyntaxReference?.GetSyntax().DescendantNodes() + .Where(t => t.Kind != SyntaxKind.LengthDataType) // handeld on AnalyzeLengthDataType method + .Where(n => !n.ToString().AsSpan().StartsWith("array")); - private void AnalyseTokens(SymbolAnalysisContext ctx) + foreach (SyntaxNode descendantNode in descendantNodes ?? Enumerable.Empty()) { - IEnumerable? descendantTokens = ctx.Symbol.DeclaringSyntaxReference?.GetSyntax().DescendantTokens() - .Where(t => t.Kind.IsKeyword()) - .Where(t => !_dataTypeSyntaxKinds.Contains(t.Parent.Kind)) - .Where(t => !string.IsNullOrEmpty(t.ToString())); + ctx.CancellationToken.ThrowIfCancellationRequested(); - foreach (SyntaxToken descendantToken in descendantTokens ?? Enumerable.Empty()) + var syntaxNodeKindSpan = descendantNode.Kind.ToString().AsSpan(); + var syntaxNodeSpan = descendantNode.ToString(); + + if ((descendantNode.IsKind(SyntaxKind.SimpleTypeReference) || + syntaxNodeKindSpan.Contains("DataType", StringComparison.Ordinal)) && + !syntaxNodeKindSpan.StartsWith("Codeunit") && + !syntaxNodeKindSpan.StartsWith("Enum") && + !syntaxNodeKindSpan.StartsWith("Label")) { - ctx.CancellationToken.ThrowIfCancellationRequested(); + if (descendantNode is SimpleTypeReferenceSyntax simpleTypeRefSubstituteToken && simpleTypeRefSubstituteToken.DataType.Kind == SyntaxKind.LengthDataType) + continue; // handeld on AnalyzeLengthDataType method - SyntaxToken syntaxToken = SyntaxFactory.Token(descendantToken.Kind); - if (syntaxToken.Kind == SyntaxKind.None) - continue; + var targetName = _navTypeKindStrings.FirstOrDefault(Kind => + { + var kindSpan = Kind.AsSpan(); + return kindSpan.Equals(syntaxNodeSpan.AsSpan(), StringComparison.OrdinalIgnoreCase) && + !kindSpan.Equals(syntaxNodeSpan.AsSpan(), StringComparison.Ordinal); + }); - if (!syntaxToken.ToString().AsSpan().Equals(descendantToken.ToString().AsSpan(), StringComparison.Ordinal)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, descendantToken.GetLocation(), new object[] { syntaxToken, "" })); + if (targetName is not null) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, + descendantNode.GetLocation(), new object[] { targetName, "" })); + continue; + } } - } - - private void AnalyseNodes(SymbolAnalysisContext ctx) - { - IEnumerable? descendantNodes = ctx.Symbol.DeclaringSyntaxReference?.GetSyntax().DescendantNodes() - .Where(t => t.Kind != SyntaxKind.LengthDataType) // handeld on AnalyzeLengthDataType method - .Where(n => !n.ToString().AsSpan().StartsWith("array")); - foreach (SyntaxNode descendantNode in descendantNodes ?? Enumerable.Empty()) + if (IsValidKind(descendantNode.Kind)) { - ctx.CancellationToken.ThrowIfCancellationRequested(); - - var syntaxNodeKindSpan = descendantNode.Kind.ToString().AsSpan(); - var syntaxNodeSpan = descendantNode.ToString(); - - if ((descendantNode.IsKind(SyntaxKind.SimpleTypeReference) || - syntaxNodeKindSpan.Contains("DataType", StringComparison.Ordinal)) && - !syntaxNodeKindSpan.StartsWith("Codeunit") && - !syntaxNodeKindSpan.StartsWith("Enum") && + if (syntaxNodeKindSpan.StartsWith("Codeunit") || + !syntaxNodeKindSpan.StartsWith("Enum") || !syntaxNodeKindSpan.StartsWith("Label")) { - if (descendantNode is SimpleTypeReferenceSyntax simpleTypeRefSubstituteToken && simpleTypeRefSubstituteToken.DataType.Kind == SyntaxKind.LengthDataType) - continue; // handeld on AnalyzeLengthDataType method - var targetName = _navTypeKindStrings.FirstOrDefault(Kind => { var kindSpan = Kind.AsSpan(); - return kindSpan.Equals(syntaxNodeSpan.AsSpan(), StringComparison.OrdinalIgnoreCase) && - !kindSpan.Equals(syntaxNodeSpan.AsSpan(), StringComparison.Ordinal); + var readOnlySpan = syntaxNodeSpan.AsSpan(); + return readOnlySpan.StartsWith(kindSpan, StringComparison.OrdinalIgnoreCase) && + !readOnlySpan.StartsWith(kindSpan, StringComparison.Ordinal); }); - - if (targetName != null) + if (targetName is not null) { + var firstToken = descendantNode.GetFirstToken(); ctx.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, - descendantNode.GetLocation(), new object[] { targetName, "" })); - continue; - } - } - - if (IsValidKind(descendantNode.Kind)) - { - if (syntaxNodeKindSpan.StartsWith("Codeunit") || - !syntaxNodeKindSpan.StartsWith("Enum") || - !syntaxNodeKindSpan.StartsWith("Label")) - { - var targetName = _navTypeKindStrings.FirstOrDefault(Kind => - { - var kindSpan = Kind.AsSpan(); - var readOnlySpan = syntaxNodeSpan.AsSpan(); - return readOnlySpan.StartsWith(kindSpan, StringComparison.OrdinalIgnoreCase) && - !readOnlySpan.StartsWith(kindSpan, StringComparison.Ordinal); - }); - if (targetName != null) - { - var firstToken = descendantNode.GetFirstToken(); - ctx.ReportDiagnostic(Diagnostic.Create( - DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, - firstToken.GetLocation(), new object[] { targetName, "" })); - } + firstToken.GetLocation(), new object[] { targetName, "" })); } } } } + } - private static bool IsValidKind(SyntaxKind kind) + private static bool IsValidKind(SyntaxKind kind) + { + switch (kind) { - switch (kind) - { - case SyntaxKind.SubtypedDataType: - case SyntaxKind.GenericDataType: - case SyntaxKind.SimpleTypeReference: - return true; - } - - return false; + case SyntaxKind.SubtypedDataType: + case SyntaxKind.GenericDataType: + case SyntaxKind.SimpleTypeReference: + return true; } - private void CheckForBuiltInMethodsWithCasingMismatch(OperationAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; + return false; + } - var targetName = ""; + private void CheckForBuiltInMethodsWithCasingMismatch(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) return; - switch (ctx.Operation.Kind) - { - case OperationKind.InvocationExpression: - if (ctx.Operation is IInvocationExpression invocationExpression) - targetName = invocationExpression.TargetMethod.Name; - break; - case OperationKind.FieldAccess: - if (ctx.Operation is IFieldAccess fieldAccess) - targetName = fieldAccess.FieldSymbol.Name; - break; - case OperationKind.GlobalReferenceExpression: - targetName = ((IGlobalReferenceExpression)ctx.Operation).GlobalVariable.Name; - break; - case OperationKind.LocalReferenceExpression: - targetName = ((ILocalReferenceExpression)ctx.Operation).LocalVariable.Name; - break; - case OperationKind.ParameterReferenceExpression: - targetName = ((IParameterReferenceExpression)ctx.Operation).Parameter.Name; - break; - case OperationKind.ReturnValueReferenceExpression: - targetName = ((IReturnValueReferenceExpression)ctx.Operation).ReturnValue.Name; - break; - case OperationKind.XmlPortDataItemAccess: - targetName = ((IXmlPortNodeAccess)ctx.Operation).XmlPortNodeSymbol.Name; - break; - default: - return; - } + var targetName = ""; - if (OnlyDiffersInCasing(ctx.Operation.Syntax.ToString().AsSpan(), targetName.AsSpan())) - { - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Operation.Syntax.GetLocation(), new object[] { targetName, "" })); + switch (ctx.Operation.Kind) + { + case OperationKind.InvocationExpression: + if (ctx.Operation is IInvocationExpression invocationExpression) + targetName = invocationExpression.TargetMethod.Name; + break; + case OperationKind.FieldAccess: + if (ctx.Operation is IFieldAccess fieldAccess) + targetName = fieldAccess.FieldSymbol.Name; + break; + case OperationKind.GlobalReferenceExpression: + targetName = ((IGlobalReferenceExpression)ctx.Operation).GlobalVariable.Name; + break; + case OperationKind.LocalReferenceExpression: + targetName = ((ILocalReferenceExpression)ctx.Operation).LocalVariable.Name; + break; + case OperationKind.ParameterReferenceExpression: + targetName = ((IParameterReferenceExpression)ctx.Operation).Parameter.Name; + break; + case OperationKind.ReturnValueReferenceExpression: + targetName = ((IReturnValueReferenceExpression)ctx.Operation).ReturnValue.Name; + break; + case OperationKind.XmlPortDataItemAccess: + targetName = ((IXmlPortNodeAccess)ctx.Operation).XmlPortNodeSymbol.Name; + break; + default: return; - } - - var nodes = Array.Find(ctx.Operation.Syntax.DescendantNodes((SyntaxNode e) => true).ToArray(), element => OnlyDiffersInCasing(element.ToString().AsSpan(), targetName.AsSpan())); - if (nodes != null) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Operation.Syntax.GetLocation(), new object[] { targetName, "" })); } - private bool OnlyDiffersInCasing(ReadOnlySpan left, ReadOnlySpan right) + if (OnlyDiffersInCasing(ctx.Operation.Syntax.ToString().AsSpan(), targetName.AsSpan())) { - var leftSpan = left.Trim('"'); - var rightSpan = right.Trim('"'); - return leftSpan.Equals(rightSpan, StringComparison.OrdinalIgnoreCase) && - !leftSpan.Equals(rightSpan, StringComparison.Ordinal); + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Operation.Syntax.GetLocation(), new object[] { targetName, "" })); + return; } - private static bool IsNavTypeKindWithDifferentCasing(string inputNavTypeKind, out string matchedNavTypeKind) - { - matchedNavTypeKind = _navTypeKindStrings.SingleOrDefault(Kind => - { - var kindSpan = Kind.AsSpan(); - return kindSpan.Equals(inputNavTypeKind.AsSpan(), StringComparison.OrdinalIgnoreCase) && - !kindSpan.Equals(inputNavTypeKind.AsSpan(), StringComparison.Ordinal); - }); + var nodes = Array.Find(ctx.Operation.Syntax.DescendantNodes((SyntaxNode e) => true).ToArray(), element => OnlyDiffersInCasing(element.ToString().AsSpan(), targetName.AsSpan())); + if (nodes is not null) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Operation.Syntax.GetLocation(), new object[] { targetName, "" })); + } - return matchedNavTypeKind is not null; - } + private bool OnlyDiffersInCasing(ReadOnlySpan left, ReadOnlySpan right) + { + var leftSpan = left.Trim('"'); + var rightSpan = right.Trim('"'); + return leftSpan.Equals(rightSpan, StringComparison.OrdinalIgnoreCase) && + !leftSpan.Equals(rightSpan, StringComparison.Ordinal); + } + + private static bool IsNavTypeKindWithDifferentCasing(string inputNavTypeKind, out string matchedNavTypeKind) + { + matchedNavTypeKind = _navTypeKindStrings.SingleOrDefault(Kind => + { + var kindSpan = Kind.AsSpan(); + return kindSpan.Equals(inputNavTypeKind.AsSpan(), StringComparison.OrdinalIgnoreCase) && + !kindSpan.Equals(inputNavTypeKind.AsSpan(), StringComparison.Ordinal); + }); + + return matchedNavTypeKind is not null; } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0006FieldNotAutoIncrementInTemporaryTable.cs b/BusinessCentral.LinterCop/Design/Rule0006FieldNotAutoIncrementInTemporaryTable.cs index 00feeeae..3f8de39f 100644 --- a/BusinessCentral.LinterCop/Design/Rule0006FieldNotAutoIncrementInTemporaryTable.cs +++ b/BusinessCentral.LinterCop/Design/Rule0006FieldNotAutoIncrementInTemporaryTable.cs @@ -1,65 +1,37 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0006FieldNotAutoIncrementInTemporaryTable : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0006FieldNotAutoIncrementInTemporaryTable : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0006FieldNotAutoIncrementInTemporaryTable); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0006FieldNotAutoIncrementInTemporaryTable); - public override void Initialize(AnalysisContext context) - => context.RegisterSymbolAction(new Action(this.CheckTablePrimaryKeyIsNotAutoIncrement), SymbolKind.Table); + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(new Action(this.AnalyzeTemporaryTables), SymbolKind.Table); - private void CheckTablePrimaryKeyIsNotAutoIncrement(SymbolAnalysisContext context) - { - if (context.IsObsoletePendingOrRemoved()) return; - ITableTypeSymbol tableTypeSymbol = (ITableTypeSymbol)context.Symbol; - if (!IsSymbolAccessible(tableTypeSymbol)) - return; + private void AnalyzeTemporaryTables(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Symbol is not ITableTypeSymbol table) + return; - CheckTable(tableTypeSymbol, ref context); - } + if (table.TableType != TableTypeKind.Temporary) + return; - private void CheckTable(ITableTypeSymbol table, ref SymbolAnalysisContext context) + foreach (var field in table.Fields) { - if (table.TableType != TableTypeKind.Temporary) - return; + ctx.CancellationToken.ThrowIfCancellationRequested(); - foreach (var field in table.Fields) + if (field.GetBooleanPropertyValue(PropertyKind.AutoIncrement).GetValueOrDefault()) { - IPropertySymbol propertySymbol = field.GetProperty(PropertyKind.AutoIncrement); - if (propertySymbol == null) - continue; - - if (propertySymbol?.ValueText != "0") - { - context.ReportDiagnostic( - Diagnostic.Create( - DiagnosticDescriptors.Rule0006FieldNotAutoIncrementInTemporaryTable, - propertySymbol.GetLocation())); - } - } - } - - private static string GetDeclaration(ISymbol symbol) - => symbol.Location.SourceTree.GetText(CancellationToken.None).GetSubText(symbol.DeclaringSyntaxReference.Span).ToString(); - - private static bool IsSymbolAccessible(ISymbol symbol) - { - try - { - GetDeclaration(symbol); - return true; - } - catch (Exception) - { - return false; + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0006FieldNotAutoIncrementInTemporaryTable, + field.GetProperty(PropertyKind.AutoIncrement)!.GetLocation())); } } } - -} +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0007DataPerCompanyShouldAlwaysBeSet.cs b/BusinessCentral.LinterCop/Design/Rule0007DataPerCompanyShouldAlwaysBeSet.cs index 7df8f55d..ea4ed874 100644 --- a/BusinessCentral.LinterCop/Design/Rule0007DataPerCompanyShouldAlwaysBeSet.cs +++ b/BusinessCentral.LinterCop/Design/Rule0007DataPerCompanyShouldAlwaysBeSet.cs @@ -1,49 +1,32 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design -{ - [DiagnosticAnalyzer] - public class Rule0007DataPerCompanyShouldAlwaysBeSet : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0007DataPerCompanyShouldAlwaysBeSet); +namespace BusinessCentral.LinterCop.Design; - public override void Initialize(AnalysisContext context) - => context.RegisterSymbolAction(new Action(this.CheckForMissingDataPerCompanyOnTables), SymbolKind.Table); +[DiagnosticAnalyzer] +public class Rule0007DataPerCompanyShouldAlwaysBeSet : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0007DataPerCompanyShouldAlwaysBeSet); - private void CheckForMissingDataPerCompanyOnTables(SymbolAnalysisContext context) - { - if (context.IsObsoletePendingOrRemoved()) return; - ITableTypeSymbol table = (ITableTypeSymbol)context.Symbol; - if (table.TableType == TableTypeKind.Temporary) - return; + public override void Initialize(AnalysisContext context) + => context.RegisterSymbolAction(new Action(this.CheckForMissingDataPerCompanyOnTables), SymbolKind.Table); - if (!IsSymbolAccessible(table)) - return; + private void CheckForMissingDataPerCompanyOnTables(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Symbol is not ITableTypeSymbol table) + return; - if (table.GetProperty(PropertyKind.DataPerCompany) == null) - { - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0007DataPerCompanyShouldAlwaysBeSet, table.GetLocation())); - } - } + if (table.TableType == TableTypeKind.Temporary) + return; - private static bool IsSymbolAccessible(ISymbol symbol) + if (table.GetProperty(PropertyKind.DataPerCompany) is null) { - try - { - GetDeclaration(symbol); - return true; - } - catch (Exception) - { - return false; - } + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0007DataPerCompanyShouldAlwaysBeSet, + table.GetLocation())); } - - private static string GetDeclaration(ISymbol symbol) - => symbol.Location.SourceTree.GetText(CancellationToken.None).GetSubText(symbol.DeclaringSyntaxReference.Span).ToString(); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0008NoFilterOperatorsInSetRange.cs b/BusinessCentral.LinterCop/Design/Rule0008NoFilterOperatorsInSetRange.cs index 7b98a6fe..10e6faf6 100644 --- a/BusinessCentral.LinterCop/Design/Rule0008NoFilterOperatorsInSetRange.cs +++ b/BusinessCentral.LinterCop/Design/Rule0008NoFilterOperatorsInSetRange.cs @@ -1,62 +1,71 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; +using Microsoft.Dynamics.Nav.CodeAnalysis.Text; using System.Collections.Immutable; using System.Text.RegularExpressions; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0008NoFilterOperatorsInSetRange : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0008NoFilterOperatorsInSetRange : DiagnosticAnalyzer + private readonly Lazy replacementFieldPatternLazy = new Lazy((Func)(() => new Regex(@"%\d+", RegexOptions.Compiled))); + + private Regex ReplacementFieldPatternLazy => this.replacementFieldPatternLazy.Value; + + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0008NoFilterOperatorsInSetRange); + + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(new Action(this.AnalyzeInvocation), OperationKind.InvocationExpression); + + private void AnalyzeInvocation(OperationAnalysisContext ctx) { - private readonly Lazy replacementFieldPatternLazy = new Lazy((Func)(() => new Regex(@"%\d+", RegexOptions.Compiled))); - private Regex ReplacementFieldPatternLazy => this.replacementFieldPatternLazy.Value; - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0008NoFilterOperatorsInSetRange); - public override void Initialize(AnalysisContext context) => context.RegisterOperationAction(new Action(this.AnalyzeInvocation), Microsoft.Dynamics.Nav.CodeAnalysis.OperationKind.InvocationExpression); + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) + return; - private void AnalyzeInvocation(OperationAnalysisContext context) - { - if (context.IsObsoletePendingOrRemoved()) return; - IInvocationExpression operation = (IInvocationExpression)context.Operation; - if (!SemanticFacts.IsSameName(operation.TargetMethod.Name, "setrange") || operation.TargetMethod == null || operation.Arguments.Count() < 2) - return; - - CheckParameter(operation.Arguments[1].Value, ref operation, ref context); - if (operation.Arguments.Count() == 3) - CheckParameter(operation.Arguments[2].Value, ref operation, ref context); - } + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + operation.TargetMethod.Name != "SetRange" || + operation.TargetMethod.ContainingSymbol?.Name != "Table" || + operation.Arguments.Length < 2) + return; - private void CheckParameter(IOperation operand, ref IInvocationExpression operation, ref OperationAnalysisContext context) - { - if (operand.Type.GetNavTypeKindSafe() != NavTypeKind.String && operand.Type.GetNavTypeKindSafe() != NavTypeKind.Joker) - return; + CheckParameter(operation.Arguments[1].Value, ref operation, ref ctx); + + if (operation.Arguments.Length == 3) + CheckParameter(operation.Arguments[2].Value, ref operation, ref ctx); + } - if (operand.Syntax.Kind != SyntaxKind.LiteralExpression) - return; + private void CheckParameter(IOperation operand, ref IInvocationExpression operation, ref OperationAnalysisContext context) + { + if (operand.Type.GetNavTypeKindSafe() != NavTypeKind.String && operand.Type.GetNavTypeKindSafe() != NavTypeKind.Joker) + return; - string parameterString = operand.Syntax.ToFullString(); + if (operand.Syntax.Kind != SyntaxKind.LiteralExpression) + return; - if ((parameterString.Contains('<') || parameterString.Contains('>') || - parameterString.Contains("..") || parameterString.Contains('*') || - parameterString.Contains('&') || parameterString.Contains('|'))) - { - ReportDiagnostic(operation.Syntax.GetLocation(), ref context); - return; - } + string parameterString = operand.Syntax.ToString(); - Match match = this.ReplacementFieldPatternLazy.Match(parameterString); - if (match.Success) - ReportDiagnostic(operation.Syntax.GetLocation(), ref context); + if (ContainsFilterOperators(parameterString)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0008NoFilterOperatorsInSetRange, + operation.Syntax.GetLocation())); + + return; } - private void ReportDiagnostic(Microsoft.Dynamics.Nav.CodeAnalysis.Text.Location location, ref OperationAnalysisContext context) + Match match = this.ReplacementFieldPatternLazy.Match(parameterString); + if (match.Success) { - context.ReportDiagnostic( - Diagnostic.Create( - DiagnosticDescriptors.Rule0008NoFilterOperatorsInSetRange, - location)); + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0008NoFilterOperatorsInSetRange, + operation.Syntax.GetLocation())); } } -} + + private static bool ContainsFilterOperators(string parameterString) => + parameterString.IndexOfAny(new[] { '<', '>', '.', '*', '&', '|' }) >= 0 || parameterString.Contains(".."); +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0009CodeMetrics.cs b/BusinessCentral.LinterCop/Design/Rule0009CodeMetrics.cs index 362ae7f5..b42540f8 100644 --- a/BusinessCentral.LinterCop/Design/Rule0009CodeMetrics.cs +++ b/BusinessCentral.LinterCop/Design/Rule0009CodeMetrics.cs @@ -4,139 +4,135 @@ using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; using BusinessCentral.LinterCop.Helpers; -using BusinessCentral.LinterCop.AnalysisContextExtension; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0009CodeMetrics : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0009CodeMetrics : DiagnosticAnalyzer + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0009CodeMetricsInfo, DiagnosticDescriptors.Rule0010CodeMetricsWarning); + + private static readonly HashSet OperatorAndOperandKinds = + Enum.GetValues(typeof(SyntaxKind)) + .Cast() + .Where(value => + (value.ToString().Contains("Keyword") || + value.ToString().Contains("Token")) || + IsOperandKind(value)) + .ToHashSet(); + + public override void Initialize(AnalysisContext context) => + context.RegisterCodeBlockAction(new Action(this.CheckCodeMetrics)); + + private void CheckCodeMetrics(CodeBlockAnalysisContext context) { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0009CodeMetricsInfo, DiagnosticDescriptors.Rule0010CodeMetricsWarning); - - private static readonly HashSet OperatorAndOperandKinds = - Enum.GetValues(typeof(SyntaxKind)) - .Cast() - .Where(value => - (value.ToString().Contains("Keyword") || - value.ToString().Contains("Token")) || - IsOperandKind(value)) - .ToHashSet(); - - public override void Initialize(AnalysisContext context) - => context.RegisterCodeBlockAction(new Action(this.CheckCodeMetrics)); - - private void CheckCodeMetrics(CodeBlockAnalysisContext context) - { - if ((context.CodeBlock.Kind != SyntaxKind.MethodDeclaration) && - (context.CodeBlock.Kind != SyntaxKind.TriggerDeclaration)) - return; + if ((context.CodeBlock.Kind != SyntaxKind.MethodDeclaration) && + (context.CodeBlock.Kind != SyntaxKind.TriggerDeclaration)) + return; - if (context.IsObsoletePendingOrRemoved()) return; + if (context.IsObsoletePendingOrRemoved()) return; - var containingObjectTypeSymbol = context.OwningSymbol.GetContainingObjectTypeSymbol(); - if (containingObjectTypeSymbol.NavTypeKind == NavTypeKind.Interface || - containingObjectTypeSymbol.NavTypeKind == NavTypeKind.ControlAddIn) - return; + var containingObjectTypeSymbol = context.OwningSymbol.GetContainingObjectTypeSymbol(); + if (containingObjectTypeSymbol.NavTypeKind == NavTypeKind.Interface || + containingObjectTypeSymbol.NavTypeKind == NavTypeKind.ControlAddIn) + return; - SyntaxNode bodyNode = context.CodeBlock.Kind == SyntaxKind.MethodDeclaration - ? (context.CodeBlock as MethodDeclarationSyntax)?.Body - : (context.CodeBlock as TriggerDeclarationSyntax)?.Body; + SyntaxNode bodyNode = context.CodeBlock.Kind == SyntaxKind.MethodDeclaration + ? (context.CodeBlock as MethodDeclarationSyntax)?.Body + : (context.CodeBlock as TriggerDeclarationSyntax)?.Body; - if (bodyNode is null) - return; + if (bodyNode is null) + return; - var descendants = bodyNode.DescendantNodesAndTokens(e => true).ToArray(); + var descendants = bodyNode.DescendantNodesAndTokens(e => true).ToArray(); - int cyclomaticComplexity = GetCyclomaticComplexity(descendants); - double HalsteadVolume = GetHalsteadVolume(context, bodyNode, descendants, cyclomaticComplexity); + int cyclomaticComplexity = GetCyclomaticComplexity(descendants); + double HalsteadVolume = GetHalsteadVolume(context, bodyNode, descendants, cyclomaticComplexity); - if (LinterSettings.instance == null) - LinterSettings.Create(context.SemanticModel.Compilation.FileSystem.GetDirectoryPath()); + if (LinterSettings.instance is null) + LinterSettings.Create(context.SemanticModel.Compilation.FileSystem.GetDirectoryPath()); - if (cyclomaticComplexity >= LinterSettings.instance.cyclomaticComplexityThreshold || Math.Round(HalsteadVolume) <= LinterSettings.instance.maintainabilityIndexThreshold) - { - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0010CodeMetricsWarning, context.OwningSymbol.GetLocation(), new object[] { cyclomaticComplexity, LinterSettings.instance.cyclomaticComplexityThreshold, Math.Round(HalsteadVolume), LinterSettings.instance.maintainabilityIndexThreshold })); - return; - } - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0009CodeMetricsInfo, context.OwningSymbol.GetLocation(), new object[] { cyclomaticComplexity, LinterSettings.instance.cyclomaticComplexityThreshold, Math.Round(HalsteadVolume), LinterSettings.instance.maintainabilityIndexThreshold })); + if (cyclomaticComplexity >= LinterSettings.instance.cyclomaticComplexityThreshold || Math.Round(HalsteadVolume) <= LinterSettings.instance.maintainabilityIndexThreshold) + { + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0010CodeMetricsWarning, context.OwningSymbol.GetLocation(), new object[] { cyclomaticComplexity, LinterSettings.instance.cyclomaticComplexityThreshold, Math.Round(HalsteadVolume), LinterSettings.instance.maintainabilityIndexThreshold })); + return; } + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0009CodeMetricsInfo, context.OwningSymbol.GetLocation(), new object[] { cyclomaticComplexity, LinterSettings.instance.cyclomaticComplexityThreshold, Math.Round(HalsteadVolume), LinterSettings.instance.maintainabilityIndexThreshold })); + } - private static double GetHalsteadVolume(CodeBlockAnalysisContext context, SyntaxNode methodBodyNode, - SyntaxNodeOrToken[] descendantNodesAndTokens, int cyclomaticComplexity) + private static double GetHalsteadVolume(CodeBlockAnalysisContext context, SyntaxNode methodBodyNode, + SyntaxNodeOrToken[] descendantNodesAndTokens, int cyclomaticComplexity) + { + try { - try + var triviaLinesCount = methodBodyNode + .DescendantTrivia(e => true, true) + .Count(node => + node.Kind == SyntaxKind.EndOfLineTrivia && + node.GetLocation().GetLineSpan().StartLinePosition.Line == + node.Token.GetLocation().GetLineSpan().StartLinePosition.Line) - 2; //Minus 2 for Begin end of function + + context.CancellationToken.ThrowIfCancellationRequested(); + var N = 0; + using var hashSet = PooledHashSet.GetInstance(); + foreach (var nodeOrToken in descendantNodesAndTokens) { - var triviaLinesCount = methodBodyNode - .DescendantTrivia(e => true, true) - .Count(node => - node.Kind == SyntaxKind.EndOfLineTrivia && - node.GetLocation().GetLineSpan().StartLinePosition.Line == - node.Token.GetLocation().GetLineSpan().StartLinePosition.Line) - 2; //Minus 2 for Begin end of function - - context.CancellationToken.ThrowIfCancellationRequested(); - var N = 0; - using var hashSet = PooledHashSet.GetInstance(); - foreach (var nodeOrToken in descendantNodesAndTokens) + if (OperatorAndOperandKinds.Contains(nodeOrToken.Kind)) { - if (OperatorAndOperandKinds.Contains(nodeOrToken.Kind)) - { - N++; - hashSet.Add(nodeOrToken); - } + N++; + hashSet.Add(nodeOrToken); } + } - double HalsteadVolume = N * Math.Log(hashSet.Count, 2); + double HalsteadVolume = N * Math.Log(hashSet.Count, 2); - //171−5.2lnV−0.23G−16.2lnL - return Math.Max(0, (171 - 5.2 * Math.Log(HalsteadVolume) - 0.23 * cyclomaticComplexity - 16.2 * Math.Log(triviaLinesCount)) * 100 / 171); - } - catch (System.NullReferenceException) - { - return 0.0; - } + //171−5.2lnV−0.23G−16.2lnL + return Math.Max(0, (171 - 5.2 * Math.Log(HalsteadVolume) - 0.23 * cyclomaticComplexity - 16.2 * Math.Log(triviaLinesCount)) * 100 / 171); } - - private static int GetCyclomaticComplexity(SyntaxNodeOrToken[] nodesAndTokens) + catch (System.NullReferenceException) { - return nodesAndTokens.Count(syntaxNodeOrToken => IsComplexKind(syntaxNodeOrToken.Kind)) + 1; + return 0.0; } + } - private static bool IsOperandKind(SyntaxKind kind) - { - switch (kind) - { - case SyntaxKind.IdentifierToken: - case SyntaxKind.Int32LiteralToken: - case SyntaxKind.StringLiteralToken: - case SyntaxKind.BooleanLiteralValue: - case SyntaxKind.TrueKeyword: - case SyntaxKind.FalseKeyword: - return true; - } - - return false; - } + private static int GetCyclomaticComplexity(SyntaxNodeOrToken[] nodesAndTokens) + { + return nodesAndTokens.Count(syntaxNodeOrToken => IsComplexKind(syntaxNodeOrToken.Kind)) + 1; + } - private static bool IsComplexKind(SyntaxKind kind) + private static bool IsOperandKind(SyntaxKind kind) + { + switch (kind) { - switch (kind) - { - case SyntaxKind.IfKeyword: - case SyntaxKind.ElifKeyword: - case SyntaxKind.LogicalAndExpression: - case SyntaxKind.LogicalOrExpression: - case SyntaxKind.CaseLine: - case SyntaxKind.ForKeyword: - case SyntaxKind.ForEachKeyword: - case SyntaxKind.WhileKeyword: - case SyntaxKind.UntilKeyword: - return true; - } - - return false; + case SyntaxKind.IdentifierToken: + case SyntaxKind.Int32LiteralToken: + case SyntaxKind.StringLiteralToken: + case SyntaxKind.BooleanLiteralValue: + case SyntaxKind.TrueKeyword: + case SyntaxKind.FalseKeyword: + return true; } + return false; } -} + private static bool IsComplexKind(SyntaxKind kind) + { + switch (kind) + { + case SyntaxKind.IfKeyword: + case SyntaxKind.ElifKeyword: + case SyntaxKind.LogicalAndExpression: + case SyntaxKind.LogicalOrExpression: + case SyntaxKind.CaseLine: + case SyntaxKind.ForKeyword: + case SyntaxKind.ForEachKeyword: + case SyntaxKind.WhileKeyword: + case SyntaxKind.UntilKeyword: + return true; + } + return false; + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0011AccessPropertyShouldAlwaysBeSet.cs b/BusinessCentral.LinterCop/Design/Rule0011AccessPropertyShouldAlwaysBeSet.cs index 0ec69e57..1944dac5 100644 --- a/BusinessCentral.LinterCop/Design/Rule0011AccessPropertyShouldAlwaysBeSet.cs +++ b/BusinessCentral.LinterCop/Design/Rule0011AccessPropertyShouldAlwaysBeSet.cs @@ -1,43 +1,57 @@ -#nullable disable // TODO: Enable nullable and review rule -using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; using BusinessCentral.LinterCop.Helpers; -using BusinessCentral.LinterCop.AnalysisContextExtension; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0011DataPerCompanyShouldAlwaysBeSet : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0011DataPerCompanyShouldAlwaysBeSet : DiagnosticAnalyzer + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0011AccessPropertyShouldAlwaysBeSet); + + public override VersionCompatibility SupportedVersions => VersionCompatibility.Spring2021OrGreater; + + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(new Action(this.CheckForMissingAccessProperty), + SymbolKind.Codeunit, + SymbolKind.Enum, + SymbolKind.Interface, + SymbolKind.PermissionSet, + SymbolKind.Query, + SymbolKind.Table, + SymbolKind.Field); + + private void CheckForMissingAccessProperty(SymbolAnalysisContext ctx) { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0011AccessPropertyShouldAlwaysBeSet); + if (ctx.Symbol.Kind == SymbolKind.Enum || ctx.Symbol.Kind == SymbolKind.Interface) + return; - public override void Initialize(AnalysisContext context) - => context.RegisterSymbolAction(new Action(this.CheckForMissingAccessProperty), SymbolKind.Codeunit, SymbolKind.Enum, SymbolKind.Interface, SymbolKind.PermissionSet, SymbolKind.Query, SymbolKind.Table, SymbolKind.Field); + if (ctx.IsObsoletePendingOrRemoved()) + return; - private void CheckForMissingAccessProperty(SymbolAnalysisContext context) + if (ctx.Symbol.Kind == SymbolKind.Field) { - if (!VersionChecker.IsSupported(context.Symbol, VersionCompatibility.Spring2021OrGreater) && (context.Symbol.Kind == SymbolKind.Enum || context.Symbol.Kind == SymbolKind.Interface)) + if (ctx.Symbol.ContainingSymbol is null || ctx.Symbol.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoletePendingOrRemoved()) return; - if (context.IsObsoletePendingOrRemoved()) return; + LinterSettings.Create(ctx.Compilation.FileSystem?.GetDirectoryPath()); - if (context.Symbol.Kind == SymbolKind.Field) + if (LinterSettings.instance.enableRule0011ForTableFields) { - if (context.Symbol.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoletePending || context.Symbol.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoleteRemoved) return; - LinterSettings.Create(context.Compilation.FileSystem.GetDirectoryPath()); - if (LinterSettings.instance.enableRule0011ForTableFields) - { - if (context.Symbol.GetProperty(PropertyKind.Access) == null) - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0011AccessPropertyShouldAlwaysBeSet, context.Symbol.GetLocation())); - } + if (ctx.Symbol.GetProperty(PropertyKind.Access) is null) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0011AccessPropertyShouldAlwaysBeSet, + ctx.Symbol.GetLocation())); } - else - { - if (context.Symbol.GetProperty(PropertyKind.Access) == null) - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0011AccessPropertyShouldAlwaysBeSet, context.Symbol.GetLocation())); - } - + } + else + { + if (ctx.Symbol.GetProperty(PropertyKind.Access) is null) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0011AccessPropertyShouldAlwaysBeSet, + ctx.Symbol.GetLocation())); } } -} +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs b/BusinessCentral.LinterCop/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs index 2717369e..6d807aac 100644 --- a/BusinessCentral.LinterCop/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs +++ b/BusinessCentral.LinterCop/Design/Rule0012DoNotUseObjectIdInSystemFunctions.cs @@ -1,5 +1,5 @@ #nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; @@ -9,13 +9,15 @@ namespace BusinessCentral.LinterCop.Design [DiagnosticAnalyzer] public class Rule0012DoNotUseObjectIdInSystemFunctions : DiagnosticAnalyzer { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0012DoNotUseObjectIdInSystemFunctions); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0012DoNotUseObjectIdInSystemFunctions); public override void Initialize(AnalysisContext context) { context.RegisterOperationAction(new Action(this.CheckForObjectIdsInFunctionInvocations), OperationKind.InvocationExpression); context.RegisterSymbolAction(new Action(this.CheckForObjectIdsEventSubscribers), SymbolKind.Method); } + private void CheckForObjectIdsEventSubscribers(SymbolAnalysisContext context) { IMethodSymbol method = (IMethodSymbol)context.Symbol; @@ -23,7 +25,7 @@ private void CheckForObjectIdsEventSubscribers(SymbolAnalysisContext context) return; var ObjectAccessToUse = method.Attributes[0].DeclaringSyntaxReference.GetSyntax().DescendantNodes(o => true).FirstOrDefault(n => n.IsKind(SyntaxKind.OptionAccessExpression)); - if (ObjectAccessToUse == null) + if (ObjectAccessToUse is null) return; var ObjectAccessToUseText = ObjectAccessToUse.DescendantNodes().ToArray()[1].ToString(); @@ -32,20 +34,21 @@ private void CheckForObjectIdsEventSubscribers(SymbolAnalysisContext context) var wrongSyntaxLiteral = method.Attributes[0].DeclaringSyntaxReference.GetSyntax().DescendantNodes(o => true).FirstOrDefault(n => n.IsKind(SyntaxKind.Int32SignedLiteralValue)); - if (wrongSyntaxLiteral != null) + if (wrongSyntaxLiteral is not null) context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0012DoNotUseObjectIdInSystemFunctions, wrongSyntaxLiteral.GetLocation(), new object[] { ObjectAccessToUseText, "" })); } - private void CheckForObjectIdsInFunctionInvocations(OperationAnalysisContext context) + private void CheckForObjectIdsInFunctionInvocations(OperationAnalysisContext ctx) { - if (context.IsObsoletePendingOrRemoved()) return; + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) + return; - IInvocationExpression operation = (IInvocationExpression)context.Operation; - if (operation.TargetMethod.Parameters.Length == 0) return; - if (operation.Arguments.Length == 0) return; + if (operation.TargetMethod.Parameters.Length == 0 || + operation.Arguments.Length == 0) + return; RelevantFuntion CurrentFunction = FunctionCallsWithIDParamaters.RelevantFunctions.FirstOrDefault(o => (o.ObjectType.ToString().ToUpper() == operation.TargetMethod.ContainingSymbol.Name.ToUpper() && o.FunctionName == operation.TargetMethod.Name)); - if (CurrentFunction == null) return; + if (CurrentFunction is null) return; SyntaxKind[] AllowedParameterKinds = { SyntaxKind.MemberAccessExpression, SyntaxKind.IdentifierName, SyntaxKind.InvocationExpression, SyntaxKind.QualifiedName }; if (!AllowedParameterKinds.Contains(operation.Arguments[0].Syntax.Kind) && (operation.Arguments[0].Syntax.ToString() != "0" || !CurrentFunction.ZeroIDAllowed)) @@ -53,10 +56,10 @@ private void CheckForObjectIdsInFunctionInvocations(OperationAnalysisContext con if (operation.TargetMethod.Parameters[0].ParameterType.NavTypeKind == NavTypeKind.Integer) { if (int.TryParse(operation.Arguments[0].Syntax.ToString(), out int tempint)) - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0012DoNotUseObjectIdInSystemFunctions, context.Operation.Syntax.GetLocation(), new object[] { CurrentFunction.CorrectAccessSymbol, "" })); + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0012DoNotUseObjectIdInSystemFunctions, ctx.Operation.Syntax.GetLocation(), new object[] { CurrentFunction.CorrectAccessSymbol, "" })); else if (!operation.Arguments[0].Syntax.ToString().ToUpper().StartsWith(CurrentFunction.CorrectAccessSymbol.ToUpper())) - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0012DoNotUseObjectIdInSystemFunctions, context.Operation.Syntax.GetLocation(), new object[] { CurrentFunction.CorrectAccessSymbol, "" })); + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0012DoNotUseObjectIdInSystemFunctions, ctx.Operation.Syntax.GetLocation(), new object[] { CurrentFunction.CorrectAccessSymbol, "" })); } } } diff --git a/BusinessCentral.LinterCop/Design/Rule0013CheckForNotBlankOnSingleFieldPrimaryKeys.cs b/BusinessCentral.LinterCop/Design/Rule0013CheckForNotBlankOnSingleFieldPrimaryKeys.cs index b7fbaa06..786421c5 100644 --- a/BusinessCentral.LinterCop/Design/Rule0013CheckForNotBlankOnSingleFieldPrimaryKeys.cs +++ b/BusinessCentral.LinterCop/Design/Rule0013CheckForNotBlankOnSingleFieldPrimaryKeys.cs @@ -1,5 +1,4 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; @@ -11,82 +10,55 @@ namespace BusinessCentral.LinterCop.Design; [DiagnosticAnalyzer] public class Rule0013CheckForNotBlankOnSingleFieldPrimaryKeys : DiagnosticAnalyzer { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0013CheckForNotBlankOnSingleFieldPrimaryKeys, DiagnosticDescriptors.Rule0067DisableNotBlankOnSingleFieldPrimaryKey); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0013CheckForNotBlankOnSingleFieldPrimaryKeys, DiagnosticDescriptors.Rule0067DisableNotBlankOnSingleFieldPrimaryKey); - public override void Initialize(AnalysisContext context) - { - context.RegisterSymbolAction(new Action(this.CheckForSingleFieldPrimaryKeyNotBlankProperty), SymbolKind.Field); - } - private void CheckForSingleFieldPrimaryKeyNotBlankProperty(SymbolAnalysisContext context) - { - if (context.IsObsoletePendingOrRemoved()) return; + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(new Action(this.CheckForSingleFieldPrimaryKeyNotBlankProperty), SymbolKind.Table); - IFieldSymbol field = (IFieldSymbol)context.Symbol; - if (GetExitCondition(field)) + private void CheckForSingleFieldPrimaryKeyNotBlankProperty(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Symbol is not ITableTypeSymbol table) return; - ITableTypeSymbol table = (ITableTypeSymbol)field.GetContainingObjectTypeSymbol(); - if (table.PrimaryKey.Fields.Length != 1) + if (table.PrimaryKey.Fields == null || table.PrimaryKey.Fields.Length != 1) return; - if (!table.PrimaryKey.Fields[0].Equals(field)) + var field = table.PrimaryKey.Fields[0]; + if (!field.GetTypeSymbol().HasLength) return; if (TableContainsNoSeries(table)) { if (field.GetBooleanPropertyValue(PropertyKind.NotBlank).GetValueOrDefault() && !SemanticFacts.IsSameName(field.Name, "Name")) - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0067DisableNotBlankOnSingleFieldPrimaryKey, field.GetLocation())); + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0067DisableNotBlankOnSingleFieldPrimaryKey, + field.GetLocation())); } else { - if (field.GetProperty(PropertyKind.NotBlank) == null) - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0013CheckForNotBlankOnSingleFieldPrimaryKeys, field.GetLocation())); + if (field.GetProperty(PropertyKind.NotBlank) is null) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0013CheckForNotBlankOnSingleFieldPrimaryKeys, + field.GetLocation())); + } } } - private static bool GetExitCondition(IFieldSymbol field) - { - return - field.FieldClass != FieldClassKind.Normal || - field.GetContainingObjectTypeSymbol().Kind != SymbolKind.Table || - !field.DeclaringSyntaxReference.GetSyntax().DescendantNodes().Any(Token => Token.Kind == SyntaxKind.LengthDataType); - } - private static bool TableContainsNoSeries(ITableTypeSymbol table) { return table.Fields - .Where(x => x.Id > 0 && x.Id < 2000000000) - .Where(x => x.FieldClass == FieldClassKind.Normal) + .Where(x => x.FieldClass == FieldClassKind.Normal && x.Id > 0 && x.Id < 2000000000) #if !LessThenFall2024 - .Where(x => x.Type.GetNavTypeKindSafe() == NavTypeKind.Code) + .Where(x => x.Type?.GetNavTypeKindSafe() == NavTypeKind.Code) #endif .Any(field => { - IPropertySymbol propertySymbol = field.GetProperty(PropertyKind.TableRelation); - if (propertySymbol != null && propertySymbol.ContainingSymbol != null) + IPropertySymbol? propertySymbol = field.GetProperty(PropertyKind.TableRelation); + if (propertySymbol is not null && propertySymbol.ContainingSymbol is not null) return SemanticFacts.IsSameName(propertySymbol.ContainingSymbol.Name.UnquoteIdentifier(), "No. Series"); return false; }); } - - public static class DiagnosticDescriptors - { - public static readonly DiagnosticDescriptor Rule0013CheckForNotBlankOnSingleFieldPrimaryKeys = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0013", - title: LinterCopAnalyzers.GetLocalizableString("Rule0013CheckForNotBlankOnSingleFieldPrimaryKeysTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0013CheckForNotBlankOnSingleFieldPrimaryKeysFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0013CheckForNotBlankOnSingleFieldPrimaryKeysDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0013"); - - public static readonly DiagnosticDescriptor Rule0067DisableNotBlankOnSingleFieldPrimaryKey = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0067", - title: LinterCopAnalyzers.GetLocalizableString("Rule0067DisableNotBlankOnSingleFieldPrimaryKeyTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0067DisableNotBlankOnSingleFieldPrimaryKeyFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0067DisableNotBlankOnSingleFieldPrimaryKeyDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0067"); - } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0014PermissionSetCaptionLength.cs b/BusinessCentral.LinterCop/Design/Rule0014PermissionSetCaptionLength.cs index 4c325545..15e944eb 100644 --- a/BusinessCentral.LinterCop/Design/Rule0014PermissionSetCaptionLength.cs +++ b/BusinessCentral.LinterCop/Design/Rule0014PermissionSetCaptionLength.cs @@ -1,54 +1,76 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0014PermissionSetCaptionLength : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0014PermissionSetCaptionLength : DiagnosticAnalyzer - { - private const int MAXCAPTIONLENGTH = 30; + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0014PermissionSetCaptionLength); - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0014PermissionSetCaptionLength); + private const int MAXCAPTIONLENGTH = 30; - public override void Initialize(AnalysisContext context) - => context.RegisterSymbolAction(new Action(this.CheckPermissionSetNameAndCaptionLength), SymbolKind.PermissionSet); + public override void Initialize(AnalysisContext context) + => context.RegisterSymbolAction(new Action(this.CheckPermissionSetNameAndCaptionLength), SymbolKind.PermissionSet); - private void CheckPermissionSetNameAndCaptionLength(SymbolAnalysisContext context) - { - if (context.IsObsoletePendingOrRemoved()) return; + private void CheckPermissionSetNameAndCaptionLength(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) + return; - IPropertySymbol captionProperty = context.Symbol.GetProperty(PropertyKind.Caption); - if (captionProperty == null) - return; + IPropertySymbol? captionProperty = ctx.Symbol.GetProperty(PropertyKind.Caption); + if (captionProperty is null) + return; - if (captionProperty?.ValueText.Length > MAXCAPTIONLENGTH) - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0014PermissionSetCaptionLength, captionProperty.GetLocation(), new object[] { MAXCAPTIONLENGTH })); + if (captionProperty.ValueText.Length > MAXCAPTIONLENGTH) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0014PermissionSetCaptionLength, + captionProperty.GetLocation(), + MAXCAPTIONLENGTH)); - var captionSubProperties = captionProperty.DeclaringSyntaxReference.GetSyntax().DescendantNodes(e => true).FirstOrDefault(e => e.Kind == SyntaxKind.CommaSeparatedIdentifierEqualsLiteralList); - if (captionSubProperties != null) + var subProperties = ExtractSubProperties(captionProperty); + if (subProperties is null || subProperties.Any(node => node.ToString().Contains("Locked", StringComparison.OrdinalIgnoreCase))) + return; + + var maxLengthNode = subProperties.FirstOrDefault(node => node.ToString().Contains("MaxLength", StringComparison.OrdinalIgnoreCase)); + if (maxLengthNode is not null && + int.TryParse(maxLengthNode.DescendantNodes().FirstOrDefault(e => e.Kind == SyntaxKind.Int32SignedLiteralValue)?.ToString(), out int maxLength)) + { + if (maxLength > MAXCAPTIONLENGTH) { - if (captionSubProperties.DescendantNodes().Any(e => e.ToString().StartsWith("Locked"))) - return; - - var maxLengthProperty = captionSubProperties.DescendantNodes().FirstOrDefault(e => e.ToString().StartsWith("MaxLength")); - if (maxLengthProperty != null) - if (captionSubProperties.ToString() != "") - { - if (Int32.TryParse(maxLengthProperty.DescendantNodes().FirstOrDefault(e => e.Kind == SyntaxKind.Int32SignedLiteralValue).ToString(), out int maxLengthValue)) - { - if (maxLengthValue > MAXCAPTIONLENGTH) - { - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0014PermissionSetCaptionLength, captionProperty.GetLocation(), new object[] { MAXCAPTIONLENGTH })); - } - return; - } - } - } + if (captionProperty?.ValueText.Length > MAXCAPTIONLENGTH) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0014PermissionSetCaptionLength, + captionProperty.GetLocation(), + MAXCAPTIONLENGTH)); - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0014PermissionSetCaptionLength, captionProperty.GetLocation(), new object[] { MAXCAPTIONLENGTH })); + } + return; } + + if (captionProperty is not null) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0014PermissionSetCaptionLength, + captionProperty.GetLocation(), + MAXCAPTIONLENGTH)); + } + + private IEnumerable ExtractSubProperties(IPropertySymbol? captionProperty) + { + var syntaxReference = captionProperty?.DeclaringSyntaxReference; + if (syntaxReference is null) + return Enumerable.Empty(); + + var syntaxNode = syntaxReference.GetSyntax(); + if (syntaxNode is null) + return Enumerable.Empty(); + + var subPropertyNode = syntaxNode.DescendantNodes() + .FirstOrDefault(e => e.Kind == SyntaxKind.CommaSeparatedIdentifierEqualsLiteralList); + + return subPropertyNode?.DescendantNodes() ?? Enumerable.Empty(); } -} +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0015PermissionSetCoverage.cs b/BusinessCentral.LinterCop/Design/Rule0015PermissionSetCoverage.cs index 9aaeefe4..0f9d8790 100644 --- a/BusinessCentral.LinterCop/Design/Rule0015PermissionSetCoverage.cs +++ b/BusinessCentral.LinterCop/Design/Rule0015PermissionSetCoverage.cs @@ -1,164 +1,160 @@ -#nullable disable // TODO: Enable nullable and review rule -using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; using Microsoft.Dynamics.Nav.Analyzers.Common; using System.Xml.Linq; using System.Xml.XPath; -namespace BusinessCentral.LinterCop.Design -{ - [DiagnosticAnalyzer] - public class Rule0015PermissionSetCoverage : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0015PermissionSetCoverage); +namespace BusinessCentral.LinterCop.Design; - public override void Initialize(AnalysisContext context) - => context.RegisterSymbolAction(new Action(this.CheckPermissionSetCoverage), SymbolKind.Module); +[DiagnosticAnalyzer] +public class Rule0015PermissionSetCoverage : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0015PermissionSetCoverage); - private void CheckPermissionSetCoverage(SymbolAnalysisContext context) + private static readonly Dictionary navTypeToPermObjectKind = new() + { + { NavTypeKind.Codeunit, PermissionObjectKind.Codeunit }, + { NavTypeKind.Page, PermissionObjectKind.Page }, + { NavTypeKind.Query, PermissionObjectKind.Query }, + { NavTypeKind.Report, PermissionObjectKind.Report }, + { NavTypeKind.Record, PermissionObjectKind.Table }, + { NavTypeKind.XmlPort, PermissionObjectKind.Xmlport } + }; + + public override void Initialize(AnalysisContext context) + => context.RegisterSymbolAction(new Action(this.CheckPermissionSetCoverage), SymbolKind.Module); + + private void CheckPermissionSetCoverage(SymbolAnalysisContext ctx) + { + if (ctx.Compilation.FileSystem is null) + return; + + if (ctx.Symbol is not IModuleSymbol moduleSymbol) + return; + + ImmutableHashSet<(PermissionObjectKind, int)> permissionSymbols = GetPermissionSymbols(moduleSymbol); + IEnumerable permissionSetDocuments = FileSystemExtensions.GetPermissionSetDocuments(ctx.Compilation.FileSystem); + IEnumerable objects = moduleSymbol.GetObjectSymbols(SymbolKind.Codeunit); + objects = objects.Concat(moduleSymbol.GetObjectSymbols(SymbolKind.Page)); + objects = objects.Concat(moduleSymbol.GetObjectSymbols(SymbolKind.Query)); + objects = objects.Concat(moduleSymbol.GetObjectSymbols(SymbolKind.Report)); + objects = objects.Concat(moduleSymbol.GetObjectSymbols(SymbolKind.Table)); + objects = objects.Concat(moduleSymbol.GetObjectSymbols(SymbolKind.XmlPort)); + IEnumerator enumerator = objects.GetEnumerator(); + + while (enumerator.MoveNext()) { - IModuleSymbol moduleSymbol = context.Symbol as IModuleSymbol; - if (moduleSymbol == null) - { - return; - } + if (enumerator.Current is not IApplicationObjectTypeSymbol appObjTypeSymbol) + continue; - ImmutableHashSet<(PermissionObjectKind, int)> permissionSymbols = GetPermissionSymbols(moduleSymbol); - IEnumerable permissionSetDocuments = FileSystemExtensions.GetPermissionSetDocuments(context.Compilation.FileSystem); - IEnumerable objects = moduleSymbol.GetObjectSymbols(SymbolKind.Codeunit); - objects = objects.Concat(moduleSymbol.GetObjectSymbols(SymbolKind.Page)); - objects = objects.Concat(moduleSymbol.GetObjectSymbols(SymbolKind.Query)); - objects = objects.Concat(moduleSymbol.GetObjectSymbols(SymbolKind.Report)); - objects = objects.Concat(moduleSymbol.GetObjectSymbols(SymbolKind.Table)); - objects = objects.Concat(moduleSymbol.GetObjectSymbols(SymbolKind.XmlPort)); - IEnumerator enumerator = objects.GetEnumerator(); - - while (enumerator.MoveNext()) - { - ISymbol current = enumerator.Current; - IApplicationObjectTypeSymbol appObjTypeSymbol = (IApplicationObjectTypeSymbol)current; - PermissionObjectKind permObjectKind = PermissionObjectKind.Table; - int permObjectId = appObjTypeSymbol.Id; + if (appObjTypeSymbol.IsObsoleteRemoved) + continue; - if (appObjTypeSymbol.IsObsoleteRemoved) - { - continue; - } + if (appObjTypeSymbol.Properties.Where(currentProperty => currentProperty.PropertyKind == PropertyKind.InherentPermissions).Any()) + continue; - if (appObjTypeSymbol.Properties.Where(currentProperty => currentProperty.PropertyKind == PropertyKind.InherentPermissions).Any()) continue; + int permObjectId = appObjTypeSymbol.Id; - switch (appObjTypeSymbol.NavTypeKind) - { - case NavTypeKind.Codeunit: - permObjectKind = PermissionObjectKind.Codeunit; - break; - case NavTypeKind.Page: - permObjectKind = PermissionObjectKind.Page; - break; - case NavTypeKind.Query: - permObjectKind = PermissionObjectKind.Query; - break; - case NavTypeKind.Report: - permObjectKind = PermissionObjectKind.Report; - break; - case NavTypeKind.Record: - permObjectKind = PermissionObjectKind.Table; - break; - case NavTypeKind.XmlPort: - permObjectKind = PermissionObjectKind.Xmlport; - break; - } + PermissionObjectKind permObjectKind = PermissionObjectKind.Table; + if (navTypeToPermObjectKind.TryGetValue(appObjTypeSymbol.NavTypeKind, out var kind)) + permObjectKind = kind; - if (!(permissionSymbols.Contains((permObjectKind, permObjectId)) || permissionSymbols.Contains((permObjectKind, 0)) || XmlPermissionExistsForObject(permissionSetDocuments, permObjectKind, permObjectId))) - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0015PermissionSetCoverage, current.GetLocation(), new Object[] { permObjectKind.ToString(), appObjTypeSymbol.Name })); + if (!(permissionSymbols.Contains((permObjectKind, permObjectId)) || permissionSymbols.Contains((permObjectKind, 0)) || XmlPermissionExistsForObject(permissionSetDocuments, permObjectKind, permObjectId))) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0015PermissionSetCoverage, + enumerator.Current.GetLocation(), + permObjectKind.ToString(), + appObjTypeSymbol.Name)); - if (appObjTypeSymbol.NavTypeKind == NavTypeKind.Record) + if (appObjTypeSymbol.NavTypeKind == NavTypeKind.Record) + { + if (((ITableTypeSymbol)appObjTypeSymbol.OriginalDefinition).TableType == TableTypeKind.Normal) { - if (((ITableTypeSymbol)(appObjTypeSymbol.OriginalDefinition)).TableType == TableTypeKind.Normal) - { - permObjectKind = PermissionObjectKind.TableData; - - if (!(permissionSymbols.Contains((permObjectKind, permObjectId)) || permissionSymbols.Contains((permObjectKind, 0)) || XmlPermissionExistsForObject(permissionSetDocuments, permObjectKind, permObjectId))) - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0015PermissionSetCoverage, current.GetLocation(), new Object[] { permObjectKind.ToString(), appObjTypeSymbol.Name })); - } + permObjectKind = PermissionObjectKind.TableData; + + if (!(permissionSymbols.Contains((permObjectKind, permObjectId)) || permissionSymbols.Contains((permObjectKind, 0)) || XmlPermissionExistsForObject(permissionSetDocuments, permObjectKind, permObjectId))) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0015PermissionSetCoverage, + enumerator.Current.GetLocation(), + permObjectKind.ToString(), + appObjTypeSymbol.Name)); } - } } + } - private static ImmutableHashSet<(PermissionObjectKind, int)> GetPermissionSymbols(IModuleSymbol module) + private static ImmutableHashSet<(PermissionObjectKind, int)> GetPermissionSymbols(IModuleSymbol module) + { + ImmutableHashSet<(PermissionObjectKind, int)> immutableHashSet; + IEnumerable symbols = module.GetObjectSymbols(SymbolKind.PermissionSet).Concat(module.GetObjectSymbols(SymbolKind.PermissionSetExtension)); + if (!symbols.Any()) { - ImmutableHashSet<(PermissionObjectKind, int)> immutableHashSet; - IEnumerable symbols = module.GetObjectSymbols(SymbolKind.PermissionSet).Concat(module.GetObjectSymbols(SymbolKind.PermissionSetExtension)); - if (!symbols.Any()) - { - return ImmutableHashSet<(PermissionObjectKind, int)>.Empty; - } - PooledHashSet<(PermissionObjectKind, int)> instance = PooledHashSet<(PermissionObjectKind, int)>.GetInstance(); - try + return ImmutableHashSet<(PermissionObjectKind, int)>.Empty; + } + PooledHashSet<(PermissionObjectKind, int)> instance = PooledHashSet<(PermissionObjectKind, int)>.GetInstance(); + try + { + foreach (ISymbol symbol in symbols) { - foreach (ISymbol symbol in symbols) + ImmutableArray.Enumerator enumerator = ((IPermissionSetSymbol)symbol).Permissions.GetEnumerator(); + while (enumerator.MoveNext()) { - ImmutableArray.Enumerator enumerator = ((IPermissionSetSymbol)symbol).Permissions.GetEnumerator(); - while (enumerator.MoveNext()) - { - IPermissionSymbol current = enumerator.Current; - instance.Add((current.ObjectType, current.ObjectId)); - } + IPermissionSymbol current = enumerator.Current; + instance.Add((current.ObjectType, current.ObjectId)); } - immutableHashSet = instance.ToImmutableHashSet(); - } - finally - { - instance.Free(); } - return immutableHashSet; + immutableHashSet = instance.ToImmutableHashSet(); + } + finally + { + instance.Free(); } + return immutableHashSet; + } - private bool XmlPermissionExistsForObject(IEnumerable permissionSetDocuments, PermissionObjectKind objectType, int objectId) + private bool XmlPermissionExistsForObject(IEnumerable permissionSetDocuments, PermissionObjectKind objectType, int objectId) + { + using (IEnumerator permSetEnumerator = permissionSetDocuments.GetEnumerator()) { - using (IEnumerator permSetEnumerator = permissionSetDocuments.GetEnumerator()) + while (permSetEnumerator.MoveNext()) { - while (permSetEnumerator.MoveNext()) + using (IEnumerator permissionEnumerator = permSetEnumerator.Current.Root.XPathSelectElements(Constants.PermissionNodeXPath).GetEnumerator()) { - using (IEnumerator permissionEnumerator = permSetEnumerator.Current.Root.XPathSelectElements(Constants.PermissionNodeXPath).GetEnumerator()) + while (permissionEnumerator.MoveNext()) { - while (permissionEnumerator.MoveNext()) - { - XElement current = permissionEnumerator.Current; + XElement current = permissionEnumerator.Current; - string xmlObjectType = current.Element("ObjectType").Value; + string xmlObjectType = current.Element("ObjectType").Value; - if (xmlObjectType != objectType.ToString()) - { - int xmlObjectTypeAsInteger = -1; - if (!Int32.TryParse(xmlObjectType, out xmlObjectTypeAsInteger)) - { - continue; - } - if (xmlObjectTypeAsInteger != (int)objectType) - { - continue; - } - } - - int xmlObjectId = -1; - if (!Int32.TryParse(current.Element("ObjectID").Value, out xmlObjectId)) + if (xmlObjectType != objectType.ToString()) + { + int xmlObjectTypeAsInteger = -1; + if (!Int32.TryParse(xmlObjectType, out xmlObjectTypeAsInteger)) { continue; } - if (xmlObjectId != objectId) + if (xmlObjectTypeAsInteger != (int)objectType) { continue; } + } - return true; + int xmlObjectId = -1; + if (!Int32.TryParse(current.Element("ObjectID").Value, out xmlObjectId)) + { + continue; } + if (xmlObjectId != objectId) + { + continue; + } + + return true; } } - return false; } + return false; } } -} +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0016CheckForMissingCaptions.cs b/BusinessCentral.LinterCop/Design/Rule0016CheckForMissingCaptions.cs index 8f392623..9f5f29b3 100644 --- a/BusinessCentral.LinterCop/Design/Rule0016CheckForMissingCaptions.cs +++ b/BusinessCentral.LinterCop/Design/Rule0016CheckForMissingCaptions.cs @@ -1,184 +1,191 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; -using BusinessCentral.LinterCop.Helpers; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0016CheckForMissingCaptions : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0016CheckForMissingCaptions : DiagnosticAnalyzer + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0016CheckForMissingCaptions); + + private static readonly HashSet _predefinedActionCategoryNames = + SyntaxFacts.PredefinedActionCategoryNames.Select(x => x.Key.ToLowerInvariant()).ToHashSet(); + + public override void Initialize(AnalysisContext context) + => context.RegisterSymbolAction(new Action(this.CheckForMissingCaptions), + SymbolKind.Page, + SymbolKind.Query, + SymbolKind.Table, + SymbolKind.Field, + SymbolKind.Action, + SymbolKind.EnumValue, + SymbolKind.Control, + SymbolKind.PermissionSet + ); + + private void CheckForMissingCaptions(SymbolAnalysisContext context) { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0016CheckForMissingCaptions); - - private static readonly HashSet _predefinedActionCategoryNames = SyntaxFacts.PredefinedActionCategoryNames.Select(x => x.Key.ToLowerInvariant()).ToHashSet(); - - public override void Initialize(AnalysisContext context) - => context.RegisterSymbolAction(new Action(this.CheckForMissingCaptions), - SymbolKind.Page, - SymbolKind.Query, - SymbolKind.Table, - SymbolKind.Field, - SymbolKind.Action, - SymbolKind.EnumValue, - SymbolKind.Control, - SymbolKind.PermissionSet - ); - - private void CheckForMissingCaptions(SymbolAnalysisContext context) - { - if (context.IsObsoletePendingOrRemoved()) return; + if (context.IsObsoletePendingOrRemoved()) + return; - if (context.Symbol.Kind == SymbolKind.Control) + if (context.Symbol.Kind == SymbolKind.Control) + { + var Control = (IControlSymbol)context.Symbol; + switch (Control.ControlKind) { - var Control = (IControlSymbol)context.Symbol; - switch (Control.ControlKind) - { - case ControlKind.Field: - if (CaptionIsMissing(context.Symbol, context)) - if (Control.RelatedFieldSymbol != null) - { - if (CaptionIsMissing(Control.RelatedFieldSymbol, context)) - RaiseCaptionWarning(context); - } - else - { - if (!SuppressCaptionWarning(context)) - RaiseCaptionWarning(context); - } - break; + case ControlKind.Field: + if (CaptionIsMissing(context.Symbol, context)) + if (Control.RelatedFieldSymbol is not null) + { + if (CaptionIsMissing(Control.RelatedFieldSymbol, context)) + RaiseCaptionWarning(context); + } + else + { + if (!SuppressCaptionWarning(context)) + RaiseCaptionWarning(context); + } + break; - case ControlKind.Area: - break; + case ControlKind.Area: + break; - case ControlKind.Grid: - break; + case ControlKind.Grid: + break; - case ControlKind.Repeater: - break; + case ControlKind.Repeater: + break; - case ControlKind.Part: - if (CaptionIsMissing(context.Symbol, context)) - if (Control.RelatedPartSymbol != null) - if (CaptionIsMissing(Control.RelatedPartSymbol, context)) - if (!SuppressCaptionWarning(context)) - RaiseCaptionWarning(context); - break; + case ControlKind.Part: + if (CaptionIsMissing(context.Symbol, context)) + if (Control.RelatedPartSymbol is not null) + if (CaptionIsMissing(Control.RelatedPartSymbol, context)) + if (!SuppressCaptionWarning(context)) + RaiseCaptionWarning(context); + break; - case ControlKind.UserControl: - break; + case ControlKind.UserControl: + break; - case ControlKind.SystemPart: - break; + case ControlKind.SystemPart: + break; - default: - if (CaptionIsMissing(context.Symbol, context)) - RaiseCaptionWarning(context); - break; - } + default: + if (CaptionIsMissing(context.Symbol, context)) + RaiseCaptionWarning(context); + break; } - else if (context.Symbol is IActionSymbol actionSymbol) + } + else if (context.Symbol is IActionSymbol actionSymbol) + { + switch (actionSymbol.ActionKind) { - switch (actionSymbol.ActionKind) - { - case ActionKind.Action: - if (CaptionIsMissing(context.Symbol, context)) - RaiseCaptionWarning(context); - break; - - case ActionKind.Group: - if (context.Symbol.GetEnumPropertyValue(PropertyKind.ShowAs) == ShowAsKind.SplitButton) - { - // There is one specifc case where a Caption is needed on a Group where the property ShowAs is set to SplitButton - // A) The group is inside a Promoted Area - // B) Has one or more actionrefs - // C) One of the actions of the actionsrefs has Scope set to Repeater - - if (context.Symbol.ContainingSymbol is not IActionSymbol containingSymbol) - return; + case ActionKind.Action: + if (CaptionIsMissing(context.Symbol, context)) + RaiseCaptionWarning(context); + break; - if (containingSymbol.ActionKind != ActionKind.Area) - break; + case ActionKind.Group: + if (context.Symbol.GetEnumPropertyValue(PropertyKind.ShowAs) == ShowAsKind.SplitButton) + { + // There is one specifc case where a Caption is needed on a Group where the property ShowAs is set to SplitButton + // A) The group is inside a Promoted Area + // B) Has one or more actionrefs + // C) One of the actions of the actionsrefs has Scope set to Repeater - if (!SemanticFacts.IsSameName(context.Symbol.ContainingSymbol.Name, "Promoted")) - break; + if (context.Symbol.ContainingSymbol is not IActionSymbol containingSymbol) + return; - if (!actionSymbol.Actions.Where(a => a.ActionKind == ActionKind.ActionRef) - .Where(a => a.Target.GetEnumPropertyValueOrDefault(PropertyKind.Scope) == PageActionScopeKind.Repeater) - .Any()) - break; + if (containingSymbol.ActionKind != ActionKind.Area) + break; - if (CaptionIsMissing(context.Symbol, context)) - RaiseCaptionWarning(context); + if (!SemanticFacts.IsSameName(context.Symbol.ContainingSymbol.Name, "Promoted")) break; - } - else - { - if (CaptionIsMissing(context.Symbol, context)) - RaiseCaptionWarning(context); + + if (!actionSymbol.Actions.Where(a => a.ActionKind == ActionKind.ActionRef) + .Where(a => a.Target?.GetEnumPropertyValueOrDefault(PropertyKind.Scope) == PageActionScopeKind.Repeater) + .Any()) break; - } - } + + if (CaptionIsMissing(context.Symbol, context)) + RaiseCaptionWarning(context); + break; + } + else + { + if (CaptionIsMissing(context.Symbol, context)) + RaiseCaptionWarning(context); + break; + } } - else if (context.Symbol.Kind == SymbolKind.EnumValue) - { - IEnumValueSymbol enumValueSymbol = (IEnumValueSymbol)context.Symbol; - if (enumValueSymbol.Name != "" && CaptionIsMissing(context.Symbol, context)) + } + else if (context.Symbol.Kind == SymbolKind.EnumValue) + { + IEnumValueSymbol enumValueSymbol = (IEnumValueSymbol)context.Symbol; + if (enumValueSymbol.Name != "" && CaptionIsMissing(context.Symbol, context)) + RaiseCaptionWarning(context); + } + else if (context.Symbol.Kind == SymbolKind.Page) + { + if (((IPageTypeSymbol)context.Symbol).PageType != PageTypeKind.API) + if (CaptionIsMissing(context.Symbol, context)) RaiseCaptionWarning(context); - } - else if (context.Symbol.Kind == SymbolKind.Page) - { - if (((IPageTypeSymbol)context.Symbol).PageType != PageTypeKind.API) - if (CaptionIsMissing(context.Symbol, context)) - RaiseCaptionWarning(context); - } - else if (context.Symbol.Kind == SymbolKind.PermissionSet) - { - IPropertySymbol assignableProperty = context.Symbol.GetProperty(PropertyKind.Assignable); - if (assignableProperty == null || (bool)assignableProperty.Value) - if (CaptionIsMissing(context.Symbol, context)) - RaiseCaptionWarning(context); - } - else - { + } + else if (context.Symbol.Kind == SymbolKind.PermissionSet) + { + IPropertySymbol? assignableProperty = context.Symbol.GetProperty(PropertyKind.Assignable); + if (assignableProperty is null || (bool)assignableProperty.Value) if (CaptionIsMissing(context.Symbol, context)) RaiseCaptionWarning(context); - } } + else + { + if (CaptionIsMissing(context.Symbol, context)) + RaiseCaptionWarning(context); + } + } - private bool CaptionIsMissing(ISymbol Symbol, SymbolAnalysisContext context) + private bool CaptionIsMissing(ISymbol Symbol, SymbolAnalysisContext context) + { + if (Symbol.ContainingType?.Kind == SymbolKind.Table) { - if (Symbol.ContainingType?.Kind == SymbolKind.Table) - { - if (((ITableTypeSymbol)Symbol.ContainingType).Id >= 2000000000) - return false; - if (((IFieldSymbol)Symbol).Id >= 2000000000) - return false; - } + if (((ITableTypeSymbol)Symbol.ContainingType).Id >= 2000000000) + return false; - if (Symbol.Kind == SymbolKind.Action && ((IActionSymbol)Symbol).ActionKind == ActionKind.Group && _predefinedActionCategoryNames.Contains(Symbol.Name.ToLowerInvariant())) + if (((IFieldSymbol)Symbol).Id >= 2000000000) return false; + } - if (Symbol.GetBooleanPropertyValue(PropertyKind.ShowCaption) != false) - if (Symbol.GetProperty(PropertyKind.Caption) == null && Symbol.GetProperty(PropertyKind.CaptionClass) == null && Symbol.GetProperty(PropertyKind.CaptionML) == null) - return true; + if (Symbol.Kind == SymbolKind.Action && ((IActionSymbol)Symbol).ActionKind == ActionKind.Group && _predefinedActionCategoryNames.Contains(Symbol.Name.ToLowerInvariant())) return false; - } - private static bool SuppressCaptionWarning(SymbolAnalysisContext context) - { - if (context.Symbol.GetContainingObjectTypeSymbol().GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Page) return false; - IPageTypeSymbol pageTypeSymbol = (IPageTypeSymbol)context.Symbol.GetContainingObjectTypeSymbol(); - if (pageTypeSymbol.GetNavTypeKindSafe() != NavTypeKind.Page || pageTypeSymbol.PageType != PageTypeKind.API) return false; - LinterSettings.Create(context.Compilation.FileSystem.GetDirectoryPath()); - return !LinterSettings.instance.enableRule0016ForApiObjects; - } + if (Symbol.GetBooleanPropertyValue(PropertyKind.ShowCaption) != false) + if (Symbol.GetProperty(PropertyKind.Caption) is null && Symbol.GetProperty(PropertyKind.CaptionClass) is null && Symbol.GetProperty(PropertyKind.CaptionML) is null) + return true; + return false; + } - private void RaiseCaptionWarning(SymbolAnalysisContext context) - { - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0016CheckForMissingCaptions, context.Symbol.GetLocation())); - } + private static bool SuppressCaptionWarning(SymbolAnalysisContext context) + { + if (context.Symbol.GetContainingObjectTypeSymbol().GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Page) + return false; + + IPageTypeSymbol pageTypeSymbol = (IPageTypeSymbol)context.Symbol.GetContainingObjectTypeSymbol(); + if (pageTypeSymbol.GetNavTypeKindSafe() != NavTypeKind.Page || pageTypeSymbol.PageType != PageTypeKind.API) + return false; + + LinterSettings.Create(context.Compilation.FileSystem!.GetDirectoryPath()); + return !LinterSettings.instance.enableRule0016ForApiObjects; + } + + private void RaiseCaptionWarning(SymbolAnalysisContext context) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0016CheckForMissingCaptions, + context.Symbol.GetLocation())); } -} +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0017WriteToFlowField.cs b/BusinessCentral.LinterCop/Design/Rule0017WriteToFlowField.cs index 9e36d01b..eaf79701 100644 --- a/BusinessCentral.LinterCop/Design/Rule0017WriteToFlowField.cs +++ b/BusinessCentral.LinterCop/Design/Rule0017WriteToFlowField.cs @@ -1,4 +1,4 @@ -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Semantics; diff --git a/BusinessCentral.LinterCop/Design/Rule0018NoEventsInInternalCodeunits.cs b/BusinessCentral.LinterCop/Design/Rule0018NoEventsInInternalCodeunits.cs index e863da48..f8968daa 100644 --- a/BusinessCentral.LinterCop/Design/Rule0018NoEventsInInternalCodeunits.cs +++ b/BusinessCentral.LinterCop/Design/Rule0018NoEventsInInternalCodeunits.cs @@ -1,54 +1,52 @@ -#nullable disable // TODO: Enable nullable and review rule +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.InternalSyntax; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0018NoEventsInInternalCodeunits : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0018NoEventsInInternalCodeunits : DiagnosticAnalyzer + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0018NoEventsInInternalCodeunitsAnalyzerDescriptor); + + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(new Action(CheckPublicEventInInternalCodeunit), SymbolKind.Method); + + private void CheckPublicEventInInternalCodeunit(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Symbol is not IMethodSymbol methodSymbol) + return; + + if (!methodSymbol.IsEvent) + return; + + IApplicationObjectTypeSymbol? applicationObject = methodSymbol.GetContainingApplicationObjectTypeSymbol(); + if (applicationObject is null || !IsInternalCodeunit(applicationObject)) + return; + + IAttributeSymbol? attributeSymbol; + if (!TryGetEventAttribute(methodSymbol, out attributeSymbol) || attributeSymbol?.AttributeKind == AttributeKind.InternalEvent) + return; + + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0018NoEventsInInternalCodeunitsAnalyzerDescriptor, + methodSymbol.GetLocation(), + methodSymbol.Name, + applicationObject.Name)); + } + + private bool IsInternalCodeunit(IApplicationObjectTypeSymbol applicationObject) => + applicationObject is ICodeunitTypeSymbol && + applicationObject.DeclaredAccessibility == Accessibility.Internal && + !applicationObject.IsObsoletePendingOrRemoved(); + + private bool TryGetEventAttribute(IMethodSymbol methodSymbol, out IAttributeSymbol? attribute) { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0018NoEventsInInternalCodeunitsAnalyzerDescriptor); - - public override void Initialize(AnalysisContext context) - { - context.RegisterSymbolAction(new Action(CheckPublicEventInInternalCodeunit), SymbolKind.Method); - } - - private void CheckPublicEventInInternalCodeunit(SymbolAnalysisContext symbolAnalysisContext) - { - IMethodSymbol methodSymbol = symbolAnalysisContext.Symbol as IMethodSymbol; - if (methodSymbol == null || !methodSymbol.IsEvent || methodSymbol.IsObsoleteRemoved || methodSymbol.IsObsoletePending) - return; - - IApplicationObjectTypeSymbol applicationObject = methodSymbol.GetContainingApplicationObjectTypeSymbol(); - if (!(applicationObject is ICodeunitTypeSymbol) || applicationObject.DeclaredAccessibility != Accessibility.Internal || applicationObject.IsObsoleteRemoved || applicationObject.IsObsoletePending) - return; - - IAttributeSymbol attributeSymbol; - if (!TryGetEventAttribute(methodSymbol, out attributeSymbol) || attributeSymbol.AttributeKind == AttributeKind.InternalEvent) - return; - - symbolAnalysisContext.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0018NoEventsInInternalCodeunitsAnalyzerDescriptor, methodSymbol.GetLocation(), new Object[] { methodSymbol.Name, applicationObject.Name })); - } - - private bool TryGetEventAttribute(IMethodSymbol methodSymbol, out IAttributeSymbol attribute) - { - ImmutableArray.Enumerator enumerator = methodSymbol.Attributes.GetEnumerator(); - while (enumerator.MoveNext()) - { - IAttributeSymbol current = enumerator.Current; - AttributeKind attributeKind = current.AttributeKind; - if (attributeKind == AttributeKind.IntegrationEvent) - { - attribute = current; - return true; - } - } - attribute = null; - return false; - } + attribute = methodSymbol.Attributes.FirstOrDefault(attr => attr.AttributeKind == AttributeKind.IntegrationEvent); + return attribute is not null; } -} +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0019DataClassificationFieldEqualsTable.cs b/BusinessCentral.LinterCop/Design/Rule0019DataClassificationFieldEqualsTable.cs index 24999a1b..00a43af4 100644 --- a/BusinessCentral.LinterCop/Design/Rule0019DataClassificationFieldEqualsTable.cs +++ b/BusinessCentral.LinterCop/Design/Rule0019DataClassificationFieldEqualsTable.cs @@ -1,40 +1,39 @@ -#nullable disable // TODO: Enable nullable and review rule +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design -{ - [DiagnosticAnalyzer] - public class Rule0019DataClassificationFieldEqualsTable : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0019DataClassificationFieldEqualsTable); +namespace BusinessCentral.LinterCop.Design; - public override void Initialize(AnalysisContext context) - { - context.RegisterSymbolAction(new Action(CheckDataClassificationRedundancy), SymbolKind.Field); - } +[DiagnosticAnalyzer] +public class Rule0019DataClassificationFieldEqualsTable : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0019DataClassificationFieldEqualsTable); - private void CheckDataClassificationRedundancy(SymbolAnalysisContext symbolAnalysisContext) - { - IFieldSymbol Field = (IFieldSymbol)symbolAnalysisContext.Symbol; - if (Field == null || Field.IsObsoleteRemoved || Field.IsObsoletePending) - return; + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(new Action(CheckDataClassificationRedundancy), SymbolKind.Field); - IApplicationObjectTypeSymbol applicationObject = Field.GetContainingApplicationObjectTypeSymbol(); - if (!(applicationObject is ITableTypeSymbol) || applicationObject.IsObsoleteRemoved || applicationObject.IsObsoletePending) - return; + private void CheckDataClassificationRedundancy(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Symbol is not IFieldSymbol field) + return; - ITableTypeSymbol Table = (ITableTypeSymbol)Field.ContainingSymbol; - IPropertySymbol fieldClassification = Field.GetProperty(PropertyKind.DataClassification) as IPropertySymbol; - IPropertySymbol tableClassification = Table.GetProperty(PropertyKind.DataClassification) as IPropertySymbol; + IApplicationObjectTypeSymbol? applicationObject = field.GetContainingApplicationObjectTypeSymbol(); + if (applicationObject is not ITableTypeSymbol || applicationObject.IsObsoletePendingOrRemoved() || field.ContainingSymbol is not ITableTypeSymbol table) + return; - if (fieldClassification == null || tableClassification == null) - return; + IPropertySymbol? fieldClassification = field.GetProperty(PropertyKind.DataClassification); + if (fieldClassification is null) + return; - if (fieldClassification.ValueText == tableClassification.ValueText) - symbolAnalysisContext.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0019DataClassificationFieldEqualsTable, fieldClassification.GetLocation())); - } + IPropertySymbol? tableClassification = table.GetProperty(PropertyKind.DataClassification); + if (tableClassification is null) + return; + if (fieldClassification.ValueText == tableClassification.ValueText) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0019DataClassificationFieldEqualsTable, + fieldClassification.GetLocation())); } -} +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0020ApplicationAreaEqualsToPage.cs b/BusinessCentral.LinterCop/Design/Rule0020ApplicationAreaEqualsToPage.cs index a1fea75a..0e52dc93 100644 --- a/BusinessCentral.LinterCop/Design/Rule0020ApplicationAreaEqualsToPage.cs +++ b/BusinessCentral.LinterCop/Design/Rule0020ApplicationAreaEqualsToPage.cs @@ -1,41 +1,40 @@ -#nullable disable // TODO: Enable nullable and review rule +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design -{ - [DiagnosticAnalyzer] - public class Rule0020ApplicationAreaEqualsToPage : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0020ApplicationAreaEqualsToPage); - public override VersionCompatibility SupportedVersions { get; } = VersionCompatibility.Fall2022OrGreater; +namespace BusinessCentral.LinterCop.Design; - public override void Initialize(AnalysisContext context) - { - context.RegisterSymbolAction(new Action(CheckDataClassificationRedundancy), SymbolKind.Control); - } +[DiagnosticAnalyzer] +public class Rule0020ApplicationAreaEqualsToPage : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0020ApplicationAreaEqualsToPage); + public override VersionCompatibility SupportedVersions { get; } = VersionCompatibility.Fall2022OrGreater; - private void CheckDataClassificationRedundancy(SymbolAnalysisContext symbolAnalysisContext) - { - IControlSymbol Control = (IControlSymbol)symbolAnalysisContext.Symbol; - if (Control == null || Control.IsObsoleteRemoved || Control.IsObsoletePending) - return; + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(new Action(CheckDataClassificationRedundancy), SymbolKind.Control); - IApplicationObjectTypeSymbol applicationObject = Control.GetContainingApplicationObjectTypeSymbol(); - if (!(applicationObject is IPageTypeSymbol) || applicationObject.IsObsoleteRemoved || applicationObject.IsObsoletePending) - return; + private void CheckDataClassificationRedundancy(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Symbol is not IControlSymbol control) + return; - IPageTypeSymbol Page = (IPageTypeSymbol)applicationObject; - IPropertySymbol controlApplicationArea = Control.GetProperty(PropertyKind.ApplicationArea) as IPropertySymbol; - IPropertySymbol pageApplicationArea = Page.GetProperty(PropertyKind.ApplicationArea) as IPropertySymbol; + IApplicationObjectTypeSymbol? applicationObject = control.GetContainingApplicationObjectTypeSymbol(); + if (applicationObject is not IPageTypeSymbol page || applicationObject.IsObsoletePendingOrRemoved()) + return; - if (controlApplicationArea == null || pageApplicationArea == null) - return; + IPropertySymbol? controlApplicationArea = control.GetProperty(PropertyKind.ApplicationArea); + if (controlApplicationArea is null) + return; - if (pageApplicationArea.ValueText == controlApplicationArea.ValueText) - symbolAnalysisContext.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0020ApplicationAreaEqualsToPage, controlApplicationArea.GetLocation())); - } + IPropertySymbol? pageApplicationArea = page.GetProperty(PropertyKind.ApplicationArea); + if (pageApplicationArea is null) + return; + if (pageApplicationArea.ValueText == controlApplicationArea.ValueText) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0020ApplicationAreaEqualsToPage, + controlApplicationArea.GetLocation())); } -} +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0021BuiltInMethodImplementThroughCodeunit.cs b/BusinessCentral.LinterCop/Design/Rule0021BuiltInMethodImplementThroughCodeunit.cs index 56fad7fd..08f0fd64 100644 --- a/BusinessCentral.LinterCop/Design/Rule0021BuiltInMethodImplementThroughCodeunit.cs +++ b/BusinessCentral.LinterCop/Design/Rule0021BuiltInMethodImplementThroughCodeunit.cs @@ -1,49 +1,50 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class BuiltInMethodImplementThroughCodeunit : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class BuiltInMethodImplementThroughCodeunit : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create( DiagnosticDescriptors.Rule0021ConfirmImplementConfirmManagement, DiagnosticDescriptors.Rule0022GlobalLanguageImplementTranslationHelper ); - public override void Initialize(AnalysisContext context) - { - context.RegisterOperationAction(new Action(this.AnalyzeConfirm), OperationKind.InvocationExpression); - context.RegisterOperationAction(new Action(this.AnalyzeGlobalLanguage), OperationKind.InvocationExpression); - } - - private void AnalyzeConfirm(OperationAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(new Action(this.AnalyzeInvocation), OperationKind.InvocationExpression); - IInvocationExpression operation = (IInvocationExpression)ctx.Operation; - if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return; - if (!SemanticFacts.IsSameName(operation.TargetMethod.Name, "Confirm")) return; - - if (ctx.ContainingSymbol.GetContainingObjectTypeSymbol().NavTypeKind == NavTypeKind.Page && - ((IPageTypeSymbol)ctx.ContainingSymbol.GetContainingObjectTypeSymbol()).PageType != PageTypeKind.API) return; + private void AnalyzeInvocation(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) + return; - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0021ConfirmImplementConfirmManagement, ctx.Operation.Syntax.GetLocation())); - } + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) + return; - private void AnalyzeGlobalLanguage(OperationAnalysisContext ctx) + switch (operation.TargetMethod.Name) { - if (ctx.IsObsoletePendingOrRemoved()) return; - - IInvocationExpression operation = (IInvocationExpression)ctx.Operation; - if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return; - if (!SemanticFacts.IsSameName(operation.TargetMethod.Name, "GlobalLanguage")) return; - if (operation.Arguments.Length == 0) return; - - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0022GlobalLanguageImplementTranslationHelper, ctx.Operation.Syntax.GetLocation())); + case "Confirm": + if (ctx.ContainingSymbol.GetContainingObjectTypeSymbol().NavTypeKind == NavTypeKind.Page && + ((IPageTypeSymbol)ctx.ContainingSymbol.GetContainingObjectTypeSymbol()).PageType != PageTypeKind.API) + return; + + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0021ConfirmImplementConfirmManagement, + ctx.Operation.Syntax.GetLocation())); + break; + + case "GlobalLanguage": + if (operation.Arguments.Length == 0) + return; + + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0022GlobalLanguageImplementTranslationHelper, + ctx.Operation.Syntax.GetLocation())); + break; } } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0023AlwaysSpecifyFieldgroups.cs b/BusinessCentral.LinterCop/Design/Rule0023AlwaysSpecifyFieldgroups.cs index 857c08da..ca7eac8a 100644 --- a/BusinessCentral.LinterCop/Design/Rule0023AlwaysSpecifyFieldgroups.cs +++ b/BusinessCentral.LinterCop/Design/Rule0023AlwaysSpecifyFieldgroups.cs @@ -1,61 +1,60 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; using Microsoft.Dynamics.Nav.CodeAnalysis.Text; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0023AlwaysSpecifyFieldgroups : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0023AlwaysSpecifyFieldgroups : DiagnosticAnalyzer + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0023AlwaysSpecifyFieldgroups, DiagnosticDescriptors.Rule0000ErrorInRule); + + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(new Action(this.CheckFieldgroups), SymbolKind.Table); + + private void CheckFieldgroups(SymbolAnalysisContext ctx) { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0023AlwaysSpecifyFieldgroups, DiagnosticDescriptors.Rule0000ErrorInRule); + if (ctx.IsObsoletePendingOrRemoved() || ctx.Symbol is not ITableTypeSymbol table) + return; - public override void Initialize(AnalysisContext context) => context.RegisterSymbolAction(new Action(this.CheckFieldgroups), SymbolKind.Table); + if (IsTableOfTypeSetupTable(table)) + return; - private void CheckFieldgroups(SymbolAnalysisContext ctx) + Location FieldGroupLocation = table.GetLocation(); + if (!table.Keys.IsEmpty) { - if (ctx.IsObsoletePendingOrRemoved()) return; - - try - { - ITableTypeSymbol table = (ITableTypeSymbol)ctx.Symbol; - if (IsTableOfTypeSetupTable(table)) return; - - Location FieldGroupLocation = table.GetLocation(); - if (!table.Keys.IsEmpty) - { - FieldGroupLocation = table.Keys.Last().GetLocation(); - var span = FieldGroupLocation.SourceSpan; - FieldGroupLocation = Location.Create(FieldGroupLocation.SourceTree, new TextSpan(span.End + 9, 1)); //Should result in the blank line right after the keys section - } - - if (!table.FieldGroups.Any(item => (item.Name == "Brick"))) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0023AlwaysSpecifyFieldgroups, FieldGroupLocation, "Brick", table.Name)); - - if (!table.FieldGroups.Any(item => (item.Name == "DropDown"))) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0023AlwaysSpecifyFieldgroups, FieldGroupLocation, "DropDown", table.Name)); - } - catch (ArgumentOutOfRangeException) - { - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0000ErrorInRule, ctx.Symbol.GetLocation(), new Object[] { "Rule0023", "ArgumentOutOfRangeException", "" })); - } + FieldGroupLocation = table.Keys.Last().GetLocation(); + var span = FieldGroupLocation.SourceSpan; + FieldGroupLocation = Location.Create(FieldGroupLocation.SourceTree!, new TextSpan(span.End + 9, 1)); // Should result in the blank line right after the keys section } - private static bool IsTableOfTypeSetupTable(ITableTypeSymbol table) - { - // Expect Primary Key to contains only one field - if (table.PrimaryKey is null || table.PrimaryKey.Fields.Length != 1) return false; + if (!table.FieldGroups.Any(item => item.Name == "Brick")) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0023AlwaysSpecifyFieldgroups, FieldGroupLocation, "Brick", table.Name)); - // The field should be of type Code - if (table.PrimaryKey.Fields[0].GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Code) return false; + if (!table.FieldGroups.Any(item => item.Name == "DropDown")) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0023AlwaysSpecifyFieldgroups, FieldGroupLocation, "DropDown", table.Name)); + } - // The field should be exactly (case sensitive) called 'Primary Key' - if (table.PrimaryKey.Fields[0].Name != "Primary Key") return false; + private static bool IsTableOfTypeSetupTable(ITableTypeSymbol table) + { + // Expect Primary Key to contains only one field + if (table.PrimaryKey is null || table.PrimaryKey.Fields.Length != 1) + return false; - return (true); - } + // The field should be of type Code + if (table.PrimaryKey.Fields[0].GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Code) + return false; + + // The field should be exactly (case sensitive) called 'Primary Key' + if (table.PrimaryKey.Fields[0].Name != "Primary Key") + return false; + + return true; } } diff --git a/BusinessCentral.LinterCop/Design/Rule0024SemicolonAfterProcedureDeclaration.cs b/BusinessCentral.LinterCop/Design/Rule0024SemicolonAfterProcedureDeclaration.cs index 3e8a6778..8b29f705 100644 --- a/BusinessCentral.LinterCop/Design/Rule0024SemicolonAfterProcedureDeclaration.cs +++ b/BusinessCentral.LinterCop/Design/Rule0024SemicolonAfterProcedureDeclaration.cs @@ -1,30 +1,32 @@ -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design -{ - [DiagnosticAnalyzer] - public class Rule0024SemicolonAfterMethodOrTriggerDeclaration : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0024SemicolonAfterMethodOrTriggerDeclaration); +namespace BusinessCentral.LinterCop.Design; - public override void Initialize(AnalysisContext context) => context.RegisterSyntaxNodeAction(new Action(this.AnalyzeSemicolonAfterMethodOrTriggerDeclaration), SyntaxKind.MethodDeclaration, SyntaxKind.TriggerDeclaration); +[DiagnosticAnalyzer] +public class Rule0024SemicolonAfterMethodOrTriggerDeclaration : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0024SemicolonAfterMethodOrTriggerDeclaration); - private void AnalyzeSemicolonAfterMethodOrTriggerDeclaration(SyntaxNodeAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) - return; + public override void Initialize(AnalysisContext context) => + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeSemicolonAfterMethodOrTriggerDeclaration), + SyntaxKind.MethodDeclaration, + SyntaxKind.TriggerDeclaration); - if (ctx.Node is not MethodOrTriggerDeclarationSyntax syntax) - return; + private void AnalyzeSemicolonAfterMethodOrTriggerDeclaration(SyntaxNodeAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Node is not MethodOrTriggerDeclarationSyntax syntax) + return; - if (syntax.SemicolonToken.Kind != SyntaxKind.None) - { - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0024SemicolonAfterMethodOrTriggerDeclaration, syntax.SemicolonToken.GetLocation())); - } + if (syntax.SemicolonToken.Kind != SyntaxKind.None) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0024SemicolonAfterMethodOrTriggerDeclaration, + syntax.SemicolonToken.GetLocation())); } } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0025InternalProcedureModifier.cs b/BusinessCentral.LinterCop/Design/Rule0025InternalProcedureModifier.cs index 61f46765..00c7c944 100644 --- a/BusinessCentral.LinterCop/Design/Rule0025InternalProcedureModifier.cs +++ b/BusinessCentral.LinterCop/Design/Rule0025InternalProcedureModifier.cs @@ -1,39 +1,39 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0025InternalProcedureModifier : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0025InternalProcedureModifier : DiagnosticAnalyzer + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0025InternalProcedureModifier); + + public override void Initialize(AnalysisContext context) => + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeInternalProcedures), SyntaxKind.MethodDeclaration); + + private void AnalyzeInternalProcedures(SyntaxNodeAnalysisContext ctx) { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0025InternalProcedureModifier); - - public override void Initialize(AnalysisContext context) => context.RegisterSyntaxNodeAction(new Action(this.AnalyzeInternalProcedures), SyntaxKind.MethodDeclaration); - - private void AnalyzeInternalProcedures(SyntaxNodeAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; - - if (ctx.ContainingSymbol.GetContainingObjectTypeSymbol().DeclaredAccessibility != Accessibility.Public) return; - if (!ctx.Node.IsKind(SyntaxKind.MethodDeclaration)) return; - - try - { - MethodDeclarationSyntax methodDeclarationSyntax = (MethodDeclarationSyntax)ctx.Node; - SyntaxNodeOrToken accessModifier = methodDeclarationSyntax.ProcedureKeyword.GetPreviousToken(); - if (accessModifier.Kind == SyntaxKind.LocalKeyword || accessModifier.Kind == SyntaxKind.InternalKeyword) return; - if (methodDeclarationSyntax.GetLeadingTrivia().Where(x => x.Kind == SyntaxKind.SingleLineDocumentationCommentTrivia).Any()) return; - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0025InternalProcedureModifier, methodDeclarationSyntax.ProcedureKeyword.GetLocation())); - } - catch (System.InvalidCastException) - { - return; - } - } + if (ctx.IsObsoletePendingOrRemoved() || ctx.Node is not MethodDeclarationSyntax methodDeclarationSyntax) + return; + + if (!ctx.Node.IsKind(SyntaxKind.MethodDeclaration) || ctx.ContainingSymbol.GetContainingObjectTypeSymbol().DeclaredAccessibility != Accessibility.Public) + return; + + SyntaxNodeOrToken accessModifier = methodDeclarationSyntax.ProcedureKeyword.GetPreviousToken(); + + if (accessModifier.Kind == SyntaxKind.LocalKeyword || accessModifier.Kind == SyntaxKind.InternalKeyword) + return; + + if (methodDeclarationSyntax.GetLeadingTrivia().Where(x => x.Kind == SyntaxKind.SingleLineDocumentationCommentTrivia).Any()) + return; + + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0025InternalProcedureModifier, + methodDeclarationSyntax.ProcedureKeyword.GetLocation())); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0026ToolTipPunctuation.cs b/BusinessCentral.LinterCop/Design/Rule0026ToolTipPunctuation.cs index d47afeb4..0c17c06f 100644 --- a/BusinessCentral.LinterCop/Design/Rule0026ToolTipPunctuation.cs +++ b/BusinessCentral.LinterCop/Design/Rule0026ToolTipPunctuation.cs @@ -1,74 +1,91 @@ using System.Collections.Immutable; -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0026ToolTipPunctuation : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0026ToolTipPunctuation : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0026ToolTipMustEndWithDot, DiagnosticDescriptors.Rule0036ToolTipShouldStartWithSpecifies, DiagnosticDescriptors.Rule0037ToolTipDoNotUseLineBreaks, DiagnosticDescriptors.Rule0038ToolTipMaximumLength); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create( + DiagnosticDescriptors.Rule0026ToolTipMustEndWithDot, + DiagnosticDescriptors.Rule0036ToolTipShouldStartWithSpecifies, + DiagnosticDescriptors.Rule0037ToolTipDoNotUseLineBreaks, + DiagnosticDescriptors.Rule0038ToolTipMaximumLength); - // https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/user-assistance#guidelines-for-tooltip-text - // Try to not exceed 200 characters including spaces. - // Including the double quote at the beginning and end of the string, makes this a total of 202 - private const int MaxTooltipLength = 202; + // https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/user-assistance#guidelines-for-tooltip-text + // Try to not exceed 200 characters including spaces. + // Including the double quote at the beginning and end of the string, makes this a total of 202 + private const int MaxTooltipLength = 202; - public override void Initialize(AnalysisContext context) => context.RegisterSyntaxNodeAction(new Action(this.AnalyzeToolTipPunctuation), SyntaxKind.PageField, SyntaxKind.PageAction, SyntaxKind.Field); + public override void Initialize(AnalysisContext context) => + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeToolTipPunctuation), + SyntaxKind.PageField, + SyntaxKind.PageAction, + SyntaxKind.Field); - private void AnalyzeToolTipPunctuation(SyntaxNodeAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) - return; + private void AnalyzeToolTipPunctuation(SyntaxNodeAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) + return; - var tooltipProperty = ctx.Node.GetPropertyValue(PropertyKind.ToolTip); - if (tooltipProperty == null) - return; + var tooltipProperty = ctx.Node.GetPropertyValue(PropertyKind.ToolTip); + if (tooltipProperty is null) + return; - if (tooltipProperty is not LabelPropertyValueSyntax labelPropertyValueSyntax) - return; + if (tooltipProperty is not LabelPropertyValueSyntax labelPropertyValueSyntax) + return; - string tooltipText = labelPropertyValueSyntax.Value.LabelText.Value.ToString(); + string tooltipText = labelPropertyValueSyntax.Value.LabelText.Value.ToString(); - CheckEndsWithDot(ctx, tooltipText, tooltipProperty); - CheckStartsWithSpecifies(ctx, tooltipText, tooltipProperty); - CheckNoLineBreaks(ctx, tooltipText, tooltipProperty); - CheckMaximumLength(ctx, tooltipText, tooltipProperty); - } + CheckEndsWithDot(ctx, tooltipText, tooltipProperty); + CheckStartsWithSpecifies(ctx, tooltipText, tooltipProperty); + CheckNoLineBreaks(ctx, tooltipText, tooltipProperty); + CheckMaximumLength(ctx, tooltipText, tooltipProperty); + } - private static void CheckEndsWithDot(SyntaxNodeAnalysisContext ctx, string tooltipText, PropertyValueSyntax tooltipProperty) + private static void CheckEndsWithDot(SyntaxNodeAnalysisContext ctx, string tooltipText, PropertyValueSyntax tooltipProperty) + { + if (!tooltipText.EndsWith(".'", StringComparison.OrdinalIgnoreCase)) { - if (!tooltipText.EndsWith(".'", StringComparison.OrdinalIgnoreCase)) - { - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0026ToolTipMustEndWithDot, tooltipProperty.GetLocation())); - } + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0026ToolTipMustEndWithDot, + tooltipProperty.GetLocation())); } + } - private static void CheckStartsWithSpecifies(SyntaxNodeAnalysisContext ctx, string tooltipText, PropertyValueSyntax tooltipProperty) + private static void CheckStartsWithSpecifies(SyntaxNodeAnalysisContext ctx, string tooltipText, PropertyValueSyntax tooltipProperty) + { + if (ctx.ContainingSymbol.Kind == SymbolKind.Control && + ((IControlSymbol)ctx.ContainingSymbol).ControlKind == ControlKind.Field && + !tooltipText.StartsWith("'Specifies", StringComparison.Ordinal)) { - if (ctx.ContainingSymbol.Kind == SymbolKind.Control && - ((IControlSymbol)ctx.ContainingSymbol).ControlKind == ControlKind.Field && - !tooltipText.StartsWith("'Specifies", StringComparison.Ordinal)) - { - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0036ToolTipShouldStartWithSpecifies, tooltipProperty.GetLocation())); - } + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0036ToolTipShouldStartWithSpecifies, + tooltipProperty.GetLocation())); } + } - private static void CheckNoLineBreaks(SyntaxNodeAnalysisContext ctx, string tooltipText, PropertyValueSyntax tooltipProperty) + private static void CheckNoLineBreaks(SyntaxNodeAnalysisContext ctx, string tooltipText, PropertyValueSyntax tooltipProperty) + { + if (tooltipText.Contains("\\")) { - if (tooltipText.Contains("\\")) - { - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0037ToolTipDoNotUseLineBreaks, tooltipProperty.GetLocation())); - } + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0037ToolTipDoNotUseLineBreaks, + tooltipProperty.GetLocation())); } - private static void CheckMaximumLength(SyntaxNodeAnalysisContext ctx, string tooltipText, PropertyValueSyntax tooltipProperty) + } + + private static void CheckMaximumLength(SyntaxNodeAnalysisContext ctx, string tooltipText, PropertyValueSyntax tooltipProperty) + { + if (tooltipText.Length > MaxTooltipLength) { - if (tooltipText.Length > MaxTooltipLength) - { - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0038ToolTipMaximumLength, tooltipProperty.GetLocation())); - } + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0038ToolTipMaximumLength, + tooltipProperty.GetLocation())); } } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0027RunPageImplementPageManagement.cs b/BusinessCentral.LinterCop/Design/Rule0027RunPageImplementPageManagement.cs index 3658478f..998b07e8 100644 --- a/BusinessCentral.LinterCop/Design/Rule0027RunPageImplementPageManagement.cs +++ b/BusinessCentral.LinterCop/Design/Rule0027RunPageImplementPageManagement.cs @@ -1,116 +1,102 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design -{ - [DiagnosticAnalyzer] - public class Rule0027RunPageImplementPageManagement : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0027RunPageImplementPageManagement); +namespace BusinessCentral.LinterCop.Design; - private static readonly Dictionary _supportedRecords = new Dictionary - { - { 36, "Sales Header" }, - { 38, "Purchase Header" }, - { 79, "Company Information" }, - { 80, "Gen. Journal Template" }, - { 81, "Gen. Journal Line" }, - { 91, "User Setup" }, - { 98, "General Ledger Setup" }, - { 112, "Sales Invoice Header" }, - { 131, "Incoming Documents Setup" }, - { 207, "Res. Journal Line" }, - { 210, "Job Journal Line" }, - { 232, "Gen. Journal Batch" }, - { 312, "Purchases & Payables Setup" }, - { 454, "Approval Entry" }, - { 843, "Cash Flow Setup" }, - { 1251, "Text-to-Account Mapping" }, - { 1275, "Doc. Exch. Service Setup" }, - { 5107, "Sales Header Archive" }, - { 5109, "Purchase Header Archive" }, - { 5200, "Employee" }, - { 5405, "Production Order" }, - { 5900, "Service Header" }, - { 5965, "Service Contract Header" }, - { 7152, "Item Analysis View" }, - { 2000000120, "User" } - }; - - public override void Initialize(AnalysisContext context) - => context.RegisterOperationAction(new Action(this.CheckRunPageImplementPageManagement), OperationKind.InvocationExpression); +[DiagnosticAnalyzer] +public class Rule0027RunPageImplementPageManagement : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0027RunPageImplementPageManagement); - private void CheckRunPageImplementPageManagement(OperationAnalysisContext ctx) + private static readonly Dictionary supportedRecords = new Dictionary { - if (ctx.IsObsoletePendingOrRemoved()) return; + { 36, "Sales Header" }, + { 38, "Purchase Header" }, + { 79, "Company Information" }, + { 80, "Gen. Journal Template" }, + { 81, "Gen. Journal Line" }, + { 91, "User Setup" }, + { 98, "General Ledger Setup" }, + { 112, "Sales Invoice Header" }, + { 131, "Incoming Documents Setup" }, + { 207, "Res. Journal Line" }, + { 210, "Job Journal Line" }, + { 232, "Gen. Journal Batch" }, + { 312, "Purchases & Payables Setup" }, + { 454, "Approval Entry" }, + { 843, "Cash Flow Setup" }, + { 1251, "Text-to-Account Mapping" }, + { 1275, "Doc. Exch. Service Setup" }, + { 5107, "Sales Header Archive" }, + { 5109, "Purchase Header Archive" }, + { 5200, "Employee" }, + { 5405, "Production Order" }, + { 5900, "Service Header" }, + { 5965, "Service Contract Header" }, + { 7152, "Item Analysis View" }, + { 2000000120, "User" } + }; - IInvocationExpression operation = (IInvocationExpression)ctx.Operation; - if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return; - if (operation.TargetMethod.ContainingType.GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Page) return; - if (operation.Arguments.Count() < 2) return; + public override void Initialize(AnalysisContext context) + => context.RegisterOperationAction(new Action(this.CheckRunPageImplementPageManagement), OperationKind.InvocationExpression); - // do not execute on CurrPage.EnqueueBackgroundTask - if (SemanticFacts.IsSameName(operation.TargetMethod.Name, "EnqueueBackgroundTask")) return; - - // Page Management Codeunit doesn't support returntype Action - if (operation.TargetMethod.ReturnValueSymbol.ReturnType.GetNavTypeKindSafe() == NavTypeKind.Action) return; + private void CheckRunPageImplementPageManagement(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) + return; - switch (operation.Arguments[0].Syntax.Kind) - { - case SyntaxKind.LiteralExpression: - if (operation.Arguments[0].Syntax.GetIdentifierOrLiteralValue() == "0") - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0027RunPageImplementPageManagement, ctx.Operation.Syntax.GetLocation())); - break; + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + operation.TargetMethod.Name != "EnqueueBackgroundTask" || // do not execute on CurrPage.EnqueueBackgroundTask + operation.TargetMethod.ContainingType?.GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Page || + operation.TargetMethod.ReturnValueSymbol.ReturnType.GetNavTypeKindSafe() == NavTypeKind.Action || // Page Management Codeunit doesn't support returntype Action + operation.Arguments.Length < 2) + return; - case SyntaxKind.OptionAccessExpression: - if (IsSupportedRecord(((IConversionExpression)operation.Arguments[1].Value).Operand)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0027RunPageImplementPageManagement, ctx.Operation.Syntax.GetLocation())); - break; + switch (operation.Arguments[0].Syntax.Kind) + { + case SyntaxKind.LiteralExpression: + if (operation.Arguments[0].Syntax.GetIdentifierOrLiteralValue() == "0") + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0027RunPageImplementPageManagement, + ctx.Operation.Syntax.GetLocation())); + break; - default: - return; - } + case SyntaxKind.OptionAccessExpression: + if (IsSupportedRecord(((IConversionExpression)operation.Arguments[1].Value).Operand)) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0027RunPageImplementPageManagement, + ctx.Operation.Syntax.GetLocation())); + break; } + } - private static bool IsSupportedRecord(IOperation operation) + private static bool IsSupportedRecord(IOperation operation) + { + IRecordTypeSymbol? recordTypeSymbol = null; + switch (operation.Kind) { - IRecordTypeSymbol recordTypeSymbol = null; - switch (operation.Kind) - { - case OperationKind.GlobalReferenceExpression: - case OperationKind.LocalReferenceExpression: - recordTypeSymbol = operation.GetSymbol().GetTypeSymbol() as IRecordTypeSymbol; - break; - case OperationKind.InvocationExpression: - recordTypeSymbol = operation.Type.GetTypeSymbol() as IRecordTypeSymbol; - break; - default: - return false; - } - - if (recordTypeSymbol == null || recordTypeSymbol.Temporary) return false; - - if (_supportedRecords.ContainsKey(recordTypeSymbol.Id)) - return SemanticFacts.IsSameName(recordTypeSymbol.Name, _supportedRecords[recordTypeSymbol.Id]); + case OperationKind.GlobalReferenceExpression: + case OperationKind.LocalReferenceExpression: + recordTypeSymbol = operation.GetSymbol()?.GetTypeSymbol() as IRecordTypeSymbol; + break; + case OperationKind.InvocationExpression: + recordTypeSymbol = operation.Type.GetTypeSymbol() as IRecordTypeSymbol; + break; + default: + return false; + } + if (recordTypeSymbol is null || recordTypeSymbol.Temporary) return false; - } - public static class DiagnosticDescriptors - { - public static readonly DiagnosticDescriptor Rule0027RunPageImplementPageManagement = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0027", - title: LinterCopAnalyzers.GetLocalizableString("Rule0027RunPageImplementPageManagementTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0027RunPageImplementPageManagementFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0027RunPageImplementPageManagementDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0027"); - } + if (supportedRecords.ContainsKey(recordTypeSymbol.Id)) + return SemanticFacts.IsSameName(recordTypeSymbol.Name, supportedRecords[recordTypeSymbol.Id]); + + return false; } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0028IdentifiersInEventSubscribers.cs b/BusinessCentral.LinterCop/Design/Rule0028IdentifiersInEventSubscribers.cs index d968491c..879243b2 100644 --- a/BusinessCentral.LinterCop/Design/Rule0028IdentifiersInEventSubscribers.cs +++ b/BusinessCentral.LinterCop/Design/Rule0028IdentifiersInEventSubscribers.cs @@ -1,39 +1,47 @@ #nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0028CodeNavigabilityOnEventSubscribers : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0028CodeNavigabilityOnEventSubscribers : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0028IdentifiersInEventSubscribers); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0028IdentifiersInEventSubscribers); - public override void Initialize(AnalysisContext context) => context.RegisterCodeBlockAction(new Action(this.AnalyzeIdentifiersInEventSubscribers)); + public override void Initialize(AnalysisContext context) => + context.RegisterCodeBlockAction(new Action(this.AnalyzeIdentifiersInEventSubscribers)); - private void AnalyzeIdentifiersInEventSubscribers(CodeBlockAnalysisContext context) - { - if (!VersionChecker.IsSupported(context.OwningSymbol, Feature.IdentifiersInEventSubscribers)) return; + private void AnalyzeIdentifiersInEventSubscribers(CodeBlockAnalysisContext ctx) + { + if (!VersionChecker.IsSupported(ctx.OwningSymbol, Feature.IdentifiersInEventSubscribers)) + return; - if (context.IsObsoletePendingOrRemoved()) return; - if (!context.CodeBlock.IsKind(SyntaxKind.MethodDeclaration)) return; + if (ctx.IsObsoletePendingOrRemoved() || !ctx.CodeBlock.IsKind(SyntaxKind.MethodDeclaration)) + return; - var SyntaxList = ((MethodDeclarationSyntax)context.CodeBlock).Attributes.Where(value => SemanticFacts.IsSameName(value.GetIdentifierOrLiteralValue(), "EventSubscriber")); + if (ctx.CodeBlock is not MethodDeclarationSyntax syntax) + return; - if (SyntaxList.Where(value => value.ArgumentList.Arguments[2].IsKind(SyntaxKind.LiteralAttributeArgument)).Any()) - { - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0028IdentifiersInEventSubscribers, context.OwningSymbol.GetLocation())); - return; - } + var syntaxList = syntax.Attributes.Where(value => SemanticFacts.IsSameName(value.GetIdentifierOrLiteralValue(), "EventSubscriber")); - if (SyntaxList.Where(value => !(value.ArgumentList.Arguments[3].GetIdentifierOrLiteralValue() == "") && value.ArgumentList.Arguments[3].IsKind(SyntaxKind.LiteralAttributeArgument)).Any()) - { - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0028IdentifiersInEventSubscribers, context.OwningSymbol.GetLocation())); - return; - } + var eventName = syntaxList.Where(value => value.ArgumentList.Arguments[2].IsKind(SyntaxKind.LiteralAttributeArgument)).FirstOrDefault(); + if (eventName is not null) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0028IdentifiersInEventSubscribers, + eventName.GetLocation())); + return; } + + var elementName = syntaxList.Where(value => !(value.ArgumentList.Arguments[3].GetIdentifierOrLiteralValue() == "") && value.ArgumentList.Arguments[3].IsKind(SyntaxKind.LiteralAttributeArgument)).FirstOrDefault(); + if (elementName is not null) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0028IdentifiersInEventSubscribers, + elementName.GetLocation())); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0029CompareDateTimeThroughCodeunit.cs b/BusinessCentral.LinterCop/Design/Rule0029CompareDateTimeThroughCodeunit.cs index 01e6a6c3..063d45fc 100644 --- a/BusinessCentral.LinterCop/Design/Rule0029CompareDateTimeThroughCodeunit.cs +++ b/BusinessCentral.LinterCop/Design/Rule0029CompareDateTimeThroughCodeunit.cs @@ -1,37 +1,46 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0029CompareDateTimeThroughCodeunit : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0029CompareDateTimeThroughCodeunit : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0029CompareDateTimeThroughCodeunit); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0029CompareDateTimeThroughCodeunit); - public override void Initialize(AnalysisContext context) => context.RegisterOperationAction(new Action(this.CompareDateTimeWithTypeHelper), OperationKind.BinaryOperatorExpression); + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(new Action(this.CompareDateTimeWithTypeHelper), OperationKind.BinaryOperatorExpression); + + private void CompareDateTimeWithTypeHelper(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IBinaryOperatorExpression operation) + return; - private void CompareDateTimeWithTypeHelper(OperationAnalysisContext context) - { - if (context.IsObsoletePendingOrRemoved()) return; + if (!(operation.LeftOperand.Type.NavTypeKind == NavTypeKind.DateTime && + operation.RightOperand.Type.NavTypeKind == NavTypeKind.DateTime)) + return; - IBinaryOperatorExpression operation = (IBinaryOperatorExpression)context.Operation; + if (operation.LeftOperand.Syntax.IsKind(SyntaxKind.LiteralExpression) && + operation.LeftOperand.Syntax.GetIdentifierOrLiteralValue() == "0DT") + return; - if (!(operation.LeftOperand.Type.NavTypeKind == NavTypeKind.DateTime && operation.RightOperand.Type.NavTypeKind == NavTypeKind.DateTime)) return; - if (operation.LeftOperand.Syntax.IsKind(SyntaxKind.LiteralExpression) && operation.LeftOperand.Syntax.GetIdentifierOrLiteralValue() == "0DT") return; - if (operation.RightOperand.Syntax.IsKind(SyntaxKind.LiteralExpression) && operation.RightOperand.Syntax.GetIdentifierOrLiteralValue() == "0DT") return; + if (operation.RightOperand.Syntax.IsKind(SyntaxKind.LiteralExpression) && + operation.RightOperand.Syntax.GetIdentifierOrLiteralValue() == "0DT") + return; - if (operation.Syntax.IsKind(SyntaxKind.EqualsExpression) || - operation.Syntax.IsKind(SyntaxKind.NotEqualsExpression) || - operation.Syntax.IsKind(SyntaxKind.GreaterThanExpression) || - operation.Syntax.IsKind(SyntaxKind.GreaterThanOrEqualExpression) || - operation.Syntax.IsKind(SyntaxKind.LessThanExpression) || - operation.Syntax.IsKind(SyntaxKind.LessThanOrEqualExpression) - ) - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0029CompareDateTimeThroughCodeunit, context.Operation.Syntax.GetLocation())); - } + if (operation.Syntax.IsKind(SyntaxKind.EqualsExpression) || + operation.Syntax.IsKind(SyntaxKind.NotEqualsExpression) || + operation.Syntax.IsKind(SyntaxKind.GreaterThanExpression) || + operation.Syntax.IsKind(SyntaxKind.GreaterThanOrEqualExpression) || + operation.Syntax.IsKind(SyntaxKind.LessThanExpression) || + operation.Syntax.IsKind(SyntaxKind.LessThanOrEqualExpression) + ) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0029CompareDateTimeThroughCodeunit, + ctx.Operation.Syntax.GetLocation())); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0030AccessInternalForInstallOrUpgradeCodeunits.cs b/BusinessCentral.LinterCop/Design/Rule0030AccessInternalForInstallOrUpgradeCodeunits.cs index d3f6766e..9e2bd4eb 100644 --- a/BusinessCentral.LinterCop/Design/Rule0030AccessInternalForInstallOrUpgradeCodeunits.cs +++ b/BusinessCentral.LinterCop/Design/Rule0030AccessInternalForInstallOrUpgradeCodeunits.cs @@ -1,29 +1,31 @@ -#nullable disable // TODO: Enable nullable and review rule using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0030AccessInternalForInstallAndUpgradeCodeunits : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0030AccessInternalForInstallAndUpgradeCodeunits : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0030AccessInternalForInstallAndUpgradeCodeunits); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0030AccessInternalForInstallAndUpgradeCodeunits); - public override void Initialize(AnalysisContext context) => context.RegisterSymbolAction(new Action(this.CheckAccessOnInstallAndUpgradeCodeunits), SymbolKind.Codeunit); + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(new Action(this.CheckAccessOnInstallAndUpgradeCodeunits), SymbolKind.Codeunit); - private void CheckAccessOnInstallAndUpgradeCodeunits(SymbolAnalysisContext context) - { - if (context.IsObsoletePendingOrRemoved()) return; + private void CheckAccessOnInstallAndUpgradeCodeunits(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Symbol is not ICodeunitTypeSymbol symbol) + return; - ICodeunitTypeSymbol symbol = (ICodeunitTypeSymbol)context.Symbol; - if (symbol.Subtype != CodeunitSubtypeKind.Install && symbol.Subtype != CodeunitSubtypeKind.Upgrade) - return; + if (symbol.Subtype != CodeunitSubtypeKind.Install && symbol.Subtype != CodeunitSubtypeKind.Upgrade) + return; - if (symbol.DeclaredAccessibility == Accessibility.Public) - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0030AccessInternalForInstallAndUpgradeCodeunits, symbol.GetLocation())); - } + if (symbol.DeclaredAccessibility == Accessibility.Public) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0030AccessInternalForInstallAndUpgradeCodeunits, + symbol.GetLocation())); } -} \ No newline at end of file +} diff --git a/BusinessCentral.LinterCop/Design/Rule0031RecordInstanceIsolationLevel.cs b/BusinessCentral.LinterCop/Design/Rule0031RecordInstanceIsolationLevel.cs index 202f9d01..664bd678 100644 --- a/BusinessCentral.LinterCop/Design/Rule0031RecordInstanceIsolationLevel.cs +++ b/BusinessCentral.LinterCop/Design/Rule0031RecordInstanceIsolationLevel.cs @@ -1,29 +1,32 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0031RecordInstanceIsolationLevel : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0031RecordInstanceIsolationLevel : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0031RecordInstanceIsolationLevel); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0031RecordInstanceIsolationLevel); - public override void Initialize(AnalysisContext context) => context.RegisterOperationAction(new Action(this.CheckLockTable), OperationKind.InvocationExpression); + public override VersionCompatibility SupportedVersions => VersionCompatibility.Spring2023OrGreater; - private void CheckLockTable(OperationAnalysisContext ctx) - { - if (!VersionChecker.IsSupported(ctx.ContainingSymbol, VersionCompatibility.Spring2023OrGreater)) return; - if (ctx.IsObsoletePendingOrRemoved()) return; + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(new Action(this.CheckLockTable), OperationKind.InvocationExpression); - IInvocationExpression operation = (IInvocationExpression)ctx.Operation; - if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return; + private void CheckLockTable(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) + return; - if (!SemanticFacts.IsSameName(operation.TargetMethod.Name, "LockTable")) return; + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + operation.TargetMethod.Name != "LockTable") + return; - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0031RecordInstanceIsolationLevel, ctx.Operation.Syntax.GetLocation())); - } + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0031RecordInstanceIsolationLevel, + ctx.Operation.Syntax.GetLocation())); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0032ClearCodeunitSingleInstance.cs b/BusinessCentral.LinterCop/Design/Rule0032ClearCodeunitSingleInstance.cs index 1e0ec8c6..9c5b9745 100644 --- a/BusinessCentral.LinterCop/Design/Rule0032ClearCodeunitSingleInstance.cs +++ b/BusinessCentral.LinterCop/Design/Rule0032ClearCodeunitSingleInstance.cs @@ -1,107 +1,141 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; +using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0032ClearCodeunitSingleInstance : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0032ClearCodeunitSingleInstance : DiagnosticAnalyzer + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0032ClearCodeunitSingleInstance, DiagnosticDescriptors.Rule0000ErrorInRule); + + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(new Action(this.AnalyzeInvocation), OperationKind.InvocationExpression); + + private void AnalyzeInvocation(OperationAnalysisContext ctx) { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0032ClearCodeunitSingleInstance, DiagnosticDescriptors.Rule0000ErrorInRule); + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) + return; - public override void Initialize(AnalysisContext context) + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) + return; + + switch (operation.TargetMethod.Name) { - context.RegisterOperationAction(new Action(this.ClearCodeunit), OperationKind.InvocationExpression); - context.RegisterOperationAction(new Action(this.ClearAllCodeunit), OperationKind.InvocationExpression); + case "Clear": + if (operation.Arguments.Length > 0) + AnalyzeClearInvocation(operation, ctx); + break; + + case "ClearAll": + AnalyzeClearAllInvocation(operation, ctx); + break; } + } - private void ClearCodeunit(OperationAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; + private static void AnalyzeClearInvocation(IInvocationExpression operation, OperationAnalysisContext ctx) + { + if (operation.Arguments[0].Value is not IConversionExpression boundConversion || + boundConversion.Operand is not IOperation operand || + operand.GetSymbol()?.GetTypeSymbol() is not ICodeunitTypeSymbol codeunit) + return; - IInvocationExpression operation = (IInvocationExpression)ctx.Operation; - if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return; + if (IsSingleInstanceCodeunitWithGlobalVars(codeunit)) + { + var variableName = operand.GetSymbol()?.Name.QuoteIdentifierIfNeeded() ?? string.Empty; + var objectName = codeunit.Name.QuoteIdentifierIfNeeded(); + + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0032ClearCodeunitSingleInstance, + ctx.Operation.Syntax.GetLocation(), + variableName, + objectName)); + } + } - if (!SemanticFacts.IsSameName(operation.TargetMethod.Name, "Clear")) return; - if (operation.Arguments.Count() < 1) return; + private static void AnalyzeClearAllInvocation(IInvocationExpression operation, OperationAnalysisContext ctx) + { + if (ctx.ContainingSymbol.GetContainingObjectTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Codeunit) + return; - if (operation.Arguments[0].Value is not IConversionExpression boundConversion) - return; + IEnumerable localVariables = ((IMethodSymbol)ctx.ContainingSymbol.OriginalDefinition).LocalVariables + .Where(var => var.OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.Codeunit && + var.OriginalDefinition.GetTypeSymbol().OriginalDefinition != ctx.ContainingSymbol.GetContainingObjectTypeSymbol().OriginalDefinition); - IOperation operand = boundConversion.Operand; - if (operand.GetSymbol().GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Codeunit) return; + if (HasSingleInstanceCodeunitWithGlobalVars(localVariables, out ISymbol? localCodeunitVariable) && localCodeunitVariable is not null) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0032ClearCodeunitSingleInstance, + ctx.Operation.Syntax.GetLocation(), + localCodeunitVariable.Name.QuoteIdentifierIfNeeded(), + localCodeunitVariable.GetTypeSymbol().Name.QuoteIdentifierIfNeeded())); - if (IsSingleInstanceCodeunitWithGlobalVars((ICodeunitTypeSymbol)operand.GetSymbol().GetTypeSymbol())) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0032ClearCodeunitSingleInstance, ctx.Operation.Syntax.GetLocation(), new Object[] { operand.GetSymbol().Name, operand.GetSymbol().GetTypeSymbol().Name })); + return; } - private void ClearAllCodeunit(OperationAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; - if (ctx.ContainingSymbol.GetContainingObjectTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Codeunit) return; - - IInvocationExpression operation = (IInvocationExpression)ctx.Operation; - if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return; - if (!SemanticFacts.IsSameName(operation.TargetMethod.Name, "ClearAll")) return; - - IEnumerable localVariables = ((IMethodSymbol)ctx.ContainingSymbol.OriginalDefinition).LocalVariables - .Where(var => var.OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.Codeunit) - .Where(var => var.OriginalDefinition.GetTypeSymbol().OriginalDefinition != ctx.ContainingSymbol.GetContainingObjectTypeSymbol().OriginalDefinition); - IEnumerable globalVariables = ctx.ContainingSymbol.GetContainingObjectTypeSymbol() - .GetMembers() - .Where(members => members.Kind == SymbolKind.GlobalVariable) - .Where(var => var.OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.Codeunit) - .Where(var => var.OriginalDefinition.GetTypeSymbol().OriginalDefinition != ctx.ContainingSymbol.GetContainingObjectTypeSymbol().OriginalDefinition); - - if (HasSingleInstanceCodeunitWithGlobalVars(localVariables, out ISymbol codeunit) || HasSingleInstanceCodeunitWithGlobalVars(globalVariables, out codeunit)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0032ClearCodeunitSingleInstance, ctx.Operation.Syntax.GetLocation(), new Object[] { codeunit.Name, codeunit.GetTypeSymbol().Name })); - } + IEnumerable globalVariables = ctx.ContainingSymbol.GetContainingObjectTypeSymbol() + .GetMembers() + .Where(var => var.Kind == SymbolKind.GlobalVariable && + var.OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.Codeunit && + var.OriginalDefinition.GetTypeSymbol().OriginalDefinition != ctx.ContainingSymbol.GetContainingObjectTypeSymbol().OriginalDefinition); - private static bool HasSingleInstanceCodeunitWithGlobalVars(IEnumerable variables, out ISymbol codeunit) + if (HasSingleInstanceCodeunitWithGlobalVars(globalVariables, out ISymbol? globalCodeunitVariables) && globalCodeunitVariables is not null) { - foreach (ISymbol variable in variables.Where(var => var.OriginalDefinition.ContainingType.GetNavTypeKindSafe() == NavTypeKind.Codeunit)) - if (IsSingleInstanceCodeunitWithGlobalVars((ICodeunitTypeSymbol)variable.OriginalDefinition.GetTypeSymbol())) - { - codeunit = variable; - return true; - } - - codeunit = null; - return false; + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0032ClearCodeunitSingleInstance, + ctx.Operation.Syntax.GetLocation(), + globalCodeunitVariables.Name.QuoteIdentifierIfNeeded(), + globalCodeunitVariables.GetTypeSymbol().Name.QuoteIdentifierIfNeeded())); + + return; } + } - private static bool IsSingleInstanceCodeunitWithGlobalVars(ICodeunitTypeSymbol codeunitTypeSymbol) - { - if (!IsSingleInstanceCodeunit(codeunitTypeSymbol)) + private static bool HasSingleInstanceCodeunitWithGlobalVars(IEnumerable variables, out ISymbol? codeunit) + { + foreach (ISymbol variable in variables.Where(var => var.OriginalDefinition?.ContainingType?.GetNavTypeKindSafe() == NavTypeKind.Codeunit)) + if (IsSingleInstanceCodeunitWithGlobalVars((ICodeunitTypeSymbol)variable.OriginalDefinition.GetTypeSymbol())) { - return false; + codeunit = variable; + return true; } - var globalVariables = codeunitTypeSymbol.GetMembers().Where(members => members.Kind == SymbolKind.GlobalVariable); - var globalVariablesNonRecordTypes = globalVariables.Where(vars => vars.GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Record); + codeunit = null; + return false; + } - bool globalVariablesExists = globalVariablesNonRecordTypes.Count() != 0; - return globalVariablesExists; + private static bool IsSingleInstanceCodeunitWithGlobalVars(ICodeunitTypeSymbol codeunitTypeSymbol) + { + if (!IsSingleInstanceCodeunit(codeunitTypeSymbol)) + { + return false; } - private static bool IsSingleInstanceCodeunit(ICodeunitTypeSymbol codeunitTypeSymbol) - { - IPropertySymbol singleInstanceProperty = codeunitTypeSymbol.GetProperty(PropertyKind.SingleInstance); - if (singleInstanceProperty == null) - { - return false; - } + var globalVariables = codeunitTypeSymbol.GetMembers().Where(members => members.Kind == SymbolKind.GlobalVariable); + var globalVariablesNonRecordTypes = globalVariables.Where(vars => vars.GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Record); - // codeunits without source code could return "1" for the SingleInstance value - if (singleInstanceProperty.Value is not bool booleanValue) - { - return false; - } + bool globalVariablesExists = globalVariablesNonRecordTypes.Count() != 0; + return globalVariablesExists; + } - return booleanValue; + private static bool IsSingleInstanceCodeunit(ICodeunitTypeSymbol codeunitTypeSymbol) + { + IPropertySymbol? singleInstanceProperty = codeunitTypeSymbol.GetProperty(PropertyKind.SingleInstance); + if (singleInstanceProperty is null) + { + return false; } + + // codeunits without source code could return "1" for the SingleInstance value + if (singleInstanceProperty.Value is not bool booleanValue) + { + return false; + } + + return booleanValue; } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0033AppManifestRuntimeBehind.cs b/BusinessCentral.LinterCop/Design/Rule0033AppManifestRuntimeBehind.cs index b7a6b5b9..f36afd39 100644 --- a/BusinessCentral.LinterCop/Design/Rule0033AppManifestRuntimeBehind.cs +++ b/BusinessCentral.LinterCop/Design/Rule0033AppManifestRuntimeBehind.cs @@ -1,4 +1,3 @@ -#nullable disable // TODO: Enable nullable and review rule using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Packaging; @@ -9,90 +8,102 @@ using Microsoft.Dynamics.Nav.Analyzers.Common.AppSourceCopConfiguration; #endif +namespace BusinessCentral.LinterCop.Design; -namespace BusinessCentral.LinterCop.Design +[DiagnosticAnalyzer] +class Rule0033AppManifestRuntimeBehind : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - class Rule0033AppManifestRuntimeBehind : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0033AppManifestRuntimeBehind); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0033AppManifestRuntimeBehind); - public override void Initialize(AnalysisContext context) => context.RegisterCompilationAction(new Action(this.CheckAppManifestRuntime)); + private static readonly SortedList SupportedRuntimeVersionList = GetSupportedRuntimeVersions(); - private void CheckAppManifestRuntime(CompilationAnalysisContext ctx) - { + public override void Initialize(AnalysisContext context) => + context.RegisterCompilationAction(new Action(this.CheckAppManifestRuntime)); + + private void CheckAppManifestRuntime(CompilationAnalysisContext ctx) + { #if !LessThenSpring2024 - NavAppManifest manifest = ManifestHelper.GetManifest(ctx.Compilation); + NavAppManifest? manifest = ManifestHelper.GetManifest(ctx.Compilation); #else - NavAppManifest manifest = AppSourceCopConfigurationProvider.GetManifest(ctx.Compilation); + NavAppManifest? manifest = AppSourceCopConfigurationProvider.GetManifest(ctx.Compilation); #endif - if (manifest == null) return; - if (manifest.Runtime == null || manifest.Runtime == RuntimeVersion.CurrentRelease) return; // In the case the runtime version isn't specified in the app.json it returns the RuntimeVersion.CurrentRelease - if (manifest.Application == null && manifest.Platform == null) return; - - GetTargetProperty(manifest, out string propertyName, out Version propertyVersion); - Version supportedRuntime = FindValueOfFirstValueLessThan(GetSupportedRuntimeVersions(), propertyVersion); - if (supportedRuntime == null) return; - - if (manifest.Runtime < supportedRuntime) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0033AppManifestRuntimeBehind, manifest.GetDiagnosticLocation("runtime"), new object[] { propertyName, propertyVersion, manifest.Runtime, supportedRuntime })); - } + // In the case the runtime version isn't specified in the app.json it returns the RuntimeVersion.CurrentRelease + if (manifest is null || manifest.Runtime is null || manifest.Runtime == RuntimeVersion.CurrentRelease) + return; + + if (manifest.Application is null && manifest.Platform is null) + return; + + GetTargetProperty(manifest, out string propertyName, out Version propertyVersion); + Version supportedRuntime = FindValueOfFirstValueLessThan(SupportedRuntimeVersionList, propertyVersion); + if (supportedRuntime is null) + return; + + if (manifest.Runtime < supportedRuntime) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0033AppManifestRuntimeBehind, + manifest.GetDiagnosticLocation("runtime"), + propertyName, + propertyVersion, + manifest.Runtime, + supportedRuntime)); + } - private static void GetTargetProperty(NavAppManifest manifest, out string propertyName, out Version propertyVersion) + private static void GetTargetProperty(NavAppManifest manifest, out string propertyName, out Version propertyVersion) + { + if (manifest.Application >= manifest.Platform) { - if (manifest.Application >= manifest.Platform) - { - propertyName = "application"; - propertyVersion = new Version(manifest.Application.Major, manifest.Application.Minor); - } - else - { - propertyName = "platform"; - propertyVersion = new Version(manifest.Platform.Major, manifest.Platform.Minor); - } + propertyName = "application"; + propertyVersion = new Version(manifest.Application.Major, manifest.Application.Minor); } - - private static SortedList GetSupportedRuntimeVersions() + else { - // Populate a SortedList with platform version and runtime version combined - SortedList AvailableRuntimeVersion = new SortedList(); - - // https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-choosing-runtime#currently-available-runtime-versions - // When in the future the offset between the platform and runtime versions isn't exactly is eleven, we're in trouble - // By populating a list here, in stead of just adding up eleven somewhere else, we probably can resolve this here - int offset = 11; - - foreach (var v in RuntimeVersion.SupportedVersions) - { - AvailableRuntimeVersion.Add(new Version(v.Major + offset, v.Minor), new Version(v.Major, v.Minor)); - } - return AvailableRuntimeVersion; + propertyName = "platform"; + propertyVersion = new Version(manifest.Platform.Major, manifest.Platform.Minor); } + } - private static Version FindValueOfFirstValueLessThan(SortedList sortedList, Version version) + private static SortedList GetSupportedRuntimeVersions() + { + // Populate a SortedList with platform version and runtime version combined + SortedList AvailableRuntimeVersion = new SortedList(); + + // https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-choosing-runtime#currently-available-runtime-versions + // When in the future the offset between the platform and runtime versions isn't exactly is eleven, we're in trouble + // By populating a list here, instead of just adding up eleven somewhere else, we probably can resolve this here + int offset = 11; + + foreach (var v in RuntimeVersion.SupportedVersions) { - int index = FindIndexOfFirstValueLessThan(sortedList.Keys.ToList(), version); - return sortedList.ElementAtOrDefault(index).Value; + AvailableRuntimeVersion.Add(new Version(v.Major + offset, v.Minor), new Version(v.Major, v.Minor)); } + return AvailableRuntimeVersion; + } - private static int FindIndexOfFirstValueLessThan(List sortedList, T value, IComparer comparer = null) - { - var index = sortedList.BinarySearch(value, comparer); + private static Version FindValueOfFirstValueLessThan(SortedList sortedList, Version version) + { + int index = FindIndexOfFirstValueLessThan(sortedList.Keys.ToList(), version); + return sortedList.ElementAtOrDefault(index).Value; + } - // The value was found in the list. Just return its index. - if (index >= 0) - return index; + private static int FindIndexOfFirstValueLessThan(List sortedList, T value, IComparer? comparer = null) + { + var index = sortedList.BinarySearch(value, comparer); - // The value was not found and "~index" is the index of the next value greater than the search value. - index = ~index; + // The value was found in the list. Just return its index. + if (index >= 0) + return index; - // There are values in the list less than the search value. Return the index of the closest one. - if (index > 0) - return index - 1; + // The value was not found and "~index" is the index of the next value greater than the search value. + index = ~index; - // All values in the list are greater than the search value. - return -1; - } + // There are values in the list less than the search value. Return the index of the closest one. + if (index > 0) + return index - 1; + + // All values in the list are greater than the search value. + return -1; } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0034ExtensiblePropertyShouldAlwaysBeSet.cs b/BusinessCentral.LinterCop/Design/Rule0034ExtensiblePropertyShouldAlwaysBeSet.cs index caf2b039..a99f897e 100644 --- a/BusinessCentral.LinterCop/Design/Rule0034ExtensiblePropertyShouldAlwaysBeSet.cs +++ b/BusinessCentral.LinterCop/Design/Rule0034ExtensiblePropertyShouldAlwaysBeSet.cs @@ -1,36 +1,43 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0034ExtensiblePropertyShouldAlwaysBeSet : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0034ExtensiblePropertyShouldAlwaysBeSet : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0034ExtensiblePropertyShouldAlwaysBeSet); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0034ExtensiblePropertyShouldAlwaysBeSet); - public override void Initialize(AnalysisContext context) - => context.RegisterSymbolAction(new Action(this.CheckForMissingExtensibleProperty), new SymbolKind[] { + public override VersionCompatibility SupportedVersions => VersionCompatibility.Fall2019OrGreater; + + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(new Action(this.CheckForMissingExtensibleProperty), new SymbolKind[] { SymbolKind.Table, SymbolKind.Page, SymbolKind.Report - }); + }); - private void CheckForMissingExtensibleProperty(SymbolAnalysisContext ctx) - { - if (!VersionChecker.IsSupported(ctx.Symbol, VersionCompatibility.Fall2019OrGreater)) return; + private void CheckForMissingExtensibleProperty(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) + return; - if (ctx.IsObsoletePendingOrRemoved()) return; + var typeSymbol = ctx.Symbol.GetTypeSymbol(); - if (ctx.Symbol.GetTypeSymbol().Kind == SymbolKind.Table && ctx.Symbol.DeclaredAccessibility != Accessibility.Public) return; - if (ctx.Symbol.GetTypeSymbol().Kind == SymbolKind.Page && ((IPageTypeSymbol)ctx.Symbol.GetTypeSymbol()).PageType == PageTypeKind.API) return; + if (typeSymbol.Kind == SymbolKind.Table && ctx.Symbol.DeclaredAccessibility != Accessibility.Public) + return; - if (ctx.Symbol.GetProperty(PropertyKind.Extensible) != null) return; + if (typeSymbol.Kind == SymbolKind.Page && ((IPageTypeSymbol)ctx.Symbol.GetTypeSymbol()).PageType == PageTypeKind.API) + return; - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0034ExtensiblePropertyShouldAlwaysBeSet, ctx.Symbol.GetLocation(), new object[] { Accessibility.Public.ToString().ToLower() })); - } + if (ctx.Symbol.GetProperty(PropertyKind.Extensible) is null) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0034ExtensiblePropertyShouldAlwaysBeSet, + ctx.Symbol.GetLocation(), + Accessibility.Public.ToString().ToLower())); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0035ExplicitSetAllowInCustomizations.cs b/BusinessCentral.LinterCop/Design/Rule0035ExplicitSetAllowInCustomizations.cs index 4893fef2..8a055caf 100644 --- a/BusinessCentral.LinterCop/Design/Rule0035ExplicitSetAllowInCustomizations.cs +++ b/BusinessCentral.LinterCop/Design/Rule0035ExplicitSetAllowInCustomizations.cs @@ -1,150 +1,157 @@ #if !LessThenFall2023RV1 -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; using System.Collections.Immutable; using System.Collections.ObjectModel; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0035ExplicitSetAllowInCustomizations : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0035ExplicitSetAllowInCustomizations : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0035ExplicitSetAllowInCustomizations); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0035ExplicitSetAllowInCustomizations); - public override void Initialize(AnalysisContext context) - => context.RegisterSymbolAction(new Action(this.AnalyzeAllowInCustomization), new SymbolKind[] { + public override void Initialize(AnalysisContext context) + => context.RegisterSymbolAction(new Action(this.AnalyzeAllowInCustomization), new SymbolKind[] { SymbolKind.Table, SymbolKind.TableExtension, - }); + }); - private void AnalyzeAllowInCustomization(SymbolAnalysisContext ctx) + private void AnalyzeAllowInCustomization(SymbolAnalysisContext ctx) + { + if (!VersionChecker.IsSupported(ctx.Symbol, Feature.AddPageControlInPageCustomization)) + return; + + if (ctx.IsObsoletePendingOrRemoved()) + return; + + ICollection tableFields = GetTableFields(ctx.Symbol).Where(x => x.Id > 0 && x.Id < 2000000000) + .Where(x => x.DeclaredAccessibility != Accessibility.Local && x.DeclaredAccessibility != Accessibility.Protected) + .Where(x => x.FieldClass != FieldClassKind.FlowFilter) + .Where(x => x.GetBooleanPropertyValue(PropertyKind.Enabled) != false) + .Where(x => x.GetProperty(PropertyKind.AllowInCustomizations) is null) + .Where(x => x.GetProperty(PropertyKind.ObsoleteState) is null) + .Where(x => IsSupportedType(x.OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe())) + .ToList(); + if (!tableFields.Any()) + return; + + IEnumerable? relatedPages = GetRelatedPages(ctx); + + if (!relatedPages.Any()) { - if (!VersionChecker.IsSupported(ctx.Symbol, Feature.AddPageControlInPageCustomization)) return; - if (ctx.IsObsoletePendingOrRemoved()) return; - - ICollection tableFields = GetTableFields(ctx.Symbol).Where(x => x.Id > 0 && x.Id < 2000000000) - .Where(x => x.DeclaredAccessibility != Accessibility.Local && x.DeclaredAccessibility != Accessibility.Protected) - .Where(x => x.FieldClass != FieldClassKind.FlowFilter) - .Where(x => x.GetBooleanPropertyValue(PropertyKind.Enabled) != false) - .Where(x => x.GetProperty(PropertyKind.AllowInCustomizations) is null) - .Where(x => x.GetProperty(PropertyKind.ObsoleteState) is null) - .Where(x => IsSupportedType(x.OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe())) - .ToList(); - if (!tableFields.Any()) return; - - IEnumerable? relatedPages = GetRelatedPages(ctx); - - if (!relatedPages.Any()) - { - if (ctx.Symbol.GetTypeSymbol().Kind != SymbolKind.TableExtension) - return; - ITableExtensionTypeSymbol tableExtension = (ITableExtensionTypeSymbol)ctx.Symbol; - if (tableExtension.Target is not null && !LookupOrDrillDownPageIsSet((ITableTypeSymbol)tableExtension.Target)) - return; - // allows diagnostic for table extension fields where base table has lookup or drilldown page set - // even if no relatedPages exist directly - } + if (ctx.Symbol.GetTypeSymbol().Kind != SymbolKind.TableExtension) + return; - ICollection pageFields = GetPageFields(relatedPages); - ICollection fieldsNotReferencedOnPage = tableFields.Except(pageFields).ToList(); - foreach (IFieldSymbol field in fieldsNotReferencedOnPage) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0035ExplicitSetAllowInCustomizations, field.Location!)); - } + if (ctx.Symbol is not ITableExtensionTypeSymbol tableExtension) + return; - private static ICollection GetTableFields(ISymbol symbol) - { - switch (symbol.GetContainingObjectTypeSymbol().GetNavTypeKindSafe()) - { - case NavTypeKind.Record: - return ((ITableTypeSymbol)symbol).Fields; - case NavTypeKind.TableExtension: - return ((ITableExtensionTypeSymbol)symbol).AddedFields; - default: - return new Collection(); - } + if (tableExtension.Target is not null && !LookupOrDrillDownPageIsSet((ITableTypeSymbol)tableExtension.Target)) + return; + // allows diagnostic for table extension fields where base table has lookup or drilldown page set + // even if no relatedPages exist directly } - private static ICollection GetPageFields(IEnumerable? relatedPages) - { - if (relatedPages == null) - return []; + ICollection pageFields = GetPageFields(relatedPages); + ICollection fieldsNotReferencedOnPage = tableFields.Except(pageFields).ToList(); + foreach (IFieldSymbol field in fieldsNotReferencedOnPage) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0035ExplicitSetAllowInCustomizations, field.Location!)); + } - ICollection pageFields = new Collection(); - foreach (IApplicationObjectTypeSymbol relatedPageLike in relatedPages) - { - switch (relatedPageLike.GetNavTypeKindSafe()) - { - case NavTypeKind.Page: - IEnumerable fields = ((IPageTypeSymbol)relatedPageLike).FlattenedControls.Where(x => x.ControlKind == ControlKind.Field && x.RelatedFieldSymbol != null) - .Select(x => (IFieldSymbol)x.RelatedFieldSymbol!.OriginalDefinition); - pageFields = pageFields.Union(fields).Distinct().ToList(); - break; - case NavTypeKind.PageExtension: - IEnumerable extFields = ((IPageExtensionTypeSymbol)relatedPageLike).AddedControlsFlattened.Where(x => x.ControlKind == ControlKind.Field && x.RelatedFieldSymbol != null) - .Select(x => (IFieldSymbol)x.RelatedFieldSymbol!.OriginalDefinition); - - pageFields = pageFields.Union(extFields).Distinct().ToList(); - break; - } - } - return pageFields; + private static ICollection GetTableFields(ISymbol symbol) + { + switch (symbol.GetContainingObjectTypeSymbol().GetNavTypeKindSafe()) + { + case NavTypeKind.Record: + return ((ITableTypeSymbol)symbol).Fields; + case NavTypeKind.TableExtension: + return ((ITableExtensionTypeSymbol)symbol).AddedFields; + default: + return new Collection(); } + } + + private static ICollection GetPageFields(IEnumerable? relatedPages) + { + if (relatedPages is null) + return []; - private static IEnumerable? GetRelatedPages(SymbolAnalysisContext ctx) + ICollection pageFields = new Collection(); + foreach (IApplicationObjectTypeSymbol relatedPageLike in relatedPages) { - // table and tableextension fields can each be referenced on both pages and pageextensions - ITableTypeSymbol? table = null; - switch (ctx.Symbol.GetContainingObjectTypeSymbol().GetNavTypeKindSafe()) + switch (relatedPageLike.GetNavTypeKindSafe()) { - case NavTypeKind.Record: - table = ctx.Symbol as ITableTypeSymbol; + case NavTypeKind.Page: + IEnumerable fields = ((IPageTypeSymbol)relatedPageLike).FlattenedControls.Where(x => x.ControlKind == ControlKind.Field && x.RelatedFieldSymbol is not null) + .Select(x => (IFieldSymbol)x.RelatedFieldSymbol!.OriginalDefinition); + pageFields = pageFields.Union(fields).Distinct().ToList(); break; - case NavTypeKind.TableExtension: - if (ctx.Symbol is IApplicationObjectExtensionTypeSymbol typeSymbol) - table = typeSymbol.Target as ITableTypeSymbol; + case NavTypeKind.PageExtension: + IEnumerable extFields = ((IPageExtensionTypeSymbol)relatedPageLike).AddedControlsFlattened.Where(x => x.ControlKind == ControlKind.Field && x.RelatedFieldSymbol is not null) + .Select(x => (IFieldSymbol)x.RelatedFieldSymbol!.OriginalDefinition); + + pageFields = pageFields.Union(extFields).Distinct().ToList(); break; - default: - return null; } + } + return pageFields; + } - if (table is null) - return []; + private static IEnumerable? GetRelatedPages(SymbolAnalysisContext ctx) + { + // table and tableextension fields can each be referenced on both pages and pageextensions + ITableTypeSymbol? table = null; + switch (ctx.Symbol.GetContainingObjectTypeSymbol().GetNavTypeKindSafe()) + { + case NavTypeKind.Record: + table = ctx.Symbol as ITableTypeSymbol; + break; + case NavTypeKind.TableExtension: + if (ctx.Symbol is IApplicationObjectExtensionTypeSymbol typeSymbol) + table = typeSymbol.Target as ITableTypeSymbol; + break; + default: + return null; + } - IEnumerable pages = ctx.Compilation.GetDeclaredApplicationObjectSymbols() - .Where(x => x.GetNavTypeKindSafe() == NavTypeKind.Page) - .Where(x => ((IPageTypeSymbol)x.GetTypeSymbol()).PageType != PageTypeKind.API) - .Where(x => ((IPageTypeSymbol)x.GetTypeSymbol()).RelatedTable == table); + if (table is null) + return []; - IEnumerable pageExtensions = ctx.Compilation.GetDeclaredApplicationObjectSymbols() - .Where(x => x.GetNavTypeKindSafe() == NavTypeKind.PageExtension) - .Where(x => ((IApplicationObjectExtensionTypeSymbol)x).Target != null) - .Where(x => ((IPageTypeSymbol)((IApplicationObjectExtensionTypeSymbol)x).Target!.GetTypeSymbol()).RelatedTable == table); + IEnumerable pages = ctx.Compilation.GetDeclaredApplicationObjectSymbols() + .Where(x => x.GetNavTypeKindSafe() == NavTypeKind.Page) + .Where(x => ((IPageTypeSymbol)x.GetTypeSymbol()).PageType != PageTypeKind.API) + .Where(x => ((IPageTypeSymbol)x.GetTypeSymbol()).RelatedTable == table); - return pages.Union(pageExtensions); - } + IEnumerable pageExtensions = ctx.Compilation.GetDeclaredApplicationObjectSymbols() + .Where(x => x.GetNavTypeKindSafe() == NavTypeKind.PageExtension) + .Where(x => ((IApplicationObjectExtensionTypeSymbol)x).Target is not null) + .Where(x => ((IPageTypeSymbol)((IApplicationObjectExtensionTypeSymbol)x).Target!.GetTypeSymbol()).RelatedTable == table); - private static bool IsSupportedType(NavTypeKind navTypeKind) - { - switch (navTypeKind) - { - case NavTypeKind.Blob: - case NavTypeKind.Media: - case NavTypeKind.MediaSet: - case NavTypeKind.RecordId: - case NavTypeKind.TableFilter: - return false; - - default: - return true; - } - } + return pages.Union(pageExtensions); + } - private static bool LookupOrDrillDownPageIsSet(ITableTypeSymbol table) + private static bool IsSupportedType(NavTypeKind navTypeKind) + { + switch (navTypeKind) { - return table.Properties.Any(e => e.PropertyKind == PropertyKind.DrillDownPageId || e.PropertyKind == PropertyKind.LookupPageId); + case NavTypeKind.Blob: + case NavTypeKind.Media: + case NavTypeKind.MediaSet: + case NavTypeKind.RecordId: + case NavTypeKind.TableFilter: + return false; + + default: + return true; } } + + private static bool LookupOrDrillDownPageIsSet(ITableTypeSymbol table) + { + return table.Properties.Any(e => e.PropertyKind == PropertyKind.DrillDownPageId || e.PropertyKind == PropertyKind.LookupPageId); + } } #endif \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0039ArgumentDifferentTypeThenExpected.cs b/BusinessCentral.LinterCop/Design/Rule0039ArgumentDifferentTypeThenExpected.cs index 32fe43ea..d5e0caac 100644 --- a/BusinessCentral.LinterCop/Design/Rule0039ArgumentDifferentTypeThenExpected.cs +++ b/BusinessCentral.LinterCop/Design/Rule0039ArgumentDifferentTypeThenExpected.cs @@ -1,156 +1,221 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0039ArgumentDifferentTypeThenExpected : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0039ArgumentDifferentTypeThenExpected : DiagnosticAnalyzer + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0039ArgumentDifferentTypeThenExpected, DiagnosticDescriptors.Rule0049PageWithoutSourceTable, DiagnosticDescriptors.Rule0058PageVariableMethodOnTemporaryTable); + + internal static readonly ImmutableHashSet pageProcedureNames = (new string[4] { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0039ArgumentDifferentTypeThenExpected, DiagnosticDescriptors.Rule0049PageWithoutSourceTable, DiagnosticDescriptors.Rule0058PageVariableMethodOnTemporaryTable); + "GetRecord", + "SetRecord", + "SetSelectionFilter", + "SetTableView", + }).ToImmutableHashSet(); - private static readonly string[] pageProcedureNames = ["GetRecord", "SetRecord", "SetSelectionFilter", "SetTableView"]; - private static readonly string[] pageRunProcedureNames = ["Run", "RunModal"]; + internal static readonly ImmutableHashSet pageRunProcedureNames = (new string[2] + { + "Run", + "RunModal", + }).ToImmutableHashSet(); - private static readonly List referencePageProviders = new List + private static readonly List referencePageProviders = new List { PropertyKind.LookupPageId, PropertyKind.DrillDownPageId }; - public override void Initialize(AnalysisContext context) + public override void Initialize(AnalysisContext context) + { + context.RegisterOperationAction(new Action(this.AnalyzeRunPageArguments), OperationKind.InvocationExpression); + context.RegisterOperationAction(new Action(this.AnalyzeSetRecordArgument), OperationKind.InvocationExpression); + context.RegisterSymbolAction(new Action(this.AnalyzeTableReferencePageProvider), SymbolKind.Table); + context.RegisterSymbolAction(new Action(this.AnalyzeTableExtensionReferencePageProvider), SymbolKind.TableExtension); + } + + private void AnalyzeRunPageArguments(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) + return; + + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + !pageRunProcedureNames.Contains(operation.TargetMethod.Name) || + operation.Arguments.Length < 2) + return; + + if (operation.Arguments[0].Syntax.Kind != SyntaxKind.OptionAccessExpression) + return; + + if (operation.Arguments[1].Syntax.Kind != SyntaxKind.IdentifierName || operation.Arguments[1].Value.Kind != OperationKind.ConversionExpression) + return; + + if (operation.TargetMethod?.ContainingType?.GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Page) + return; + + IApplicationObjectTypeSymbol applicationObjectTypeSymbol = ((IApplicationObjectAccess)operation.Arguments[0].Value).ApplicationObjectTypeSymbol; + if (applicationObjectTypeSymbol.GetNavTypeKindSafe() != NavTypeKind.Page) + return; + + ITableTypeSymbol? pageSourceTable = ((IPageTypeSymbol)applicationObjectTypeSymbol.GetTypeSymbol()).RelatedTable; + if (pageSourceTable is null) + return; + + IOperation operand = ((IConversionExpression)operation.Arguments[1].Value).Operand; + if (operand.GetSymbol()?.GetTypeSymbol() is not IRecordTypeSymbol recordTypeSymbol) return; + ITableTypeSymbol recordArgument = recordTypeSymbol.BaseTable; + + if (!AreTheSameNavObjects(recordArgument, pageSourceTable)) { - context.RegisterOperationAction(new Action(this.AnalyzeRunPageArguments), OperationKind.InvocationExpression); - context.RegisterOperationAction(new Action(this.AnalyzeSetRecordArgument), OperationKind.InvocationExpression); - context.RegisterSymbolAction(new Action(this.AnalyzeTableReferencePageProvider), SymbolKind.Table); - context.RegisterSymbolAction(new Action(this.AnalyzeTableExtensionReferencePageProvider), SymbolKind.TableExtension); + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0039ArgumentDifferentTypeThenExpected, + ctx.Operation.Syntax.GetLocation(), + 2, + operand.GetSymbol()!.GetTypeSymbol().ToString(), pageSourceTable.GetNavTypeKindSafe() + pageSourceTable.Name.QuoteIdentifierIfNeeded())); } + } - private void AnalyzeRunPageArguments(OperationAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; + private void AnalyzeSetRecordArgument(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) return; + + IInvocationExpression operation = (IInvocationExpression)ctx.Operation; + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return; - IInvocationExpression operation = (IInvocationExpression)ctx.Operation; - if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return; + if (operation?.TargetMethod?.ContainingType?.GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Page) return; + if (!pageProcedureNames.Contains(operation.TargetMethod.Name)) return; + if (operation.Arguments.Length != 1) return; - if (operation.TargetMethod.ContainingType.GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Page) return; - if (!pageRunProcedureNames.Contains(operation.TargetMethod.Name)) return; - if (operation.Arguments.Count() < 2) return; + if (operation.Arguments[0].Syntax.Kind != SyntaxKind.IdentifierName || operation.Arguments[0].Value.Kind != OperationKind.ConversionExpression) return; - if (operation.Arguments[0].Syntax.Kind != SyntaxKind.OptionAccessExpression) return; - if (operation.Arguments[1].Syntax.Kind != SyntaxKind.IdentifierName || operation.Arguments[1].Value.Kind != OperationKind.ConversionExpression) return; + IOperation pageReference = ctx.Operation.DescendantsAndSelf().Where(x => x.GetSymbol() is not null) + .Where(x => x.Type.GetNavTypeKindSafe() == NavTypeKind.Page) + .SingleOrDefault(); + if (pageReference is null) + return; - IApplicationObjectTypeSymbol applicationObjectTypeSymbol = ((IApplicationObjectAccess)operation.Arguments[0].Value).ApplicationObjectTypeSymbol; - if (applicationObjectTypeSymbol.GetNavTypeKindSafe() != NavTypeKind.Page) return; - ITableTypeSymbol pageSourceTable = ((IPageTypeSymbol)applicationObjectTypeSymbol.GetTypeSymbol()).RelatedTable; - if (pageSourceTable == null) return; + ISymbol? variableSymbol = pageReference.GetSymbol()?.OriginalDefinition; + if (variableSymbol is null) + return; - IOperation operand = ((IConversionExpression)operation.Arguments[1].Value).Operand; - if (operand.GetSymbol().GetTypeSymbol() is not IRecordTypeSymbol recordTypeSymbol) return; - ITableTypeSymbol recordArgument = recordTypeSymbol.BaseTable; + IPageTypeSymbol pageTypeSymbol = (IPageTypeSymbol)variableSymbol.GetTypeSymbol().OriginalDefinition; + if (pageTypeSymbol.RelatedTable is null) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0049PageWithoutSourceTable, + ctx.Operation.Syntax.GetLocation(), + NavTypeKind.Page, + GetFullyQualifiedObjectName(pageTypeSymbol))); - if (!AreTheSameNavObjects(recordArgument, pageSourceTable)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0039ArgumentDifferentTypeThenExpected, ctx.Operation.Syntax.GetLocation(), new object[] { 2, operand.GetSymbol().GetTypeSymbol().ToString(), pageSourceTable.GetNavTypeKindSafe() + pageSourceTable.Name.QuoteIdentifierIfNeeded() })); + return; } - private void AnalyzeSetRecordArgument(OperationAnalysisContext ctx) + IOperation operand = ((IConversionExpression)operation.Arguments[0].Value).Operand; + IRecordTypeSymbol? recordTypeSymbol = operand.GetSymbol()?.GetTypeSymbol() as IRecordTypeSymbol; + if (recordTypeSymbol is null) return; + if (recordTypeSymbol.Temporary && SemanticFacts.IsSameName(operation.TargetMethod.Name, "SetRecord")) { - if (ctx.IsObsoletePendingOrRemoved()) return; - - IInvocationExpression operation = (IInvocationExpression)ctx.Operation; - if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return; - - if (operation.TargetMethod.ContainingType.GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Page) return; - if (!pageProcedureNames.Contains(operation.TargetMethod.Name)) return; - if (operation.Arguments.Count() != 1) return; - - if (operation.Arguments[0].Syntax.Kind != SyntaxKind.IdentifierName || operation.Arguments[0].Value.Kind != OperationKind.ConversionExpression) return; - - IOperation pageReference = ctx.Operation.DescendantsAndSelf().Where(x => x.GetSymbol() != null) - .Where(x => x.Type.GetNavTypeKindSafe() == NavTypeKind.Page) - .SingleOrDefault(); - if (pageReference == null) return; - ISymbol variableSymbol = pageReference.GetSymbol().OriginalDefinition; - IPageTypeSymbol pageTypeSymbol = (IPageTypeSymbol)variableSymbol.GetTypeSymbol().OriginalDefinition; - if (pageTypeSymbol.RelatedTable == null) - { - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0049PageWithoutSourceTable, ctx.Operation.Syntax.GetLocation(), new object[] { NavTypeKind.Page, GetFullyQualifiedObjectName(pageTypeSymbol) })); - return; - } - - IOperation operand = ((IConversionExpression)operation.Arguments[0].Value).Operand; - IRecordTypeSymbol recordTypeSymbol = operand.GetSymbol().GetTypeSymbol() as IRecordTypeSymbol; - if (recordTypeSymbol.Temporary && SemanticFacts.IsSameName(operation.TargetMethod.Name, "SetRecord")) - { - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0058PageVariableMethodOnTemporaryTable, ctx.Operation.Syntax.GetLocation(), new object[] { variableSymbol.ToString().QuoteIdentifierIfNeeded(), operation.TargetMethod.Name })); - return; - } - ITableTypeSymbol pageSourceTable = pageTypeSymbol.RelatedTable; - ITableTypeSymbol recordArgument = recordTypeSymbol.BaseTable; - - if (!AreTheSameNavObjects(recordArgument, pageSourceTable)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0039ArgumentDifferentTypeThenExpected, ctx.Operation.Syntax.GetLocation(), new object[] { 1, operand.GetSymbol().GetTypeSymbol().ToString(), pageSourceTable.GetNavTypeKindSafe().ToString() + ' ' + pageSourceTable.Name.QuoteIdentifierIfNeeded() })); + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0058PageVariableMethodOnTemporaryTable, + ctx.Operation.Syntax.GetLocation(), + variableSymbol.ToString().QuoteIdentifierIfNeeded(), + operation.TargetMethod.Name)); + + return; } + ITableTypeSymbol pageSourceTable = pageTypeSymbol.RelatedTable; + ITableTypeSymbol recordArgument = recordTypeSymbol.BaseTable; + + if (!AreTheSameNavObjects(recordArgument, pageSourceTable)) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0039ArgumentDifferentTypeThenExpected, + ctx.Operation.Syntax.GetLocation(), + 1, + operand.GetSymbol()!.GetTypeSymbol().ToString(), + pageSourceTable.GetNavTypeKindSafe().ToString() + ' ' + pageSourceTable.Name.QuoteIdentifierIfNeeded() + )); + } + + private void AnalyzeTableReferencePageProvider(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) return; - private void AnalyzeTableReferencePageProvider(SymbolAnalysisContext ctx) + ITableTypeSymbol table = (ITableTypeSymbol)ctx.Symbol; + foreach (PropertyKind propertyKind in referencePageProviders) { - if (ctx.IsObsoletePendingOrRemoved()) return; - - ITableTypeSymbol table = (ITableTypeSymbol)ctx.Symbol; - foreach (PropertyKind propertyKind in referencePageProviders) - { - IPropertySymbol pageReference = table.GetProperty(propertyKind); - if (pageReference == null) continue; - IPageTypeSymbol page = (IPageTypeSymbol)pageReference.Value; - if (page == null) continue; - ITableTypeSymbol pageSourceTable = page.RelatedTable; - if (pageSourceTable == null) continue; - - if (!AreTheSameNavObjects(table, pageSourceTable)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0039ArgumentDifferentTypeThenExpected, pageReference.GetLocation(), new object[] { 1, table.GetTypeSymbol().GetNavTypeKindSafe().ToString() + ' ' + table.Name.QuoteIdentifierIfNeeded(), pageSourceTable.GetNavTypeKindSafe().ToString() + ' ' + pageSourceTable.Name.QuoteIdentifierIfNeeded() })); - } + IPropertySymbol? pageReference = table.GetProperty(propertyKind); + if (pageReference is null) continue; + IPageTypeSymbol page = (IPageTypeSymbol)pageReference.Value; + if (page is null) continue; + ITableTypeSymbol? pageSourceTable = page.RelatedTable; + if (pageSourceTable is null) continue; + + if (!AreTheSameNavObjects(table, pageSourceTable)) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0039ArgumentDifferentTypeThenExpected, + pageReference.GetLocation(), + 1, + table.GetTypeSymbol().GetNavTypeKindSafe().ToString() + ' ' + table.Name.QuoteIdentifierIfNeeded(), + pageSourceTable.GetNavTypeKindSafe().ToString() + ' ' + pageSourceTable.Name.QuoteIdentifierIfNeeded())); } + } + + private void AnalyzeTableExtensionReferencePageProvider(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) return; - private void AnalyzeTableExtensionReferencePageProvider(SymbolAnalysisContext ctx) + ITableExtensionTypeSymbol tableExtension = (ITableExtensionTypeSymbol)ctx.Symbol; + if (tableExtension.Target is not ITableTypeSymbol table) + return; + + foreach (PropertyKind propertyKind in referencePageProviders) { - if (ctx.IsObsoletePendingOrRemoved()) return; - - ITableExtensionTypeSymbol tableExtension = (ITableExtensionTypeSymbol)ctx.Symbol; - ITableTypeSymbol table = (ITableTypeSymbol)tableExtension.Target; - foreach (PropertyKind propertyKind in referencePageProviders) - { - IPropertySymbol pageReference = tableExtension.GetProperty(propertyKind); - if (pageReference == null) continue; - IPageTypeSymbol page = (IPageTypeSymbol)pageReference.Value; - ITableTypeSymbol pageSourceTable = page.RelatedTable; - if (pageSourceTable == null) continue; - - if (!AreTheSameNavObjects(table, pageSourceTable)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0039ArgumentDifferentTypeThenExpected, pageReference.GetLocation(), new object[] { 1, table.GetTypeSymbol().GetNavTypeKindSafe().ToString() + ' ' + table.Name.QuoteIdentifierIfNeeded(), pageSourceTable.GetNavTypeKindSafe().ToString() + ' ' + pageSourceTable.Name.QuoteIdentifierIfNeeded() })); - } + IPropertySymbol? pageReference = tableExtension.GetProperty(propertyKind); + if (pageReference is null) continue; + IPageTypeSymbol page = (IPageTypeSymbol)pageReference.Value; + ITableTypeSymbol? pageSourceTable = page.RelatedTable; + if (pageSourceTable is null) continue; + + if (!AreTheSameNavObjects(table, pageSourceTable)) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0039ArgumentDifferentTypeThenExpected, + pageReference.GetLocation(), + 1, + table.GetTypeSymbol().GetNavTypeKindSafe().ToString() + ' ' + table.Name.QuoteIdentifierIfNeeded(), + pageSourceTable.GetNavTypeKindSafe().ToString() + ' ' + pageSourceTable.Name.QuoteIdentifierIfNeeded())); } + } + + private static bool AreTheSameNavObjects(ITableTypeSymbol left, ITableTypeSymbol right) + { + if (left.GetNavTypeKindSafe() != right.GetNavTypeKindSafe()) + return false; - private static bool AreTheSameNavObjects(ITableTypeSymbol left, ITableTypeSymbol right) - { - if (left.GetNavTypeKindSafe() != right.GetNavTypeKindSafe()) return false; #if !LessThenFall2023RV1 - if (((INamespaceSymbol)left.ContainingSymbol).QualifiedName != ((INamespaceSymbol)right.ContainingSymbol).QualifiedName) return false; + if (left.ContainingSymbol is not INamespaceSymbol leftNamespaceSymbol || + right.ContainingSymbol is not INamespaceSymbol rightNamespaceSymbol || + leftNamespaceSymbol.QualifiedName != rightNamespaceSymbol.QualifiedName) + return false; #endif - if (left.Name != right.Name) return false; - return true; - } + if (left.Name != right.Name) + return false; - private static string GetFullyQualifiedObjectName(IPageTypeSymbol page) - { + return true; + } + + private static string GetFullyQualifiedObjectName(IPageTypeSymbol page) + { #if !LessThenFall2023RV1 - if (page.ContainingNamespace.QualifiedName != "") - return page.ContainingNamespace.QualifiedName + "." + page.Name.QuoteIdentifierIfNeeded(); + if (page.ContainingNamespace?.QualifiedName != "") + return page.ContainingNamespace?.QualifiedName + "." + page.Name.QuoteIdentifierIfNeeded(); #endif - return page.Name.QuoteIdentifierIfNeeded(); - } + return page.Name.QuoteIdentifierIfNeeded(); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0040ExplicitlySetRunTrigger.cs b/BusinessCentral.LinterCop/Design/Rule0040ExplicitlySetRunTrigger.cs index 85a54e6c..5720d99f 100644 --- a/BusinessCentral.LinterCop/Design/Rule0040ExplicitlySetRunTrigger.cs +++ b/BusinessCentral.LinterCop/Design/Rule0040ExplicitlySetRunTrigger.cs @@ -1,39 +1,46 @@ -#nullable disable // TODO: Enable nullable and review rule using System.Collections.Immutable; -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0040ExplicitlySetRunTrigger : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0040ExplicitlySetRunTrigger : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0040ExplicitlySetRunTrigger); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0040ExplicitlySetRunTrigger); - private static readonly List buildInMethodNames = new List + private static readonly HashSet buildInMethodNames = new() { - "insert", - "modify", - "modifyall", - "delete", - "deleteall" + "Insert", + "Modify", + "ModifyAll", + "Delete", + "DeleteAll" }; - public override void Initialize(AnalysisContext context) => context.RegisterOperationAction(new Action(this.AnalyzeRunTriggerParameters), OperationKind.InvocationExpression); + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(new Action(this.AnalyzeRunTriggerParameters), OperationKind.InvocationExpression); - private void AnalyzeRunTriggerParameters(OperationAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; + private void AnalyzeRunTriggerParameters(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) + return; + + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + !buildInMethodNames.Contains(operation.TargetMethod.Name)) + return; - IInvocationExpression operation = (IInvocationExpression)ctx.Operation; - if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return; - if (!buildInMethodNames.Contains(operation.TargetMethod.Name.ToLowerInvariant())) return; - if (!(operation.Instance?.GetSymbol().GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.Record || operation.Instance?.GetSymbol().GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.RecordRef)) return; + var operationTypeSymbolNavType = operation.Instance?.GetSymbol()?.GetTypeSymbol().GetNavTypeKindSafe(); + if (operationTypeSymbolNavType is null || + !(operationTypeSymbolNavType == NavTypeKind.Record || operationTypeSymbolNavType == NavTypeKind.RecordRef)) + return; - if (operation.Arguments.Where(args => SemanticFacts.IsSameName(args.Parameter.Name, "RunTrigger")).SingleOrDefault() == null) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0040ExplicitlySetRunTrigger, ctx.Operation.Syntax.GetLocation())); - } + if (operation.Arguments.Where(args => args.Parameter.Name.Equals("RunTrigger")).SingleOrDefault() is null) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0040ExplicitlySetRunTrigger, + ctx.Operation.Syntax.GetLocation())); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0041EmptyCaptionLocked.cs b/BusinessCentral.LinterCop/Design/Rule0041EmptyCaptionLocked.cs index 5b614fd7..11a00bf3 100644 --- a/BusinessCentral.LinterCop/Design/Rule0041EmptyCaptionLocked.cs +++ b/BusinessCentral.LinterCop/Design/Rule0041EmptyCaptionLocked.cs @@ -1,65 +1,73 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0041EmptyCaptionLocked : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0041EmptyCaptionLocked : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0041EmptyCaptionLocked); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0041EmptyCaptionLocked); - // List based on https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/properties/devenv-caption-property - public override void Initialize(AnalysisContext context) - => context.RegisterSyntaxNodeAction(new Action(AnalyzeCaptionProperty), new SyntaxKind[] { - SyntaxKind.TableObject, - SyntaxKind.Field, // TableField - SyntaxKind.PageField, - SyntaxKind.PageGroup, - SyntaxKind.PageObject, - SyntaxKind.RequestPage, - SyntaxKind.PageLabel, - SyntaxKind.PageGroup, - SyntaxKind.PagePart, - SyntaxKind.PageSystemPart, - SyntaxKind.PageAction, - SyntaxKind.PageActionSeparator, - SyntaxKind.PageActionGroup, - SyntaxKind.XmlPortObject, - SyntaxKind.ReportObject, - SyntaxKind.QueryObject, - SyntaxKind.QueryColumn, - SyntaxKind.QueryFilter, - SyntaxKind.ReportColumn, - SyntaxKind.EnumValue, - SyntaxKind.PageCustomAction, + // List based on https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/properties/devenv-caption-property + public override void Initialize(AnalysisContext context) => + context.RegisterSyntaxNodeAction(new Action(AnalyzeCaptionProperty), new SyntaxKind[] { + SyntaxKind.TableObject, + SyntaxKind.Field, // TableField + SyntaxKind.PageField, + SyntaxKind.PageGroup, + SyntaxKind.PageObject, + SyntaxKind.RequestPage, + SyntaxKind.PageLabel, + SyntaxKind.PageGroup, + SyntaxKind.PagePart, + SyntaxKind.PageSystemPart, + SyntaxKind.PageAction, + SyntaxKind.PageActionSeparator, + SyntaxKind.PageActionGroup, + SyntaxKind.XmlPortObject, + SyntaxKind.ReportObject, + SyntaxKind.QueryObject, + SyntaxKind.QueryColumn, + SyntaxKind.QueryFilter, + SyntaxKind.ReportColumn, + SyntaxKind.EnumValue, + SyntaxKind.PageCustomAction, #if !LessThenSpring2024 - SyntaxKind.PageSystemAction, + SyntaxKind.PageSystemAction, #endif - SyntaxKind.PageView, - SyntaxKind.ReportLayout, - SyntaxKind.ProfileObject, - SyntaxKind.EnumType, - SyntaxKind.PermissionSet, - SyntaxKind.TableExtensionObject, - SyntaxKind.PageExtensionObject - }); + SyntaxKind.PageView, + SyntaxKind.ReportLayout, + SyntaxKind.ProfileObject, + SyntaxKind.EnumType, + SyntaxKind.PermissionSet, + SyntaxKind.TableExtensionObject, + SyntaxKind.PageExtensionObject + }); + + private void AnalyzeCaptionProperty(SyntaxNodeAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) + return; - private void AnalyzeCaptionProperty(SyntaxNodeAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; + if (ctx.Node.IsKind(SyntaxKind.EnumValue) && ctx.ContainingSymbol.Kind == SymbolKind.Enum) + return; // Prevent double raising the rule on EnumValue in a EnumObject - if (ctx.Node.IsKind(SyntaxKind.EnumValue) && ctx.ContainingSymbol.Kind == SymbolKind.Enum) return; // Prevent double raising the rule on EnumValue in a EnumObject + if (ctx.Node?.GetProperty("Caption")?.Value is not LabelPropertyValueSyntax captionProperty) + return; - LabelPropertyValueSyntax captionProperty = ctx.Node?.GetProperty("Caption")?.Value as LabelPropertyValueSyntax; - if (captionProperty?.Value.LabelText.GetLiteralValue() == null || captionProperty.Value.LabelText.GetLiteralValue().ToString().Trim() != "") return; + if (captionProperty?.Value.LabelText.GetLiteralValue() is null || + captionProperty.Value.LabelText.GetLiteralValue().ToString().Trim() != "") + return; - if (captionProperty.Value.Properties?.Values.Where(prop => prop.Identifier.Text.ToLowerInvariant() == "locked").FirstOrDefault() != null) return; + if (captionProperty.Value.Properties?.Values.Where(prop => prop.Identifier.Text.Equals("Locked", StringComparison.OrdinalIgnoreCase)).FirstOrDefault() is not null) + return; - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0041EmptyCaptionLocked, captionProperty.GetLocation())); - } + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0041EmptyCaptionLocked, + captionProperty.GetLocation())); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0042AutoCalcFieldsOnNormalFields.cs b/BusinessCentral.LinterCop/Design/Rule0042AutoCalcFieldsOnNormalFields.cs index 2b680dee..804566f4 100644 --- a/BusinessCentral.LinterCop/Design/Rule0042AutoCalcFieldsOnNormalFields.cs +++ b/BusinessCentral.LinterCop/Design/Rule0042AutoCalcFieldsOnNormalFields.cs @@ -3,28 +3,27 @@ using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0042AutoCalcFieldsOnNormalFields : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0042AutoCalcFieldsOnNormalFields : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0042AutoCalcFieldsOnNormalFields); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0042AutoCalcFieldsOnNormalFields); - public override void Initialize(AnalysisContext context) => context.RegisterSyntaxNodeAction(syntaxContext => - { - if (!syntaxContext.Node.ToString().ToLowerInvariant().Contains("setautocalcfields")) - return; + public override void Initialize(AnalysisContext context) => context.RegisterSyntaxNodeAction(syntaxContext => + { + if (!syntaxContext.Node.ToString().ToLowerInvariant().Contains("setautocalcfields")) + return; - IInvocationExpression operation = (IInvocationExpression)syntaxContext.SemanticModel.GetOperation(syntaxContext.Node); - IMethodSymbol targetMethod = operation.TargetMethod; - if (targetMethod == null || !SemanticFacts.IsSameName(targetMethod.Name, "setautocalcfields") || targetMethod.MethodKind != MethodKind.BuiltInMethod) - return; + IInvocationExpression operation = (IInvocationExpression)syntaxContext.SemanticModel.GetOperation(syntaxContext.Node); + IMethodSymbol targetMethod = operation.TargetMethod; + if (targetMethod is null || !SemanticFacts.IsSameName(targetMethod.Name, "setautocalcfields") || targetMethod.MethodKind != MethodKind.BuiltInMethod) + return; - foreach (IArgument obj in operation.Arguments) - { - if ((obj.Value is IConversionExpression conversionExpression2 ? conversionExpression2.Operand : (IOperation)null) is IFieldAccess fieldAccess2 && fieldAccess2.FieldSymbol.FieldClass != FieldClassKind.FlowField && fieldAccess2.Type.NavTypeKind != NavTypeKind.Blob) - syntaxContext.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0042AutoCalcFieldsOnNormalFields, fieldAccess2.Syntax.GetLocation(), (object)fieldAccess2.FieldSymbol.Name)); - } - }, SyntaxKind.InvocationExpression); - } + foreach (IArgument obj in operation.Arguments) + { + if ((obj.Value is IConversionExpression conversionExpression2 ? conversionExpression2.Operand : (IOperation)null) is IFieldAccess fieldAccess2 && fieldAccess2.FieldSymbol.FieldClass != FieldClassKind.FlowField && fieldAccess2.Type.NavTypeKind != NavTypeKind.Blob) + syntaxContext.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0042AutoCalcFieldsOnNormalFields, fieldAccess2.Syntax.GetLocation(), (object)fieldAccess2.FieldSymbol.Name)); + } + }, SyntaxKind.InvocationExpression); } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0043SecretText.cs b/BusinessCentral.LinterCop/Design/Rule0043SecretText.cs index ad21255e..09c4be26 100644 --- a/BusinessCentral.LinterCop/Design/Rule0043SecretText.cs +++ b/BusinessCentral.LinterCop/Design/Rule0043SecretText.cs @@ -1,113 +1,130 @@ #if !LessThenFall2023RV1 using System.Collections.Immutable; -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0043SecretText : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0043SecretText : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0043SecretText); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0043SecretText); - private static readonly string authorization = "Authorization"; + public override VersionCompatibility SupportedVersions => VersionCompatibility.Fall2023OrGreater; - private static readonly List buildInMethodNames = new List + private static readonly string authorization = "Authorization"; + + private static readonly List buildInMethodNames = new List { "add", "getvalues", "tryaddwithoutvalidation" }; - public override void Initialize(AnalysisContext context) - { - context.RegisterOperationAction(new Action(this.AnalyzeHttpObjects), OperationKind.InvocationExpression); - context.RegisterOperationAction(new Action(this.AnalyzeIsolatedStorage), OperationKind.InvocationExpression); - } + public override void Initialize(AnalysisContext context) + { + context.RegisterOperationAction(new Action(this.AnalyzeHttpObjects), OperationKind.InvocationExpression); + context.RegisterOperationAction(new Action(this.AnalyzeIsolatedStorage), OperationKind.InvocationExpression); + } - private void AnalyzeIsolatedStorage(OperationAnalysisContext ctx) - { + private void AnalyzeIsolatedStorage(OperationAnalysisContext ctx) + { #if !LessThenSpring2024 - if (!VersionChecker.IsSupported(ctx.ContainingSymbol, VersionCompatibility.Spring2024OrGreater)) return; + if (!VersionChecker.IsSupported(ctx.ContainingSymbol, VersionCompatibility.Spring2024OrGreater)) + return; - if (ctx.IsObsoletePendingOrRemoved()) return; + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) + return; - if (ctx.Operation is not IInvocationExpression operation) return; - if (operation.TargetMethod is not IMethodSymbol targetMethod) return; - if (targetMethod.ContainingSymbol?.Kind != SymbolKind.Class) return; - if (!SemanticFacts.IsSameName(targetMethod.ContainingSymbol.Name, "IsolatedStorage")) return; + if (operation.TargetMethod is not IMethodSymbol targetMethod) + return; + if (targetMethod.ContainingSymbol?.Kind != SymbolKind.Class) + return; - IArgument? argument = operation.TargetMethod.Name.ToLowerInvariant() switch - { - "get" => operation.Arguments.FirstOrDefault(a => a.Parameter.IsVar), - "set" or "setencrypted" when operation.Arguments.Count() > 1 => operation.Arguments[1], - _ => null - }; + if (!SemanticFacts.IsSameName(targetMethod.ContainingSymbol.Name, "IsolatedStorage")) + return; - if (argument == null) - return; + IArgument? argument = operation.TargetMethod.Name.ToLowerInvariant() switch + { + "get" => operation.Arguments.FirstOrDefault(a => a.Parameter.IsVar), + "set" or "setencrypted" when operation.Arguments.Length > 1 => operation.Arguments[1], + _ => null + }; - if (!IsArgumentOfTypeSecretText(argument)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0043SecretText, argument.Syntax.GetLocation())); + if (argument is null) + return; + + if (!IsArgumentOfTypeSecretText(argument)) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0043SecretText, + argument.Syntax.GetLocation())); #endif - } + } + + private void AnalyzeHttpObjects(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) + return; + + // We need at least two arguments + if (operation.Arguments.Length < 2) return; - private void AnalyzeHttpObjects(OperationAnalysisContext ctx) + switch (operation.TargetMethod.MethodKind) { - if (!VersionChecker.IsSupported(ctx.ContainingSymbol, VersionCompatibility.Fall2023OrGreater)) return; - if (ctx.IsObsoletePendingOrRemoved()) return; - - IInvocationExpression operation = (IInvocationExpression)ctx.Operation; - - // We need at least two arguments - if (operation.Arguments.Count() < 2) return; - - switch (operation.TargetMethod.MethodKind) - { - case MethodKind.BuiltInMethod: - if (!buildInMethodNames.Contains(operation.TargetMethod.Name.ToLowerInvariant())) return; - if (!(operation.Instance?.GetSymbol()?.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.HttpHeaders || operation.Instance?.GetSymbol()?.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.HttpClient)) return; - break; - case MethodKind.Method: - if (operation.TargetMethod.ContainingType?.GetNavTypeKindSafe() != NavTypeKind.Codeunit) return; - ICodeunitTypeSymbol codeunitTypeSymbol = (ICodeunitTypeSymbol)operation.TargetMethod.GetContainingObjectTypeSymbol(); - if (!SemanticFacts.IsSameName(((INamespaceSymbol)codeunitTypeSymbol.ContainingSymbol!).QualifiedName, "System.RestClient")) return; - if (!SemanticFacts.IsSameName(codeunitTypeSymbol.Name, "Rest Client")) return; - if (!SemanticFacts.IsSameName(operation.TargetMethod.Name, "SetDefaultRequestHeader")) return; - break; - default: + case MethodKind.BuiltInMethod: + if (!buildInMethodNames.Contains(operation.TargetMethod.Name.ToLowerInvariant())) return; - } - if (!IsAuthorizationArgument(operation.Arguments[0])) return; + if (!(operation.Instance?.GetSymbol()?.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.HttpHeaders || operation.Instance?.GetSymbol()?.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.HttpClient)) + return; - if (!IsArgumentOfTypeSecretText(operation.Arguments[1])) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0043SecretText, ctx.Operation.Syntax.GetLocation())); + break; + case MethodKind.Method: + if (operation.TargetMethod.ContainingType?.GetNavTypeKindSafe() != NavTypeKind.Codeunit) + return; + ICodeunitTypeSymbol codeunitTypeSymbol = (ICodeunitTypeSymbol)operation.TargetMethod.GetContainingObjectTypeSymbol(); + if (!SemanticFacts.IsSameName(((INamespaceSymbol)codeunitTypeSymbol.ContainingSymbol!).QualifiedName, "System.RestClient")) + return; + if (!SemanticFacts.IsSameName(codeunitTypeSymbol.Name, "Rest Client")) + return; + if (!SemanticFacts.IsSameName(operation.TargetMethod.Name, "SetDefaultRequestHeader")) + return; + break; + default: + return; } - private bool IsArgumentOfTypeSecretText(IArgument argument) - { - return argument.Parameter?.OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.SecretText; - } + if (!IsAuthorizationArgument(operation.Arguments[0])) + return; + + if (!IsArgumentOfTypeSecretText(operation.Arguments[1])) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0043SecretText, + ctx.Operation.Syntax.GetLocation())); + } - private static bool IsAuthorizationArgument(IArgument argument) + private bool IsArgumentOfTypeSecretText(IArgument argument) + { + return argument.Parameter?.OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.SecretText; + } + + private static bool IsAuthorizationArgument(IArgument argument) + { + switch (argument.Syntax.Kind) { - switch (argument.Syntax.Kind) - { - case SyntaxKind.LiteralExpression: - return SemanticFacts.IsSameName(argument.Value.ConstantValue.Value.ToString(), authorization); - case SyntaxKind.IdentifierName: - if (argument.Value.Kind != OperationKind.ConversionExpression) return false; - IOperation operand = ((IConversionExpression)argument.Value).Operand; - if (operand.GetSymbol()?.OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Label) return false; - ILabelTypeSymbol label = (ILabelTypeSymbol)operand.GetSymbol()!.OriginalDefinition.GetTypeSymbol(); - return SemanticFacts.IsSameName(label.GetLabelText(), authorization); - default: - return false; - } + case SyntaxKind.LiteralExpression: + return SemanticFacts.IsSameName(argument.Value.ConstantValue.Value.ToString(), authorization); + case SyntaxKind.IdentifierName: + if (argument.Value.Kind != OperationKind.ConversionExpression) return false; + IOperation operand = ((IConversionExpression)argument.Value).Operand; + if (operand.GetSymbol()?.OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Label) return false; + ILabelTypeSymbol label = (ILabelTypeSymbol)operand.GetSymbol()!.OriginalDefinition.GetTypeSymbol(); + return SemanticFacts.IsSameName(label.GetLabelText(), authorization); + default: + return false; } } } diff --git a/BusinessCentral.LinterCop/Design/Rule0044AnalyzeTransferField.cs b/BusinessCentral.LinterCop/Design/Rule0044AnalyzeTransferField.cs index d866d1a8..0be11418 100644 --- a/BusinessCentral.LinterCop/Design/Rule0044AnalyzeTransferField.cs +++ b/BusinessCentral.LinterCop/Design/Rule0044AnalyzeTransferField.cs @@ -14,7 +14,7 @@ public class Rule0044AnalyzeTransferFields : DiagnosticAnalyzer { private List> tablePairs = new List>(); - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0044AnalyzeTransferFields, DiagnosticDescriptors.Rule0000ErrorInRule); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0044AnalyzeTransferFields, DiagnosticDescriptors.Rule0000ErrorInRule); public override void Initialize(AnalysisContext context) { @@ -29,7 +29,7 @@ private void AnalyzeTableExtension(SyntaxNodeAnalysisContext ctx) return; string? baseObject = GetIdentifierName(tableExtensionSyntax.BaseObject.Identifier); - if (baseObject == null) + if (baseObject is null) return; IEnumerable> tables = tablePairs.Where(x => x.Item1.Equals(baseObject) || x.Item2.Equals(baseObject)); @@ -58,74 +58,74 @@ private async void AnalyzeTransferFields(OperationAnalysisContext ctx) // Investigate https://github.com/StefanMaron/BusinessCentral.LinterCop/issues/828 try { - if (ctx.Operation.Syntax.GetType() != typeof(InvocationExpressionSyntax)) - return; + if (ctx.Operation.Syntax.GetType() != typeof(InvocationExpressionSyntax)) + return; - if (((IInvocationExpression)ctx.Operation).TargetMethod.MethodKind != MethodKind.BuiltInMethod) - return; + if (((IInvocationExpression)ctx.Operation).TargetMethod.MethodKind != MethodKind.BuiltInMethod) + return; - if (ctx.Operation.Syntax is not InvocationExpressionSyntax invocationExpression) - return; + if (ctx.Operation.Syntax is not InvocationExpressionSyntax invocationExpression) + return; - Tuple? records = GetInvokingRecordNames(invocationExpression); + Tuple? records = GetInvokingRecordNames(invocationExpression); - if (records == null) - return; + if (records is null) + return; - Task localVariablesTask = ctx.ContainingSymbol.DeclaringSyntaxReference!.GetSyntaxAsync(); - Task globalVariablesTask = ctx.ContainingSymbol.ContainingSymbol!.DeclaringSyntaxReference!.GetSyntaxAsync(); + Task localVariablesTask = ctx.ContainingSymbol.DeclaringSyntaxReference!.GetSyntaxAsync(); + Task globalVariablesTask = ctx.ContainingSymbol.ContainingSymbol!.DeclaringSyntaxReference!.GetSyntaxAsync(); - List variables = new List(); + List variables = new List(); - SyntaxNode localVariables = await localVariablesTask; - variables.AddRange(FindVariables(localVariables, SyntaxKind.VarSection)); - SyntaxNode globalVariables = await globalVariablesTask; - variables.AddRange(FindVariables(globalVariables, SyntaxKind.GlobalVarSection)); + SyntaxNode localVariables = await localVariablesTask; + variables.AddRange(FindVariables(localVariables, SyntaxKind.VarSection)); + SyntaxNode globalVariables = await globalVariablesTask; + variables.AddRange(FindVariables(globalVariables, SyntaxKind.GlobalVarSection)); - string? tableName1 = GetObjectName(variables.FirstOrDefault(x => - { - string? name = x.GetNameStringValue(); + string? tableName1 = GetObjectName(variables.FirstOrDefault(x => + { + string? name = x.GetNameStringValue(); - if (name == null) - return false; + if (name is null) + return false; - return name.Equals(records.Item1); - })); + return name.Equals(records.Item1); + })); - string? tableName2 = GetObjectName(variables.FirstOrDefault(x => - { - string? name = x.GetNameStringValue(); + string? tableName2 = GetObjectName(variables.FirstOrDefault(x => + { + string? name = x.GetNameStringValue(); - if (name == null) - return false; + if (name is null) + return false; - return name.Equals(records.Item2); - })); + return name.Equals(records.Item2); + })); - if (tableName1 == null && (records.Item1.ToLower().Equals("rec") || records.Item1.ToLower().Equals("xrec"))) - tableName1 = GetObjectSourceTable(globalVariables, ctx.Compilation); + if (tableName1 is null && (records.Item1.ToLower().Equals("rec") || records.Item1.ToLower().Equals("xrec"))) + tableName1 = GetObjectSourceTable(globalVariables, ctx.Compilation); - if (tableName2 == null && (records.Item2.ToLower().Equals("rec") || records.Item2.ToLower().Equals("xrec"))) - tableName2 = GetObjectSourceTable(globalVariables, ctx.Compilation); + if (tableName2 is null && (records.Item2.ToLower().Equals("rec") || records.Item2.ToLower().Equals("xrec"))) + tableName2 = GetObjectSourceTable(globalVariables, ctx.Compilation); - if (tableName1 == tableName2 || tableName1 == null || tableName2 == null) - return; + if (tableName1 == tableName2 || tableName1 is null || tableName2 is null) + return; - Dictionary tableExtensions = GetTableExtensions(ctx.Compilation); - Table table1 = GetTableWithFieldsByTableName(ctx.Compilation, tableName1); - Table table2 = GetTableWithFieldsByTableName(ctx.Compilation, tableName2); + Dictionary tableExtensions = GetTableExtensions(ctx.Compilation); + Table table1 = GetTableWithFieldsByTableName(ctx.Compilation, tableName1); + Table table2 = GetTableWithFieldsByTableName(ctx.Compilation, tableName2); - List> fieldGroups = GetFieldsWithSameIDAndApplyFilter(table1.Fields, table2.Fields, DifferentNameAndTypeFilter); + List> fieldGroups = GetFieldsWithSameIDAndApplyFilter(table1.Fields, table2.Fields, DifferentNameAndTypeFilter); - if (fieldGroups.Any()) - { - ReportFieldDiagnostics(ctx, table1, fieldGroups); - ReportFieldDiagnostics(ctx, table2, fieldGroups); + if (fieldGroups.Any()) + { + ReportFieldDiagnostics(ctx, table1, fieldGroups); + ReportFieldDiagnostics(ctx, table2, fieldGroups); - if (table1.Fields.Any(x => x.Location != null) || table2.Fields.Any(x => x.Location != null)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0044AnalyzeTransferFields, invocationExpression.GetLocation(), table1.Name, table2.Name)); + if (table1.Fields.Any(x => x.Location is not null) || table2.Fields.Any(x => x.Location is not null)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0044AnalyzeTransferFields, invocationExpression.GetLocation(), table1.Name, table2.Name)); - } + } } catch (InvalidCastException) { @@ -141,7 +141,7 @@ private void ReportFieldDiagnostics(OperationAnalysisContext ctx, Table table, L Field field = fieldGroupValues.First(x => x.Table.Equals(table)); - if (field.Location == null) + if (field.Location is null) continue; foreach (Field fieldGroupValue in fieldGroupValues) @@ -162,7 +162,7 @@ private void ReportFieldDiagnostics(SyntaxNodeAnalysisContext ctx, Table table, Field field = fieldGroupValues.First(x => x.Table.Equals(table)); - if (field.Location == null) + if (field.Location is null) continue; foreach (Field fieldGroupValue in fieldGroupValues) @@ -203,7 +203,7 @@ private void ReportFieldDiagnostics(SyntaxNodeAnalysisContext ctx, Table table, case PageExtensionSyntax pageExtensionSyntax: string? pageExtensionName = GetIdentifierName(pageExtensionSyntax.BaseObject.Identifier); - if (pageExtensionName == null) + if (pageExtensionName is null) return null; return GetSourceTableByPageName(compilation, pageExtensionName); @@ -263,7 +263,7 @@ private Dictionary GetTableExtensions(Compilation string? extendedTable = GetIdentifierName(tableExtension.BaseObject.Identifier); - if (extendedTable == null) + if (extendedTable is null) continue; if (!tableExtensions.ContainsKey(extendedTable)) @@ -282,11 +282,11 @@ private Table GetTableWithFieldsByTableName(Compilation compilation, string tabl Table table = new Table(tableName); - if (tableSymbol != null) + if (tableSymbol is not null) { SyntaxReference? syntaxReference = tableSymbol.DeclaringSyntaxReference; - if (syntaxReference != null) + if (syntaxReference is not null) { TableSyntax tableSyntax = (TableSyntax)syntaxReference.GetSyntax(); @@ -296,7 +296,7 @@ private Table GetTableWithFieldsByTableName(Compilation compilation, string tabl table.PopulateFields(tableSymbol); } - if (tableExtensions == null) + if (tableExtensions is null) return table; TableExtensionSyntax? tableExtension; @@ -312,11 +312,11 @@ private Table GetTableWithFieldsByTableName(Compilation compilation, string tabl { IApplicationObjectTypeSymbol? pageSymbol = compilation.GetApplicationObjectTypeSymbolsByNameAcrossModules(SymbolKind.Page, pageName).FirstOrDefault(); - if (pageSymbol != null) + if (pageSymbol is not null) { SyntaxReference? syntaxReference = pageSymbol.DeclaringSyntaxReference; - if (syntaxReference != null) + if (syntaxReference is not null) { PageSyntax pageSyntax = (PageSyntax)syntaxReference.GetSyntax(); @@ -344,7 +344,7 @@ private Table GetTableWithFieldsByTableName(Compilation compilation, string tabl object obj = method.Invoke(pageSymbol, null); - if (obj == null) + if (obj is null) return null; type = assembly.GetType(obj.GetType().ToString()); @@ -384,7 +384,7 @@ private bool DifferentNameAndTypeFilter(IGrouping fieldGroup) private string? GetObjectName(VariableDeclarationBaseSyntax variable) { - if (variable == null || variable.Type.DataType.GetType() == typeof(SimpleNamedDataTypeSyntax)) + if (variable is null || variable.Type.DataType.GetType() == typeof(SimpleNamedDataTypeSyntax)) return null; SubtypedDataTypeSyntax subtypedData = (SubtypedDataTypeSyntax)variable.Type.DataType; @@ -395,7 +395,7 @@ private List FindVariables(SyntaxNode node, Synta { var nodeFound = node.DescendantNodes().FirstOrDefault(x => x.Kind == syntaxKind); - if (nodeFound == null || nodeFound is not VarSectionBaseSyntax varSection) + if (nodeFound is null || nodeFound is not VarSectionBaseSyntax varSection) return new List(); return varSection.Variables.ToList(); @@ -669,7 +669,7 @@ public Table(string name) public void PopulateFields(IApplicationObjectTypeSymbol table) { - if (table == null) + if (table is null) return; Assembly assembly = typeof(Microsoft.Dynamics.Nav.CodeAnalysis.Symbols.VariableKind).Assembly; @@ -698,10 +698,10 @@ public void PopulateFields(IApplicationObjectTypeSymbol table) // Remove the QualifiedName from the Enum for now. // In the future refactor this to support Enums with the same object name cross different namespaces IEnumBaseTypeSymbol? enumBaseTypeSymbol = typeprop.GetValue(field) as IEnumBaseTypeSymbol; - if (enumBaseTypeSymbol != null) + if (enumBaseTypeSymbol is not null) { INamespaceSymbol? namespaceSymbol = enumBaseTypeSymbol.ContainingSymbol as INamespaceSymbol; - if (namespaceSymbol != null) + if (namespaceSymbol is not null) objtype = objtype.Replace(namespaceSymbol.QualifiedName + '.', ""); } #endif @@ -712,7 +712,7 @@ public void PopulateFields(IApplicationObjectTypeSymbol table) public void PopulateFields(FieldExtensionListSyntax fieldList) { - if (fieldList == null) return; + if (fieldList is null) return; foreach (FieldSyntax field in fieldList.Fields.Where(fld => fld.IsKind(SyntaxKind.Field))) { @@ -723,7 +723,7 @@ public void PopulateFields(FieldExtensionListSyntax fieldList) public void PopulateFields(FieldListSyntax fieldList) { - if (fieldList == null) return; + if (fieldList is null) return; foreach (FieldSyntax field in fieldList.Fields) { @@ -775,7 +775,7 @@ private FieldClassKind GetFieldClass(FieldSyntax field) .Where(prop => ((PropertySyntax)prop).Name.Identifier.ToString().Equals("FieldClass")) .Where(prop => ((PropertySyntax)prop).Value.GetType() == typeof(EnumPropertyValueSyntax)) .SingleOrDefault(); - if (fieldClassProperty == null) + if (fieldClassProperty is null) return FieldClassKind.Normal; EnumPropertyValueSyntax fieldClassPropertyValue = (EnumPropertyValueSyntax)fieldClassProperty.Value; diff --git a/BusinessCentral.LinterCop/Design/Rule0045ZeroEnumValueReservedForEmpty.cs b/BusinessCentral.LinterCop/Design/Rule0045ZeroEnumValueReservedForEmpty.cs index 0f0a1735..33f79022 100644 --- a/BusinessCentral.LinterCop/Design/Rule0045ZeroEnumValueReservedForEmpty.cs +++ b/BusinessCentral.LinterCop/Design/Rule0045ZeroEnumValueReservedForEmpty.cs @@ -1,38 +1,44 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design -{ - [DiagnosticAnalyzer] - public class Rule0045ZeroEnumValueReservedForEmpty : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0045ZeroEnumValueReservedForEmpty); +namespace BusinessCentral.LinterCop.Design; - public override void Initialize(AnalysisContext context) => context.RegisterSyntaxNodeAction(new Action(this.AnalyzeReservedEnum), SyntaxKind.EnumValue); +[DiagnosticAnalyzer] +public class Rule0045ZeroEnumValueReservedForEmpty : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0045ZeroEnumValueReservedForEmpty); - private void AnalyzeReservedEnum(SyntaxNodeAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; + public override void Initialize(AnalysisContext context) => + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeReservedEnum), SyntaxKind.EnumValue); - IEnumTypeSymbol enumTypeSymbol = ctx.ContainingSymbol.GetContainingObjectTypeSymbol() as IEnumTypeSymbol; - if (enumTypeSymbol != null && enumTypeSymbol.ImplementedInterfaces.Any()) return; + private void AnalyzeReservedEnum(SyntaxNodeAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Node is not EnumValueSyntax enumValue) + return; - LabelPropertyValueSyntax captionProperty = ctx.Node?.GetProperty("Caption")?.Value as LabelPropertyValueSyntax; - EnumValueSyntax enumValue = ctx.Node as EnumValueSyntax; + if (ctx.ContainingSymbol.Kind != SymbolKind.Enum || + enumValue.Id.ValueText != "0") + return; - if (enumValue == null) return; + if (ctx.ContainingSymbol.GetContainingApplicationObjectTypeSymbol() is not IEnumTypeSymbol enumTypeSymbol || + enumTypeSymbol.ImplementedInterfaces.Any()) + return; - if (enumValue.Id.ValueText != "0" || ctx.ContainingSymbol.Kind != SymbolKind.Enum) return; + if (enumValue.GetNameStringValue()?.Trim() != "") + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0045ZeroEnumValueReservedForEmpty, + enumValue.Name.GetLocation())); - if (enumValue.GetNameStringValue().Trim() != "") - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0045ZeroEnumValueReservedForEmpty, enumValue.Name.GetLocation())); + if (ctx.Node?.GetProperty("Caption")?.Value is not LabelPropertyValueSyntax captionProperty) + return; - if (captionProperty != null && captionProperty.Value.LabelText.Value.Value.ToString().Trim() != "") - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0045ZeroEnumValueReservedForEmpty, captionProperty.GetLocation())); - } + if (captionProperty.Value.LabelText.Value.Value.ToString().Trim() != "") + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0045ZeroEnumValueReservedForEmpty, + captionProperty.GetLocation())); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0046LockedTokLabels.cs b/BusinessCentral.LinterCop/Design/Rule0046LockedTokLabels.cs index 2dd728a0..16b70c8d 100644 --- a/BusinessCentral.LinterCop/Design/Rule0046LockedTokLabels.cs +++ b/BusinessCentral.LinterCop/Design/Rule0046LockedTokLabels.cs @@ -1,36 +1,39 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design -{ - [DiagnosticAnalyzer] - public class Rule0046LockedTokLabels : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0046TokLabelsLocked, DiagnosticDescriptors.Rule0047LockedLabelsTok); - public override void Initialize(AnalysisContext context) => context.RegisterSymbolAction(new Action(this.AnalyzeLockedLabel), SymbolKind.GlobalVariable, SymbolKind.LocalVariable); +namespace BusinessCentral.LinterCop.Design; - private void AnalyzeLockedLabel(SymbolAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; +[DiagnosticAnalyzer] +public class Rule0046LockedTokLabels : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0046TokLabelsLocked, DiagnosticDescriptors.Rule0047LockedLabelsTok); - IVariableSymbol symbol = (IVariableSymbol)ctx.Symbol; + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(new Action(this.AnalyzeLockedLabel), + SymbolKind.GlobalVariable, + SymbolKind.LocalVariable); - ITypeSymbol type1 = symbol.Type; - if (type1 == null || type1.NavTypeKind != NavTypeKind.Label) - return; + private void AnalyzeLockedLabel(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Symbol is not IVariableSymbol symbol) + return; - ILabelTypeSymbol type = symbol.Type as ILabelTypeSymbol; - if (type.Locked) - if (!type.Name.EndsWith("Tok")) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0047LockedLabelsTok, symbol.GetLocation(), symbol.Name)); + if (symbol.Type is not ILabelTypeSymbol type) + return; - if (type.Name.EndsWith("Tok")) - if (!type.Locked) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0046TokLabelsLocked, symbol.GetLocation())); + if (type.Locked) + if (!type.Name.EndsWith("Tok")) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0047LockedLabelsTok, + symbol.GetLocation(), symbol.Name)); - } + if (type.Name.EndsWith("Tok")) + if (!type.Locked) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0046TokLabelsLocked, + symbol.GetLocation())); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0048ErrorWithTextConstant.cs b/BusinessCentral.LinterCop/Design/Rule0048ErrorWithTextConstant.cs index e8c12128..4971048e 100644 --- a/BusinessCentral.LinterCop/Design/Rule0048ErrorWithTextConstant.cs +++ b/BusinessCentral.LinterCop/Design/Rule0048ErrorWithTextConstant.cs @@ -1,44 +1,53 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0048ErrorWithTextConstant : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0048ErrorWithTextConstant : DiagnosticAnalyzer + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0048ErrorWithTextConstant); + + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(new Action(this.AnalyzeErrorMethod), OperationKind.InvocationExpression); + + private void AnalyzeErrorMethod(OperationAnalysisContext ctx) { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0048ErrorWithTextConstant); + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) + return; - public override void Initialize(AnalysisContext context) => context.RegisterOperationAction(new Action(this.AnalyzeErrorMethod), OperationKind.InvocationExpression); + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + operation.TargetMethod.Name != "Error" || + operation.Arguments.Length == 0 || + operation.Arguments[0].Value.Type.GetNavTypeKindSafe() == NavTypeKind.ErrorInfo) + return; - private void AnalyzeErrorMethod(OperationAnalysisContext ctx) + switch (operation.Arguments[0].Syntax.Kind) { - if (ctx.IsObsoletePendingOrRemoved()) return; + case SyntaxKind.IdentifierName: + if (operation.Arguments[0].Value.Kind != OperationKind.ConversionExpression) + break; - IInvocationExpression operation = (IInvocationExpression)ctx.Operation; - if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return; - if (!SemanticFacts.IsSameName(operation.TargetMethod.Name, "Error")) return; - if (operation.Arguments.Length == 0) return; + IOperation operand = ((IConversionExpression)operation.Arguments[0].Value).Operand; + if (operand.GetSymbol()?.OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.Label) + return; - if (operation.Arguments[0].Value.Type.GetNavTypeKindSafe() == NavTypeKind.ErrorInfo) return; + break; - switch (operation.Arguments[0].Syntax.Kind) - { - case SyntaxKind.IdentifierName: - if (operation.Arguments[0].Value.Kind != OperationKind.ConversionExpression) break; - IOperation operand = ((IConversionExpression)operation.Arguments[0].Value).Operand; - if (operand.GetSymbol().OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.Label) return; - break; - case SyntaxKind.LiteralExpression: - if (operation.Arguments[0].Syntax.GetIdentifierOrLiteralValue() == "") return; - break; - } + case SyntaxKind.LiteralExpression: + if (operation.Arguments[0].Syntax.GetIdentifierOrLiteralValue() == "") + return; - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0048ErrorWithTextConstant, ctx.Operation.Syntax.GetLocation())); + break; } + + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0048ErrorWithTextConstant, + ctx.Operation.Syntax.GetLocation())); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0050OperatorAndPlaceholderInFilterExpression.cs b/BusinessCentral.LinterCop/Design/Rule0050OperatorAndPlaceholderInFilterExpression.cs index 2830e3d2..bbdf302c 100644 --- a/BusinessCentral.LinterCop/Design/Rule0050OperatorAndPlaceholderInFilterExpression.cs +++ b/BusinessCentral.LinterCop/Design/Rule0050OperatorAndPlaceholderInFilterExpression.cs @@ -1,60 +1,62 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; using System.Collections.Immutable; +using System.Reflection.Emit; using System.Text.RegularExpressions; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0050OperatorAndPlaceholderInFilterExpression : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0050OperatorAndPlaceholderInFilterExpression : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0050OperatorAndPlaceholderInFilterExpression, DiagnosticDescriptors.Rule0059SingleQuoteEscapingIssueDetected); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0050OperatorAndPlaceholderInFilterExpression, DiagnosticDescriptors.Rule0059SingleQuoteEscapingIssueDetected); - public override void Initialize(AnalysisContext context) => context.RegisterOperationAction(new Action(this.AnalyzeInvocation), OperationKind.InvocationExpression); + private static readonly string InvalidUnaryEqualsFilter = "'<>'''"; - private void AnalyzeInvocation(OperationAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; + public override void Initialize(AnalysisContext context) => context.RegisterOperationAction(new Action(this.AnalyzeInvocation), OperationKind.InvocationExpression); + + private void AnalyzeInvocation(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) + return; - IInvocationExpression operation = (IInvocationExpression)ctx.Operation; - if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return; + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + operation.TargetMethod.Name != "SetFilter" || + operation.Arguments.Length < 2) + return; - if (operation.TargetMethod == null || !SemanticFacts.IsSameName(operation.TargetMethod.Name, "SetFilter") || operation.Arguments.Count() < 2) - return; + if (operation.Arguments[1].Value is not IOperation operand) + return; - CheckParameter(operation.Arguments[1].Value, ref operation, ref ctx); - } + if (operand.Type.GetNavTypeKindSafe() != NavTypeKind.String && operand.Type.GetNavTypeKindSafe() != NavTypeKind.Joker) + return; - private void CheckParameter(IOperation operand, ref IInvocationExpression operation, ref OperationAnalysisContext ctx) + if (operand.Syntax.Kind != SyntaxKind.LiteralExpression) + return; + + string parameterString = operand.Syntax.ToFullString(); + if (parameterString.Equals(InvalidUnaryEqualsFilter)) { - if (operand.Type.GetNavTypeKindSafe() != NavTypeKind.String && operand.Type.GetNavTypeKindSafe() != NavTypeKind.Joker) - return; - - if (operand.Syntax.Kind != SyntaxKind.LiteralExpression) - return; - - string parameterString = operand.Syntax.ToFullString(); - if (parameterString.Equals("'<>'''")) - { - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0059SingleQuoteEscapingIssueDetected, operand.Syntax.GetLocation())); - return; - } - - string pattern = @"%\d+"; // Only when a placeholders (%1) is used in the filter expression we need to raise the rule that the placeholders won't work as expected - Regex regex = new Regex(pattern); - Match match = regex.Match(parameterString); - if (!match.Success) return; - - int operatorIndex = parameterString.IndexOfAny("*?@".ToCharArray()); // Only the *, ? and @ operator changes the behavior of the placeholder - if (operatorIndex == -1) return; - - ctx.ReportDiagnostic( - Diagnostic.Create( - DiagnosticDescriptors.Rule0050OperatorAndPlaceholderInFilterExpression, - operation.Syntax.GetLocation(), new object[] { parameterString.Substring(operatorIndex, 1), match.Value })); + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0059SingleQuoteEscapingIssueDetected, operand.Syntax.GetLocation())); + return; } + + string pattern = @"%\d+"; // Only when a placeholders (%1) is used in the filter expression we need to raise the rule that the placeholders won't work as expected + Regex regex = new Regex(pattern); + Match match = regex.Match(parameterString); + if (!match.Success) + return; + + int operatorIndex = parameterString.IndexOfAny("*?@".ToCharArray()); // Only the *, ? and @ operator changes the behavior of the placeholder + if (operatorIndex == -1) + return; + + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0050OperatorAndPlaceholderInFilterExpression, + operation.Syntax.GetLocation(), + parameterString.Substring(operatorIndex, 1), + match.Value)); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0051PossibleOverflowAssigning.cs b/BusinessCentral.LinterCop/Design/Rule0051PossibleOverflowAssigning.cs index a77558c4..5924c4fb 100644 --- a/BusinessCentral.LinterCop/Design/Rule0051PossibleOverflowAssigning.cs +++ b/BusinessCentral.LinterCop/Design/Rule0051PossibleOverflowAssigning.cs @@ -1,6 +1,5 @@ #if !LessThenFall2023RV1 -using BusinessCentral.LinterCop.AnalysisContextExtension; -using BusinessCentral.LinterCop.ArgumentExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; @@ -8,339 +7,335 @@ using System.Collections.Immutable; using System.Text.RegularExpressions; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0051PossibleOverflowAssigning : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0051PossibleOverflowAssigning : DiagnosticAnalyzer - { - private readonly Lazy strSubstNoPatternLazy = new Lazy((Func)(() => new Regex("[#%](\\d+)", RegexOptions.Compiled))); + private readonly Lazy strSubstNoPatternLazy = new Lazy(() => new Regex("[#%](\\d+)", RegexOptions.Compiled)); - // Build-in methods like Database.CompanyName() and Database.UserId() have indirectly a return length - private static readonly Dictionary BuiltInMethodNameWithReturnLength = new() + // Build-in methods like Database.CompanyName() and Database.UserId() have indirectly a return length + private static readonly Dictionary BuiltInMethodNameWithReturnLength = new() { { "CompanyName", 30 }, { "UserId", 50 } }; - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0051PossibleOverflowAssigning); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0051PossibleOverflowAssigning); - private Regex StrSubstNoPattern => this.strSubstNoPatternLazy.Value; + private Regex StrSubstNoPattern => this.strSubstNoPatternLazy.Value; - public override void Initialize(AnalysisContext context) - { - context.RegisterOperationAction(new Action(this.AnalyzeSetFilter), OperationKind.InvocationExpression); + public override void Initialize(AnalysisContext context) + { + context.RegisterOperationAction(new Action(this.AnalyzeSetFilter), OperationKind.InvocationExpression); #if !LessThenSpring2024 - context.RegisterOperationAction(new Action(this.AnalyzeGetMethod), OperationKind.InvocationExpression); + context.RegisterOperationAction(new Action(this.AnalyzeGetMethod), OperationKind.InvocationExpression); #endif - } - private void AnalyzeSetFilter(OperationAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) - return; + } + private void AnalyzeSetFilter(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) + return; - if ((ctx.Operation is not IInvocationExpression operation) || - operation.TargetMethod is null || - operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || - !SemanticFacts.IsSameName(operation.TargetMethod.Name, "SetFilter") || - operation.Arguments.Count() < 3 || - operation.Arguments[0].Value.Kind != OperationKind.ConversionExpression) - return; + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + operation.TargetMethod.Name != "SetFilter" || + operation.Arguments.Length < 3 || + operation.Arguments[0].Value.Kind != OperationKind.ConversionExpression) + return; - var fieldOperand = ((IConversionExpression)operation.Arguments[0].Value).Operand; - if (fieldOperand.Type is not ITypeSymbol fieldType) - return; + var fieldOperand = ((IConversionExpression)operation.Arguments[0].Value).Operand; + if (fieldOperand.Type is not ITypeSymbol fieldType) + return; - if (fieldType.GetNavTypeKindSafe() == NavTypeKind.Text) - return; + if (fieldType.GetNavTypeKindSafe() == NavTypeKind.Text) + return; - bool isError = false; - int typeLength = GetTypeLength(fieldType, ref isError); - if (isError || typeLength == int.MaxValue) - return; + bool isError = false; + int typeLength = GetTypeLength(fieldType, ref isError); + if (isError || typeLength == int.MaxValue) + return; - foreach (int argIndex in GetArgumentIndexes(operation.Arguments[1].Value)) - { - int index = argIndex + 1; // The placeholders are defines as %1, %2, %3, where in case of %1 we need the second (zero based) index of the arguments of the SetFilter method - if ((index < 2) || - (index >= operation.Arguments.Count()) || - (operation.Arguments[index].Value.Kind != OperationKind.ConversionExpression)) - continue; - - if (operation.Arguments[index].Value is not IConversionExpression argValue) - continue; - - int expressionLength = this.CalculateMaxExpressionLength(argValue.Operand, ref isError); - if (!isError && expressionLength > typeLength) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0051PossibleOverflowAssigning, operation.Syntax.GetLocation(), GetDisplayString(operation.Arguments[index], operation), GetDisplayString(operation.Arguments[0], operation))); - } + foreach (int argIndex in GetArgumentIndexes(operation.Arguments[1].Value)) + { + int index = argIndex + 1; // The placeholders are defines as %1, %2, %3, where in case of %1 we need the second (zero based) index of the arguments of the SetFilter method + if ((index < 2) || + (index >= operation.Arguments.Length) || + (operation.Arguments[index].Value.Kind != OperationKind.ConversionExpression)) + continue; + + if (operation.Arguments[index].Value is not IConversionExpression argValue) + continue; + + int expressionLength = this.CalculateMaxExpressionLength(argValue.Operand, ref isError); + if (!isError && expressionLength > typeLength) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0051PossibleOverflowAssigning, operation.Syntax.GetLocation(), GetDisplayString(operation.Arguments[index], operation), GetDisplayString(operation.Arguments[0], operation))); } + } #if !LessThenSpring2024 - private void AnalyzeGetMethod(OperationAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) - return; + private void AnalyzeGetMethod(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) + return; - if ((ctx.Operation is not IInvocationExpression operation) || - operation.TargetMethod is null || - operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || - !SemanticFacts.IsSameName(operation.TargetMethod.Name, "Get") || - operation.Arguments.Count() < 1) - return; + if ((ctx.Operation is not IInvocationExpression operation) || + operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + !SemanticFacts.IsSameName(operation.TargetMethod.Name, "Get") || + operation.Arguments.Length < 1) + return; - if (operation.Instance?.Type.GetTypeSymbol()?.OriginalDefinition is not ITableTypeSymbol table) - return; + if (operation.Instance?.Type.GetTypeSymbol()?.OriginalDefinition is not ITableTypeSymbol table) + return; - if (operation.Arguments.Length < table.PrimaryKey.Fields.Length) - return; + if (operation.Arguments.Length < table.PrimaryKey.Fields.Length) + return; - for (int index = 0; index < operation.Arguments.Length; index++) - { - var fieldType = table.PrimaryKey.Fields[index].Type; - var argumentType = operation.Arguments[index].GetTypeSymbol(); + for (int index = 0; index < operation.Arguments.Length; index++) + { + var fieldType = table.PrimaryKey.Fields[index].Type; + var argumentType = operation.Arguments[index].GetTypeSymbol(); - if (fieldType is null || argumentType is null || argumentType.HasLength) - continue; + if (fieldType is null || argumentType is null || argumentType.HasLength) + continue; - bool isError = false; - int fieldLength = GetTypeLength(fieldType, ref isError); - if (isError || fieldLength == 0) - continue; + bool isError = false; + int fieldLength = GetTypeLength(fieldType, ref isError); + if (isError || fieldLength == 0) + continue; - if (operation.Arguments[index].Value is not IConversionExpression argValue) - continue; + if (operation.Arguments[index].Value is not IConversionExpression argValue) + continue; - int expressionLength = this.CalculateMaxExpressionLength(argValue.Operand, ref isError); - if (!isError && expressionLength > fieldLength) - { - string lengthSuffix = expressionLength < int.MaxValue - ? $"[{expressionLength}]" - : string.Empty; - - ctx.ReportDiagnostic(Diagnostic.Create( - DiagnosticDescriptors.Rule0051PossibleOverflowAssigning, - operation.Arguments[index].Syntax.GetLocation(), - $"{argumentType.ToDisplayString()}{lengthSuffix}", - fieldType.ToDisplayString())); - } + int expressionLength = this.CalculateMaxExpressionLength(argValue.Operand, ref isError); + if (!isError && expressionLength > fieldLength) + { + string lengthSuffix = expressionLength < int.MaxValue + ? $"[{expressionLength}]" + : string.Empty; + + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0051PossibleOverflowAssigning, + operation.Arguments[index].Syntax.GetLocation(), + $"{argumentType.ToDisplayString()}{lengthSuffix}", + fieldType.ToDisplayString())); } } + } #endif - private static int GetTypeLength(ITypeSymbol type, ref bool isError) + private static int GetTypeLength(ITypeSymbol type, ref bool isError) + { + if (!type.IsTextType()) { - if (!type.IsTextType()) - { - isError = true; - return 0; - } - if (type.HasLength) - return type.Length; - return type.NavTypeKind == NavTypeKind.Label ? GetLabelTypeLength(type) : int.MaxValue; + isError = true; + return 0; } + if (type.HasLength) + return type.Length; + return type.NavTypeKind == NavTypeKind.Label ? GetLabelTypeLength(type) : int.MaxValue; + } - private static int GetLabelTypeLength(ITypeSymbol type) - { - ILabelTypeSymbol labelType = (ILabelTypeSymbol)type; + private static int GetLabelTypeLength(ITypeSymbol type) + { + ILabelTypeSymbol labelType = (ILabelTypeSymbol)type; - if (labelType.Locked) - return labelType.GetLabelText().Length; + if (labelType.Locked) + return labelType.GetLabelText().Length; - return labelType.MaxLength; - } + return labelType.MaxLength; + } - private int CalculateMaxExpressionLength(IOperation expression, ref bool isError) + private int CalculateMaxExpressionLength(IOperation expression, ref bool isError) + { + if (expression.Syntax.Parent.IsKind(SyntaxKind.CaseLine)) { - if (expression.Syntax.Parent.IsKind(SyntaxKind.CaseLine)) - { - isError = true; - return 0; - } - switch (expression.Kind) - { - case OperationKind.LiteralExpression: - if (expression.Type.IsTextType()) - return expression.ConstantValue.Value.ToString().Length; - ITypeSymbol type = expression.Type; - if ((type != null ? (type.NavTypeKind == NavTypeKind.Char ? 1 : 0) : 0) != 0) - return 1; - break; - case OperationKind.ConversionExpression: - return this.CalculateMaxExpressionLength(((IConversionExpression)expression).Operand, ref isError); - case OperationKind.InvocationExpression: - IInvocationExpression invocation = (IInvocationExpression)expression; - IMethodSymbol targetMethod = invocation.TargetMethod; - if (targetMethod != null && targetMethod.ContainingSymbol?.Kind == SymbolKind.Class) - { - if (IsBuiltInMethodWithReturnLength(targetMethod, out int length)) - return length; - - switch (targetMethod.Name.ToLowerInvariant()) - { - case "convertstr": - case "delchr": - case "delstr": - case "incstr": - case "lowercase": - case "uppercase": - if (invocation.Arguments.Length > 0) - return this.CalculateBuiltInMethodResultLength(invocation, 0, ref isError); - break; - case "copystr": - if (invocation.Arguments.Length == 3) - return this.CalculateBuiltInMethodResultLength(invocation, 2, ref isError); - break; - case "format": - return 0; - case "padstr": - case "substring": - if (invocation.Arguments.Length >= 2) - return this.CalculateBuiltInMethodResultLength(invocation, 1, ref isError); - break; - case "strsubstno": - if (invocation.Arguments.Length > 0) - return this.CalculateStrSubstNoMethodResultLength(invocation, ref isError); - break; - case "tolower": - case "toupper": - if (invocation.Instance != null && invocation.Instance.IsBoundExpression()) - return GetTypeLength(invocation.Instance.Type, ref isError); - break; - } - } - return GetTypeLength(expression.Type, ref isError); - case OperationKind.LocalReferenceExpression: - case OperationKind.GlobalReferenceExpression: - case OperationKind.ReturnValueReferenceExpression: - case OperationKind.ParameterReferenceExpression: - case OperationKind.FieldAccess: - return GetTypeLength(expression.Type, ref isError); - case OperationKind.BinaryOperatorExpression: - IBinaryOperatorExpression operatorExpression = (IBinaryOperatorExpression)expression; - return Math.Min(int.MaxValue, this.CalculateMaxExpressionLength(operatorExpression.LeftOperand, ref isError) + this.CalculateMaxExpressionLength(operatorExpression.RightOperand, ref isError)); - } isError = true; return 0; } - - private static int? TryGetLength(IInvocationExpression invocation, int lengthArgPos) + switch (expression.Kind) { - if (!(SemanticFacts.GetBoundExpressionArgument(invocation, lengthArgPos) is IConversionExpression expressionArgument)) - return new int?(); - ITypeSymbol type = expressionArgument.Operand.Type; - return type.HasLength ? new int?(type.Length) : new int?(); + case OperationKind.LiteralExpression: + if (expression.Type.IsTextType()) + return expression.ConstantValue.Value.ToString().Length; + ITypeSymbol type = expression.Type; + if ((type is not null ? (type.NavTypeKind == NavTypeKind.Char ? 1 : 0) : 0) != 0) + return 1; + break; + case OperationKind.ConversionExpression: + return this.CalculateMaxExpressionLength(((IConversionExpression)expression).Operand, ref isError); + case OperationKind.InvocationExpression: + IInvocationExpression invocation = (IInvocationExpression)expression; + IMethodSymbol targetMethod = invocation.TargetMethod; + if (targetMethod is not null && targetMethod.ContainingSymbol?.Kind == SymbolKind.Class) + { + if (IsBuiltInMethodWithReturnLength(targetMethod, out int length)) + return length; + + switch (targetMethod.Name.ToLowerInvariant()) + { + case "convertstr": + case "delchr": + case "delstr": + case "incstr": + case "lowercase": + case "uppercase": + if (invocation.Arguments.Length > 0) + return this.CalculateBuiltInMethodResultLength(invocation, 0, ref isError); + break; + case "copystr": + if (invocation.Arguments.Length == 3) + return this.CalculateBuiltInMethodResultLength(invocation, 2, ref isError); + break; + case "format": + return 0; + case "padstr": + case "substring": + if (invocation.Arguments.Length >= 2) + return this.CalculateBuiltInMethodResultLength(invocation, 1, ref isError); + break; + case "strsubstno": + if (invocation.Arguments.Length > 0) + return this.CalculateStrSubstNoMethodResultLength(invocation, ref isError); + break; + case "tolower": + case "toupper": + if (invocation.Instance is not null && invocation.Instance.IsBoundExpression()) + return GetTypeLength(invocation.Instance.Type, ref isError); + break; + } + } + return GetTypeLength(expression.Type, ref isError); + case OperationKind.LocalReferenceExpression: + case OperationKind.GlobalReferenceExpression: + case OperationKind.ReturnValueReferenceExpression: + case OperationKind.ParameterReferenceExpression: + case OperationKind.FieldAccess: + return GetTypeLength(expression.Type, ref isError); + case OperationKind.BinaryOperatorExpression: + IBinaryOperatorExpression operatorExpression = (IBinaryOperatorExpression)expression; + return Math.Min(int.MaxValue, this.CalculateMaxExpressionLength(operatorExpression.LeftOperand, ref isError) + this.CalculateMaxExpressionLength(operatorExpression.RightOperand, ref isError)); } + isError = true; + return 0; + } + + private static int? TryGetLength(IInvocationExpression invocation, int lengthArgPos) + { + if (!(SemanticFacts.GetBoundExpressionArgument(invocation, lengthArgPos) is IConversionExpression expressionArgument)) + return new int?(); + ITypeSymbol type = expressionArgument.Operand.Type; + return type.HasLength ? new int?(type.Length) : new int?(); + } - private int CalculateBuiltInMethodResultLength( - IInvocationExpression invocation, - int lengthArgPos, - ref bool isError) + private int CalculateBuiltInMethodResultLength( + IInvocationExpression invocation, + int lengthArgPos, + ref bool isError) + { + IOperation operation = invocation.Arguments[lengthArgPos].Value; + switch (operation.Kind) { - IOperation operation = invocation.Arguments[lengthArgPos].Value; - switch (operation.Kind) - { - case OperationKind.LiteralExpression: - Optional constantValue = operation.ConstantValue; - if (constantValue.HasValue) + case OperationKind.LiteralExpression: + Optional constantValue = operation.ConstantValue; + if (constantValue.HasValue) + { + if (operation.Type.IsIntegralType()) { - if (operation.Type.IsIntegralType()) - { - constantValue = operation.ConstantValue; - return (int)constantValue.Value; - } - if (operation.Type.IsTextType()) - { - constantValue = operation.ConstantValue; - return constantValue.Value.ToString().Length; - } - break; + constantValue = operation.ConstantValue; + return (int)constantValue.Value; + } + if (operation.Type.IsTextType()) + { + constantValue = operation.ConstantValue; + return constantValue.Value.ToString().Length; } break; - case OperationKind.InvocationExpression: - invocation = (IInvocationExpression)operation; - IMethodSymbol targetMethod = invocation.TargetMethod; - if (targetMethod != null && SemanticFacts.IsSameName(targetMethod.Name, "maxstrlen") && targetMethod.ContainingSymbol?.Kind == SymbolKind.Class) + } + break; + case OperationKind.InvocationExpression: + invocation = (IInvocationExpression)operation; + IMethodSymbol targetMethod = invocation.TargetMethod; + if (targetMethod is not null && SemanticFacts.IsSameName(targetMethod.Name, "maxstrlen") && targetMethod.ContainingSymbol?.Kind == SymbolKind.Class) + { + ImmutableArray arguments = invocation.Arguments; + if (arguments.Length == 1) { - ImmutableArray arguments = invocation.Arguments; - if (arguments.Length == 1) - { - arguments = invocation.Arguments; - IOperation operand = arguments[0].Value; - if (operand.Kind == OperationKind.ConversionExpression) - operand = ((IConversionExpression)operand).Operand; - return GetTypeLength(operand.Type, ref isError); - } - break; + arguments = invocation.Arguments; + IOperation operand = arguments[0].Value; + if (operand.Kind == OperationKind.ConversionExpression) + operand = ((IConversionExpression)operand).Operand; + return GetTypeLength(operand.Type, ref isError); } break; - } - return TryGetLength(invocation, lengthArgPos) ?? GetTypeLength(invocation.Type, ref isError); + } + break; } + return TryGetLength(invocation, lengthArgPos) ?? GetTypeLength(invocation.Type, ref isError); + } - private int CalculateStrSubstNoMethodResultLength( - IInvocationExpression invocation, - ref bool isError) + private int CalculateStrSubstNoMethodResultLength( + IInvocationExpression invocation, + ref bool isError) + { + IOperation operation = invocation.Arguments[0].Value; + if (!operation.Type.IsTextType()) { - IOperation operation = invocation.Arguments[0].Value; - if (!operation.Type.IsTextType()) - { - isError = true; - return -1; - } - Optional constantValue = operation.ConstantValue; - if (!constantValue.HasValue) - { - isError = true; - return -1; - } - constantValue = operation.ConstantValue; - string input = constantValue.Value.ToString(); - Match match = this.StrSubstNoPattern.Match(input); - int num; - for (num = input.Length; !isError && match.Success && num < int.MaxValue; match = match.NextMatch()) - { - string s = match.Groups[1].Value; - int result = 0; - if (int.TryParse(s, out result) && 0 < result && result < invocation.Arguments.Length) - { - int expressionLength = this.CalculateMaxExpressionLength(invocation.Arguments[result].Value, ref isError); - num = expressionLength == int.MaxValue ? expressionLength : num + expressionLength - s.Length - 1; - } - } - return !isError ? num : -1; + isError = true; + return -1; } - - private static string GetDisplayString(IArgument argument, IInvocationExpression operation) + Optional constantValue = operation.ConstantValue; + if (!constantValue.HasValue) { - return ((IConversionExpression)argument.Value).Operand.Type.ToDisplayString(); + isError = true; + return -1; } - - private List GetArgumentIndexes(IOperation operand) + constantValue = operation.ConstantValue; + string input = constantValue.Value.ToString(); + Match match = this.StrSubstNoPattern.Match(input); + int num; + for (num = input.Length; !isError && match.Success && num < int.MaxValue; match = match.NextMatch()) { - List results = new List(); - - if (operand.Syntax.Kind != SyntaxKind.LiteralExpression) - return results; - - foreach (Match match in this.StrSubstNoPattern.Matches(operand.Syntax.ToFullString())) + string s = match.Groups[1].Value; + int result = 0; + if (int.TryParse(s, out result) && 0 < result && result < invocation.Arguments.Length) { - if (int.TryParse(match.Groups[1].Value, out int number)) - if (!results.Contains(number)) - results.Add(number); + int expressionLength = this.CalculateMaxExpressionLength(invocation.Arguments[result].Value, ref isError); + num = expressionLength == int.MaxValue ? expressionLength : num + expressionLength - s.Length - 1; } + } + return !isError ? num : -1; + } + + private static string GetDisplayString(IArgument argument, IInvocationExpression operation) + { + return ((IConversionExpression)argument.Value).Operand.Type.ToDisplayString(); + } + + private List GetArgumentIndexes(IOperation operand) + { + List results = new List(); + if (operand.Syntax.Kind != SyntaxKind.LiteralExpression) return results; - } - private static bool IsBuiltInMethodWithReturnLength(IMethodSymbol targetMethod, out int length) + foreach (Match match in this.StrSubstNoPattern.Matches(operand.Syntax.ToFullString())) { - length = 0; + if (int.TryParse(match.Groups[1].Value, out int number)) + if (!results.Contains(number)) + results.Add(number); + } - if (targetMethod.MethodKind != MethodKind.BuiltInMethod) - return false; + return results; + } - return BuiltInMethodNameWithReturnLength.TryGetValue(targetMethod.Name, out length); - } + private static bool IsBuiltInMethodWithReturnLength(IMethodSymbol targetMethod, out int length) + { + length = 0; + + if (targetMethod.MethodKind != MethodKind.BuiltInMethod) + return false; + + return BuiltInMethodNameWithReturnLength.TryGetValue(targetMethod.Name, out length); } } #endif \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0052and0053InternalProceduresNotReferenced.cs b/BusinessCentral.LinterCop/Design/Rule0052and0053InternalProceduresNotReferenced.cs index 8cf4b328..e39ba472 100644 --- a/BusinessCentral.LinterCop/Design/Rule0052and0053InternalProceduresNotReferenced.cs +++ b/BusinessCentral.LinterCop/Design/Rule0052and0053InternalProceduresNotReferenced.cs @@ -15,231 +15,230 @@ #endif -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0052InternalProceduresNotReferencedAnalyzer : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0052InternalProceduresNotReferencedAnalyzer : DiagnosticAnalyzer - { - private class MethodSymbolAnalyzer : IDisposable - { - private readonly PooledDictionary methodSymbols = PooledDictionary.GetInstance(); + private class MethodSymbolAnalyzer : IDisposable + { + private readonly PooledDictionary methodSymbols = PooledDictionary.GetInstance(); - private readonly PooledDictionary internalMethodsUnused = PooledDictionary.GetInstance(); - private readonly PooledDictionary internalMethodsUsedInCurrentObject = PooledDictionary.GetInstance(); - private readonly PooledDictionary internalMethodsUsedInOtherObjects = PooledDictionary.GetInstance(); + private readonly PooledDictionary internalMethodsUnused = PooledDictionary.GetInstance(); + private readonly PooledDictionary internalMethodsUsedInCurrentObject = PooledDictionary.GetInstance(); + private readonly PooledDictionary internalMethodsUsedInOtherObjects = PooledDictionary.GetInstance(); - private readonly AttributeKind[] attributeKindsOfMethodsToSkip = new AttributeKind[] { AttributeKind.ConfirmHandler, AttributeKind.FilterPageHandler, AttributeKind.HyperlinkHandler, AttributeKind.MessageHandler, AttributeKind.ModalPageHandler, AttributeKind.PageHandler, AttributeKind.RecallNotificationHandler, AttributeKind.ReportHandler, AttributeKind.RequestPageHandler, AttributeKind.SendNotificationHandler, AttributeKind.SessionSettingsHandler, AttributeKind.StrMenuHandler, AttributeKind.Test }; + private readonly AttributeKind[] attributeKindsOfMethodsToSkip = new AttributeKind[] { AttributeKind.ConfirmHandler, AttributeKind.FilterPageHandler, AttributeKind.HyperlinkHandler, AttributeKind.MessageHandler, AttributeKind.ModalPageHandler, AttributeKind.PageHandler, AttributeKind.RecallNotificationHandler, AttributeKind.ReportHandler, AttributeKind.RequestPageHandler, AttributeKind.SendNotificationHandler, AttributeKind.SessionSettingsHandler, AttributeKind.StrMenuHandler, AttributeKind.Test }; - public MethodSymbolAnalyzer(CompilationAnalysisContext compilationAnalysisContext) - { + public MethodSymbolAnalyzer(CompilationAnalysisContext compilationAnalysisContext) + { #if !LessThenSpring2024 - NavAppManifest manifest = ManifestHelper.GetManifest(compilationAnalysisContext.Compilation); + NavAppManifest manifest = ManifestHelper.GetManifest(compilationAnalysisContext.Compilation); #else - NavAppManifest manifest = AppSourceCopConfigurationProvider.GetManifest(compilationAnalysisContext.Compilation); + NavAppManifest manifest = AppSourceCopConfigurationProvider.GetManifest(compilationAnalysisContext.Compilation); #endif - if (manifest.InternalsVisibleTo != null && manifest.InternalsVisibleTo.Any()) - { - return; - } + if (manifest is not null && manifest.InternalsVisibleTo is not null && manifest.InternalsVisibleTo.Any()) + { + return; + } - ImmutableArray.Enumerator objectEnumerator = compilationAnalysisContext.Compilation.GetDeclaredApplicationObjectSymbols().GetEnumerator(); - while (objectEnumerator.MoveNext()) + ImmutableArray.Enumerator objectEnumerator = compilationAnalysisContext.Compilation.GetDeclaredApplicationObjectSymbols().GetEnumerator(); + while (objectEnumerator.MoveNext()) + { + IApplicationObjectTypeSymbol applicationSymbol = objectEnumerator.Current; + ImmutableArray.Enumerator objectMemberEnumerator = applicationSymbol.GetMembers().GetEnumerator(); + while (objectMemberEnumerator.MoveNext()) { - IApplicationObjectTypeSymbol applicationSymbol = objectEnumerator.Current; - ImmutableArray.Enumerator objectMemberEnumerator = applicationSymbol.GetMembers().GetEnumerator(); - while (objectMemberEnumerator.MoveNext()) + ISymbol objectMember = objectMemberEnumerator.Current; + if (objectMember.Kind == SymbolKind.Method) { - ISymbol objectMember = objectMemberEnumerator.Current; - if (objectMember.Kind == SymbolKind.Method) + IMethodSymbol methodSymbol = objectMember as IMethodSymbol; + if (MethodNeedsReferenceCheck(methodSymbol)) { - IMethodSymbol methodSymbol = objectMember as IMethodSymbol; - if (MethodNeedsReferenceCheck(methodSymbol)) - { - methodSymbols.Add(methodSymbol, methodSymbol.Name.ToLowerInvariant()); - internalMethodsUnused.Add(methodSymbol, methodSymbol.Name.ToLowerInvariant()); - } + methodSymbols.Add(methodSymbol, methodSymbol.Name.ToLowerInvariant()); + internalMethodsUnused.Add(methodSymbol, methodSymbol.Name.ToLowerInvariant()); } } } } + } - private bool MethodNeedsReferenceCheck(IMethodSymbol methodSymbol) + private bool MethodNeedsReferenceCheck(IMethodSymbol methodSymbol) + { + if (methodSymbol.MethodKind != MethodKind.Method) { - if (methodSymbol.MethodKind != MethodKind.Method) - { - return false; - } - if (methodSymbol.IsObsoletePending) + return false; + } + if (methodSymbol.IsObsoletePending) + { + return false; + } + if (methodSymbol.Attributes.Any(attr => attributeKindsOfMethodsToSkip.Contains(attr.AttributeKind))) + { + return false; + } + // If the procedure and implements an interface, then we do not need to check for references for this procedure + IApplicationObjectTypeSymbol objectSymbol = methodSymbol.GetContainingApplicationObjectTypeSymbol(); + if (objectSymbol is not null && HelperFunctions.MethodImplementsInterfaceMethod(objectSymbol, methodSymbol)) + { + return false; + } + if (!methodSymbol.IsInternal) + { + // Check if public procedure in internal object + if (methodSymbol.DeclaredAccessibility == Accessibility.Public && objectSymbol is not null) { - return false; + // If the containing object is not an internal object, then we do not need to check for references for this public procedure. + if (objectSymbol.DeclaredAccessibility != Accessibility.Internal) + { + return false; + } } - if (methodSymbol.Attributes.Any(attr => attributeKindsOfMethodsToSkip.Contains(attr.AttributeKind))) + else { return false; } - // If the procedure and implements an interface, then we do not need to check for references for this procedure - IApplicationObjectTypeSymbol objectSymbol = methodSymbol.GetContainingApplicationObjectTypeSymbol(); - if (objectSymbol != null && HelperFunctions.MethodImplementsInterfaceMethod(objectSymbol, methodSymbol)) + } + + // If the procedure has signature ProcedureName(HostNotification: Notification) or ProcedureName(ErrorInfo: ErrorInfo), then the procedure does not need a reference check + if (methodSymbol.Parameters.Length == 1) + { + ITypeSymbol firstParameterTypeSymbol = methodSymbol.Parameters[0].ParameterType; + if (firstParameterTypeSymbol.GetNavTypeKindSafe() == NavTypeKind.Notification || firstParameterTypeSymbol.GetNavTypeKindSafe() == NavTypeKind.ErrorInfo) { return false; } - if (!methodSymbol.IsInternal) - { - // Check if public procedure in internal object - if (methodSymbol.DeclaredAccessibility == Accessibility.Public && objectSymbol != null) - { - // If the containing object is not an internal object, then we do not need to check for references for this public procedure. - if (objectSymbol.DeclaredAccessibility != Accessibility.Internal) - { - return false; - } - } - else - { - return false; - } - } + } - // If the procedure has signature ProcedureName(HostNotification: Notification) or ProcedureName(ErrorInfo: ErrorInfo), then the procedure does not need a reference check - if (methodSymbol.Parameters.Length == 1) - { - ITypeSymbol firstParameterTypeSymbol = methodSymbol.Parameters[0].ParameterType; - if (firstParameterTypeSymbol.GetNavTypeKindSafe() == NavTypeKind.Notification || firstParameterTypeSymbol.GetNavTypeKindSafe() == NavTypeKind.ErrorInfo) - { - return false; - } - } + return true; + } - return true; + public void AnalyzeObjectSyntax(CompilationAnalysisContext compilationAnalysisContext) + { + if (methodSymbols.Count == 0) + { + return; } - public void AnalyzeObjectSyntax(CompilationAnalysisContext compilationAnalysisContext) + Compilation compilation = compilationAnalysisContext.Compilation; + ImmutableArray.Enumerator enumerator = compilation.SyntaxTrees.GetEnumerator(); + while (enumerator.MoveNext()) { if (methodSymbols.Count == 0) { - return; + break; } - Compilation compilation = compilationAnalysisContext.Compilation; - ImmutableArray.Enumerator enumerator = compilation.SyntaxTrees.GetEnumerator(); - while (enumerator.MoveNext()) + SyntaxTree syntaxTree = enumerator.Current; + SemanticModel semanticModel = compilation.GetSemanticModel(syntaxTree); + + syntaxTree.GetRoot().WalkDescendantsAndPerformAction(delegate (SyntaxNode syntaxNode) { if (methodSymbols.Count == 0) { - break; + return; } - - SyntaxTree syntaxTree = enumerator.Current; - SemanticModel semanticModel = compilation.GetSemanticModel(syntaxTree); - - syntaxTree.GetRoot().WalkDescendantsAndPerformAction(delegate (SyntaxNode syntaxNode) + if (syntaxNode.Parent.IsKind(SyntaxKind.MethodDeclaration) || !syntaxNode.IsKind(SyntaxKind.IdentifierName)) { - if (methodSymbols.Count == 0) - { - return; - } - if (syntaxNode.Parent.IsKind(SyntaxKind.MethodDeclaration) || !syntaxNode.IsKind(SyntaxKind.IdentifierName)) - { - return; - } - IdentifierNameSyntax identifierNameSyntax = (IdentifierNameSyntax)syntaxNode; - if (methodSymbols.ContainsValue(identifierNameSyntax.Identifier.ValueText.ToLowerInvariant()) && TryGetSymbolFromIdentifier(semanticModel, (IdentifierNameSyntax)syntaxNode, SymbolKind.Method, out var methodSymbol)) + return; + } + IdentifierNameSyntax identifierNameSyntax = (IdentifierNameSyntax)syntaxNode; + if (methodSymbols.ContainsValue(identifierNameSyntax.Identifier.ValueText.ToLowerInvariant()) && TryGetSymbolFromIdentifier(semanticModel, (IdentifierNameSyntax)syntaxNode, SymbolKind.Method, out var methodSymbol)) + { + if (methodSymbol.IsInternal) { - if (methodSymbol.IsInternal) + var objectSyntax = syntaxNode.GetContainingApplicationObjectSyntax(); + var objectSyntaxName = objectSyntax.Name.Identifier.ValueText.ToLowerInvariant(); + + var methodObjectSymbol = methodSymbol.GetContainingApplicationObjectTypeSymbol(); + var methodObjectSymbolName = methodObjectSymbol.Name.ToLowerInvariant(); + + if ( + (methodObjectSymbolName == objectSyntaxName) && + (objectSyntax.Kind.ToString().Replace("Object", "").ToLowerInvariant() == methodObjectSymbol.Kind.ToString().ToLowerInvariant()) + ) + { + internalMethodsUsedInCurrentObject[methodSymbol] = methodSymbol.Name.ToLowerInvariant(); + } + else { - var objectSyntax = syntaxNode.GetContainingApplicationObjectSyntax(); - var objectSyntaxName = objectSyntax.Name.Identifier.ValueText.ToLowerInvariant(); - - var methodObjectSymbol = methodSymbol.GetContainingApplicationObjectTypeSymbol(); - var methodObjectSymbolName = methodObjectSymbol.Name.ToLowerInvariant(); - - if ( - (methodObjectSymbolName == objectSyntaxName) && - (objectSyntax.Kind.ToString().Replace("Object", "").ToLowerInvariant() == methodObjectSymbol.Kind.ToString().ToLowerInvariant()) - ) - { - internalMethodsUsedInCurrentObject[methodSymbol] = methodSymbol.Name.ToLowerInvariant(); - } - else - { - internalMethodsUsedInOtherObjects[methodSymbol] = methodSymbol.Name.ToLowerInvariant(); - } + internalMethodsUsedInOtherObjects[methodSymbol] = methodSymbol.Name.ToLowerInvariant(); } - internalMethodsUnused.Remove(methodSymbol); } - }); - } + internalMethodsUnused.Remove(methodSymbol); + } + }); } + } - internal static bool TryGetSymbolFromIdentifier(SemanticModel semanticModel, IdentifierNameSyntax identifierName, SymbolKind symbolKind, out IMethodSymbol methodSymbol) + internal static bool TryGetSymbolFromIdentifier(SemanticModel semanticModel, IdentifierNameSyntax identifierName, SymbolKind symbolKind, out IMethodSymbol methodSymbol) + { + methodSymbol = null; + SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(identifierName); + ISymbol symbol = symbolInfo.Symbol; + if (symbol is null || symbol.Kind != symbolKind) { - methodSymbol = null; - SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(identifierName); - ISymbol symbol = symbolInfo.Symbol; - if (symbol == null || symbol.Kind != symbolKind) - { - return false; - } - methodSymbol = symbolInfo.Symbol as IMethodSymbol; - if (methodSymbol == null) - { - return false; - } - return true; + return false; } - - public void ReportUnchangedReferencePassedParameters(Action action) + methodSymbol = symbolInfo.Symbol as IMethodSymbol; + if (methodSymbol is null) { - if (internalMethodsUnused.Count == 0) - { - return; - } - foreach (KeyValuePair unusedInternalMethod in internalMethodsUnused) - { - IMethodSymbol methodSymbol = unusedInternalMethod.Key; - IApplicationObjectTypeSymbol objectSymbol = methodSymbol.GetContainingApplicationObjectTypeSymbol(); - - Diagnostic diagnostic = Diagnostic.Create(DiagnosticDescriptors.Rule0052InternalProceduresNotReferencedAnalyzerDescriptor, methodSymbol.OriginalDefinition.GetLocation(), methodSymbol.DeclaredAccessibility.ToString().ToLowerInvariant(), methodSymbol.Name.QuoteIdentifierIfNeeded(), objectSymbol.NavTypeKind, objectSymbol.Name.QuoteIdentifierIfNeeded(), objectSymbol.DeclaredAccessibility); - action(diagnostic); - } + return false; } + return true; + } - public void ReportInternalMethodOnlyReferencedInCurrentObject(Action action) + public void ReportUnchangedReferencePassedParameters(Action action) + { + if (internalMethodsUnused.Count == 0) { - var internalMethodsUsedOnlyInCurrentObject = internalMethodsUsedInCurrentObject.Except(internalMethodsUsedInOtherObjects); - - foreach (KeyValuePair internalMethodPair in internalMethodsUsedOnlyInCurrentObject) - { - IMethodSymbol methodSymbol = internalMethodPair.Key; - IApplicationObjectTypeSymbol objectSymbol = methodSymbol.GetContainingApplicationObjectTypeSymbol(); - - Diagnostic diagnostic = Diagnostic.Create(DiagnosticDescriptors.Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzerDescriptor, methodSymbol.OriginalDefinition.GetLocation(), methodSymbol.DeclaredAccessibility.ToString().ToLowerInvariant(), methodSymbol.Name.QuoteIdentifierIfNeeded(), objectSymbol.NavTypeKind, objectSymbol.Name.QuoteIdentifierIfNeeded(), objectSymbol.DeclaredAccessibility); - action(diagnostic); - } + return; } - - public void Dispose() + foreach (KeyValuePair unusedInternalMethod in internalMethodsUnused) { - methodSymbols.Free(); + IMethodSymbol methodSymbol = unusedInternalMethod.Key; + IApplicationObjectTypeSymbol objectSymbol = methodSymbol.GetContainingApplicationObjectTypeSymbol(); + + Diagnostic diagnostic = Diagnostic.Create(DiagnosticDescriptors.Rule0052InternalProceduresNotReferencedAnalyzerDescriptor, methodSymbol.OriginalDefinition.GetLocation(), methodSymbol.DeclaredAccessibility.ToString().ToLowerInvariant(), methodSymbol.Name.QuoteIdentifierIfNeeded(), objectSymbol.NavTypeKind, objectSymbol.Name.QuoteIdentifierIfNeeded(), objectSymbol.DeclaredAccessibility); + action(diagnostic); } } - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0052InternalProceduresNotReferencedAnalyzerDescriptor, DiagnosticDescriptors.Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzerDescriptor); + public void ReportInternalMethodOnlyReferencedInCurrentObject(Action action) + { + var internalMethodsUsedOnlyInCurrentObject = internalMethodsUsedInCurrentObject.Except(internalMethodsUsedInOtherObjects); + foreach (KeyValuePair internalMethodPair in internalMethodsUsedOnlyInCurrentObject) + { + IMethodSymbol methodSymbol = internalMethodPair.Key; + IApplicationObjectTypeSymbol objectSymbol = methodSymbol.GetContainingApplicationObjectTypeSymbol(); - public override void Initialize(AnalysisContext context) - { - context.RegisterCompilationAction(CheckApplicationObjects); + Diagnostic diagnostic = Diagnostic.Create(DiagnosticDescriptors.Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzerDescriptor, methodSymbol.OriginalDefinition.GetLocation(), methodSymbol.DeclaredAccessibility.ToString().ToLowerInvariant(), methodSymbol.Name.QuoteIdentifierIfNeeded(), objectSymbol.NavTypeKind, objectSymbol.Name.QuoteIdentifierIfNeeded(), objectSymbol.DeclaredAccessibility); + action(diagnostic); + } } - private static void CheckApplicationObjects(CompilationAnalysisContext compilationAnalysisContext) + public void Dispose() { - MethodSymbolAnalyzer methodSymbolAnalyzer = new MethodSymbolAnalyzer(compilationAnalysisContext); - methodSymbolAnalyzer.AnalyzeObjectSyntax(compilationAnalysisContext); - methodSymbolAnalyzer.ReportUnchangedReferencePassedParameters(compilationAnalysisContext.ReportDiagnostic); - methodSymbolAnalyzer.ReportInternalMethodOnlyReferencedInCurrentObject(compilationAnalysisContext.ReportDiagnostic); + methodSymbols.Free(); } } -} + + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0052InternalProceduresNotReferencedAnalyzerDescriptor, DiagnosticDescriptors.Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzerDescriptor); + + + public override void Initialize(AnalysisContext context) + { + context.RegisterCompilationAction(CheckApplicationObjects); + } + + private static void CheckApplicationObjects(CompilationAnalysisContext compilationAnalysisContext) + { + MethodSymbolAnalyzer methodSymbolAnalyzer = new MethodSymbolAnalyzer(compilationAnalysisContext); + methodSymbolAnalyzer.AnalyzeObjectSyntax(compilationAnalysisContext); + methodSymbolAnalyzer.ReportUnchangedReferencePassedParameters(compilationAnalysisContext.ReportDiagnostic); + methodSymbolAnalyzer.ReportInternalMethodOnlyReferencedInCurrentObject(compilationAnalysisContext.ReportDiagnostic); + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0054FollowInterfaceObjectNameGuide.cs b/BusinessCentral.LinterCop/Design/Rule0054FollowInterfaceObjectNameGuide.cs index b2673e7d..219eedc4 100644 --- a/BusinessCentral.LinterCop/Design/Rule0054FollowInterfaceObjectNameGuide.cs +++ b/BusinessCentral.LinterCop/Design/Rule0054FollowInterfaceObjectNameGuide.cs @@ -1,127 +1,123 @@ -#nullable disable // TODO: Enable nullable and review rule using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.Analyzers.Common.AppSourceCopConfiguration; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; using System.Text; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0054FollowInterfaceObjectNameGuide : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0054FollowInterfaceObjectNameGuide : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0054FollowInterfaceObjectNameGuide); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0054FollowInterfaceObjectNameGuide); - private static IEnumerable _affixes; - private static readonly char _charCapitalI = 'I'; + private static IEnumerable? Affixes = null; + private static readonly char CharOfCapitalI = 'I'; - public override void Initialize(AnalysisContext context) - { - context.RegisterCompilationStartAction(new Action(this.PopulateListOfAffixes)); - context.RegisterSymbolAction(new Action(this.AnalyzeObjectName), SymbolKind.Interface); - } + public override void Initialize(AnalysisContext context) + { + context.RegisterCompilationStartAction(new Action(this.PopulateListOfAffixes)); + context.RegisterSymbolAction(new Action(this.AnalyzeObjectName), SymbolKind.Interface); + } - private void AnalyzeObjectName(SymbolAnalysisContext context) - { - if (context.IsObsoletePendingOrRemoved()) return; + private void AnalyzeObjectName(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Symbol is not IInterfaceTypeSymbol interfaceTypeSymbol) + return; - if (context.Symbol is not IInterfaceTypeSymbol interfaceTypeSymbol) - return; + // The interface object should start with a capital 'I' and should not have a space after it + if (interfaceTypeSymbol.Name.StartsWith(CharOfCapitalI) && !char.IsWhiteSpace(interfaceTypeSymbol.Name[1])) + return; - // The interface object should start with a capital 'I' and should not have a space after it - if (interfaceTypeSymbol.Name.StartsWith(_charCapitalI) && !char.IsWhiteSpace(interfaceTypeSymbol.Name[1])) - return; + int? indexAfterAffix = GetIndexAfterAffix(interfaceTypeSymbol.Name); + if (indexAfterAffix is null) + { + ReportDiagnostic(ctx, interfaceTypeSymbol); + return; + } - int? indexAfterAffix = GetIndexAfterAffix(interfaceTypeSymbol.Name); - if (indexAfterAffix is null) - { - ReportDiagnostic(context, interfaceTypeSymbol); - return; - } + string objectNameWithoutPrefix = interfaceTypeSymbol.Name.Remove(0, indexAfterAffix.GetValueOrDefault()); - string objectNameWithoutPrefix = interfaceTypeSymbol.Name.Remove(0, indexAfterAffix.GetValueOrDefault()); + // The first character after the prefix should be a capital 'I' + if (RemoveSpecialCharacters(objectNameWithoutPrefix)[0] != CharOfCapitalI) + { + ReportDiagnostic(ctx, interfaceTypeSymbol); + return; + } - // The first character after the prefix should be a capital 'I' - if (RemoveSpecialCharacters(objectNameWithoutPrefix)[0] != _charCapitalI) + // The character after the capital 'I' should not be a whitespace + int index = objectNameWithoutPrefix.IndexOf(CharOfCapitalI); + if (index != -1 && index < objectNameWithoutPrefix.Length - 1) + { + if (char.IsWhiteSpace(objectNameWithoutPrefix[index + 1])) { - ReportDiagnostic(context, interfaceTypeSymbol); + ReportDiagnostic(ctx, interfaceTypeSymbol); return; } - - // The character after the capital 'I' should not be a whitespace - int index = objectNameWithoutPrefix.IndexOf(_charCapitalI); - if (index != -1 && index < objectNameWithoutPrefix.Length - 1) - { - if (char.IsWhiteSpace(objectNameWithoutPrefix[index + 1])) - { - ReportDiagnostic(context, interfaceTypeSymbol); - return; - } - } } + } - private void PopulateListOfAffixes(CompilationStartAnalysisContext context) - { - _affixes = GetAffixes(context.Compilation); - } + private void PopulateListOfAffixes(CompilationStartAnalysisContext context) + { + Affixes = GetAffixes(context.Compilation); + } - private static string RemoveSpecialCharacters(string str) + private static string RemoveSpecialCharacters(string str) + { + StringBuilder sb = new StringBuilder(); + foreach (char c in str) { - StringBuilder sb = new StringBuilder(); - foreach (char c in str) + if (char.IsLetterOrDigit(c)) { - if (char.IsLetterOrDigit(c)) - { - sb.Append(c); - } + sb.Append(c); } - return sb.ToString(); } + return sb.ToString(); + } - private static int? GetIndexAfterAffix(string typeSymbolName) + private static int? GetIndexAfterAffix(string typeSymbolName) + { + foreach (var affix in Affixes ?? Enumerable.Empty()) { - foreach (var affix in _affixes ?? Enumerable.Empty()) + if (typeSymbolName.StartsWith(affix, StringComparison.OrdinalIgnoreCase)) { - if (typeSymbolName.StartsWith(affix, StringComparison.OrdinalIgnoreCase)) + int affixLength = affix.Length; + if (typeSymbolName.Length > affixLength) { - int affixLength = affix.Length; - if (typeSymbolName.Length > affixLength) - { - return affixLength; - } + return affixLength; } } - - // Return null if no affix is found or no character is present after the affix - return null; } - private static List GetAffixes(Compilation compilation) - { - AppSourceCopConfiguration copConfiguration = AppSourceCopConfigurationProvider.GetAppSourceCopConfiguration(compilation); - if (copConfiguration is null) - return null; + // Return null if no affix is found or no character is present after the affix + return null; + } + + private static List? GetAffixes(Compilation compilation) + { + AppSourceCopConfiguration? copConfiguration = AppSourceCopConfigurationProvider.GetAppSourceCopConfiguration(compilation); + if (copConfiguration is null) + return null; - List affixes = new List(); - if (!string.IsNullOrEmpty(copConfiguration.MandatoryPrefix) && !affixes.Contains(copConfiguration.MandatoryPrefix, StringComparer.OrdinalIgnoreCase)) - affixes.Add(copConfiguration.MandatoryPrefix); + List affixes = new List(); + if (!string.IsNullOrEmpty(copConfiguration.MandatoryPrefix) && !affixes.Contains(copConfiguration.MandatoryPrefix, StringComparer.OrdinalIgnoreCase)) + affixes.Add(copConfiguration.MandatoryPrefix); - if (copConfiguration.MandatoryAffixes != null) + if (copConfiguration.MandatoryAffixes is not null) + { + foreach (string mandatoryAffix in copConfiguration.MandatoryAffixes) { - foreach (string mandatoryAffix in copConfiguration.MandatoryAffixes) - { - if (!string.IsNullOrEmpty(mandatoryAffix) && !affixes.Contains(mandatoryAffix, StringComparer.OrdinalIgnoreCase)) - affixes.Add(mandatoryAffix); - } + if (!string.IsNullOrEmpty(mandatoryAffix) && !affixes.Contains(mandatoryAffix, StringComparer.OrdinalIgnoreCase)) + affixes.Add(mandatoryAffix); } - return affixes; } + return affixes; + } - private void ReportDiagnostic(SymbolAnalysisContext context, IInterfaceTypeSymbol interfaceTypeSymbol) - { - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0054FollowInterfaceObjectNameGuide, interfaceTypeSymbol.GetLocation())); - } + private void ReportDiagnostic(SymbolAnalysisContext context, IInterfaceTypeSymbol interfaceTypeSymbol) + { + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0054FollowInterfaceObjectNameGuide, interfaceTypeSymbol.GetLocation())); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0055TokSuffixForTokenLabels.cs b/BusinessCentral.LinterCop/Design/Rule0055TokSuffixForTokenLabels.cs index dc73896d..17be66c7 100644 --- a/BusinessCentral.LinterCop/Design/Rule0055TokSuffixForTokenLabels.cs +++ b/BusinessCentral.LinterCop/Design/Rule0055TokSuffixForTokenLabels.cs @@ -1,54 +1,59 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; -using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0055TokSuffixForTokenLabels : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0055TokSuffixForTokenLabels : DiagnosticAnalyzer + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0055TokSuffixForTokenLabels); + + // https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/analyzers/codecop-aa0074#remarks + internal static readonly ImmutableHashSet approvedSuffixes = (new string[6] { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0055TokSuffixForTokenLabels); + "Msg", + "Tok", + "Err", + "Qst", + "Lbl", + "Txt" + }).ToImmutableHashSet(); + + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(new Action(this.AnalyzeLockedLabel), SymbolKind.GlobalVariable, SymbolKind.LocalVariable); + + private void AnalyzeLockedLabel(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Symbol is not IVariableSymbol variable) + return; - // https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/analyzers/codecop-aa0074#remarks - internal static readonly ImmutableHashSet approvedSuffixes = ((IEnumerable)new string[6] - { - "Msg", - "Tok", - "Err", - "Qst", - "Lbl", - "Txt" - }).ToImmutableHashSet(); + if (variable.Type is not ILabelTypeSymbol label) + return; - public override void Initialize(AnalysisContext context) => context.RegisterSymbolAction(new Action(this.AnalyzeLockedLabel), SymbolKind.GlobalVariable, SymbolKind.LocalVariable); + if (!label.Locked || label.Name.EndsWith("Tok")) + return; - private void AnalyzeLockedLabel(SymbolAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; + string labelNameNoSuffix = label.Name; - IVariableSymbol variable = (IVariableSymbol)ctx.Symbol; - if (variable.Type == null || variable.Type.GetNavTypeKindSafe() != NavTypeKind.Label) - return; + if (label.Name.Length > 3 && approvedSuffixes.Any(label.Name.EndsWith)) + labelNameNoSuffix = label.Name.Substring(0, label.Name.Length - 3); - ILabelTypeSymbol label = variable.Type as ILabelTypeSymbol; - if (!label.Locked || label.Name.EndsWith("Tok")) return; + if (labelNameNoSuffix.ToLowerInvariant() == label.GetLabelText().ToLowerInvariant()) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0055TokSuffixForTokenLabels, + variable.GetLocation())); - string labelNameNoSuffix = label.Name; - if (label.Name.Length > 3 && approvedSuffixes.Any(label.Name.EndsWith)) - labelNameNoSuffix = label.Name.Substring(0, label.Name.Length - 3); + return; + } - if (labelNameNoSuffix.ToLowerInvariant() == label.GetLabelText().ToLowerInvariant()) - { - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0055TokSuffixForTokenLabels, variable.GetLocation())); - return; - } + string LabelTextAlphanumeric = string.Join("", label.GetLabelText().Where(c => Char.IsLetterOrDigit(c))); - string LabelTextAlphanumeric = String.Join("", label.GetLabelText().Where(c => Char.IsLetterOrDigit(c))); - if (labelNameNoSuffix.ToLowerInvariant() == LabelTextAlphanumeric.ToLowerInvariant()) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0055TokSuffixForTokenLabels, variable.GetLocation())); - } + if (labelNameNoSuffix.ToLowerInvariant() == LabelTextAlphanumeric.ToLowerInvariant()) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0055TokSuffixForTokenLabels, + variable.GetLocation())); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0056AccessibilityEnumValueWithCaption.cs b/BusinessCentral.LinterCop/Design/Rule0056AccessibilityEnumValueWithCaption.cs index 01fdcb54..080459bc 100644 --- a/BusinessCentral.LinterCop/Design/Rule0056AccessibilityEnumValueWithCaption.cs +++ b/BusinessCentral.LinterCop/Design/Rule0056AccessibilityEnumValueWithCaption.cs @@ -1,38 +1,43 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0056AccessibilityEnumValueWithCaption : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0056AccessibilityEnumValueWithCaption : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0056EmptyEnumValueWithCaption, DiagnosticDescriptors.Rule0057EnumValueWithEmptyCaption); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0056EmptyEnumValueWithCaption, DiagnosticDescriptors.Rule0057EnumValueWithEmptyCaption); - public override void Initialize(AnalysisContext context) => context.RegisterSyntaxNodeAction(new Action(this.AnalyzeEnumWithCaption), SyntaxKind.EnumValue); + public override void Initialize(AnalysisContext context) => + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeEnumWithCaption), SyntaxKind.EnumValue); - private void AnalyzeEnumWithCaption(SyntaxNodeAnalysisContext ctx) - { - // Prevent possible duplicate diagnostic - if (ctx.ContainingSymbol.ContainingType is null) return; + private void AnalyzeEnumWithCaption(SyntaxNodeAnalysisContext ctx) + { + // Prevent possible duplicate diagnostic + if (ctx.ContainingSymbol.ContainingType is null) + return; - if (ctx.IsObsoletePendingOrRemoved()) return; + if (ctx.IsObsoletePendingOrRemoved() || ctx.Node is not EnumValueSyntax enumValue) + return; - EnumValueSyntax enumValue = ctx.Node as EnumValueSyntax; - if (enumValue == null) return; - LabelPropertyValueSyntax captionProperty = ctx.Node?.GetProperty("Caption")?.Value as LabelPropertyValueSyntax; - if (captionProperty == null) return; + if (ctx.Node?.GetProperty("Caption")?.Value is not LabelPropertyValueSyntax captionProperty) + return; - if (enumValue.GetNameStringValue() == "" && captionProperty.Value.LabelText.Value.Value.ToString() != "") - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0056EmptyEnumValueWithCaption, ctx.Node.GetLocation())); + if (enumValue.GetNameStringValue() == "" && captionProperty.Value.LabelText.Value.Value.ToString() != "") + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0056EmptyEnumValueWithCaption, + ctx.Node.GetLocation())); - if (captionProperty.Value.Properties?.Values.Where(prop => prop.Identifier.Text.ToLowerInvariant() == "locked").FirstOrDefault() != null) return; + if (captionProperty.Value.Properties?.Values.Where(prop => prop.Identifier.Text.Equals("Locked", StringComparison.OrdinalIgnoreCase)).FirstOrDefault() is not null) + return; - if (enumValue.GetNameStringValue() != "" && captionProperty.Value.LabelText.Value.Value.ToString() == "") - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0057EnumValueWithEmptyCaption, ctx.Node.GetLocation())); - } + if (enumValue.GetNameStringValue() != "" && captionProperty.Value.LabelText.Value.Value.ToString() == "") + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0057EnumValueWithEmptyCaption, + ctx.Node.GetLocation())); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0059SingleQuoteEscaping.cs b/BusinessCentral.LinterCop/Design/Rule0059SingleQuoteEscaping.cs index 0e93e0ab..43119270 100644 --- a/BusinessCentral.LinterCop/Design/Rule0059SingleQuoteEscaping.cs +++ b/BusinessCentral.LinterCop/Design/Rule0059SingleQuoteEscaping.cs @@ -1,57 +1,59 @@ -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0059SingleQuoteEscaping : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0059SingleQuoteEscaping : DiagnosticAnalyzer + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0050OperatorAndPlaceholderInFilterExpression, DiagnosticDescriptors.Rule0059SingleQuoteEscapingIssueDetected, DiagnosticDescriptors.Rule0000ErrorInRule); + + private static readonly string InvalidUnaryEqualsFilter = "'<>'''"; + + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(new Action(this.AnalyzeCalcFormula), SymbolKind.Field); + + private void AnalyzeCalcFormula(SymbolAnalysisContext ctx) { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0050OperatorAndPlaceholderInFilterExpression, DiagnosticDescriptors.Rule0059SingleQuoteEscapingIssueDetected, DiagnosticDescriptors.Rule0000ErrorInRule); + if (ctx.IsObsoletePendingOrRemoved()) + return; + + SyntaxNode? syntaxNode = ctx.Symbol.DeclaringSyntaxReference?.GetSyntax(ctx.CancellationToken); + if (syntaxNode is null) + return; + + if (syntaxNode is not FieldSyntax fieldSyntax) + return; + + // Retrieve the 'CalcFormula' property from the field's property list + var calcFormulaPropertySyntax = fieldSyntax.PropertyList?.Properties + .OfType() + .Select(p => p.Value) + .OfType() + .FirstOrDefault(); + + if (calcFormulaPropertySyntax is null) + return; - private static readonly string InvalidUnaryEqualsFilter = "'<>'''"; + // Retrieve the filter expression from the 'Where' expression of the CalcFormula + var filterExpressions = calcFormulaPropertySyntax.WhereExpression?.Filter.Conditions + .OfType() + .Where(c => c.Filter.Kind == SyntaxKind.UnaryEqualsFilterExpression) + .Select(c => c.Filter); - public override void Initialize(AnalysisContext context) => - context.RegisterSymbolAction(new Action(this.AnalyzeCalcFormula), SymbolKind.Field); + if (filterExpressions is null) + return; - private void AnalyzeCalcFormula(SymbolAnalysisContext ctx) + foreach (var filter in filterExpressions) { - if (ctx.IsObsoletePendingOrRemoved()) - return; - - SyntaxNode? syntaxNode = ctx.Symbol.DeclaringSyntaxReference?.GetSyntax(ctx.CancellationToken); - if (syntaxNode == null) - return; - - if (syntaxNode is not FieldSyntax fieldSyntax) - return; - - // Retrieve the 'CalcFormula' property from the field's property list - var calcFormulaPropertySyntax = fieldSyntax.PropertyList?.Properties - .OfType() - .Select(p => p.Value) - .OfType() - .FirstOrDefault(); - - if (calcFormulaPropertySyntax is null) - return; - - // Retrieve the filter expression from the 'Where' expression of the CalcFormula - var filterExpressions = calcFormulaPropertySyntax.WhereExpression?.Filter.Conditions - .OfType() - .Where(c => c.Filter.Kind == SyntaxKind.UnaryEqualsFilterExpression) - .Select(c => c.Filter); - - if (filterExpressions is null) - return; - - foreach (var filter in filterExpressions) - { - if (filter.ToString().Equals(InvalidUnaryEqualsFilter)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0059SingleQuoteEscapingIssueDetected, filter.GetLocation())); - } + if (filter.ToString().Equals(InvalidUnaryEqualsFilter)) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0059SingleQuoteEscapingIssueDetected, + filter.GetLocation())); } } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0060RemovePropertyApplicationAreaOnApiPage.cs b/BusinessCentral.LinterCop/Design/Rule0060RemovePropertyApplicationAreaOnApiPage.cs index 05b450be..21422631 100644 --- a/BusinessCentral.LinterCop/Design/Rule0060RemovePropertyApplicationAreaOnApiPage.cs +++ b/BusinessCentral.LinterCop/Design/Rule0060RemovePropertyApplicationAreaOnApiPage.cs @@ -1,38 +1,40 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Text; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design -{ - [DiagnosticAnalyzer] - public class Rule0060PropertyApplicationAreaOnApiPage : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0060PropertyApplicationAreaOnApiPage); +namespace BusinessCentral.LinterCop.Design; - public override void Initialize(AnalysisContext context) - => context.RegisterSymbolAction(new Action(this.AnalyzePropertyApplicationAreaOnApiPage), SymbolKind.Page); +[DiagnosticAnalyzer] +public class Rule0060PropertyApplicationAreaOnApiPage : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0060PropertyApplicationAreaOnApiPage); - private void AnalyzePropertyApplicationAreaOnApiPage(SymbolAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(new Action(this.AnalyzePropertyApplicationAreaOnApiPage), SymbolKind.Page); - if (ctx.Symbol is not IPageTypeSymbol pageTypeSymbol) - return; + private void AnalyzePropertyApplicationAreaOnApiPage(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Symbol is not IPageTypeSymbol pageTypeSymbol) + return; - if (pageTypeSymbol.PageType != PageTypeKind.API) - return; + if (pageTypeSymbol.PageType != PageTypeKind.API) + return; - if (pageTypeSymbol.GetProperty(PropertyKind.ApplicationArea) is IPropertySymbol propertyApplicationArea) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0060PropertyApplicationAreaOnApiPage, propertyApplicationArea.GetLocation())); + if (pageTypeSymbol.GetProperty(PropertyKind.ApplicationArea) is IPropertySymbol propertyApplicationArea) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0060PropertyApplicationAreaOnApiPage, + propertyApplicationArea.GetLocation())); - IEnumerable pageFields = pageTypeSymbol.FlattenedControls - .Where(e => e.ControlKind == ControlKind.Field) - .Where(e => e.GetProperty(PropertyKind.ApplicationArea) is not null); + IEnumerable Locations = pageTypeSymbol.FlattenedControls + .Where(e => e.ControlKind == ControlKind.Field && e.GetProperty(PropertyKind.ApplicationArea) is not null) + .Select(e => e.GetLocation()); - foreach (IControlSymbol pageField in pageFields) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0060PropertyApplicationAreaOnApiPage, pageField.GetProperty(PropertyKind.ApplicationArea).GetLocation())); - } + foreach (Location location in Locations) + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0060PropertyApplicationAreaOnApiPage, + location)); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0061SetODataKeyFieldsWithSystemIdField.cs b/BusinessCentral.LinterCop/Design/Rule0061SetODataKeyFieldsWithSystemIdField.cs index e5699c16..cb61e780 100644 --- a/BusinessCentral.LinterCop/Design/Rule0061SetODataKeyFieldsWithSystemIdField.cs +++ b/BusinessCentral.LinterCop/Design/Rule0061SetODataKeyFieldsWithSystemIdField.cs @@ -1,42 +1,42 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Text; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design -{ - [DiagnosticAnalyzer] - public class Rule0061SetODataKeyFieldsWithSystemIdField : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0061SetODataKeyFieldsWithSystemIdField); +namespace BusinessCentral.LinterCop.Design; - public override void Initialize(AnalysisContext context) - => context.RegisterSymbolAction(new Action(this.AnalyzeODataKeyFieldsPropertyOnApiPage), SymbolKind.Page); +[DiagnosticAnalyzer] +public class Rule0061SetODataKeyFieldsWithSystemIdField : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0061SetODataKeyFieldsWithSystemIdField); - private void AnalyzeODataKeyFieldsPropertyOnApiPage(SymbolAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; + public override void Initialize(AnalysisContext context) + => context.RegisterSymbolAction(new Action(this.AnalyzeODataKeyFieldsPropertyOnApiPage), SymbolKind.Page); - if (ctx.Symbol is not IPageTypeSymbol pageTypeSymbol) - return; + private void AnalyzeODataKeyFieldsPropertyOnApiPage(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Symbol is not IPageTypeSymbol pageTypeSymbol) + return; - if (pageTypeSymbol.PageType != PageTypeKind.API) - return; + if (pageTypeSymbol.PageType != PageTypeKind.API) + return; - if (pageTypeSymbol.GetBooleanPropertyValue(PropertyKind.SourceTableTemporary).GetValueOrDefault()) - return; + if (pageTypeSymbol.GetBooleanPropertyValue(PropertyKind.SourceTableTemporary).GetValueOrDefault()) + return; - IPropertySymbol property = pageTypeSymbol.GetProperty(PropertyKind.ODataKeyFields); + if (pageTypeSymbol.GetProperty(PropertyKind.ODataKeyFields) is not IPropertySymbol property) + return; - // Set the location of the diagnostic on the property itself (if exists) - Location location = pageTypeSymbol.GetLocation(); - if (property != null) - location = property.GetLocation(); + // Set the location of the diagnostic on the property itself (if exists) + Location location = pageTypeSymbol.GetLocation(); + if (property is not null) + location = property.GetLocation(); - if (property == null || property.Value == null || property.ValueText != "2000000000") - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0061SetODataKeyFieldsWithSystemIdField, location)); - } + if (property is null || property.Value is null || property.ValueText != "2000000000") + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0061SetODataKeyFieldsWithSystemIdField, + location)); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0062MandatoryFieldMissingOnApiPage.cs b/BusinessCentral.LinterCop/Design/Rule0062MandatoryFieldMissingOnApiPage.cs index 40ef931e..17733c11 100644 --- a/BusinessCentral.LinterCop/Design/Rule0062MandatoryFieldMissingOnApiPage.cs +++ b/BusinessCentral.LinterCop/Design/Rule0062MandatoryFieldMissingOnApiPage.cs @@ -1,5 +1,4 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; @@ -9,23 +8,20 @@ namespace BusinessCentral.LinterCop.Design [DiagnosticAnalyzer] public class Rule0062MandatoryFieldMissingOnApiPage : DiagnosticAnalyzer { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0062MandatoryFieldMissingOnApiPage); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0062MandatoryFieldMissingOnApiPage); - private static readonly Dictionary _mandatoryFields = new Dictionary + private static readonly Dictionary MandatoryFields = new Dictionary { { "SystemId", "id" }, { "SystemModifiedAt", "lastModifiedDateTime" } }; - public override void Initialize(AnalysisContext context) - => context.RegisterSymbolAction(new Action(this.AnalyzeRule0062MandatoryFieldOnApiPage), SymbolKind.Page); + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(new Action(this.AnalyzeRule0062MandatoryFieldOnApiPage), SymbolKind.Page); private void AnalyzeRule0062MandatoryFieldOnApiPage(SymbolAnalysisContext ctx) { - if (ctx.IsObsoletePendingOrRemoved()) return; - - if (ctx.Symbol is not IPageTypeSymbol pageTypeSymbol) - return; + if (ctx.IsObsoletePendingOrRemoved() || ctx.Symbol is not IPageTypeSymbol pageTypeSymbol) return; if (pageTypeSymbol.PageType != PageTypeKind.API) return; @@ -34,15 +30,18 @@ private void AnalyzeRule0062MandatoryFieldOnApiPage(SymbolAnalysisContext ctx) return; IEnumerable pageFields = pageTypeSymbol.FlattenedControls - .Where(e => e.ControlKind == ControlKind.Field) - .Where(e => e.RelatedFieldSymbol != null); + .Where(e => e.ControlKind == ControlKind.Field && e.RelatedFieldSymbol is not null); - IEnumerable> missingMandatoryFields = _mandatoryFields + IEnumerable> missingMandatoryFields = MandatoryFields .Where(mf => !pageFields.Any(pf => mf.Key == pf.RelatedFieldSymbol?.Name && mf.Value == pf.Name)); foreach (KeyValuePair field in missingMandatoryFields) { - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0062MandatoryFieldMissingOnApiPage, pageTypeSymbol.GetLocation(), new object[] { field.Key, field.Value })); + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0062MandatoryFieldMissingOnApiPage, + pageTypeSymbol.GetLocation(), + field.Key, + field.Value)); } } } diff --git a/BusinessCentral.LinterCop/Design/Rule0063GiveFieldMoreDescriptiveName.cs b/BusinessCentral.LinterCop/Design/Rule0063GiveFieldMoreDescriptiveName.cs index cb73d086..985023d1 100644 --- a/BusinessCentral.LinterCop/Design/Rule0063GiveFieldMoreDescriptiveName.cs +++ b/BusinessCentral.LinterCop/Design/Rule0063GiveFieldMoreDescriptiveName.cs @@ -1,95 +1,89 @@ -#nullable disable // TODO: Enable nullable and review rule -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0063GiveFieldMoreDescriptiveName : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0063GiveFieldMoreDescriptiveName : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0063GiveFieldMoreDescriptiveName); - private static readonly Dictionary _descriptiveNames = new Dictionary + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0063GiveFieldMoreDescriptiveName); + + private static readonly Dictionary DescriptiveNames = new Dictionary { { "SystemId", "id" }, { "Name", "displayName" }, { "SystemModifiedAt", "lastModifiedDateTime" } }; - public override void Initialize(AnalysisContext context) - => context.RegisterSymbolAction(new Action(this.AnalyzeFieldNames), SymbolKind.Page); + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(new Action(this.AnalyzeFieldNames), SymbolKind.Page); - private void AnalyzeFieldNames(SymbolAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; + private void AnalyzeFieldNames(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Symbol is not IPageTypeSymbol pageTypeSymbol) return; - if (ctx.Symbol is not IPageTypeSymbol pageTypeSymbol) - return; + if (pageTypeSymbol.PageType != PageTypeKind.API) + return; - if (pageTypeSymbol.PageType != PageTypeKind.API) - return; + IEnumerable pageFields = pageTypeSymbol.FlattenedControls + .Where(e => e.ControlKind == ControlKind.Field && + e.RelatedFieldSymbol is not null && + IsIdentifierValueTextRec(e, ctx)); - IEnumerable pageFields = pageTypeSymbol.FlattenedControls - .Where(e => e.ControlKind == ControlKind.Field) - .Where(e => IsIdentifierValueTextRec(e, ctx)) - .Where(e => e.RelatedFieldSymbol != null); + foreach (IControlSymbol field in pageFields) + { + string? descriptiveName = GetDescriptiveName(field); - foreach (IControlSymbol field in pageFields) + if (!string.IsNullOrEmpty(descriptiveName) && field.Name != descriptiveName) { - ctx.CancellationToken.ThrowIfCancellationRequested(); - string descriptiveName = GetDescriptiveName(field); - if (!string.IsNullOrEmpty(descriptiveName) && field.Name != descriptiveName) - { - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0063GiveFieldMoreDescriptiveName, field.GetLocation(), new object[] { descriptiveName })); - } + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0063GiveFieldMoreDescriptiveName, + field.GetLocation(), + descriptiveName)); } } + } - private static string GetDescriptiveName(IControlSymbol field) - { - if (_descriptiveNames.ContainsKey(field.RelatedFieldSymbol.Name)) - return _descriptiveNames[field.RelatedFieldSymbol.Name]; + private static string? GetDescriptiveName(IControlSymbol field) + { + if (field.RelatedFieldSymbol is null) + return null; - if (field.RelatedFieldSymbol.Name.Contains("No.") - && field.Name.Contains("no", StringComparison.OrdinalIgnoreCase) - && !field.Name.Contains("number", StringComparison.OrdinalIgnoreCase)) - return ReplaceNoWithNumber(field.Name); + if (DescriptiveNames.ContainsKey(field.RelatedFieldSymbol.Name)) + return DescriptiveNames[field.RelatedFieldSymbol.Name]; - return null; - } - private static string ReplaceNoWithNumber(string input) - { - input = input.Replace("No", "Number"); - input = input.Replace("no", "number"); - return input; - } + if (field.RelatedFieldSymbol.Name.Contains("No.") + && field.Name.Contains("no", StringComparison.OrdinalIgnoreCase) + && !field.Name.Contains("number", StringComparison.OrdinalIgnoreCase)) + return ReplaceNoWithNumber(field.Name); - private static bool IsIdentifierValueTextRec(IControlSymbol controlSymbol, SymbolAnalysisContext ctx) - { - if (controlSymbol.DeclaringSyntaxReference.GetSyntax(ctx.CancellationToken) is not PageFieldSyntax pageFieldSyntax) - return false; + return null; + } + private static string ReplaceNoWithNumber(string input) + { + input = input.Replace("No", "Number"); + input = input.Replace("no", "number"); + return input; + } - if (pageFieldSyntax.Expression is not MemberAccessExpressionSyntax memberAccessExpressionSyntax) - return false; + private static bool IsIdentifierValueTextRec(IControlSymbol controlSymbol, SymbolAnalysisContext ctx) + { + if (controlSymbol.DeclaringSyntaxReference?.GetSyntax(ctx.CancellationToken) is not PageFieldSyntax pageFieldSyntax) + return false; - if (memberAccessExpressionSyntax.Expression is not IdentifierNameSyntax identifierNameSyntax) - return false; + if (pageFieldSyntax.Expression is not MemberAccessExpressionSyntax memberAccessExpressionSyntax) + return false; - return SemanticFacts.IsSameName(identifierNameSyntax.Identifier.ValueText, "Rec"); - } + if (memberAccessExpressionSyntax.Expression is not IdentifierNameSyntax identifierNameSyntax) + return false; - public static class DiagnosticDescriptors - { - public static readonly DiagnosticDescriptor Rule0063GiveFieldMoreDescriptiveName = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0063", - title: LinterCopAnalyzers.GetLocalizableString("Rule0063GiveFieldMoreDescriptiveNameTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0063GiveFieldMoreDescriptiveNameFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0063GiveFieldMoreDescriptiveNameDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0063"); - } + if (identifierNameSyntax.Identifier.ValueText is null) + return false; + + return SemanticFacts.IsSameName(identifierNameSyntax.Identifier.ValueText, "Rec"); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0064UseTableFieldToolTip.cs b/BusinessCentral.LinterCop/Design/Rule0064UseTableFieldToolTip.cs index fd388946..3b431b90 100644 --- a/BusinessCentral.LinterCop/Design/Rule0064UseTableFieldToolTip.cs +++ b/BusinessCentral.LinterCop/Design/Rule0064UseTableFieldToolTip.cs @@ -1,88 +1,82 @@ -#nullable disable // TODO: Enable nullable and review rule #if !LessThenSpring2024 -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0064UseTableFieldToolTip : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0064UseTableFieldToolTip : DiagnosticAnalyzer + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create( + DiagnosticDescriptors.Rule0064TableFieldMissingToolTip, + DiagnosticDescriptors.Rule0066DuplicateToolTipBetweenPageAndTable); + + public override VersionCompatibility SupportedVersions => VersionCompatibility.Spring2024OrGreater; + + public override void Initialize(AnalysisContext context) => + context.RegisterSymbolAction(new Action(this.AnalyzeToolTipProperty), + SymbolKind.Page, + SymbolKind.PageExtension); + + private void AnalyzeToolTipProperty(SymbolAnalysisContext ctx) { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0064TableFieldMissingToolTip, DiagnosticDescriptors.Rule0066DuplicateToolTipBetweenPageAndTable); + if (ctx.IsObsoletePendingOrRemoved()) + return; - public override void Initialize(AnalysisContext context) => context.RegisterSymbolAction(new Action(this.AnalyzeFlowFieldEditable), SymbolKind.Page, SymbolKind.PageExtension); + var pageFields = GetFlattenedControls(ctx.Symbol)? + .Where(e => e.ControlKind == ControlKind.Field && + e.RelatedFieldSymbol is not null && + e.GetProperty(PropertyKind.ToolTip) is not null); - private void AnalyzeFlowFieldEditable(SymbolAnalysisContext ctx) + if (pageFields is null || !pageFields.Any()) + return; + + foreach (var pageField in pageFields) { - if (!VersionChecker.IsSupported(ctx.Symbol, VersionCompatibility.Spring2024OrGreater)) return; + ctx.CancellationToken.ThrowIfCancellationRequested(); - if (ctx.IsObsoletePendingOrRemoved()) return; + var pageToolTip = pageField.GetProperty(PropertyKind.ToolTip); + var tableToolTip = pageField.RelatedFieldSymbol?.GetProperty(PropertyKind.ToolTip); - IEnumerable pageFields = GetFlattenedControls(ctx.Symbol) - .Where(e => e.ControlKind == ControlKind.Field) - .Where(e => e.GetProperty(PropertyKind.ToolTip) != null) - .Where(e => e.RelatedFieldSymbol != null); - if (pageFields == null) return; + if (pageToolTip is null || pageField.RelatedFieldSymbol is null) + continue; - foreach (IControlSymbol pageField in pageFields) + // Page field has a value for the ToolTip property and table field does not have a value for the ToolTip property + if (tableToolTip is null && pageField.RelatedFieldSymbol.IsSourceSymbol()) { - ctx.CancellationToken.ThrowIfCancellationRequested(); - - IPropertySymbol pageToolTip = pageField.GetProperty(PropertyKind.ToolTip); - IPropertySymbol tableToolTip = pageField.RelatedFieldSymbol.GetProperty(PropertyKind.ToolTip); - - // Page field has a value for the ToolTip property and table field does not have a value for the ToolTip property - if (tableToolTip == null && pageField.RelatedFieldSymbol.IsSourceSymbol()) - { - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0064TableFieldMissingToolTip, pageToolTip.GetLocation(), new object[] { pageField.RelatedFieldSymbol.Name.QuoteIdentifierIfNeeded(), pageField.Name.QuoteIdentifierIfNeeded() })); - continue; - } + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0064TableFieldMissingToolTip, + pageToolTip.GetLocation(), + pageField.RelatedFieldSymbol.Name.QuoteIdentifierIfNeeded(), + pageField.Name.QuoteIdentifierIfNeeded())); - // Page field has a value for the ToolTip property and table field also has a value for the ToolTip property but the value is exactly the same - if (tableToolTip != null && pageToolTip.ValueText == tableToolTip.ValueText) - { - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0066DuplicateToolTipBetweenPageAndTable, pageToolTip.GetLocation(), new object[] { pageField.Name.QuoteIdentifierIfNeeded(), pageField.RelatedFieldSymbol.Name.QuoteIdentifierIfNeeded() })); - continue; - } + continue; } - } - private static IEnumerable GetFlattenedControls(ISymbol symbol) - { - switch (symbol.Kind) + // Page field has a value for the ToolTip property and table field also has a value for the ToolTip property but the value is exactly the same + if (tableToolTip is not null && string.Equals(pageToolTip.ValueText, tableToolTip.ValueText, StringComparison.Ordinal)) { - case SymbolKind.Page: - return ((IPageBaseTypeSymbol)symbol).FlattenedControls; - case SymbolKind.PageExtension: - return ((IPageExtensionBaseTypeSymbol)symbol).AddedControlsFlattened; - default: - return null; + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0066DuplicateToolTipBetweenPageAndTable, + pageToolTip.GetLocation(), + pageField.Name.QuoteIdentifierIfNeeded(), + pageField.RelatedFieldSymbol.Name.QuoteIdentifierIfNeeded())); + + continue; } } + } - public static class DiagnosticDescriptors + private static IEnumerable? GetFlattenedControls(ISymbol symbol) => + symbol switch { - public static readonly DiagnosticDescriptor Rule0064TableFieldMissingToolTip = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0064", - title: LinterCopAnalyzers.GetLocalizableString("Rule0064TableFieldMissingToolTipTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0064TableFieldMissingToolTipFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0064TableFieldMissingToolTipDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0064"); - - public static readonly DiagnosticDescriptor Rule0066DuplicateToolTipBetweenPageAndTable = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0066", - title: LinterCopAnalyzers.GetLocalizableString("Rule0066DuplicateToolTipBetweenPageAndTableTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0066DuplicateToolTipBetweenPageAndTableFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0066DuplicateToolTipBetweenPageAndTableDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0066"); - } - } + IPageBaseTypeSymbol page => page.FlattenedControls, + IPageExtensionBaseTypeSymbol pageExtension => pageExtension.AddedControlsFlattened, + _ => null + }; } #endif \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0065CheckEventSubscriberVarKeyword.cs b/BusinessCentral.LinterCop/Design/Rule0065CheckEventSubscriberVarKeyword.cs index 6404edb8..e615053c 100644 --- a/BusinessCentral.LinterCop/Design/Rule0065CheckEventSubscriberVarKeyword.cs +++ b/BusinessCentral.LinterCop/Design/Rule0065CheckEventSubscriberVarKeyword.cs @@ -2,6 +2,7 @@ using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; using Microsoft.Dynamics.Nav.CodeAnalysis.InternalSyntax; +using BusinessCentral.LinterCop.Helpers; namespace BusinessCentral.LinterCop.Design; @@ -9,82 +10,60 @@ namespace BusinessCentral.LinterCop.Design; public class Rule0065CheckEventSubscriberVarKeyword : DiagnosticAnalyzer { public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(DiagnosticDescriptors.Rule0065EventSubscriberVarCheck); + ImmutableArray.Create(DiagnosticDescriptors.Rule0065EventSubscriberVarCheck); - public override void Initialize(AnalysisContext context) - { + public override void Initialize(AnalysisContext context) => context.RegisterSymbolAction(CheckForEventSubscriberVar, SymbolKind.Method); - } - private void CheckForEventSubscriberVar(SymbolAnalysisContext context) + private void CheckForEventSubscriberVar(SymbolAnalysisContext ctx) { - var methodSymbol = (IMethodSymbol)context.Symbol; + if (ctx.IsObsoletePendingOrRemoved() || ctx.Symbol is not IMethodSymbol methodSymbol) + return; + var eventSubscriberAttribute = methodSymbol.Attributes .FirstOrDefault(attr => attr.AttributeKind == AttributeKind.EventSubscriber); - if (eventSubscriberAttribute == null) - { + if (eventSubscriberAttribute is null) return; - } - var method = GetReferencedEventPublisherMethodSymbol(context, eventSubscriberAttribute); - if (method == null) - { + var method = GetReferencedEventPublisherMethodSymbol(ctx, eventSubscriberAttribute); + if (method is null) return; - } - var publisherParameters = method.Parameters; + var publisherParameters = method.Parameters.ToDictionary( + p => p.Name, + StringComparer.OrdinalIgnoreCase); foreach (var subscriberParameter in methodSymbol.Parameters) { - var publisherParameter = publisherParameters.FirstOrDefault(p => p.Name.Equals(subscriberParameter.Name, StringComparison.OrdinalIgnoreCase)); - if (publisherParameter == null) - { - continue; - } + ctx.CancellationToken.ThrowIfCancellationRequested(); - if (publisherParameter.IsVar && !subscriberParameter.IsVar) + if (publisherParameters.TryGetValue(subscriberParameter.Name, out var publisherParameter)) { - context.ReportDiagnostic(Diagnostic.Create( - DiagnosticDescriptors.Rule0065EventSubscriberVarCheck, - subscriberParameter.GetLocation(), - new object[] { subscriberParameter.Name })); + if (publisherParameter.IsVar && !subscriberParameter.IsVar) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0065EventSubscriberVarCheck, + subscriberParameter.GetLocation(), + subscriberParameter.Name)); + } } } } private static IMethodSymbol? GetReferencedEventPublisherMethodSymbol(SymbolAnalysisContext context, IAttributeSymbol eventSubscriberAttribute) { - var applicationObject = eventSubscriberAttribute.GetReferencedApplicationObject(); - if (applicationObject == null) - { + if (context.CancellationToken.IsCancellationRequested) return null; - } - if (eventSubscriberAttribute.Arguments.Length < 3) - { + var applicationObject = eventSubscriberAttribute.GetReferencedApplicationObject(); + if (applicationObject is null || eventSubscriberAttribute.Arguments.Length < 3) return null; - } var eventName = eventSubscriberAttribute.Arguments[2].ValueText; - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (eventName == null) - { + if (string.IsNullOrEmpty(eventName)) return null; - } return applicationObject.GetFirstMethod(eventName, context.Compilation); } - - public static class DiagnosticDescriptors - { - public static readonly DiagnosticDescriptor Rule0065EventSubscriberVarCheck = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0065", - title: LinterCopAnalyzers.GetLocalizableString("Rule0065EventSubscriberVarCheckTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0065EventSubscriberVarCheckFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0065EventSubscriberVarCheckDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0065"); - } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0068CheckObjectPermission.cs b/BusinessCentral.LinterCop/Design/Rule0068CheckObjectPermission.cs index 4b7f14ba..6890f93f 100644 --- a/BusinessCentral.LinterCop/Design/Rule0068CheckObjectPermission.cs +++ b/BusinessCentral.LinterCop/Design/Rule0068CheckObjectPermission.cs @@ -1,276 +1,264 @@ using System.Collections.Immutable; -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0068CheckObjectPermission : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0068CheckObjectPermission : DiagnosticAnalyzer + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0068CheckObjectPermission); + + private static readonly HashSet buildInTableDataReadMethodNames = new() + { + "find", + "findfirst", + "findlast", + "findset", + "get", + "isempty" + }; + private static readonly HashSet buildInTableDataModifyMethodNames = new() { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0068CheckObjectPermission); + "modify", + "modifyall", + "rename" + }; + private static readonly HashSet buildInTableDataInsertMethodNames = new() + { + "insert" + }; + private static readonly HashSet buildInTableDataDeleteMethodNames = new() + { + "delete", + "deleteall" + }; - private static readonly List buildInTableDataReadMethodNames = new List - { - "find", - "findfirst", - "findlast", - "findset", - "get", - "isempty" - }; - private static readonly List buildInTableDataModifyMethodNames = new List - { - "modify", - "modifyall", - "rename" - }; - private static readonly List buildInTableDataInsertMethodNames = new List - { - "insert" - }; - private static readonly List buildInTableDataDeleteMethodNames = new List - { - "delete", - "deleteall" - }; - public override void Initialize(AnalysisContext context) - { - context.RegisterOperationAction(new Action(this.CheckObjectPermission), OperationKind.InvocationExpression); - context.RegisterSymbolAction(new Action(this.CheckReportDataItemObjectPermission), SymbolKind.ReportDataItem); - context.RegisterSymbolAction(new Action(this.CheckQueryDataItemObjectPermission), SymbolKind.QueryDataItem); - context.RegisterSymbolAction(new Action(this.CheckXmlportNodeObjectPermission), SymbolKind.XmlPortNode); - } + public override void Initialize(AnalysisContext context) + { + context.RegisterOperationAction(new Action(this.CheckObjectPermission), OperationKind.InvocationExpression); + context.RegisterSymbolAction(new Action(this.CheckReportDataItemObjectPermission), SymbolKind.ReportDataItem); + context.RegisterSymbolAction(new Action(this.CheckQueryDataItemObjectPermission), SymbolKind.QueryDataItem); + context.RegisterSymbolAction(new Action(this.CheckXmlportNodeObjectPermission), SymbolKind.XmlPortNode); + } - private void CheckXmlportNodeObjectPermission(SymbolAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; - if (((IXmlPortNodeSymbol)ctx.Symbol.OriginalDefinition).SourceTypeKind != XmlPortSourceTypeKind.Table) return; + private void CheckXmlportNodeObjectPermission(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) return; + if (((IXmlPortNodeSymbol)ctx.Symbol.OriginalDefinition).SourceTypeKind != XmlPortSourceTypeKind.Table) return; - string direction = ""; + string direction = ""; - IXmlPortTypeSymbol xmlPort = (IXmlPortTypeSymbol)ctx.Symbol.GetContainingObjectTypeSymbol(); + IXmlPortTypeSymbol xmlPort = (IXmlPortTypeSymbol)ctx.Symbol.GetContainingObjectTypeSymbol(); - IPropertySymbol? objectPermissions = xmlPort.GetProperty(PropertyKind.Permissions); - ITypeSymbol targetSymbol = ((IXmlPortNodeSymbol)ctx.Symbol.OriginalDefinition).GetTypeSymbol(); - var directionProperty = xmlPort.Properties.FirstOrDefault(property => property.Name == "Direction"); + IPropertySymbol? objectPermissions = xmlPort.GetProperty(PropertyKind.Permissions); + ITypeSymbol targetSymbol = ((IXmlPortNodeSymbol)ctx.Symbol.OriginalDefinition).GetTypeSymbol(); + var directionProperty = xmlPort.Properties.FirstOrDefault(property => property.Name == "Direction"); - if (directionProperty == null) - direction = DirectionKind.Both.ToString(); - else - direction = directionProperty.ValueText; + if (directionProperty is null) + direction = DirectionKind.Both.ToString(); + else + direction = directionProperty.ValueText; - bool? AutoReplace = (bool?)ctx.Symbol.Properties.FirstOrDefault(property => property.PropertyKind == PropertyKind.AutoReplace)?.Value; // modify permissions - bool? AutoUpdate = (bool?)ctx.Symbol.Properties.FirstOrDefault(property => property.PropertyKind == PropertyKind.AutoUpdate)?.Value; // modify permissions - bool? AutoSave = (bool?)ctx.Symbol.Properties.FirstOrDefault(property => property.PropertyKind == PropertyKind.AutoSave)?.Value; // insert permissions + bool? AutoReplace = (bool?)ctx.Symbol.Properties.FirstOrDefault(property => property.PropertyKind == PropertyKind.AutoReplace)?.Value; // modify permissions + bool? AutoUpdate = (bool?)ctx.Symbol.Properties.FirstOrDefault(property => property.PropertyKind == PropertyKind.AutoUpdate)?.Value; // modify permissions + bool? AutoSave = (bool?)ctx.Symbol.Properties.FirstOrDefault(property => property.PropertyKind == PropertyKind.AutoSave)?.Value; // insert permissions - AutoReplace ??= true; - AutoUpdate ??= true; - AutoSave ??= true; + AutoReplace ??= true; + AutoUpdate ??= true; + AutoSave ??= true; - direction = direction.ToLowerInvariant(); + direction = direction.ToLowerInvariant(); - if (direction == "import" || direction == "both") - { - if (AutoReplace == true || AutoUpdate == true) - CheckProcedureInvocation(objectPermissions, targetSymbol, 'm', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); - if (AutoSave == true) - CheckProcedureInvocation(objectPermissions, targetSymbol, 'i', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); - } - if (direction == "export" || direction == "both") - CheckProcedureInvocation(objectPermissions, targetSymbol, 'r', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); + if (direction == "import" || direction == "both") + { + if (AutoReplace == true || AutoUpdate == true) + CheckProcedureInvocation(objectPermissions, targetSymbol, 'm', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); + if (AutoSave == true) + CheckProcedureInvocation(objectPermissions, targetSymbol, 'i', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); } + if (direction == "export" || direction == "both") + CheckProcedureInvocation(objectPermissions, targetSymbol, 'r', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); + } - private void CheckQueryDataItemObjectPermission(SymbolAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; + private void CheckQueryDataItemObjectPermission(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) return; - IPropertySymbol? objectPermissions = ctx.Symbol.GetContainingApplicationObjectTypeSymbol()?.GetProperty(PropertyKind.Permissions); - ITypeSymbol targetSymbol = ((IQueryDataItemSymbol)ctx.Symbol).GetTypeSymbol(); - CheckProcedureInvocation(objectPermissions, targetSymbol, 'r', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); - } + IPropertySymbol? objectPermissions = ctx.Symbol.GetContainingApplicationObjectTypeSymbol()?.GetProperty(PropertyKind.Permissions); + ITypeSymbol targetSymbol = ((IQueryDataItemSymbol)ctx.Symbol).GetTypeSymbol(); + CheckProcedureInvocation(objectPermissions, targetSymbol, 'r', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); + } - private void CheckReportDataItemObjectPermission(SymbolAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; - if (ctx.Symbol.GetBooleanPropertyValue(PropertyKind.UseTemporary) == true) return; - if (((ITableTypeSymbol)((IRecordTypeSymbol)((IReportDataItemSymbol)ctx.Symbol).GetTypeSymbol()).OriginalDefinition).TableType == TableTypeKind.Temporary) return; + private void CheckReportDataItemObjectPermission(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) return; + if (ctx.Symbol.GetBooleanPropertyValue(PropertyKind.UseTemporary) == true) return; + if (((ITableTypeSymbol)((IRecordTypeSymbol)((IReportDataItemSymbol)ctx.Symbol).GetTypeSymbol()).OriginalDefinition).TableType == TableTypeKind.Temporary) return; - IPropertySymbol? objectPermissions = ctx.Symbol.GetContainingApplicationObjectTypeSymbol()?.GetProperty(PropertyKind.Permissions); - ITypeSymbol targetSymbol = ((IReportDataItemSymbol)ctx.Symbol).GetTypeSymbol(); - CheckProcedureInvocation(objectPermissions, targetSymbol, 'r', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); - } + IPropertySymbol? objectPermissions = ctx.Symbol.GetContainingApplicationObjectTypeSymbol()?.GetProperty(PropertyKind.Permissions); + ITypeSymbol targetSymbol = ((IReportDataItemSymbol)ctx.Symbol).GetTypeSymbol(); + CheckProcedureInvocation(objectPermissions, targetSymbol, 'r', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); + } - private void CheckObjectPermission(OperationAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; + private void CheckObjectPermission(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) return; - IInvocationExpression operation = (IInvocationExpression)ctx.Operation; - if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return; + IInvocationExpression operation = (IInvocationExpression)ctx.Operation; + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return; - ITypeSymbol? variableType = operation.Instance?.GetSymbol()?.GetTypeSymbol(); - if (variableType?.GetNavTypeKindSafe() != NavTypeKind.Record) return; + ITypeSymbol? variableType = operation.Instance?.GetSymbol()?.GetTypeSymbol(); + if (variableType?.GetNavTypeKindSafe() != NavTypeKind.Record) return; - ITableTypeSymbol targetTable = (ITableTypeSymbol)((IRecordTypeSymbol)variableType).OriginalDefinition; + ITableTypeSymbol targetTable = (ITableTypeSymbol)((IRecordTypeSymbol)variableType).OriginalDefinition; - if (ctx.ContainingSymbol.GetContainingApplicationObjectTypeSymbol()?.NavTypeKind == NavTypeKind.Page) - { - IPropertySymbol? sourceTableProperty = ctx.ContainingSymbol.GetContainingApplicationObjectTypeSymbol()?.GetProperty(PropertyKind.SourceTable); - if (sourceTableProperty != null) - if (sourceTableProperty.Value == targetTable) - return; - } + if (ctx.ContainingSymbol.GetContainingApplicationObjectTypeSymbol()?.NavTypeKind == NavTypeKind.Page) + { + IPropertySymbol? sourceTableProperty = ctx.ContainingSymbol.GetContainingApplicationObjectTypeSymbol()?.GetProperty(PropertyKind.SourceTable); + if (sourceTableProperty is not null) + if (sourceTableProperty.Value == targetTable) + return; + } - if (variableType.ToString().ToLower().EndsWith("temporary") || (targetTable.TableType == TableTypeKind.Temporary)) return; + if (variableType.ToString().ToLower().EndsWith("temporary") || (targetTable.TableType == TableTypeKind.Temporary)) return; - IEnumerable inherentPermissions = []; + IEnumerable inherentPermissions = []; - if (ctx.ContainingSymbol is IMethodSymbol symbol) - inherentPermissions = symbol.Attributes.Where(attribute => attribute.Name == "InherentPermissions"); + if (ctx.ContainingSymbol is IMethodSymbol symbol) + inherentPermissions = symbol.Attributes.Where(attribute => attribute.Name == "InherentPermissions"); - IPropertySymbol? objectPermissions = ctx.ContainingSymbol.GetContainingApplicationObjectTypeSymbol()?.GetProperty(PropertyKind.Permissions); - //variableType.OriginalDefinition.ContainingNamespace - if (buildInTableDataReadMethodNames.Contains(operation.TargetMethod.Name.ToLowerInvariant())) - { - if (!ProcedureHasInherentPermission(inherentPermissions, variableType, 'r')) - CheckProcedureInvocation(objectPermissions, variableType, 'r', ctx.ReportDiagnostic, ctx.Operation.Syntax.GetLocation(), targetTable); - } - else if (buildInTableDataInsertMethodNames.Contains(operation.TargetMethod.Name.ToLowerInvariant())) - { - if (!ProcedureHasInherentPermission(inherentPermissions, variableType, 'i')) - CheckProcedureInvocation(objectPermissions, variableType, 'i', ctx.ReportDiagnostic, ctx.Operation.Syntax.GetLocation(), targetTable); - } - else if (buildInTableDataModifyMethodNames.Contains(operation.TargetMethod.Name.ToLowerInvariant())) - { - if (!ProcedureHasInherentPermission(inherentPermissions, variableType, 'm')) - CheckProcedureInvocation(objectPermissions, variableType, 'm', ctx.ReportDiagnostic, ctx.Operation.Syntax.GetLocation(), targetTable); - } - else if (buildInTableDataDeleteMethodNames.Contains(operation.TargetMethod.Name.ToLowerInvariant())) - { - if (!ProcedureHasInherentPermission(inherentPermissions, variableType, 'd')) - CheckProcedureInvocation(objectPermissions, variableType, 'd', ctx.ReportDiagnostic, ctx.Operation.Syntax.GetLocation(), targetTable); - } + IPropertySymbol? objectPermissions = ctx.ContainingSymbol.GetContainingApplicationObjectTypeSymbol()?.GetProperty(PropertyKind.Permissions); + //variableType.OriginalDefinition.ContainingNamespace + if (buildInTableDataReadMethodNames.Contains(operation.TargetMethod.Name.ToLowerInvariant())) + { + if (!ProcedureHasInherentPermission(inherentPermissions, variableType, 'r')) + CheckProcedureInvocation(objectPermissions, variableType, 'r', ctx.ReportDiagnostic, ctx.Operation.Syntax.GetLocation(), targetTable); } - - private bool ProcedureHasInherentPermission(IEnumerable inherentPermissions, ITypeSymbol variableType, char requestedPermission) + else if (buildInTableDataInsertMethodNames.Contains(operation.TargetMethod.Name.ToLowerInvariant())) + { + if (!ProcedureHasInherentPermission(inherentPermissions, variableType, 'i')) + CheckProcedureInvocation(objectPermissions, variableType, 'i', ctx.ReportDiagnostic, ctx.Operation.Syntax.GetLocation(), targetTable); + } + else if (buildInTableDataModifyMethodNames.Contains(operation.TargetMethod.Name.ToLowerInvariant())) + { + if (!ProcedureHasInherentPermission(inherentPermissions, variableType, 'm')) + CheckProcedureInvocation(objectPermissions, variableType, 'm', ctx.ReportDiagnostic, ctx.Operation.Syntax.GetLocation(), targetTable); + } + else if (buildInTableDataDeleteMethodNames.Contains(operation.TargetMethod.Name.ToLowerInvariant())) { - //[InherentPermissions(PermissionObjectType::TableData, Database::"SomeTable", 'r'),InherentPermissions(PermissionObjectType::TableData, Database::"SomeOtherTable", 'w')] + if (!ProcedureHasInherentPermission(inherentPermissions, variableType, 'd')) + CheckProcedureInvocation(objectPermissions, variableType, 'd', ctx.ReportDiagnostic, ctx.Operation.Syntax.GetLocation(), targetTable); + } + } - if (inherentPermissions == null || inherentPermissions.Count() == 0) return false; + private bool ProcedureHasInherentPermission(IEnumerable inherentPermissions, ITypeSymbol variableType, char requestedPermission) + { + //[InherentPermissions(PermissionObjectType::TableData, Database::"SomeTable", 'r'),InherentPermissions(PermissionObjectType::TableData, Database::"SomeOtherTable", 'w')] - foreach (var inherentPermission in inherentPermissions) - { - var inherentPermissionAsString = inherentPermission.DeclaringSyntaxReference?.GetSyntax().ToString(); + if (inherentPermissions is null || inherentPermissions.Count() == 0) return false; + + foreach (var inherentPermission in inherentPermissions) + { + var inherentPermissionAsString = inherentPermission.DeclaringSyntaxReference?.GetSyntax().ToString(); - var permissions = inherentPermissionAsString?.Split(new[] { '[', ']', '(', ')', ',' }, StringSplitOptions.RemoveEmptyEntries); - if (permissions?[1].Trim() != "PermissionObjectType::TableData") continue; + var permissions = inherentPermissionAsString?.Split(new[] { '[', ']', '(', ')', ',' }, StringSplitOptions.RemoveEmptyEntries); + if (permissions?[1].Trim() != "PermissionObjectType::TableData") continue; - var typeAndObjectName = permissions[2].Trim(); - var permissionValue = permissions[3].Trim().Trim(new[] { '\'', ' ' }).ToLowerInvariant(); + var typeAndObjectName = permissions[2].Trim(); + var permissionValue = permissions[3].Trim().Trim(new[] { '\'', ' ' }).ToLowerInvariant(); - var typeParts = typeAndObjectName.Split(new[] { "::" }, StringSplitOptions.RemoveEmptyEntries); - if (typeParts.Length < 2) continue; + var typeParts = typeAndObjectName.Split(new[] { "::" }, StringSplitOptions.RemoveEmptyEntries); + if (typeParts.Length < 2) continue; - var objectName = typeParts[1].Trim().Trim('"'); - if (objectName.ToLowerInvariant() != variableType.Name.ToLowerInvariant()) + var objectName = typeParts[1].Trim().Trim('"'); + if (objectName.ToLowerInvariant() != variableType.Name.ToLowerInvariant()) #if !LessThenFall2023RV1 - if (objectName.UnquoteIdentifier().ToLowerInvariant() != (variableType.OriginalDefinition.ContainingNamespace?.QualifiedName.ToLowerInvariant() + "." + variableType.Name.ToLowerInvariant())) + if (objectName.UnquoteIdentifier().ToLowerInvariant() != (variableType.OriginalDefinition.ContainingNamespace?.QualifiedName.ToLowerInvariant() + "." + variableType.Name.ToLowerInvariant())) #endif - continue; + continue; - if (permissionValue.Contains(requestedPermission.ToString().ToLowerInvariant()[0])) - { - return true; - } + if (permissionValue.Contains(requestedPermission.ToString().ToLowerInvariant()[0])) + { + return true; } - return false; } + return false; + } + + private void CheckProcedureInvocation(IPropertySymbol? objectPermissions, ITypeSymbol variableType, char requestedPermission, Action ReportDiagnostic, Microsoft.Dynamics.Nav.CodeAnalysis.Text.Location location, ITableTypeSymbol targetTable) + { + if (targetTable.Id > 2000000000) return; + if (TableHasInherentPermission(targetTable, requestedPermission)) return; - private void CheckProcedureInvocation(IPropertySymbol? objectPermissions, ITypeSymbol variableType, char requestedPermission, Action ReportDiagnostic, Microsoft.Dynamics.Nav.CodeAnalysis.Text.Location location, ITableTypeSymbol targetTable) + if (objectPermissions is null) { - if (targetTable.Id > 2000000000) return; - if (TableHasInherentPermission(targetTable, requestedPermission)) return; + ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0068CheckObjectPermission, location, requestedPermission, variableType.Name)); + return; + } + + bool permissionContainRequestedObject = false; - if (objectPermissions == null) + foreach (var permission in objectPermissions.Value.ToString().ToLowerInvariant().Split(',')) + { + var parts = permission.Trim().Split(new[] { '=' }, 2); + if (parts.Length != 2) { - ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0068CheckObjectPermission, location, requestedPermission, variableType.Name)); - return; + // Handle invalid permission format + continue; } - bool permissionContainRequestedObject = false; + var typeAndObjectName = parts[0].Trim(); + var permissionValue = parts[1].Trim(); - foreach (var permission in objectPermissions.Value.ToString().ToLowerInvariant().Split(',')) + // Extract type and object name, handling quoted object names + var typeEndIndex = typeAndObjectName.IndexOf(' '); + if (typeEndIndex == -1) { - var parts = permission.Trim().Split(new[] { '=' }, 2); - if (parts.Length != 2) - { - // Handle invalid permission format - continue; - } - - var typeAndObjectName = parts[0].Trim(); - var permissionValue = parts[1].Trim(); - - // Extract type and object name, handling quoted object names - var typeEndIndex = typeAndObjectName.IndexOf(' '); - if (typeEndIndex == -1) - { - // Handle invalid type/object name format - continue; - } + // Handle invalid type/object name format + continue; + } - var type = typeAndObjectName[..typeEndIndex].Trim(); - var objectName = typeAndObjectName[typeEndIndex..].Trim().Trim('"'); + var type = typeAndObjectName[..typeEndIndex].Trim(); + var objectName = typeAndObjectName[typeEndIndex..].Trim().Trim('"'); - bool nameSpaceNameMatch = false; + bool nameSpaceNameMatch = false; #if !LessThenFall2023RV1 - nameSpaceNameMatch = objectName.UnquoteIdentifier() == (variableType.OriginalDefinition.ContainingNamespace?.QualifiedName.ToLowerInvariant() + "." + variableType.Name.ToLowerInvariant()); + nameSpaceNameMatch = objectName.UnquoteIdentifier() == (variableType.OriginalDefinition.ContainingNamespace?.QualifiedName.ToLowerInvariant() + "." + variableType.Name.ToLowerInvariant()); #endif - // Match against the parameters of the procedure - if (type == "tabledata" && (objectName == variableType.Name.ToLowerInvariant() || nameSpaceNameMatch)) + // Match against the parameters of the procedure + if (type == "tabledata" && (objectName == variableType.Name.ToLowerInvariant() || nameSpaceNameMatch)) + { + permissionContainRequestedObject = true; + // Handle tabledata permissions + var permissions = permissionValue.ToCharArray(); + if (!permissions.Contains(requestedPermission)) { - permissionContainRequestedObject = true; - // Handle tabledata permissions - var permissions = permissionValue.ToCharArray(); - if (!permissions.Contains(requestedPermission)) - { - ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0068CheckObjectPermission, location, requestedPermission, variableType.Name)); - } + ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0068CheckObjectPermission, location, requestedPermission, variableType.Name)); } } - if (!permissionContainRequestedObject) - { - ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0068CheckObjectPermission, location, requestedPermission, variableType.Name)); - } } - - private bool TableHasInherentPermission(ITableTypeSymbol table, char requestedPermission) + if (!permissionContainRequestedObject) { - IPropertySymbol? permissionProperty = table.GetProperty(PropertyKind.InherentPermissions); - // InherentPermissions = RIMD; - char[]? permissions = permissionProperty?.Value.ToString().ToLowerInvariant().Split(new[] { '=' }, 2)[0].Trim().ToCharArray(); + ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0068CheckObjectPermission, location, requestedPermission, variableType.Name)); + } + } - if (permissions is not null && permissions.Contains(requestedPermission.ToString().ToLowerInvariant()[0])) - return true; + private bool TableHasInherentPermission(ITableTypeSymbol table, char requestedPermission) + { + IPropertySymbol? permissionProperty = table.GetProperty(PropertyKind.InherentPermissions); + // InherentPermissions = RIMD; + char[]? permissions = permissionProperty?.Value.ToString().ToLowerInvariant().Split(new[] { '=' }, 2)[0].Trim().ToCharArray(); - return false; - } + if (permissions is not null && permissions.Contains(requestedPermission.ToString().ToLowerInvariant()[0])) + return true; - public static class DiagnosticDescriptors - { - public static readonly DiagnosticDescriptor Rule0068CheckObjectPermission = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0068", - title: LinterCopAnalyzers.GetLocalizableString("Rule0068CheckObjectPermissionTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0068CheckObjectPermissionFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0068CheckObjectPermissionDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0068"); - } + return false; } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0069EmptyStatements.cs b/BusinessCentral.LinterCop/Design/Rule0069EmptyStatements.cs index 5bad3746..2554ddfd 100644 --- a/BusinessCentral.LinterCop/Design/Rule0069EmptyStatements.cs +++ b/BusinessCentral.LinterCop/Design/Rule0069EmptyStatements.cs @@ -1,31 +1,35 @@ -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0069EmptyStatements : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0069EmptyStatements : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0069EmptyStatements); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0069EmptyStatements); - public override void Initialize(AnalysisContext context) => context.RegisterOperationAction(new Action(this.AnalyzeEmptyStatement), OperationKind.EmptyStatement); + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(new Action(this.AnalyzeEmptyStatement), OperationKind.EmptyStatement); - private void AnalyzeEmptyStatement(OperationAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; + private void AnalyzeEmptyStatement(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) + return; - foreach (SyntaxTrivia trivia in ctx.Operation.Syntax.Parent.GetLeadingTrivia()) - if (trivia.IsKind(SyntaxKind.LineCommentTrivia)) - return; + foreach (SyntaxTrivia trivia in ctx.Operation.Syntax.Parent.GetLeadingTrivia()) + if (trivia.IsKind(SyntaxKind.LineCommentTrivia)) + return; - foreach (SyntaxTrivia trivia in ctx.Operation.Syntax.Parent.GetTrailingTrivia()) - if (trivia.IsKind(SyntaxKind.LineCommentTrivia)) - return; + foreach (SyntaxTrivia trivia in ctx.Operation.Syntax.Parent.GetTrailingTrivia()) + if (trivia.IsKind(SyntaxKind.LineCommentTrivia)) + return; - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0069EmptyStatements, ctx.Operation.Syntax.GetLocation())); - } + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0069EmptyStatements, + ctx.Operation.Syntax.GetLocation())); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0070ListObjectsAreOneBased.cs b/BusinessCentral.LinterCop/Design/Rule0070ListObjectsAreOneBased.cs index 2d4f0c67..fbbc5324 100644 --- a/BusinessCentral.LinterCop/Design/Rule0070ListObjectsAreOneBased.cs +++ b/BusinessCentral.LinterCop/Design/Rule0070ListObjectsAreOneBased.cs @@ -1,4 +1,4 @@ -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; @@ -10,35 +10,35 @@ namespace BusinessCentral.LinterCop.Design; [DiagnosticAnalyzer] public class Rule0070ListObjectsAreOneBased : DiagnosticAnalyzer { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0070ListObjectsAreOneBased); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0070ListObjectsAreOneBased); - public override void Initialize(AnalysisContext context) - => context.RegisterOperationAction(new Action(this.Analyze), OperationKind.InvocationExpression); + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(new Action(this.Analyze), OperationKind.InvocationExpression); - private void Analyze(OperationAnalysisContext context) + private void Analyze(OperationAnalysisContext ctx) { - if (context.IsObsoletePendingOrRemoved()) + if (ctx.Operation is not IInvocationExpression operation) return; - if (IsExitCondition(context)) + + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + operation.TargetMethod.ContainingType?.GetNavTypeKindSafe() != NavTypeKind.List) return; - switch (((IInvocationExpression)context.Operation).TargetMethod.Name) + switch (operation.TargetMethod.Name) { case "Get": - AnalyzeGetOperator(context); + AnalyzeGetOperator(operation, ctx); break; + case "Count": - AnalyzeCountOperator(context); - break; - default: + AnalyzeCountOperator(operation, ctx); break; } } - private static void AnalyzeGetOperator(OperationAnalysisContext context) + private static void AnalyzeGetOperator(IInvocationExpression operation, OperationAnalysisContext ctx) { - if (context.Operation is not IInvocationExpression operation) - return; if (operation.Arguments.Length < 1) return; @@ -47,59 +47,30 @@ private static void AnalyzeGetOperator(OperationAnalysisContext context) case LiteralExpressionSyntax literalExpressionSyntax: if (literalExpressionSyntax.Literal.GetLiteralValue().ToString() == "0") { - context.ReportDiagnostic(Diagnostic.Create( + ctx.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.Rule0070ListObjectsAreOneBased, operation.Syntax.GetLocation())); } break; - - default: - break; } } - private static void AnalyzeCountOperator(OperationAnalysisContext context) + private static void AnalyzeCountOperator(IInvocationExpression operation, OperationAnalysisContext ctx) { - if (context.Operation is not IInvocationExpression operation) - return; if (operation.Syntax.Parent is not ForStatementSyntax statementSyntax) return; + if (statementSyntax.InitialValue is not LiteralExpressionSyntax expressionSyntax) return; + if (expressionSyntax.Literal is not Int32SignedLiteralValueSyntax valueSyntax) return; if (valueSyntax.Number.ValueText == "0") { - context.ReportDiagnostic(Diagnostic.Create( + ctx.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.Rule0070ListObjectsAreOneBased, valueSyntax.GetLocation())); } } - - private static bool IsExitCondition(OperationAnalysisContext ctx) - { - if (ctx.Operation is not IInvocationExpression operation) - return true; - - if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) - return true; - - if (operation.TargetMethod.ContainingType?.GetNavTypeKindSafe() != NavTypeKind.List) - return true; - - return false; - } - - public static class DiagnosticDescriptors - { - public static readonly DiagnosticDescriptor Rule0070ListObjectsAreOneBased = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0070", - title: LinterCopAnalyzers.GetLocalizableString("Rule0070ListObjectsAreOneBasedTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0070ListObjectsAreOneBasedFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0070ListObjectsAreOneBasedDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0070"); - } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0071DoNotSetIsHandledToFalse.cs b/BusinessCentral.LinterCop/Design/Rule0071DoNotSetIsHandledToFalse.cs index 6a88dc49..a7a3c8f6 100644 --- a/BusinessCentral.LinterCop/Design/Rule0071DoNotSetIsHandledToFalse.cs +++ b/BusinessCentral.LinterCop/Design/Rule0071DoNotSetIsHandledToFalse.cs @@ -1,144 +1,141 @@ -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Semantics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0071DoNotSetIsHandledToFalse : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0071DoNotSetIsHandledToFalse : DiagnosticAnalyzer + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0071DoNotSetIsHandledToFalse); + + public override void Initialize(AnalysisContext context) { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0071DoNotSetIsHandledToFalse); + context.RegisterOperationAction(new Action(this.CheckIsHandledAssignments), OperationKind.AssignmentStatement); + context.RegisterOperationAction(new Action(this.CheckIsHandledInvocations), OperationKind.InvocationExpression); + } - public override void Initialize(AnalysisContext context) - { - context.RegisterOperationAction(new Action(this.CheckIsHandledAssignments), OperationKind.AssignmentStatement); - context.RegisterOperationAction(new Action(this.CheckIsHandledInvocations), OperationKind.InvocationExpression); - } + private void CheckIsHandledInvocations(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression invocation) + return; - private void CheckIsHandledInvocations(OperationAnalysisContext ctx) + // Ensure TargetMethod has valid parameters + if (invocation.TargetMethod.Parameters.Length != invocation.Arguments.Length) + return; + + for (int i = 0; i < invocation.Arguments.Length; i++) { - if (ctx.IsObsoletePendingOrRemoved()) + var argument = invocation.Arguments[i]; + var parameter = invocation.TargetMethod.Parameters[i]; + + // Check if argument is a reference to IsHandled passed as a var parameter + if (argument.Value is not IParameterReferenceExpression parameterRef) return; - if (ctx.Operation is not IInvocationExpression invocation) + if (!parameter.IsVar) return; - // Ensure TargetMethod has valid parameters - if (invocation.TargetMethod.Parameters.Length != invocation.Arguments.Length) + if (!IsIsHandledEventSubscriberParameter(parameterRef.Parameter)) return; - for (int i = 0; i < invocation.Arguments.Length; i++) - { - var argument = invocation.Arguments[i]; - var parameter = invocation.TargetMethod.Parameters[i]; + if (HasPrecedingExitStatement(ctx.Operation, parameterRef.Parameter)) + return; - // Check if argument is a reference to IsHandled passed as a var parameter - if (argument.Value is not IParameterReferenceExpression parameterRef) - return; + // Report the diagnostic + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0071DoNotSetIsHandledToFalse, + argument.Value.Syntax.GetLocation())); + } + } - if (!parameter.IsVar) - return; + private void CheckIsHandledAssignments(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) + return; - if (!IsIsHandledEventSubscriberParameter(parameterRef.Parameter)) - return; + if (ctx.Operation is not IAssignmentStatement assignment) + return; - if (HasPrecedingExitStatement(ctx.Operation, parameterRef.Parameter)) - return; + if (assignment.Target.Kind != OperationKind.ParameterReferenceExpression) + return; // check the parameter is assigned a value - // Report the diagnostic - ctx.ReportDiagnostic(Diagnostic.Create( - DiagnosticDescriptors.Rule0071DoNotSetIsHandledToFalse, - argument.Value.Syntax.GetLocation())); - } - } + IParameterSymbol parameter = ((IParameterReferenceExpression)assignment.Target).Parameter; - private void CheckIsHandledAssignments(OperationAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) - return; + if (!IsIsHandledEventSubscriberParameter(parameter)) + return; - if (ctx.Operation is not IAssignmentStatement assignment) - return; + if (assignment.Value.ConstantValue.HasValue && (bool)assignment.Value.ConstantValue.Value) + return; // check for true assignment - if (assignment.Target.Kind != OperationKind.ParameterReferenceExpression) - return; // check the parameter is assigned a value + if (HasPrecedingExitStatement(ctx.Operation, parameter)) + return; - IParameterSymbol parameter = ((IParameterReferenceExpression)assignment.Target).Parameter; + // any other not true assignment should not be done + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0071DoNotSetIsHandledToFalse, + assignment.Syntax.GetLocation())); + } - if (!IsIsHandledEventSubscriberParameter(parameter)) - return; + private bool IsIsHandledEventSubscriberParameter(IParameterSymbol parameter) + { + if (parameter.ContainingSymbol is null) + return false; + // check for event subscriber method + if (parameter.ContainingSymbol.Kind != SymbolKind.Method) + return false; + if (!((IMethodSymbol)parameter.ContainingSymbol).IsEventSubscriber()) + return false; - if (assignment.Value.ConstantValue.HasValue && (bool)assignment.Value.ConstantValue.Value) - return; // check for true assignment + // check for "var IsHandled: Boolean" parameter + if (!CheckIsHandledName(parameter.Name) || (parameter.ParameterType.NavTypeKind != NavTypeKind.Boolean) || !parameter.IsVar) + return false; - if (HasPrecedingExitStatement(ctx.Operation, parameter)) - return; + return true; + } - // any other not true assignment should not be done - ctx.ReportDiagnostic(Diagnostic.Create( - DiagnosticDescriptors.Rule0071DoNotSetIsHandledToFalse, - assignment.Syntax.GetLocation())); - } + private bool CheckIsHandledName(string name) + { + // checks for name(s) used with the "IsHandled Pattern" + // "Handled" is also used in the Base / System App, see: https://github.com/search?q=repo%3AStefanMaron%2FMSDyn365BC.Code.History+%22var+Handled%3A+Boolean%22&type=code + return SemanticFacts.IsSameName(name, "ishandled") || SemanticFacts.IsSameName(name, "handled"); + } - private bool IsIsHandledEventSubscriberParameter(IParameterSymbol parameter) + private static bool HasPrecedingExitStatement(IOperation operation, IParameterSymbol parameter) + { + var parent = operation.Syntax.Parent; + while (parent.Kind != SyntaxKind.Block && parent.Kind != SyntaxKind.None) { - if (parameter.ContainingSymbol is null) - return false; - // check for event subscriber method - if (parameter.ContainingSymbol.Kind != SymbolKind.Method) - return false; - if (!((IMethodSymbol)parameter.ContainingSymbol).IsEventSubscriber()) - return false; - - // check for "var IsHandled: Boolean" parameter - if (!CheckIsHandledName(parameter.Name) || (parameter.ParameterType.NavTypeKind != NavTypeKind.Boolean) || !parameter.IsVar) - return false; - - return true; + parent = parent.Parent; } - private bool CheckIsHandledName(string name) - { - // checks for name(s) used with the "IsHandled Pattern" - // "Handled" is also used in the Base / System App, see: https://github.com/search?q=repo%3AStefanMaron%2FMSDyn365BC.Code.History+%22var+Handled%3A+Boolean%22&type=code - return SemanticFacts.IsSameName(name, "ishandled") || SemanticFacts.IsSameName(name, "handled"); - } + IEnumerable identifiers = parent.DescendantNodes() + .OfType() + .Where(n => n.Identifier.ValueText == parameter.Name && n.SpanStart < operation.Syntax.SpanStart); - private static bool HasPrecedingExitStatement(IOperation operation, IParameterSymbol parameter) + foreach (var identifier in identifiers) { - var parent = operation.Syntax.Parent; - while (parent.Kind != SyntaxKind.Block && parent.Kind != SyntaxKind.None) - { - parent = parent.Parent; - } - - IEnumerable identifiers = parent.DescendantNodes() - .OfType() - .Where(n => n.Identifier.ValueText == parameter.Name && n.SpanStart < operation.Syntax.SpanStart); - - foreach (var identifier in identifiers) + if (identifier.Parent is IfStatementSyntax ifStatement) { - if (identifier.Parent is IfStatementSyntax ifStatement) + // Check if the condition is the identifier "IsHandled" + if (ifStatement.Condition is IdentifierNameSyntax condition && + condition.Identifier.ValueText == parameter.Name) { - // Check if the condition is the identifier "IsHandled" - if (ifStatement.Condition is IdentifierNameSyntax condition && - condition.Identifier.ValueText == parameter.Name) + // Check if the body is a single "exit;" statement + if (ifStatement.Statement is ExitStatementSyntax) { - // Check if the body is a single "exit;" statement - if (ifStatement.Statement is ExitStatementSyntax) - { - // Valid "if IsHandled then exit;" detected - return true; - } + // Valid "if IsHandled then exit;" detected + return true; } } } - - // No valid preceding exit statement was found - return false; } + + // No valid preceding exit statement was found + return false; } } diff --git a/BusinessCentral.LinterCop/Design/Rule0072CheckProcedureDocumentationComment.cs b/BusinessCentral.LinterCop/Design/Rule0072CheckProcedureDocumentationComment.cs index d0fb3d4b..ac4f2638 100644 --- a/BusinessCentral.LinterCop/Design/Rule0072CheckProcedureDocumentationComment.cs +++ b/BusinessCentral.LinterCop/Design/Rule0072CheckProcedureDocumentationComment.cs @@ -1,72 +1,73 @@ -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0072CheckProcedureDocumentationComment : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0072CheckProcedureDocumentationComment : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0072CheckProcedureDocumentationComment); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0072CheckProcedureDocumentationComment); - public override void Initialize(AnalysisContext context) => context.RegisterSyntaxNodeAction(new Action(this.AnalyzeDocumentationComments), SyntaxKind.MethodDeclaration); + public override void Initialize(AnalysisContext context) => + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeDocumentationComments), SyntaxKind.MethodDeclaration); - private void AnalyzeDocumentationComments(SyntaxNodeAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) return; + private void AnalyzeDocumentationComments(SyntaxNodeAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) return; - if (ctx.Node is not MethodDeclarationSyntax methodDeclarationSyntax) - return; - var docCommentTrivia = methodDeclarationSyntax.GetLeadingTrivia().FirstOrDefault(trivia => trivia.Kind == SyntaxKind.SingleLineDocumentationCommentTrivia); - if (docCommentTrivia.IsKind(SyntaxKind.None)) return; // no documentation comment exists + if (ctx.Node is not MethodDeclarationSyntax methodDeclarationSyntax) + return; + var docCommentTrivia = methodDeclarationSyntax.GetLeadingTrivia().FirstOrDefault(trivia => trivia.Kind == SyntaxKind.SingleLineDocumentationCommentTrivia); + if (docCommentTrivia.IsKind(SyntaxKind.None)) return; // no documentation comment exists - Dictionary docCommentParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); - XmlElementSyntax? docCommentReturns = null; + Dictionary docCommentParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + XmlElementSyntax? docCommentReturns = null; - var docCommentStructure = (DocumentationCommentTriviaSyntax)docCommentTrivia.GetStructure(); - var docCommentElements = docCommentStructure.Content.Where(xmlNode => xmlNode.Kind == SyntaxKind.XmlElement); + var docCommentStructure = (DocumentationCommentTriviaSyntax)docCommentTrivia.GetStructure(); + var docCommentElements = docCommentStructure.Content.Where(xmlNode => xmlNode.Kind == SyntaxKind.XmlElement); - // evaluate documentation comment syntax - foreach (XmlElementSyntax element in docCommentElements.Cast()) + // evaluate documentation comment syntax + foreach (XmlElementSyntax element in docCommentElements.Cast()) + { + switch (element.StartTag.Name.LocalName.Text.ToLowerInvariant()) { - switch (element.StartTag.Name.LocalName.Text.ToLowerInvariant()) - { - case "param": - var nameAttribute = (XmlNameAttributeSyntax)element.StartTag.Attributes.First(att => att.IsKind(SyntaxKind.XmlNameAttribute)); - var parameterName = nameAttribute.Identifier.Identifier.ValueText; - if (!docCommentParameters.ContainsKey(parameterName)) - docCommentParameters.Add(parameterName, element); - break; - case "returns": - docCommentReturns = element; - break; - } + case "param": + var nameAttribute = (XmlNameAttributeSyntax)element.StartTag.Attributes.First(att => att.IsKind(SyntaxKind.XmlNameAttribute)); + var parameterName = nameAttribute.Identifier.Identifier.ValueText; + if (!docCommentParameters.ContainsKey(parameterName)) + docCommentParameters.Add(parameterName, element); + break; + case "returns": + docCommentReturns = element; + break; } + } - // excess documentation comment return value - if (docCommentReturns is not null && methodDeclarationSyntax.ReturnValue is null) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0072CheckProcedureDocumentationComment, docCommentReturns.GetLocation())); + // excess documentation comment return value + if (docCommentReturns is not null && methodDeclarationSyntax.ReturnValue is null) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0072CheckProcedureDocumentationComment, docCommentReturns.GetLocation())); - // return value without documentation comment - if (docCommentReturns is null && methodDeclarationSyntax.ReturnValue is not null) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0072CheckProcedureDocumentationComment, methodDeclarationSyntax.ReturnValue.GetLocation())); + // return value without documentation comment + if (docCommentReturns is null && methodDeclarationSyntax.ReturnValue is not null) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0072CheckProcedureDocumentationComment, methodDeclarationSyntax.ReturnValue.GetLocation())); - // check documentation comment parameters against method syntax - foreach (var docCommentParameter in docCommentParameters) - { - if (!methodDeclarationSyntax.ParameterList.Parameters.Any(param => param.Name.Identifier.ValueText.UnquoteIdentifier().Equals(docCommentParameter.Key, StringComparison.OrdinalIgnoreCase))) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0072CheckProcedureDocumentationComment, docCommentParameter.Value.GetLocation())); - } + // check documentation comment parameters against method syntax + foreach (var docCommentParameter in docCommentParameters) + { + if (!methodDeclarationSyntax.ParameterList.Parameters.Any(param => param.Name.Identifier.ValueText.UnquoteIdentifier().Equals(docCommentParameter.Key, StringComparison.OrdinalIgnoreCase))) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0072CheckProcedureDocumentationComment, docCommentParameter.Value.GetLocation())); + } - // check method parameters against documentation comment syntax - foreach (var methodParameter in methodDeclarationSyntax.ParameterList.Parameters) - { - if (!docCommentParameters.Any(docParam => docParam.Key.Equals(methodParameter.Name.Identifier.ValueText.UnquoteIdentifier(), StringComparison.OrdinalIgnoreCase))) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0072CheckProcedureDocumentationComment, methodParameter.GetLocation())); - } + // check method parameters against documentation comment syntax + foreach (var methodParameter in methodDeclarationSyntax.ParameterList.Parameters) + { + if (!docCommentParameters.Any(docParam => docParam.Key.Equals(methodParameter.Name.Identifier.ValueText.UnquoteIdentifier(), StringComparison.OrdinalIgnoreCase))) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0072CheckProcedureDocumentationComment, methodParameter.GetLocation())); } } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0073EventPublisherIsHandledByVar.cs b/BusinessCentral.LinterCop/Design/Rule0073EventPublisherIsHandledByVar.cs index b98dbc61..9513e5c7 100644 --- a/BusinessCentral.LinterCop/Design/Rule0073EventPublisherIsHandledByVar.cs +++ b/BusinessCentral.LinterCop/Design/Rule0073EventPublisherIsHandledByVar.cs @@ -2,7 +2,7 @@ using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; using Microsoft.Dynamics.Nav.CodeAnalysis.InternalSyntax; -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; namespace BusinessCentral.LinterCop.Design; @@ -10,7 +10,7 @@ namespace BusinessCentral.LinterCop.Design; public class Rule0073EventPublisherIsHandledByVar : DiagnosticAnalyzer { public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(DiagnosticDescriptors.Rule0073EventPublisherIsHandledByVar); + ImmutableArray.Create(DiagnosticDescriptors.Rule0073EventPublisherIsHandledByVar); public override void Initialize(AnalysisContext context) => context.RegisterSymbolAction(AnalyzerEventPublisher, SymbolKind.Method); @@ -48,16 +48,4 @@ private static bool IsInvalidHandledParameter(IParameterSymbol parameter) (SemanticFacts.IsSameName(parameter.Name, "IsHandled") || SemanticFacts.IsSameName(parameter.Name, "Handled")); } - - public static class DiagnosticDescriptors - { - public static readonly DiagnosticDescriptor Rule0073EventPublisherIsHandledByVar = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0073", - title: LinterCopAnalyzers.GetLocalizableString("Rule0073EventPublisherIsHandledByVarTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0073EventPublisherIsHandledByVarFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0073EventPublisherIsHandledByVarDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0073"); - } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0074FlowFilterAssignment.cs b/BusinessCentral.LinterCop/Design/Rule0074FlowFilterAssignment.cs index 62a3b67b..61e24ff7 100644 --- a/BusinessCentral.LinterCop/Design/Rule0074FlowFilterAssignment.cs +++ b/BusinessCentral.LinterCop/Design/Rule0074FlowFilterAssignment.cs @@ -1,4 +1,4 @@ -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; @@ -10,16 +10,18 @@ namespace BusinessCentral.LinterCop.Design; [DiagnosticAnalyzer] public class Rule0074FlowFilterAssignment : DiagnosticAnalyzer { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0074FlowFilterAssignment); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0074FlowFilterAssignment); public override void Initialize(AnalysisContext context) { - context.RegisterSyntaxNodeAction(AnalyzeAssignmentStatement, SyntaxKind.AssignmentStatement, SyntaxKind.CompoundAssignmentStatement); + context.RegisterSyntaxNodeAction(AnalyzeAssignmentStatement, + SyntaxKind.AssignmentStatement, + SyntaxKind.CompoundAssignmentStatement); } private void AnalyzeAssignmentStatement(SyntaxNodeAnalysisContext ctx) { - if (ctx.CancellationToken.IsCancellationRequested || ctx.IsObsoletePendingOrRemoved()) + if (ctx.IsObsoletePendingOrRemoved()) return; var target = ctx.Node switch @@ -39,19 +41,8 @@ private void AnalyzeAssignmentStatement(SyntaxNodeAnalysisContext ctx) { ctx.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.Rule0074FlowFilterAssignment, - target.GetIdentifierNameSyntax().GetLocation(), new object[] { fieldSymbol.Name.QuoteIdentifierIfNeeded() })); + target.GetIdentifierNameSyntax().GetLocation(), + fieldSymbol.Name.QuoteIdentifierIfNeeded())); } } - - public static class DiagnosticDescriptors - { - public static readonly DiagnosticDescriptor Rule0074FlowFilterAssignment = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0074", - title: LinterCopAnalyzers.GetLocalizableString("Rule0074FlowFilterAssignmentTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0074FlowFilterAssignmentFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0074FlowFilterAssignmentDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0074"); - } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs b/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs index dc0b9495..d073ae1c 100644 --- a/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs +++ b/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs @@ -1,6 +1,5 @@ #if !LessThenSpring2024 -using BusinessCentral.LinterCop.AnalysisContextExtension; -using BusinessCentral.LinterCop.ArgumentExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; @@ -40,11 +39,9 @@ public override void Initialize(AnalysisContext context) private void AnalyzeAssignmentStatement(OperationAnalysisContext ctx) { - if (ctx.IsObsoletePendingOrRemoved()) + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) return; - if (ctx.Operation is not IInvocationExpression operation) - return; if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || !SemanticFacts.IsSameName(operation.TargetMethod.Name, "Get")) @@ -82,7 +79,10 @@ private void AnalyzeAssignmentStatement(OperationAnalysisContext ctx) ctx.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.Rule0075RecordGetProcedureArguments, - ctx.Operation.Syntax.GetLocation(), new object[] { table.Name.QuoteIdentifierIfNeeded(), expectedArgs })); + ctx.Operation.Syntax.GetLocation(), + table.Name.QuoteIdentifierIfNeeded(), + expectedArgs)); + return; } @@ -97,7 +97,9 @@ private void AnalyzeAssignmentStatement(OperationAnalysisContext ctx) ctx.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.Rule0075RecordGetProcedureArguments, - ctx.Operation.Syntax.GetLocation(), new object[] { table.Name.QuoteIdentifierIfNeeded(), expectedArgs })); + ctx.Operation.Syntax.GetLocation(), + table.Name.QuoteIdentifierIfNeeded(), + expectedArgs)); return; } } @@ -108,7 +110,7 @@ private bool AreFieldCompatible(IArgument argument, IFieldSymbol field) var argumentType = argument.GetTypeSymbol(); var fieldType = field.Type; - if (argumentType == null || fieldType is null) + if (argumentType is null || fieldType is null) return true; var argumentNavType = argumentType.GetNavTypeKindSafe(); @@ -138,17 +140,5 @@ private static bool IsSingletonTable(ITableTypeSymbol table) table.PrimaryKey.Fields[0].OriginalDefinition.GetTypeSymbol() is { } typeSymbol && typeSymbol.GetNavTypeKindSafe() == NavTypeKind.Code; } - - public static class DiagnosticDescriptors - { - public static readonly DiagnosticDescriptor Rule0075RecordGetProcedureArguments = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0075", - title: LinterCopAnalyzers.GetLocalizableString("Rule0075RecordGetProcedureArgumentsTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0075RecordGetProcedureArgumentsFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0075RecordGetProcedureArgumentsDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0075"); - } } #endif \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0076TableRelationTooLong.cs b/BusinessCentral.LinterCop/Design/Rule0076TableRelationTooLong.cs index bcc70611..4fb0401b 100644 --- a/BusinessCentral.LinterCop/Design/Rule0076TableRelationTooLong.cs +++ b/BusinessCentral.LinterCop/Design/Rule0076TableRelationTooLong.cs @@ -1,4 +1,4 @@ -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; @@ -24,7 +24,7 @@ private void AnalyzeSymbol(SymbolAnalysisContext context) var tableRelation = currentField .GetProperty(PropertyKind.TableRelation) ?.GetPropertyValueSyntax(); - + if (tableRelation is null) return; @@ -95,17 +95,4 @@ private static void ReportLengthMismatch(SymbolAnalysisContext context, IFieldSy .SelectMany(ext => ext.AddedFields) .FirstOrDefault(field => field.Name == fieldName); } - - public static class DiagnosticDescriptors - { - public static readonly DiagnosticDescriptor Rule0076TableRelationTooLong = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0076", - title: LinterCopAnalyzers.GetLocalizableString("Rule0076TableRelationTooLongTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0076TableRelationTooLongFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0076TableRelationTooLongDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0076"); - } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0077MissingParenthesis.cs b/BusinessCentral.LinterCop/Design/Rule0077MissingParenthesis.cs index 53a7510e..33bf77d2 100644 --- a/BusinessCentral.LinterCop/Design/Rule0077MissingParenthesis.cs +++ b/BusinessCentral.LinterCop/Design/Rule0077MissingParenthesis.cs @@ -2,22 +2,29 @@ using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; namespace BusinessCentral.LinterCop.Design; [DiagnosticAnalyzer] public class Rule0077MissingParenthesis : DiagnosticAnalyzer { - private static readonly ImmutableHashSet MethodsRequiringParenthesis = ImmutableHashSet.Create( + private static readonly HashSet MethodsRequiringParenthesis = [ + "CurrentDateTime", + "CompanyName", "Count", + "GetLastErrorCallStack", + "GetLastErrorCode", + "GuiAllowed", + "HasCollectedErrors", "IsEmpty", "Today", - "WorkDate", - "GuiAllowed" - ); + "UserId", + "WorkDate" + ]; - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0077MissingParenthesis); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0077MissingParenthesis); public override void Initialize(AnalysisContext context) => context.RegisterOperationAction(AnalyzeParenthesis, OperationKind.InvocationExpression); @@ -41,16 +48,4 @@ private void AnalyzeParenthesis(OperationAnalysisContext ctx) method.Name)); } } - - public static class DiagnosticDescriptors - { - public static readonly DiagnosticDescriptor Rule0077MissingParenthesis = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0077", - title: LinterCopAnalyzers.GetLocalizableString("Rule0077MissingParenthesisTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0077MissingParenthesisFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0077MissingParenthesisDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0077"); - } -} +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0078TempRecRunTrigger.cs b/BusinessCentral.LinterCop/Design/Rule0078TempRecRunTrigger.cs index 7ec2e362..8abf48ec 100644 --- a/BusinessCentral.LinterCop/Design/Rule0078TempRecRunTrigger.cs +++ b/BusinessCentral.LinterCop/Design/Rule0078TempRecRunTrigger.cs @@ -1,4 +1,4 @@ -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; @@ -8,58 +8,43 @@ namespace BusinessCentral.LinterCop.Design; [DiagnosticAnalyzer] public class Rule0078TemporaryRecordsShouldNotTriggerTableTriggers : DiagnosticAnalyzer { - private static readonly HashSet methodsToCheck = new() { "Insert", "Modify", "Delete", "DeleteAll", "Validate", "ModifyAll" }; + private static readonly HashSet methodsToCheck = ["Insert", "Modify", "Delete", "DeleteAll", "Validate", "ModifyAll"]; - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0078TemporaryRecordsShouldNotTriggerTableTriggers); + public override ImmutableArray SupportedDiagnostics { get; } + = ImmutableArray.Create(DiagnosticDescriptors.Rule0078TemporaryRecordsShouldNotTriggerTableTriggers); public override void Initialize(AnalysisContext context) => context.RegisterOperationAction(AnalyzeTemporaryRecords, OperationKind.InvocationExpression); private void AnalyzeTemporaryRecords(OperationAnalysisContext ctx) { - if (ctx.IsObsoletePendingOrRemoved()) + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) return; - if (ctx.Operation is not IInvocationExpression invocationExpression) - return; - if (!methodsToCheck.Contains(invocationExpression.TargetMethod.Name)) + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + !methodsToCheck.Contains(operation.TargetMethod.Name)) return; - if (invocationExpression.Instance?.Type is not IRecordTypeSymbol record || + if (operation.Instance?.Type is not IRecordTypeSymbol record || !record.Temporary || record.BaseTable.TableType == TableTypeKind.Temporary) return; - bool isExecutingTriggersOrValidation = invocationExpression.TargetMethod.Name switch + bool isExecutingTriggersOrValidation = operation.TargetMethod.Name switch { "Validate" => true, - "ModifyAll" => invocationExpression.Arguments.Length == 3 && - IsRunTriggerEnabled(invocationExpression.Arguments[2]), - _ => invocationExpression.Arguments.Length == 1 && - IsRunTriggerEnabled(invocationExpression.Arguments[0]) + "ModifyAll" => operation.Arguments.Length == 3 && IsRunTriggerEnabled(operation.Arguments[2]), + _ => operation.Arguments.Length == 1 && IsRunTriggerEnabled(operation.Arguments[0]) }; if (isExecutingTriggersOrValidation) - { - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0078TemporaryRecordsShouldNotTriggerTableTriggers, + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0078TemporaryRecordsShouldNotTriggerTableTriggers, ctx.Operation.Syntax.GetLocation())); - } } private static bool IsRunTriggerEnabled(IArgument argument) => argument.Value.ConstantValue.HasValue && argument.Value.ConstantValue.Value is bool isEnabled && isEnabled; - - public static class DiagnosticDescriptors - { - public static readonly DiagnosticDescriptor Rule0078TemporaryRecordsShouldNotTriggerTableTriggers = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0078", - title: LinterCopAnalyzers.GetLocalizableString("Rule0078TemporaryRecordsTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0078TemporaryRecordsFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0078TemporaryRecordsDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0078"); - } -} +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0079NonPublicEventPublisher.cs b/BusinessCentral.LinterCop/Design/Rule0079NonPublicEventPublisher.cs index d7e0316c..bc48c5b3 100644 --- a/BusinessCentral.LinterCop/Design/Rule0079NonPublicEventPublisher.cs +++ b/BusinessCentral.LinterCop/Design/Rule0079NonPublicEventPublisher.cs @@ -1,4 +1,4 @@ -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using System.Collections.Immutable; @@ -8,32 +8,18 @@ namespace BusinessCentral.LinterCop.Design; [DiagnosticAnalyzer] public class Rule0079NonPublicEventPublisher : DiagnosticAnalyzer { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0079NonPublicEventPublisher); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0079NonPublicEventPublisher); public override void Initialize(AnalysisContext context) => context.RegisterSymbolAction(AnalyzeEventPublisher, SymbolKind.Method); private void AnalyzeEventPublisher(SymbolAnalysisContext ctx) { - if (ctx.IsObsoletePendingOrRemoved()) + if (ctx.IsObsoletePendingOrRemoved() || ctx.Symbol is not IMethodSymbol symbol) return; - if (ctx.Symbol is not IMethodSymbol symbol || !symbol.IsEvent) - return; - - if (!symbol.IsLocal && !symbol.IsInternal) + if (symbol.IsEvent && !symbol.IsLocal && !symbol.IsInternal) ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0079NonPublicEventPublisher, symbol.GetLocation())); } - - public static class DiagnosticDescriptors - { - public static readonly DiagnosticDescriptor Rule0079NonPublicEventPublisher = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0079", - title: LinterCopAnalyzers.GetLocalizableString("Rule0079NonPublicEventPublisherTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0079NonPublicEventPublisherFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0079NonPublicEventPublisherDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0079"); - } -} +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0080AnalyzeJsonTokenJPath.cs b/BusinessCentral.LinterCop/Design/Rule0080AnalyzeJsonTokenJPath.cs index b55e753b..1585f865 100644 --- a/BusinessCentral.LinterCop/Design/Rule0080AnalyzeJsonTokenJPath.cs +++ b/BusinessCentral.LinterCop/Design/Rule0080AnalyzeJsonTokenJPath.cs @@ -1,4 +1,3 @@ -using BusinessCentral.LinterCop.AnalysisContextExtension; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; @@ -16,9 +15,6 @@ public override void Initialize(AnalysisContext context) => private void AnalyzeSelectToken(OperationAnalysisContext ctx) { - if (ctx.IsObsoletePendingOrRemoved()) - return; - if (ctx.Operation is not IInvocationExpression operation) return; @@ -42,17 +38,5 @@ private void AnalyzeSelectToken(OperationAnalysisContext ctx) stringLiteral.GetLocation())); } } - - public static class DiagnosticDescriptors - { - public static readonly DiagnosticDescriptor Rule0080AnalyzeJsonTokenJPath = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0080", - title: LinterCopAnalyzers.GetLocalizableString("Rule0080AnalyzeJsonTokenJPathTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0080AnalyzeJsonTokenJPathFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0080AnalyzeJsonTokenJPathDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0080"); - } } } diff --git a/BusinessCentral.LinterCop/Design/Rule0081AnalyzeCountMethod.cs b/BusinessCentral.LinterCop/Design/Rule0081AnalyzeCountMethod.cs index 2963501b..2ac04165 100644 --- a/BusinessCentral.LinterCop/Design/Rule0081AnalyzeCountMethod.cs +++ b/BusinessCentral.LinterCop/Design/Rule0081AnalyzeCountMethod.cs @@ -1,146 +1,121 @@ -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design -{ - [DiagnosticAnalyzer] - public class Rule0081AnalyzeCountMethod : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(DiagnosticDescriptors.Rule0081UseIsEmptyMethod, DiagnosticDescriptors.Rule0082UseFindWithNext); - - public override void Initialize(AnalysisContext context) => - context.RegisterOperationAction(new Action(this.AnalyzeCountMethod), OperationKind.InvocationExpression); - - private void AnalyzeCountMethod(OperationAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) - return; - - if (ctx.Operation is not IInvocationExpression operation) - return; - - if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || - operation.TargetMethod.Name != "Count" || - operation.TargetMethod.ContainingSymbol?.Name != "Table") - return; - - if (operation.Instance?.GetSymbol() is not IVariableSymbol { Type: IRecordTypeSymbol recordTypeSymbol } || recordTypeSymbol.Temporary) - return; - - if (operation.Syntax.Parent is not BinaryExpressionSyntax binaryExpression) - return; - - int rightValue = GetLiteralExpressionValue(binaryExpression.Right); - if (rightValue > Literals.MaxRelevantValue) - return; - - int leftValue = GetLiteralExpressionValue(binaryExpression.Left); - if (leftValue > Literals.MaxRelevantValue) - return; - - if (IsZeroComparison(leftValue, rightValue)) - { - ReportUseIsEmptyDiagnostic(ctx, operation); - return; - } - - if (IsLessThanOneComparison(binaryExpression, rightValue) || IsGreaterThanOneComparison(binaryExpression, leftValue)) - { - ReportUseIsEmptyDiagnostic(ctx, operation); - return; - } - - if (IsOneComparison(leftValue, rightValue)) - { - ReportUseFindWithNextDiagnostic(ctx, operation, GetOperatorKind(binaryExpression.OperatorToken.Kind)); - return; - } - - if (IsLessThanTwoComparison(binaryExpression, rightValue) || IsGreaterThanTwoComparison(binaryExpression, leftValue)) - { - ReportUseFindWithNextDiagnostic(ctx, operation, SyntaxKind.EqualsToken); - return; - } - } +namespace BusinessCentral.LinterCop.Design; - private static int GetLiteralExpressionValue(CodeExpressionSyntax codeExpression) => - codeExpression is LiteralExpressionSyntax { Literal.Kind: SyntaxKind.Int32SignedLiteralValue } literalExpression && - literalExpression.Literal.GetLiteralValue() is int value ? value : -1; +[DiagnosticAnalyzer] +public class Rule0081AnalyzeCountMethod : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0081UseIsEmptyMethod, DiagnosticDescriptors.Rule0082UseFindWithNext); - private static SyntaxKind GetOperatorKind(SyntaxKind tokenKind) => - tokenKind == SyntaxKind.EqualsToken ? SyntaxKind.EqualsToken : SyntaxKind.NotEqualsToken; + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(new Action(this.AnalyzeCountMethod), OperationKind.InvocationExpression); - private static bool IsZeroComparison(int left, int right) - => left == Literals.Zero || right == Literals.Zero; + private void AnalyzeCountMethod(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) + return; - private static bool IsLessThanOneComparison(BinaryExpressionSyntax expr, int right) => - expr.OperatorToken.Kind == SyntaxKind.LessThanToken && right == Literals.One; + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + operation.TargetMethod.Name != "Count" || + operation.TargetMethod.ContainingSymbol?.Name != "Table") + return; - private static bool IsGreaterThanOneComparison(BinaryExpressionSyntax expr, int left) => - expr.OperatorToken.Kind == SyntaxKind.GreaterThanToken && left == Literals.One; + if (operation.Instance?.GetSymbol() is not IVariableSymbol { Type: IRecordTypeSymbol recordTypeSymbol } || recordTypeSymbol.Temporary) + return; - private static bool IsOneComparison(int left, int right) => - left == Literals.One || right == Literals.One; + if (operation.Syntax.Parent is not BinaryExpressionSyntax binaryExpression) + return; - private static bool IsLessThanTwoComparison(BinaryExpressionSyntax expr, int right) => - expr.OperatorToken.Kind == SyntaxKind.LessThanToken && right == Literals.Two; + int rightValue = GetLiteralExpressionValue(binaryExpression.Right); + if (rightValue > Literals.MaxRelevantValue) + return; - private static bool IsGreaterThanTwoComparison(BinaryExpressionSyntax expr, int left) => - expr.OperatorToken.Kind == SyntaxKind.GreaterThanToken && left == Literals.Two; + int leftValue = GetLiteralExpressionValue(binaryExpression.Left); + if (leftValue > Literals.MaxRelevantValue) + return; - private static class Literals + if (IsZeroComparison(leftValue, rightValue)) { - public const int Zero = 0; - public const int One = 1; - public const int Two = 2; - public const int MaxRelevantValue = 2; + ReportUseIsEmptyDiagnostic(ctx, operation); + return; } - private static void ReportUseIsEmptyDiagnostic(OperationAnalysisContext ctx, IInvocationExpression operation) + if (IsLessThanOneComparison(binaryExpression, rightValue) || IsGreaterThanOneComparison(binaryExpression, leftValue)) { - ctx.ReportDiagnostic(Diagnostic.Create( - DiagnosticDescriptors.Rule0081UseIsEmptyMethod, - operation.Syntax.Parent.GetLocation(), - new object[] { GetSymbolName(operation) })); + ReportUseIsEmptyDiagnostic(ctx, operation); + return; } - private static void ReportUseFindWithNextDiagnostic(OperationAnalysisContext ctx, IInvocationExpression operation, SyntaxKind operatorToken) + if (IsOneComparison(leftValue, rightValue)) { - string operatorSign = operatorToken == SyntaxKind.EqualsToken ? "=" : "<>"; - - ctx.ReportDiagnostic(Diagnostic.Create( - DiagnosticDescriptors.Rule0082UseFindWithNext, - operation.Syntax.Parent.GetLocation(), - new object[] { GetSymbolName(operation), operatorSign })); + ReportUseFindWithNextDiagnostic(ctx, operation, GetOperatorKind(binaryExpression.OperatorToken.Kind)); + return; } - private static string GetSymbolName(IInvocationExpression operation) => - operation.Instance?.GetSymbol()?.Name.QuoteIdentifierIfNeeded() ?? string.Empty; - - public static class DiagnosticDescriptors + if (IsLessThanTwoComparison(binaryExpression, rightValue) || IsGreaterThanTwoComparison(binaryExpression, leftValue)) { - public static readonly DiagnosticDescriptor Rule0081UseIsEmptyMethod = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0081", - title: LinterCopAnalyzers.GetLocalizableString("Rule0081UseIsEmptyMethodTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0081UseIsEmptyMethodFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0081UseIsEmptyMethodDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0081"); - - public static readonly DiagnosticDescriptor Rule0082UseFindWithNext = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0082", - title: LinterCopAnalyzers.GetLocalizableString("Rule0082UseFindWithNextTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0082UseFindWithNextFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0082UseFindWithNextDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0082"); + ReportUseFindWithNextDiagnostic(ctx, operation, SyntaxKind.EqualsToken); + return; } } + + private static int GetLiteralExpressionValue(CodeExpressionSyntax codeExpression) => + codeExpression is LiteralExpressionSyntax { Literal.Kind: SyntaxKind.Int32SignedLiteralValue } literalExpression && + literalExpression.Literal.GetLiteralValue() is int value ? value : -1; + + private static SyntaxKind GetOperatorKind(SyntaxKind tokenKind) => + tokenKind == SyntaxKind.EqualsToken ? SyntaxKind.EqualsToken : SyntaxKind.NotEqualsToken; + + private static bool IsZeroComparison(int left, int right) + => left == Literals.Zero || right == Literals.Zero; + + private static bool IsLessThanOneComparison(BinaryExpressionSyntax expr, int right) => + expr.OperatorToken.Kind == SyntaxKind.LessThanToken && right == Literals.One; + + private static bool IsGreaterThanOneComparison(BinaryExpressionSyntax expr, int left) => + expr.OperatorToken.Kind == SyntaxKind.GreaterThanToken && left == Literals.One; + + private static bool IsOneComparison(int left, int right) => + left == Literals.One || right == Literals.One; + + private static bool IsLessThanTwoComparison(BinaryExpressionSyntax expr, int right) => + expr.OperatorToken.Kind == SyntaxKind.LessThanToken && right == Literals.Two; + + private static bool IsGreaterThanTwoComparison(BinaryExpressionSyntax expr, int left) => + expr.OperatorToken.Kind == SyntaxKind.GreaterThanToken && left == Literals.Two; + + private static class Literals + { + public const int Zero = 0; + public const int One = 1; + public const int Two = 2; + public const int MaxRelevantValue = 2; + } + + private static void ReportUseIsEmptyDiagnostic(OperationAnalysisContext ctx, IInvocationExpression operation) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0081UseIsEmptyMethod, + operation.Syntax.Parent.GetLocation(), + GetSymbolName(operation))); + } + + private static void ReportUseFindWithNextDiagnostic(OperationAnalysisContext ctx, IInvocationExpression operation, SyntaxKind operatorToken) + { + string operatorSign = operatorToken == SyntaxKind.EqualsToken ? "=" : "<>"; + + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0082UseFindWithNext, + operation.Syntax.Parent.GetLocation(), + GetSymbolName(operation), operatorSign)); + } + + private static string GetSymbolName(IInvocationExpression operation) => + operation.Instance?.GetSymbol()?.Name.QuoteIdentifierIfNeeded() ?? string.Empty; } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0083BuiltInDateTimeMethod.cs b/BusinessCentral.LinterCop/Design/Rule0083BuiltInDateTimeMethod.cs index 990ee7ca..a133f581 100644 --- a/BusinessCentral.LinterCop/Design/Rule0083BuiltInDateTimeMethod.cs +++ b/BusinessCentral.LinterCop/Design/Rule0083BuiltInDateTimeMethod.cs @@ -1,112 +1,96 @@ #if !LessThenFall2024 -using BusinessCentral.LinterCop.AnalysisContextExtension; +using BusinessCentral.LinterCop.Helpers; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; using System.Collections.Immutable; -namespace BusinessCentral.LinterCop.Design +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0083BuiltInDateTimeMethod : DiagnosticAnalyzer { - [DiagnosticAnalyzer] - public class Rule0083BuiltInDateTimeMethod : DiagnosticAnalyzer - { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0083BuiltInDateTimeMethod); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0083BuiltInDateTimeMethod); - public override VersionCompatibility SupportedVersions => VersionCompatibility.Fall2024OrGreater; + public override VersionCompatibility SupportedVersions => VersionCompatibility.Fall2024OrGreater; - public override void Initialize(AnalysisContext context) => - context.RegisterOperationAction(new Action(this.AnalyzeInvocation), OperationKind.InvocationExpression); + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(new Action(this.AnalyzeInvocation), OperationKind.InvocationExpression); - private void AnalyzeInvocation(OperationAnalysisContext ctx) - { - if (ctx.IsObsoletePendingOrRemoved()) - return; - - if ((ctx.Operation is not IInvocationExpression operation) || - operation.TargetMethod is null || - operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || - operation.Arguments.Count() < 1) - return; - - string? recommendedMethod = operation.TargetMethod.Name switch - { - "Date2DMY" => GetDate2DMYReplacement(operation), - "Date2DWY" => GetDate2DWYReplacement(operation), - "DT2Date" => "Date", - "DT2Time" => "Time", - "Format" => GetFormatReplacement(operation), - _ => null - }; - - if (string.IsNullOrEmpty(recommendedMethod)) - return; - - ctx.ReportDiagnostic(Diagnostic.Create( - DiagnosticDescriptors.Rule0083BuiltInDateTimeMethod, - ctx.Operation.Syntax.GetLocation(), - new object[] { operation.Arguments[0].Value.Syntax.ToString().QuoteIdentifierIfNeeded(), recommendedMethod })); - } - - private string? GetDate2DMYReplacement(IInvocationExpression operation) + private void AnalyzeInvocation(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) + return; + + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + operation.Arguments.Length < 1) + return; + + string? recommendedMethod = operation.TargetMethod.Name switch { - if (operation.Arguments.Length < 2) - return null; - - return operation.Arguments[1].Value.ConstantValue.Value switch - { - 1 => "Day", - 2 => "Month", - 3 => "Year", - _ => "" - }; - } - - private string? GetDate2DWYReplacement(IInvocationExpression operation) + "Date2DMY" => GetDate2DMYReplacement(operation), + "Date2DWY" => GetDate2DWYReplacement(operation), + "DT2Date" => "Date", + "DT2Time" => "Time", + "Format" => GetFormatReplacement(operation), + _ => null + }; + + if (string.IsNullOrEmpty(recommendedMethod)) + return; + + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0083BuiltInDateTimeMethod, + ctx.Operation.Syntax.GetLocation(), + operation.Arguments[0].Value.Syntax.ToString().QuoteIdentifierIfNeeded(), + recommendedMethod)); + } + + private string? GetDate2DMYReplacement(IInvocationExpression operation) + { + if (operation.Arguments.Length < 2) + return null; + + return operation.Arguments[1].Value.ConstantValue.Value switch { - int formatSpecifier = -1; - - if (operation.Arguments.Length >= 2 && - operation.Arguments[1].Value.ConstantValue.Value is int extractedValue) - { - formatSpecifier = extractedValue; - } - - return formatSpecifier switch - { - 1 => "DayOfWeek", - 2 => "Year", - _ => "" - }; - } - - private string? GetFormatReplacement(IInvocationExpression operation) + 1 => "Day", + 2 => "Month", + 3 => "Year", + _ => "" + }; + } + + private string? GetDate2DWYReplacement(IInvocationExpression operation) + { + int formatSpecifier = -1; + + if (operation.Arguments.Length >= 2 && operation.Arguments[1].Value.ConstantValue.Value is int extractedValue) + formatSpecifier = extractedValue; + + return formatSpecifier switch { - string? formatSpecifier = String.Empty; - - if (operation.Arguments.Length >= 3) - formatSpecifier = operation.Arguments[2].Value.ConstantValue.Value?.ToString(); - - return formatSpecifier switch - { - "" => "Hour", - "" => "Minute", - "" => "Second", - "" => "Millisecond", - _ => "" - }; - } - - public static class DiagnosticDescriptors + 1 => "DayOfWeek", + 2 => "Year", + _ => "" + }; + } + + private string? GetFormatReplacement(IInvocationExpression operation) + { + string? formatSpecifier = string.Empty; + + if (operation.Arguments.Length >= 3) + formatSpecifier = operation.Arguments[2].Value.ConstantValue.Value?.ToString(); + + return formatSpecifier switch { - public static readonly DiagnosticDescriptor Rule0083BuiltInDateTimeMethod = new( - id: LinterCopAnalyzers.AnalyzerPrefix + "0083", - title: LinterCopAnalyzers.GetLocalizableString("Rule0083BuiltInDateTimeMethodTitle"), - messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0083BuiltInDateTimeMethodFormat"), - category: "Design", - defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, - description: LinterCopAnalyzers.GetLocalizableString("Rule0083BuiltInDateTimeMethodDescription"), - helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0082"); - } + "" => "Hour", + "" => "Minute", + "" => "Second", + "" => "Millisecond", + _ => "" + }; } } #endif \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Extensions.cs b/BusinessCentral.LinterCop/Extensions.cs deleted file mode 100644 index 0505ffc7..00000000 --- a/BusinessCentral.LinterCop/Extensions.cs +++ /dev/null @@ -1,72 +0,0 @@ -#nullable enable -using Microsoft.Dynamics.Nav.CodeAnalysis; -using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; -using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; -using System.Text.RegularExpressions; - - -namespace BusinessCentral.LinterCop -{ - internal static class Extensions - { - private static readonly Regex CamelCaseRegex = new Regex("^[a-z][a-zA-Z0-9]*$", RegexOptions.Compiled); - - internal static bool IsTestCodeunit(this IApplicationObjectTypeSymbol symbol) => symbol is ICodeunitTypeSymbol codeunitTypeSymbol && codeunitTypeSymbol.Subtype == CodeunitSubtypeKind.Test; - - internal static bool IsUpgradeCodeunit(this IApplicationObjectTypeSymbol symbol) => symbol is ICodeunitTypeSymbol codeunitTypeSymbol && codeunitTypeSymbol.Subtype == CodeunitSubtypeKind.Upgrade; - - internal static bool IsAllowedLowerPermissionObject(this IApplicationObjectTypeSymbol symbol) => symbol.Kind == SymbolKind.Codeunit && (symbol.Id == 132218 && SemanticFacts.IsSameName(symbol.Name, "Permission Test Catalog") || symbol.Id == 132230 && SemanticFacts.IsSameName(symbol.Name, "Library - E2E Role Permissions")); - - internal static int GetTokenLine(this SyntaxToken token) => token.GetLocation().GetMappedLineSpan().StartLinePosition.Line; - - internal static bool IsValidCamelCase(this string str) => !string.IsNullOrEmpty(str) && Extensions.CamelCaseRegex.IsMatch(str); - - internal static IdentifierNameSyntax? GetIdentifierNameSyntax( - this SyntaxNodeAnalysisContext context) - { - if (context.Node.IsKind(SyntaxKind.IdentifierName)) - return (IdentifierNameSyntax?) context.Node; - return !context.Node.IsKind(SyntaxKind.IdentifierNameOrEmpty) ? (IdentifierNameSyntax?) null : ((IdentifierNameOrEmptySyntax) context.Node).IdentifierName; - } - - internal static bool TryGetSymbolFromIdentifier( - SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, - IdentifierNameSyntax identifierName, - SymbolKind symbolKind, - out ISymbol? symbol) - { - symbol = (ISymbol?) null; - SymbolInfo symbolInfo = syntaxNodeAnalysisContext.SemanticModel.GetSymbolInfo((ExpressionSyntax) identifierName, new CancellationToken()); - ISymbol? symbol1 = symbolInfo.Symbol; - if ((symbol1 != null ? (symbol1.Kind != symbolKind ? 1 : 0) : 1) != 0) - return false; - symbol = symbolInfo.Symbol; - return true; - } - - internal static IMethodSymbol? GetFirstMethod( - this IApplicationObjectTypeSymbol applicationObject, - string memberName, - Compilation compilation) - { - foreach (ISymbol member in applicationObject.GetMembers(memberName)) - { - if (member.Kind == SymbolKind.Method) - return (IMethodSymbol) member; - } - foreach (var extensionsAcrossModule in compilation.GetApplicationObjectExtensionTypeSymbolsAcrossModules(applicationObject)) - { - foreach (var member in extensionsAcrossModule.GetMembers(memberName)) - { - if (member.Kind == SymbolKind.Method) - { - IMethodSymbol firstMethod = (IMethodSymbol) member; - return firstMethod; - } - } - } - return null; - } - - } -} diff --git a/BusinessCentral.LinterCop/Helpers/AnalysisContextHelper.cs b/BusinessCentral.LinterCop/Helpers/AnalysisContextHelper.cs index ee2a9972..fec28e6b 100644 --- a/BusinessCentral.LinterCop/Helpers/AnalysisContextHelper.cs +++ b/BusinessCentral.LinterCop/Helpers/AnalysisContextHelper.cs @@ -1,65 +1,64 @@ using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; -namespace BusinessCentral.LinterCop.AnalysisContextExtension +namespace BusinessCentral.LinterCop.Helpers; + +public static class AnalysisContextExtensions { - public static class AnalysisContextExtensions + public static bool IsObsoletePendingOrRemoved(this SymbolAnalysisContext context) { - public static bool IsObsoletePendingOrRemoved(this SymbolAnalysisContext context) + if (context.Symbol.IsObsoletePendingOrRemoved()) { - if (context.Symbol.IsObsoletePendingOrRemoved()) - { - return true; - } - if (context.Symbol.GetContainingObjectTypeSymbol().IsObsoletePendingOrRemoved()) - { - return true; - } - return false; + return true; } - - public static bool IsObsoletePendingOrRemoved(this OperationAnalysisContext context) + if (context.Symbol.GetContainingObjectTypeSymbol().IsObsoletePendingOrRemoved()) { - if (context.ContainingSymbol.IsObsoletePendingOrRemoved()) - { - return true; - } - if (context.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoletePendingOrRemoved()) - { - return true; - } - return false; + return true; } + return false; + } - public static bool IsObsoletePendingOrRemoved(this SyntaxNodeAnalysisContext context) + public static bool IsObsoletePendingOrRemoved(this OperationAnalysisContext context) + { + if (context.ContainingSymbol.IsObsoletePendingOrRemoved()) { - if (context.ContainingSymbol.IsObsoletePendingOrRemoved()) - { - return true; - } - if (context.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoletePendingOrRemoved()) - { - return true; - } - return false; + return true; } + if (context.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoletePendingOrRemoved()) + { + return true; + } + return false; + } - public static bool IsObsoletePendingOrRemoved(this CodeBlockAnalysisContext context) + public static bool IsObsoletePendingOrRemoved(this SyntaxNodeAnalysisContext context) + { + if (context.ContainingSymbol.IsObsoletePendingOrRemoved()) { - if (context.OwningSymbol.IsObsoletePendingOrRemoved()) - { - return true; - } - if (context.OwningSymbol.GetContainingObjectTypeSymbol().IsObsoletePendingOrRemoved()) - { - return true; - } - return false; + return true; } + if (context.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoletePendingOrRemoved()) + { + return true; + } + return false; + } - public static bool IsObsoletePendingOrRemoved(this ISymbol symbol) + public static bool IsObsoletePendingOrRemoved(this CodeBlockAnalysisContext context) + { + if (context.OwningSymbol.IsObsoletePendingOrRemoved()) + { + return true; + } + if (context.OwningSymbol.GetContainingObjectTypeSymbol().IsObsoletePendingOrRemoved()) { - return symbol.IsObsoletePending || symbol.IsObsoleteRemoved; + return true; } + return false; + } + + public static bool IsObsoletePendingOrRemoved(this ISymbol symbol) + { + return symbol.IsObsoletePending || symbol.IsObsoletePendingMove || symbol.IsObsoleteRemoved || symbol.IsObsoleteMoved; } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Helpers/ArgumentHelper.cs b/BusinessCentral.LinterCop/Helpers/ArgumentHelper.cs index d91fceb7..1e40d226 100644 --- a/BusinessCentral.LinterCop/Helpers/ArgumentHelper.cs +++ b/BusinessCentral.LinterCop/Helpers/ArgumentHelper.cs @@ -1,19 +1,18 @@ using Microsoft.Dynamics.Nav.CodeAnalysis; -namespace BusinessCentral.LinterCop.ArgumentExtension +namespace BusinessCentral.LinterCop.Helpers; + +public static class ArgumentExtensions { - public static class ArgumentExtensions + public static ITypeSymbol? GetTypeSymbol(this IArgument argument) { - public static ITypeSymbol? GetTypeSymbol(this IArgument argument) + switch (argument.Value.Kind) { - switch (argument.Value.Kind) - { - case OperationKind.ConversionExpression: - return ((IConversionExpression)argument.Value).Operand.Type; - case OperationKind.InvocationExpression: - return ((IInvocationExpression)argument.Value).TargetMethod.ReturnValueSymbol.ReturnType; - } - return null; + case OperationKind.ConversionExpression: + return ((IConversionExpression)argument.Value).Operand.Type; + case OperationKind.InvocationExpression: + return ((IInvocationExpression)argument.Value).TargetMethod.ReturnValueSymbol.ReturnType; } + return null; } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Helpers/HelperFunctions.cs b/BusinessCentral.LinterCop/Helpers/HelperFunctions.cs index 7d26196e..7b47fee0 100644 --- a/BusinessCentral.LinterCop/Helpers/HelperFunctions.cs +++ b/BusinessCentral.LinterCop/Helpers/HelperFunctions.cs @@ -1,65 +1,63 @@ #nullable disable // TODO: Enable nullable and review rule using Microsoft.Dynamics.Nav.CodeAnalysis; -namespace BusinessCentral.LinterCop.Helpers +namespace BusinessCentral.LinterCop.Helpers; + +public class HelperFunctions { - public class HelperFunctions + public static bool MethodImplementsInterfaceMethod(IMethodSymbol methodSymbol) + { + return MethodImplementsInterfaceMethod(methodSymbol.GetContainingApplicationObjectTypeSymbol(), methodSymbol); + } + + public static bool MethodImplementsInterfaceMethod(IApplicationObjectTypeSymbol objectSymbol, IMethodSymbol methodSymbol) { - public static bool MethodImplementsInterfaceMethod(IMethodSymbol methodSymbol) + if (objectSymbol is not ICodeunitTypeSymbol codeunitSymbol) { - return MethodImplementsInterfaceMethod(methodSymbol.GetContainingApplicationObjectTypeSymbol(), methodSymbol); + return false; } - public static bool MethodImplementsInterfaceMethod(IApplicationObjectTypeSymbol objectSymbol, IMethodSymbol methodSymbol) + foreach (var implementedInterface in codeunitSymbol.ImplementedInterfaces) { - if (!(objectSymbol is ICodeunitTypeSymbol)) + if (implementedInterface.GetMembers().OfType().Any(interfaceMethodSymbol => MethodImplementsInterfaceMethod(methodSymbol, interfaceMethodSymbol))) { - return false; + return true; } + } - var codeunitSymbol = objectSymbol as ICodeunitTypeSymbol; - foreach (var implementedInterface in codeunitSymbol.ImplementedInterfaces) - { - if (implementedInterface.GetMembers().OfType().Any(interfaceMethodSymbol => MethodImplementsInterfaceMethod(methodSymbol, interfaceMethodSymbol))) - { - return true; - } - } + return false; + } + public static bool MethodImplementsInterfaceMethod(IMethodSymbol methodSymbol, IMethodSymbol interfaceMethodSymbol) + { + if (methodSymbol.Name != interfaceMethodSymbol.Name) + { return false; } - - public static bool MethodImplementsInterfaceMethod(IMethodSymbol methodSymbol, IMethodSymbol interfaceMethodSymbol) + if (methodSymbol.Parameters.Length != interfaceMethodSymbol.Parameters.Length) { - if (methodSymbol.Name != interfaceMethodSymbol.Name) - { - return false; - } - if (methodSymbol.Parameters.Length != interfaceMethodSymbol.Parameters.Length) + return false; + } + var methodReturnValType = methodSymbol.ReturnValueSymbol?.ReturnType.NavTypeKind ?? NavTypeKind.None; + var interfaceMethodReturnValType = interfaceMethodSymbol.ReturnValueSymbol?.ReturnType.NavTypeKind ?? NavTypeKind.None; + if (methodReturnValType != interfaceMethodReturnValType) + { + return false; + } + for (int i = 0; i < methodSymbol.Parameters.Length; i++) + { + var methodParameter = methodSymbol.Parameters[i]; + var interfaceMethodParameter = interfaceMethodSymbol.Parameters[i]; + + if (methodParameter.IsVar != interfaceMethodParameter.IsVar) { return false; } - var methodReturnValType = methodSymbol.ReturnValueSymbol?.ReturnType.NavTypeKind ?? NavTypeKind.None; - var interfaceMethodReturnValType = interfaceMethodSymbol.ReturnValueSymbol?.ReturnType.NavTypeKind ?? NavTypeKind.None; - if (methodReturnValType != interfaceMethodReturnValType) + if (!methodParameter.ParameterType.Equals(interfaceMethodParameter.ParameterType)) { return false; } - for (int i = 0; i < methodSymbol.Parameters.Length; i++) - { - var methodParameter = methodSymbol.Parameters[i]; - var interfaceMethodParameter = interfaceMethodSymbol.Parameters[i]; - - if (methodParameter.IsVar != interfaceMethodParameter.IsVar) - { - return false; - } - if (!methodParameter.ParameterType.Equals(interfaceMethodParameter.ParameterType)) - { - return false; - } - } - return true; } + return true; } } diff --git a/BusinessCentral.LinterCop/Helpers/LinterSettings.cs b/BusinessCentral.LinterCop/Helpers/LinterSettings.cs index 5fde4654..27bb388d 100644 --- a/BusinessCentral.LinterCop/Helpers/LinterSettings.cs +++ b/BusinessCentral.LinterCop/Helpers/LinterSettings.cs @@ -14,7 +14,7 @@ class LinterSettings static public void Create(string WorkingDir) { - if (instance == null || instance.WorkingDir != WorkingDir) + if (instance is null || instance.WorkingDir != WorkingDir) { try { @@ -24,8 +24,8 @@ static public void Create(string WorkingDir) instance = new LinterSettings(); InternalLinterSettings internalInstance = JsonConvert.DeserializeObject(json); - instance.cyclomaticComplexityThreshold = internalInstance.cyclomaticComplexityThreshold ?? internalInstance.cyclomaticComplexetyThreshold ?? instance.cyclomaticComplexityThreshold; - instance.maintainabilityIndexThreshold = internalInstance.maintainabilityIndexThreshold ?? internalInstance.maintainablityIndexThreshold ?? instance.maintainabilityIndexThreshold; + instance.cyclomaticComplexityThreshold = internalInstance.cyclomaticComplexityThreshold ?? instance.cyclomaticComplexityThreshold; + instance.maintainabilityIndexThreshold = internalInstance.maintainabilityIndexThreshold ?? instance.maintainabilityIndexThreshold; instance.enableRule0011ForTableFields = internalInstance.enableRule0011ForTableFields; instance.enableRule0016ForApiObjects = internalInstance.enableRule0016ForApiObjects; instance.WorkingDir = WorkingDir; @@ -37,14 +37,12 @@ static public void Create(string WorkingDir) } } } + internal class InternalLinterSettings { public int? cyclomaticComplexityThreshold; public int? maintainabilityIndexThreshold; - public int? cyclomaticComplexetyThreshold; // Misspelled, deprecated - public int? maintainablityIndexThreshold; // Misspelled, deprecated public bool enableRule0011ForTableFields = false; public bool enableRule0016ForApiObjects = false; - } -} +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Helpers/TypeSymbolHelper.cs b/BusinessCentral.LinterCop/Helpers/TypeSymbolHelper.cs new file mode 100644 index 00000000..4737434a --- /dev/null +++ b/BusinessCentral.LinterCop/Helpers/TypeSymbolHelper.cs @@ -0,0 +1,27 @@ +using Microsoft.Dynamics.Nav.CodeAnalysis; + +namespace BusinessCentral.LinterCop.Helpers; + +public static class TypeSymbolExtensions +{ + internal static IMethodSymbol? GetFirstMethod(this IApplicationObjectTypeSymbol applicationObject, string memberName, Compilation compilation) + { + foreach (ISymbol member in applicationObject.GetMembers(memberName)) + { + if (member.Kind == SymbolKind.Method) + return (IMethodSymbol)member; + } + foreach (var extensionsAcrossModule in compilation.GetApplicationObjectExtensionTypeSymbolsAcrossModules(applicationObject)) + { + foreach (var member in extensionsAcrossModule.GetMembers(memberName)) + { + if (member.Kind == SymbolKind.Method) + { + IMethodSymbol firstMethod = (IMethodSymbol)member; + return firstMethod; + } + } + } + return null; + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.Designer.cs b/BusinessCentral.LinterCop/LinterCopAnalyzers.Designer.cs index 3dca7635..00aff49e 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.Designer.cs +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.Designer.cs @@ -25,7 +25,7 @@ internal static ResourceManager ResourceManager { get { - if (LinterCopAnalyzers.resourceMan == null) + if (LinterCopAnalyzers.resourceMan is null) LinterCopAnalyzers.resourceMan = new ResourceManager("BusinessCentral.LinterCop.LinterCopAnalyzers", typeof(LinterCopAnalyzers).Assembly); return LinterCopAnalyzers.resourceMan; } @@ -40,8 +40,6 @@ internal static CultureInfo Culture internal static string AnalyzerPrefix => LinterCopAnalyzers.ResourceManager.GetString(nameof(AnalyzerPrefix), LinterCopAnalyzers.resourceCulture); - internal static string Fix0021ConfirmImplementConfirmManagementMessage => LinterCopAnalyzers.ResourceManager.GetString("Fix0021ConfirmImplementConfirmManagementMessage", LinterCopAnalyzers.resourceCulture); - internal static LocalizableString GetLocalizableString(string nameOfLocalizableResource) { return new LocalizableResourceString( @@ -51,4 +49,4 @@ internal static LocalizableString GetLocalizableString(string nameOfLocalizableR ); } } -} +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs b/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs index 2dc6935f..e3647cad 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs @@ -1,73 +1,857 @@ using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; -namespace BusinessCentral.LinterCop +namespace BusinessCentral.LinterCop; + +public static class DiagnosticDescriptors { - public static class DiagnosticDescriptors - { - public static readonly DiagnosticDescriptor Rule0000ErrorInRule = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0000", (LocalizableString)new LocalizableResourceString("Rule0000ErrorInRuleTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0000ErrorInRuleFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0000ErrorInRuleDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0000"); - - public static readonly DiagnosticDescriptor Rule0001FlowFieldsShouldNotBeEditable = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0001", (LocalizableString)new LocalizableResourceString("Rule0001FlowFieldsShouldNotBeEditable", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0001FlowFieldsShouldNotBeEditableFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0001FlowFieldsShouldNotBeEditableDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0001"); - public static readonly DiagnosticDescriptor Rule0002CommitMustBeExplainedByComment = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0002", (LocalizableString)new LocalizableResourceString("Rule0002CommitMustBeExplainedByComment", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0002CommitMustBeExplainedByCommentFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0002CommitMustBeExplainedByCommentDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0002"); - public static readonly DiagnosticDescriptor Rule0003DoNotUseObjectIDsInVariablesOrProperties = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0003", (LocalizableString)new LocalizableResourceString("Rule0003DoNotUseObjectIDsInVariablesOrProperties", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0003DoNotUseObjectIDsInVariablesOrPropertiesFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0003DoNotUseObjectIDsInVariablesOrPropertiesDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0003"); - public static readonly DiagnosticDescriptor Rule0005VariableCasingShouldNotDifferFromDeclaration = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0005", (LocalizableString)new LocalizableResourceString("Rule0005VariableCasingShouldNotDifferFromDeclarationTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0005VariableCasingShouldNotDifferFromDeclarationFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0005VariableCasingShouldNotDifferFromDeclarationDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0005"); - public static readonly DiagnosticDescriptor Rule0006FieldNotAutoIncrementInTemporaryTable = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0006", (LocalizableString)new LocalizableResourceString("Rule0006FieldNotAutoIncrementInTemporaryTableTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0006FieldNotAutoIncrementInTemporaryTableFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Error, true, (LocalizableString)new LocalizableResourceString("Rule0006FieldNotAutoIncrementInTemporaryTableDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0006"); - public static readonly DiagnosticDescriptor Rule0007DataPerCompanyShouldAlwaysBeSet = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0007", (LocalizableString)new LocalizableResourceString("Rule0007DataPerCompanyShouldAlwaysBeSetTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0007DataPerCompanyShouldAlwaysBeSetFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Hidden, false, (LocalizableString)new LocalizableResourceString("Rule0007DataPerCompanyShouldAlwaysBeSetDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0007"); - public static readonly DiagnosticDescriptor Rule0008NoFilterOperatorsInSetRange = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0008", (LocalizableString)new LocalizableResourceString("Rule0008NoFilterOperatorsInSetRangeTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0008NoFilterOperatorsInSetRangeFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0008NoFilterOperatorsInSetRangeDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0008"); - public static readonly DiagnosticDescriptor Rule0009CodeMetricsInfo = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0009", (LocalizableString)new LocalizableResourceString("Rule0009CodeMetricsInfoTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0009CodeMetricsInfoFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, false, (LocalizableString)new LocalizableResourceString("Rule0009CodeMetricsInfoDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0009"); - public static readonly DiagnosticDescriptor Rule0010CodeMetricsWarning = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0010", (LocalizableString)new LocalizableResourceString("Rule0009CodeMetricsInfoTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0009CodeMetricsInfoFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0009CodeMetricsInfoDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0010"); - public static readonly DiagnosticDescriptor Rule0011AccessPropertyShouldAlwaysBeSet = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0011", (LocalizableString)new LocalizableResourceString("Rule0011AccessPropertyShouldAlwaysBeSetTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0011AccessPropertyShouldAlwaysBeSetFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Hidden, false, (LocalizableString)new LocalizableResourceString("Rule0011AccessPropertyShouldAlwaysBeSetDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0011"); - public static readonly DiagnosticDescriptor Rule0012DoNotUseObjectIdInSystemFunctions = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0012", (LocalizableString)new LocalizableResourceString("Rule0012DoNotUseObjectIdInSystemFunctionsTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0012DoNotUseObjectIdInSystemFunctionsFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0012DoNotUseObjectIdInSystemFunctionsDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0012"); - public static readonly DiagnosticDescriptor Rule0014PermissionSetCaptionLength = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0014", (LocalizableString)new LocalizableResourceString("Rule0014PermissionSetCaptionLengthTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0014PermissionSetCaptionLengthFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0014PermissionSetCaptionLengthDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0014"); - public static readonly DiagnosticDescriptor Rule0015PermissionSetCoverage = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0015", (LocalizableString)new LocalizableResourceString("Rule0015PermissionSetCoverageTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0015PermissionSetCoverageFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0015PermissionSetCoverageDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0015"); - public static readonly DiagnosticDescriptor Rule0016CheckForMissingCaptions = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0016", (LocalizableString)new LocalizableResourceString("Rule0016CheckForMissingCaptionsTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0016CheckForMissingCaptionsFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0016CheckForMissingCaptionsDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0016"); - public static readonly DiagnosticDescriptor Rule0017WriteToFlowField = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0017", (LocalizableString)new LocalizableResourceString("Rule0017WriteToFlowFieldTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0017WriteToFlowFieldFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0017WriteToFlowFieldDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0017"); - public static readonly DiagnosticDescriptor Rule0018NoEventsInInternalCodeunitsAnalyzerDescriptor = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0018", (LocalizableString)new LocalizableResourceString("Rule0018NoEventsInInternalCodeunitsTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0018NoEventsInInternalCodeunitsFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0018NoEventsInInternalCodeunitsDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0018"); - public static readonly DiagnosticDescriptor Rule0019DataClassificationFieldEqualsTable = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0019", (LocalizableString)new LocalizableResourceString("Rule0019DataClassificationFieldEqualsTableTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0019DataClassificationFieldEqualsTableFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0019DataClassificationFieldEqualsTableDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0019"); - public static readonly DiagnosticDescriptor Rule0020ApplicationAreaEqualsToPage = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0020", (LocalizableString)new LocalizableResourceString("Rule0020ApplicationAreaEqualsToPageTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0020ApplicationAreaEqualsToPageFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0020ApplicationAreaEqualsToPageDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0020"); - public static readonly DiagnosticDescriptor Rule0021ConfirmImplementConfirmManagement = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0021", (LocalizableString)new LocalizableResourceString("Rule0021ConfirmImplementConfirmManagement", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0021ConfirmImplementConfirmManagement", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0021ConfirmImplementConfirmManagement", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0021"); - public static readonly DiagnosticDescriptor Rule0022GlobalLanguageImplementTranslationHelper = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0022", (LocalizableString)new LocalizableResourceString("Rule0022GlobalLanguageImplementTranslationHelperTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0022GlobalLanguageImplementTranslationHelperFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0022GlobalLanguageImplementTranslationHelperDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0022"); - public static readonly DiagnosticDescriptor Rule0023AlwaysSpecifyFieldgroups = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0023", (LocalizableString)new LocalizableResourceString("Rule0023AlwaysSpecifyFieldgroups", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0023AlwaysSpecifyFieldgroups", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0023AlwaysSpecifyFieldgroups", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0023"); - public static readonly DiagnosticDescriptor Rule0024SemicolonAfterMethodOrTriggerDeclaration = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0024", (LocalizableString)new LocalizableResourceString("Rule0024SemicolonAfterMethodOrTriggerDeclarationTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0024SemicolonAfterMethodOrTriggerDeclarationFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0024SemicolonAfterMethodOrTriggerDeclarationDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0024"); - public static readonly DiagnosticDescriptor Rule0025InternalProcedureModifier = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0025", (LocalizableString)new LocalizableResourceString("Rule0025InternalProcedureModifierTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0025InternalProcedureModifierFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Hidden, true, (LocalizableString)new LocalizableResourceString("Rule0025InternalProcedureModifierDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0025"); - public static readonly DiagnosticDescriptor Rule0026ToolTipMustEndWithDot = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0026", (LocalizableString)new LocalizableResourceString("Rule0026ToolTipMustEndWithDotTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0026ToolTipMustEndWithDotFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0026ToolTipMustEndWithDotDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0026"); - public static readonly DiagnosticDescriptor Rule0028IdentifiersInEventSubscribers = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0028", (LocalizableString)new LocalizableResourceString("Rule0028IdentifiersInEventSubscribersTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0028IdentifiersInEventSubscribersFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0028IdentifiersInEventSubscribersDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0028"); - public static readonly DiagnosticDescriptor Rule0029CompareDateTimeThroughCodeunit = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0029", (LocalizableString)new LocalizableResourceString("Rule0029CompareDateTimeThroughCodeunitTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0029CompareDateTimeThroughCodeunitFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0029CompareDateTimeThroughCodeunitDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0029"); - public static readonly DiagnosticDescriptor Rule0030AccessInternalForInstallAndUpgradeCodeunits = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0030", (LocalizableString)new LocalizableResourceString("Rule0030AccessInternalForInstallAndUpgradeCodeunitsTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0030AccessInternalForInstallAndUpgradeCodeunitsFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0030AccessInternalForInstallAndUpgradeCodeunitsDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0030"); - public static readonly DiagnosticDescriptor Rule0031RecordInstanceIsolationLevel = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0031", (LocalizableString)new LocalizableResourceString("Rule0031RecordInstanceIsolationLevelTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0031RecordInstanceIsolationLevelFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0031RecordInstanceIsolationLevelDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0031"); - public static readonly DiagnosticDescriptor Rule0032ClearCodeunitSingleInstance = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0032", (LocalizableString)new LocalizableResourceString("Rule0032ClearCodeunitSingleInstanceTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0032ClearCodeunitSingleInstanceFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0032ClearCodeunitSingleInstanceDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0032"); - public static readonly DiagnosticDescriptor Rule0033AppManifestRuntimeBehind = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0033", (LocalizableString)new LocalizableResourceString("Rule0033AppManifestRuntimeBehindTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0033AppManifestRuntimeBehindFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0033AppManifestRuntimeBehindTitleDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0033"); - public static readonly DiagnosticDescriptor Rule0034ExtensiblePropertyShouldAlwaysBeSet = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0034", (LocalizableString)new LocalizableResourceString("Rule0034ExtensiblePropertyShouldAlwaysBeSetTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0034ExtensiblePropertyShouldAlwaysBeSetFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Hidden, true, (LocalizableString)new LocalizableResourceString("Rule0034ExtensiblePropertyShouldAlwaysBeSetDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0034"); - public static readonly DiagnosticDescriptor Rule0035ExplicitSetAllowInCustomizations = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0035", (LocalizableString)new LocalizableResourceString("Rule0035ExplicitSetAllowInCustomizationsTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0035ExplicitSetAllowInCustomizationsFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0035ExplicitSetAllowInCustomizationsDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0035"); - public static readonly DiagnosticDescriptor Rule0036ToolTipShouldStartWithSpecifies = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0036", (LocalizableString)new LocalizableResourceString("Rule0036ToolTipShouldStartWithSpecifiesTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0036ToolTipShouldStartWithSpecifiesFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0036ToolTipShouldStartWithSpecifiesDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0036"); - public static readonly DiagnosticDescriptor Rule0037ToolTipDoNotUseLineBreaks = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0037", (LocalizableString)new LocalizableResourceString("Rule0037ToolTipDoNotUseLineBreaksTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0037ToolTipDoNotUseLineBreaksFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0037ToolTipDoNotUseLineBreaksDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0037"); - public static readonly DiagnosticDescriptor Rule0038ToolTipMaximumLength = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0038", (LocalizableString)new LocalizableResourceString("Rule0038ToolTipMaximumLengthTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0038ToolTipMaximumLengthFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0038ToolTipMaximumLengthDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0038"); - public static readonly DiagnosticDescriptor Rule0039ArgumentDifferentTypeThenExpected = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0039", (LocalizableString)new LocalizableResourceString("Rule0039ArgumentDifferentTypeThenExpectedTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0039ArgumentDifferentTypeThenExpectedFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0039ArgumentDifferentTypeThenExpectedDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0039"); - public static readonly DiagnosticDescriptor Rule0040ExplicitlySetRunTrigger = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0040", (LocalizableString)new LocalizableResourceString("Rule0040ExplicitlySetRunTriggerTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0040ExplicitlySetRunTriggerFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0040ExplicitlySetRunTriggerDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0040"); - public static readonly DiagnosticDescriptor Rule0041EmptyCaptionLocked = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0041", (LocalizableString)new LocalizableResourceString("Rule0041EmptyCaptionLockedTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0041EmptyCaptionLockedFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0041EmptyCaptionLockedDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0041"); - public static readonly DiagnosticDescriptor Rule0042AutoCalcFieldsOnNormalFields = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0042", (LocalizableString)new LocalizableResourceString("Rule0042AutoCalcFieldsOnNormalFieldsTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0042AutoCalcFieldsOnNormalFieldsFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0042AutoCalcFieldsOnNormalFieldsDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0042"); - public static readonly DiagnosticDescriptor Rule0043SecretText = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0043", (LocalizableString)new LocalizableResourceString("Rule0043SecretTextTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0043SecretTextFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0043SecretTextDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0043"); - public static readonly DiagnosticDescriptor Rule0044AnalyzeTableExtension = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0044", (LocalizableString)new LocalizableResourceString("Rule0044AnalyzeTableExtensionTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0044AnalyzeTableExtensionFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0044AnalyzeTableExtensionDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0044"); - public static readonly DiagnosticDescriptor Rule0044AnalyzeTransferFields = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0044", (LocalizableString)new LocalizableResourceString("Rule0044AnalyzeTransferFieldsTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0044AnalyzeTransferFieldsFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0044AnalyzeTransferFieldsDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0044"); - public static readonly DiagnosticDescriptor Rule0045ZeroEnumValueReservedForEmpty = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0045", (LocalizableString)new LocalizableResourceString("Rule0045ZeroEnumValueReservedForEmptyTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0045ZeroEnumValueReservedForEmptyFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0045ZeroEnumValueReservedForEmptyDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0045"); - public static readonly DiagnosticDescriptor Rule0046TokLabelsLocked = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0046", (LocalizableString)new LocalizableResourceString("Rule0046TokLabelsLockedTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0046TokLabelsLockedFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0046TokLabelsLockedDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0046"); - public static readonly DiagnosticDescriptor Rule0047LockedLabelsTok = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0047", (LocalizableString)new LocalizableResourceString("Rule0047LockedLabelsTokTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0047LockedLabelsTokFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Hidden, true, (LocalizableString)new LocalizableResourceString("Rule0047LockedLabelsTokDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0047"); - public static readonly DiagnosticDescriptor Rule0048ErrorWithTextConstant = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0048", (LocalizableString)new LocalizableResourceString("Rule0048ErrorWithTextConstantTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0048ErrorWithTextConstantFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0048ErrorWithTextConstantDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0048"); - public static readonly DiagnosticDescriptor Rule0049PageWithoutSourceTable = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0049", (LocalizableString)new LocalizableResourceString("Rule0049PageWithoutSourceTableTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0049PageWithoutSourceTableFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0049PageWithoutSourceTableDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0049"); - public static readonly DiagnosticDescriptor Rule0050OperatorAndPlaceholderInFilterExpression = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0050", (LocalizableString)new LocalizableResourceString("Rule0050OperatorAndPlaceholderInFilterExpressionTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0050OperatorAndPlaceholderInFilterExpressionFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0050OperatorAndPlaceholderInFilterExpressionDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0050"); - public static readonly DiagnosticDescriptor Rule0051PossibleOverflowAssigning = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0051", (LocalizableString)new LocalizableResourceString("Rule0051PossibleOverflowAssigningTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0051PossibleOverflowAssigningFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0051PossibleOverflowAssigningDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0051"); - public static readonly DiagnosticDescriptor Rule0052InternalProceduresNotReferencedAnalyzerDescriptor = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0052", (LocalizableString)new LocalizableResourceString("Rule0052InternalProceduresNotReferencedAnalyzer", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0052InternalProceduresNotReferencedAnalyzerFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, isEnabledByDefault: true, (LocalizableString)new LocalizableResourceString("Rule0052InternalProceduresNotReferencedAnalyzerDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0052"); - public static readonly DiagnosticDescriptor Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzerDescriptor = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0053", (LocalizableString)new LocalizableResourceString("Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzer", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzerFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, isEnabledByDefault: true, (LocalizableString)new LocalizableResourceString("Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzerDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0053"); - public static readonly DiagnosticDescriptor Rule0054FollowInterfaceObjectNameGuide = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0054", (LocalizableString)new LocalizableResourceString("Rule0054FollowInterfaceObjectNameGuideTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0054FollowInterfaceObjectNameGuideFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0054FollowInterfaceObjectNameGuideDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0054"); - public static readonly DiagnosticDescriptor Rule0055TokSuffixForTokenLabels = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0055", (LocalizableString)new LocalizableResourceString("Rule0055TokSuffixForTokenLabelsTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0055TokSuffixForTokenLabelsFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0055TokSuffixForTokenLabelsDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0055"); - public static readonly DiagnosticDescriptor Rule0056EmptyEnumValueWithCaption = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0056", (LocalizableString)new LocalizableResourceString("Rule0056EmptyEnumValueWithCaptionTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0056EmptyEnumValueWithCaptionFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0056EmptyEnumValueWithCaptionDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0056"); - public static readonly DiagnosticDescriptor Rule0057EnumValueWithEmptyCaption = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0057", (LocalizableString)new LocalizableResourceString("Rule0057EnumValueWithEmptyCaptionTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0057EnumValueWithEmptyCaptionFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0057EnumValueWithEmptyCaptionDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0057"); - public static readonly DiagnosticDescriptor Rule0058PageVariableMethodOnTemporaryTable = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0058", (LocalizableString)new LocalizableResourceString("Rule0058PageVariableMethodOnTemporaryTableTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0058PageVariableMethodOnTemporaryTableFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0058PageVariableMethodOnTemporaryTableDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0058"); - public static readonly DiagnosticDescriptor Rule0059SingleQuoteEscapingIssueDetected = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0059", (LocalizableString)new LocalizableResourceString("Rule0059SingleQuoteEscapingIssueDetectedTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0059SingleQuoteEscapingIssueDetectedFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0059SingleQuoteEscapingIssueDetectedDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0059"); - public static readonly DiagnosticDescriptor Rule0060PropertyApplicationAreaOnApiPage = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0060", (LocalizableString)new LocalizableResourceString("Rule0060PropertyApplicationAreaOnApiPageTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0060PropertyApplicationAreaOnApiPageFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0060PropertyApplicationAreaOnApiPageDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0060"); - public static readonly DiagnosticDescriptor Rule0061SetODataKeyFieldsWithSystemIdField = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0061", (LocalizableString)new LocalizableResourceString("Rule0061SetODataKeyFieldsWithSystemIdFieldTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0061SetODataKeyFieldsWithSystemIdFieldFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0061SetODataKeyFieldsWithSystemIdFieldDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0061"); - public static readonly DiagnosticDescriptor Rule0062MandatoryFieldMissingOnApiPage = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0062", (LocalizableString)new LocalizableResourceString("Rule0062MandatoryFieldMissingOnApiPageTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0062MandatoryFieldMissingOnApiPageFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0062MandatoryFieldMissingOnApiPageDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0062"); - public static readonly DiagnosticDescriptor Rule0069EmptyStatements = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0069", (LocalizableString)new LocalizableResourceString("Rule0069EmptyStatementsTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0069EmptyStatementsFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0069EmptyStatementsDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0069"); - public static readonly DiagnosticDescriptor Rule0071DoNotSetIsHandledToFalse = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0071", (LocalizableString)new LocalizableResourceString("Rule0071DoNotSetIsHandledToFalseTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0071DoNotSetIsHandledToFalseFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0071DoNotSetIsHandledToFalseDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0071"); - public static readonly DiagnosticDescriptor Rule0072CheckProcedureDocumentationComment = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0072", (LocalizableString)new LocalizableResourceString("Rule0072CheckProcedureDocumentationCommentTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0072CheckProcedureDocumentationCommentFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0072CheckProcedureDocumentationCommentDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0072"); - } + public static readonly DiagnosticDescriptor Rule0000ErrorInRule = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0000", + title: LinterCopAnalyzers.GetLocalizableString("Rule0000ErrorInRuleTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0000ErrorInRuleFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0000ErrorInRuleDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0000"); + + public static readonly DiagnosticDescriptor Rule0001FlowFieldsShouldNotBeEditable = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0001", + title: LinterCopAnalyzers.GetLocalizableString("Rule0001FlowFieldsShouldNotBeEditable"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0001FlowFieldsShouldNotBeEditableFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0001FlowFieldsShouldNotBeEditableDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0001"); + + public static readonly DiagnosticDescriptor Rule0002CommitMustBeExplainedByComment = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0002", + title: LinterCopAnalyzers.GetLocalizableString("Rule0002CommitMustBeExplainedByComment"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0002CommitMustBeExplainedByCommentFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0002CommitMustBeExplainedByCommentDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0002"); + + + public static readonly DiagnosticDescriptor Rule0003DoNotUseObjectIDsInVariablesOrProperties = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0003", + title: LinterCopAnalyzers.GetLocalizableString("Rule0003DoNotUseObjectIDsInVariablesOrProperties"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0003DoNotUseObjectIDsInVariablesOrPropertiesFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0003DoNotUseObjectIDsInVariablesOrPropertiesDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0003"); + + public static readonly DiagnosticDescriptor Rule0004LookupPageIdAndDrillDownPageId = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0004", + title: LinterCopAnalyzers.GetLocalizableString("Rule0004LookupPageIdAndDrillDownPageIdTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0004LookupPageIdAndDrillDownPageIdFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0004LookupPageIdAndDrillDownPageIdDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0004"); + + public static readonly DiagnosticDescriptor Rule0005VariableCasingShouldNotDifferFromDeclaration = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0005", + title: LinterCopAnalyzers.GetLocalizableString("Rule0005VariableCasingShouldNotDifferFromDeclarationTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0005VariableCasingShouldNotDifferFromDeclarationFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0005VariableCasingShouldNotDifferFromDeclarationDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0005"); + + public static readonly DiagnosticDescriptor Rule0006FieldNotAutoIncrementInTemporaryTable = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0006", + title: LinterCopAnalyzers.GetLocalizableString("Rule0006FieldNotAutoIncrementInTemporaryTableTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0006FieldNotAutoIncrementInTemporaryTableFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0006FieldNotAutoIncrementInTemporaryTableDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0006"); + + public static readonly DiagnosticDescriptor Rule0007DataPerCompanyShouldAlwaysBeSet = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0007", + title: LinterCopAnalyzers.GetLocalizableString("Rule0007DataPerCompanyShouldAlwaysBeSetTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0007DataPerCompanyShouldAlwaysBeSetFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Hidden, + isEnabledByDefault: false, + description: LinterCopAnalyzers.GetLocalizableString("Rule0007DataPerCompanyShouldAlwaysBeSetDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0007"); + + public static readonly DiagnosticDescriptor Rule0008NoFilterOperatorsInSetRange = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0008", + title: LinterCopAnalyzers.GetLocalizableString("Rule0008NoFilterOperatorsInSetRangeTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0008NoFilterOperatorsInSetRangeFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0008NoFilterOperatorsInSetRangeDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0008"); + + public static readonly DiagnosticDescriptor Rule0009CodeMetricsInfo = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0009", + title: LinterCopAnalyzers.GetLocalizableString("Rule0009CodeMetricsInfoTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0009CodeMetricsInfoFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: false, + description: LinterCopAnalyzers.GetLocalizableString("Rule0009CodeMetricsInfoDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0009"); + + public static readonly DiagnosticDescriptor Rule0010CodeMetricsWarning = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0010", + title: LinterCopAnalyzers.GetLocalizableString("Rule0009CodeMetricsInfoTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0009CodeMetricsInfoFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0009CodeMetricsInfoDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0010"); + + public static readonly DiagnosticDescriptor Rule0011AccessPropertyShouldAlwaysBeSet = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0011", + title: LinterCopAnalyzers.GetLocalizableString("Rule0011AccessPropertyShouldAlwaysBeSetTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0011AccessPropertyShouldAlwaysBeSetFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Hidden, + isEnabledByDefault: false, + description: LinterCopAnalyzers.GetLocalizableString("Rule0011AccessPropertyShouldAlwaysBeSetDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0011"); + + public static readonly DiagnosticDescriptor Rule0012DoNotUseObjectIdInSystemFunctions = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0012", + title: LinterCopAnalyzers.GetLocalizableString("Rule0012DoNotUseObjectIdInSystemFunctionsTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0012DoNotUseObjectIdInSystemFunctionsFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0012DoNotUseObjectIdInSystemFunctionsDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0012"); + + public static readonly DiagnosticDescriptor Rule0013CheckForNotBlankOnSingleFieldPrimaryKeys = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0013", + title: LinterCopAnalyzers.GetLocalizableString("Rule0013CheckForNotBlankOnSingleFieldPrimaryKeysTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0013CheckForNotBlankOnSingleFieldPrimaryKeysFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0013CheckForNotBlankOnSingleFieldPrimaryKeysDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0013"); + + public static readonly DiagnosticDescriptor Rule0014PermissionSetCaptionLength = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0014", + title: LinterCopAnalyzers.GetLocalizableString("Rule0014PermissionSetCaptionLengthTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0014PermissionSetCaptionLengthFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0014PermissionSetCaptionLengthDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0014"); + + public static readonly DiagnosticDescriptor Rule0015PermissionSetCoverage = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0015", + title: LinterCopAnalyzers.GetLocalizableString("Rule0015PermissionSetCoverageTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0015PermissionSetCoverageFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0015PermissionSetCoverageDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0015"); + + public static readonly DiagnosticDescriptor Rule0016CheckForMissingCaptions = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0016", + title: LinterCopAnalyzers.GetLocalizableString("Rule0016CheckForMissingCaptionsTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0016CheckForMissingCaptionsFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0016CheckForMissingCaptionsDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0016"); + + public static readonly DiagnosticDescriptor Rule0017WriteToFlowField = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0017", + title: LinterCopAnalyzers.GetLocalizableString("Rule0017WriteToFlowFieldTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0017WriteToFlowFieldFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0017WriteToFlowFieldDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0017"); + + public static readonly DiagnosticDescriptor Rule0018NoEventsInInternalCodeunitsAnalyzerDescriptor = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0018", + title: LinterCopAnalyzers.GetLocalizableString("Rule0018NoEventsInInternalCodeunitsTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0018NoEventsInInternalCodeunitsFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0018NoEventsInInternalCodeunitsDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0018"); + + public static readonly DiagnosticDescriptor Rule0019DataClassificationFieldEqualsTable = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0019", + title: LinterCopAnalyzers.GetLocalizableString("Rule0019DataClassificationFieldEqualsTableTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0019DataClassificationFieldEqualsTableFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0019DataClassificationFieldEqualsTableDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0019"); + + public static readonly DiagnosticDescriptor Rule0020ApplicationAreaEqualsToPage = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0020", + title: LinterCopAnalyzers.GetLocalizableString("Rule0020ApplicationAreaEqualsToPageTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0020ApplicationAreaEqualsToPageFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0020ApplicationAreaEqualsToPageDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0020"); + + public static readonly DiagnosticDescriptor Rule0021ConfirmImplementConfirmManagement = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0021", + title: LinterCopAnalyzers.GetLocalizableString("Rule0021ConfirmImplementConfirmManagement"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0021ConfirmImplementConfirmManagement"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0021ConfirmImplementConfirmManagement"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0021"); + + public static readonly DiagnosticDescriptor Rule0022GlobalLanguageImplementTranslationHelper = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0022", + title: LinterCopAnalyzers.GetLocalizableString("Rule0022GlobalLanguageImplementTranslationHelperTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0022GlobalLanguageImplementTranslationHelperFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0022GlobalLanguageImplementTranslationHelperDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0022"); + + public static readonly DiagnosticDescriptor Rule0023AlwaysSpecifyFieldgroups = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0023", + title: LinterCopAnalyzers.GetLocalizableString("Rule0023AlwaysSpecifyFieldgroups"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0023AlwaysSpecifyFieldgroups"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0023AlwaysSpecifyFieldgroups"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0023"); + + public static readonly DiagnosticDescriptor Rule0024SemicolonAfterMethodOrTriggerDeclaration = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0024", + title: LinterCopAnalyzers.GetLocalizableString("Rule0024SemicolonAfterMethodOrTriggerDeclarationTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0024SemicolonAfterMethodOrTriggerDeclarationFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0024SemicolonAfterMethodOrTriggerDeclarationDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0024"); + + public static readonly DiagnosticDescriptor Rule0025InternalProcedureModifier = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0025", + title: LinterCopAnalyzers.GetLocalizableString("Rule0025InternalProcedureModifierTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0025InternalProcedureModifierFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Hidden, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0025InternalProcedureModifierDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0025"); + + public static readonly DiagnosticDescriptor Rule0026ToolTipMustEndWithDot = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0026", + title: LinterCopAnalyzers.GetLocalizableString("Rule0026ToolTipMustEndWithDotTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0026ToolTipMustEndWithDotFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0026ToolTipMustEndWithDotDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0026"); + + public static readonly DiagnosticDescriptor Rule0027RunPageImplementPageManagement = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0027", + title: LinterCopAnalyzers.GetLocalizableString("Rule0027RunPageImplementPageManagementTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0027RunPageImplementPageManagementFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0027RunPageImplementPageManagementDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0027"); + + public static readonly DiagnosticDescriptor Rule0028IdentifiersInEventSubscribers = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0028", + title: LinterCopAnalyzers.GetLocalizableString("Rule0028IdentifiersInEventSubscribersTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0028IdentifiersInEventSubscribersFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0028IdentifiersInEventSubscribersDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0028"); + + public static readonly DiagnosticDescriptor Rule0029CompareDateTimeThroughCodeunit = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0029", + title: LinterCopAnalyzers.GetLocalizableString("Rule0029CompareDateTimeThroughCodeunitTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0029CompareDateTimeThroughCodeunitFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0029CompareDateTimeThroughCodeunitDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0029"); + + public static readonly DiagnosticDescriptor Rule0030AccessInternalForInstallAndUpgradeCodeunits = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0030", + title: LinterCopAnalyzers.GetLocalizableString("Rule0030AccessInternalForInstallAndUpgradeCodeunitsTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0030AccessInternalForInstallAndUpgradeCodeunitsFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0030AccessInternalForInstallAndUpgradeCodeunitsDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0030"); + + public static readonly DiagnosticDescriptor Rule0031RecordInstanceIsolationLevel = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0031", + title: LinterCopAnalyzers.GetLocalizableString("Rule0031RecordInstanceIsolationLevelTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0031RecordInstanceIsolationLevelFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0031RecordInstanceIsolationLevelDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0031"); + + public static readonly DiagnosticDescriptor Rule0032ClearCodeunitSingleInstance = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0032", + title: LinterCopAnalyzers.GetLocalizableString("Rule0032ClearCodeunitSingleInstanceTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0032ClearCodeunitSingleInstanceFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0032ClearCodeunitSingleInstanceDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0032"); + + public static readonly DiagnosticDescriptor Rule0033AppManifestRuntimeBehind = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0033", + title: LinterCopAnalyzers.GetLocalizableString("Rule0033AppManifestRuntimeBehindTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0033AppManifestRuntimeBehindFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0033AppManifestRuntimeBehindTitleDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0033"); + + public static readonly DiagnosticDescriptor Rule0034ExtensiblePropertyShouldAlwaysBeSet = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0034", + title: LinterCopAnalyzers.GetLocalizableString("Rule0034ExtensiblePropertyShouldAlwaysBeSetTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0034ExtensiblePropertyShouldAlwaysBeSetFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Hidden, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0034ExtensiblePropertyShouldAlwaysBeSetDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0034"); + + public static readonly DiagnosticDescriptor Rule0035ExplicitSetAllowInCustomizations = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0035", + title: LinterCopAnalyzers.GetLocalizableString("Rule0035ExplicitSetAllowInCustomizationsTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0035ExplicitSetAllowInCustomizationsFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0035ExplicitSetAllowInCustomizationsDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0035"); + + public static readonly DiagnosticDescriptor Rule0036ToolTipShouldStartWithSpecifies = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0036", + title: LinterCopAnalyzers.GetLocalizableString("Rule0036ToolTipShouldStartWithSpecifiesTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0036ToolTipShouldStartWithSpecifiesFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0036ToolTipShouldStartWithSpecifiesDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0036"); + + public static readonly DiagnosticDescriptor Rule0037ToolTipDoNotUseLineBreaks = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0037", + title: LinterCopAnalyzers.GetLocalizableString("Rule0037ToolTipDoNotUseLineBreaksTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0037ToolTipDoNotUseLineBreaksFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0037ToolTipDoNotUseLineBreaksDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0037"); + + public static readonly DiagnosticDescriptor Rule0038ToolTipMaximumLength = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0038", + title: LinterCopAnalyzers.GetLocalizableString("Rule0038ToolTipMaximumLengthTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0038ToolTipMaximumLengthFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0038ToolTipMaximumLengthDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0038"); + + public static readonly DiagnosticDescriptor Rule0039ArgumentDifferentTypeThenExpected = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0039", + title: LinterCopAnalyzers.GetLocalizableString("Rule0039ArgumentDifferentTypeThenExpectedTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0039ArgumentDifferentTypeThenExpectedFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0039ArgumentDifferentTypeThenExpectedDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0039"); + + public static readonly DiagnosticDescriptor Rule0040ExplicitlySetRunTrigger = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0040", + title: LinterCopAnalyzers.GetLocalizableString("Rule0040ExplicitlySetRunTriggerTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0040ExplicitlySetRunTriggerFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0040ExplicitlySetRunTriggerDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0040"); + + public static readonly DiagnosticDescriptor Rule0041EmptyCaptionLocked = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0041", + title: LinterCopAnalyzers.GetLocalizableString("Rule0041EmptyCaptionLockedTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0041EmptyCaptionLockedFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0041EmptyCaptionLockedDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0041"); + + public static readonly DiagnosticDescriptor Rule0042AutoCalcFieldsOnNormalFields = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0042", + title: LinterCopAnalyzers.GetLocalizableString("Rule0042AutoCalcFieldsOnNormalFieldsTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0042AutoCalcFieldsOnNormalFieldsFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0042AutoCalcFieldsOnNormalFieldsDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0042"); + + public static readonly DiagnosticDescriptor Rule0043SecretText = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0043", + title: LinterCopAnalyzers.GetLocalizableString("Rule0043SecretTextTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0043SecretTextFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0043SecretTextDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0043"); + + public static readonly DiagnosticDescriptor Rule0044AnalyzeTableExtension = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0044", + title: LinterCopAnalyzers.GetLocalizableString("Rule0044AnalyzeTableExtensionTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0044AnalyzeTableExtensionFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0044AnalyzeTableExtensionDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0044"); + + public static readonly DiagnosticDescriptor Rule0044AnalyzeTransferFields = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0044", + title: LinterCopAnalyzers.GetLocalizableString("Rule0044AnalyzeTransferFieldsTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0044AnalyzeTransferFieldsFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0044AnalyzeTransferFieldsDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0044"); + + public static readonly DiagnosticDescriptor Rule0045ZeroEnumValueReservedForEmpty = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0045", + title: LinterCopAnalyzers.GetLocalizableString("Rule0045ZeroEnumValueReservedForEmptyTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0045ZeroEnumValueReservedForEmptyFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0045ZeroEnumValueReservedForEmptyDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0045"); + + public static readonly DiagnosticDescriptor Rule0046TokLabelsLocked = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0046", + title: LinterCopAnalyzers.GetLocalizableString("Rule0046TokLabelsLockedTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0046TokLabelsLockedFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0046TokLabelsLockedDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0046"); + + public static readonly DiagnosticDescriptor Rule0047LockedLabelsTok = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0047", + title: LinterCopAnalyzers.GetLocalizableString("Rule0047LockedLabelsTokTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0047LockedLabelsTokFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Hidden, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0047LockedLabelsTokDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0047"); + + public static readonly DiagnosticDescriptor Rule0048ErrorWithTextConstant = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0048", + title: LinterCopAnalyzers.GetLocalizableString("Rule0048ErrorWithTextConstantTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0048ErrorWithTextConstantFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0048ErrorWithTextConstantDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0048"); + + public static readonly DiagnosticDescriptor Rule0049PageWithoutSourceTable = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0049", + title: LinterCopAnalyzers.GetLocalizableString("Rule0049PageWithoutSourceTableTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0049PageWithoutSourceTableFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0049PageWithoutSourceTableDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0049"); + + public static readonly DiagnosticDescriptor Rule0050OperatorAndPlaceholderInFilterExpression = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0050", + title: LinterCopAnalyzers.GetLocalizableString("Rule0050OperatorAndPlaceholderInFilterExpressionTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0050OperatorAndPlaceholderInFilterExpressionFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0050OperatorAndPlaceholderInFilterExpressionDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0050"); + + public static readonly DiagnosticDescriptor Rule0051PossibleOverflowAssigning = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0051", + title: LinterCopAnalyzers.GetLocalizableString("Rule0051PossibleOverflowAssigningTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0051PossibleOverflowAssigningFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0051PossibleOverflowAssigningDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0051"); + + public static readonly DiagnosticDescriptor Rule0052InternalProceduresNotReferencedAnalyzerDescriptor = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0052", + title: LinterCopAnalyzers.GetLocalizableString("Rule0052InternalProceduresNotReferencedAnalyzer"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0052InternalProceduresNotReferencedAnalyzerFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0052InternalProceduresNotReferencedAnalyzerDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0052"); + + public static readonly DiagnosticDescriptor Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzerDescriptor = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0053", + title: LinterCopAnalyzers.GetLocalizableString("Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzer"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzerFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0053InternalProcedureOnlyUsedInCurrentObjectAnalyzerDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0053"); + + public static readonly DiagnosticDescriptor Rule0054FollowInterfaceObjectNameGuide = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0054", + title: LinterCopAnalyzers.GetLocalizableString("Rule0054FollowInterfaceObjectNameGuideTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0054FollowInterfaceObjectNameGuideFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0054FollowInterfaceObjectNameGuideDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0054"); + + public static readonly DiagnosticDescriptor Rule0055TokSuffixForTokenLabels = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0055", + title: LinterCopAnalyzers.GetLocalizableString("Rule0055TokSuffixForTokenLabelsTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0055TokSuffixForTokenLabelsFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0055TokSuffixForTokenLabelsDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0055"); + + public static readonly DiagnosticDescriptor Rule0056EmptyEnumValueWithCaption = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0056", + title: LinterCopAnalyzers.GetLocalizableString("Rule0056EmptyEnumValueWithCaptionTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0056EmptyEnumValueWithCaptionFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0056EmptyEnumValueWithCaptionDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0056"); + + public static readonly DiagnosticDescriptor Rule0057EnumValueWithEmptyCaption = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0057", + title: LinterCopAnalyzers.GetLocalizableString("Rule0057EnumValueWithEmptyCaptionTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0057EnumValueWithEmptyCaptionFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0057EnumValueWithEmptyCaptionDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0057"); + + public static readonly DiagnosticDescriptor Rule0058PageVariableMethodOnTemporaryTable = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0058", + title: LinterCopAnalyzers.GetLocalizableString("Rule0058PageVariableMethodOnTemporaryTableTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0058PageVariableMethodOnTemporaryTableFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0058PageVariableMethodOnTemporaryTableDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0058"); + + public static readonly DiagnosticDescriptor Rule0059SingleQuoteEscapingIssueDetected = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0059", + title: LinterCopAnalyzers.GetLocalizableString("Rule0059SingleQuoteEscapingIssueDetectedTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0059SingleQuoteEscapingIssueDetectedFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0059SingleQuoteEscapingIssueDetectedDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0059"); + + public static readonly DiagnosticDescriptor Rule0060PropertyApplicationAreaOnApiPage = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0060", + title: LinterCopAnalyzers.GetLocalizableString("Rule0060PropertyApplicationAreaOnApiPageTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0060PropertyApplicationAreaOnApiPageFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0060PropertyApplicationAreaOnApiPageDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0060"); + + public static readonly DiagnosticDescriptor Rule0061SetODataKeyFieldsWithSystemIdField = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0061", + title: LinterCopAnalyzers.GetLocalizableString("Rule0061SetODataKeyFieldsWithSystemIdFieldTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0061SetODataKeyFieldsWithSystemIdFieldFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0061SetODataKeyFieldsWithSystemIdFieldDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0061"); + + public static readonly DiagnosticDescriptor Rule0062MandatoryFieldMissingOnApiPage = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0062", + title: LinterCopAnalyzers.GetLocalizableString("Rule0062MandatoryFieldMissingOnApiPageTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0062MandatoryFieldMissingOnApiPageFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0062MandatoryFieldMissingOnApiPageDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0062"); + + public static readonly DiagnosticDescriptor Rule0063GiveFieldMoreDescriptiveName = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0063", + title: LinterCopAnalyzers.GetLocalizableString("Rule0063GiveFieldMoreDescriptiveNameTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0063GiveFieldMoreDescriptiveNameFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0063GiveFieldMoreDescriptiveNameDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0063"); + + public static readonly DiagnosticDescriptor Rule0064TableFieldMissingToolTip = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0064", + title: LinterCopAnalyzers.GetLocalizableString("Rule0064TableFieldMissingToolTipTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0064TableFieldMissingToolTipFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0064TableFieldMissingToolTipDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0064"); + + public static readonly DiagnosticDescriptor Rule0065EventSubscriberVarCheck = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0065", + title: LinterCopAnalyzers.GetLocalizableString("Rule0065EventSubscriberVarCheckTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0065EventSubscriberVarCheckFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0065EventSubscriberVarCheckDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0065"); + + public static readonly DiagnosticDescriptor Rule0066DuplicateToolTipBetweenPageAndTable = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0066", + title: LinterCopAnalyzers.GetLocalizableString("Rule0066DuplicateToolTipBetweenPageAndTableTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0066DuplicateToolTipBetweenPageAndTableFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0066DuplicateToolTipBetweenPageAndTableDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0066"); + + + public static readonly DiagnosticDescriptor Rule0067DisableNotBlankOnSingleFieldPrimaryKey = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0067", + title: LinterCopAnalyzers.GetLocalizableString("Rule0067DisableNotBlankOnSingleFieldPrimaryKeyTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0067DisableNotBlankOnSingleFieldPrimaryKeyFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0067DisableNotBlankOnSingleFieldPrimaryKeyDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0067"); + + public static readonly DiagnosticDescriptor Rule0068CheckObjectPermission = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0068", + title: LinterCopAnalyzers.GetLocalizableString("Rule0068CheckObjectPermissionTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0068CheckObjectPermissionFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0068CheckObjectPermissionDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0068"); + + public static readonly DiagnosticDescriptor Rule0069EmptyStatements = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0069", + title: LinterCopAnalyzers.GetLocalizableString("Rule0069EmptyStatementsTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0069EmptyStatementsFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0069EmptyStatementsDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0069"); + + public static readonly DiagnosticDescriptor Rule0070ListObjectsAreOneBased = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0070", + title: LinterCopAnalyzers.GetLocalizableString("Rule0070ListObjectsAreOneBasedTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0070ListObjectsAreOneBasedFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0070ListObjectsAreOneBasedDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0070"); + + public static readonly DiagnosticDescriptor Rule0071DoNotSetIsHandledToFalse = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0071", + title: LinterCopAnalyzers.GetLocalizableString("Rule0071DoNotSetIsHandledToFalseTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0071DoNotSetIsHandledToFalseFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0071DoNotSetIsHandledToFalseDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0071"); + + public static readonly DiagnosticDescriptor Rule0072CheckProcedureDocumentationComment = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0072", + title: LinterCopAnalyzers.GetLocalizableString("Rule0072CheckProcedureDocumentationCommentTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0072CheckProcedureDocumentationCommentFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0072CheckProcedureDocumentationCommentDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0072"); + + public static readonly DiagnosticDescriptor Rule0073EventPublisherIsHandledByVar = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0073", + title: LinterCopAnalyzers.GetLocalizableString("Rule0073EventPublisherIsHandledByVarTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0073EventPublisherIsHandledByVarFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0073EventPublisherIsHandledByVarDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0073"); + + public static readonly DiagnosticDescriptor Rule0074FlowFilterAssignment = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0074", + title: LinterCopAnalyzers.GetLocalizableString("Rule0074FlowFilterAssignmentTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0074FlowFilterAssignmentFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0074FlowFilterAssignmentDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0074"); + + public static readonly DiagnosticDescriptor Rule0075RecordGetProcedureArguments = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0075", + title: LinterCopAnalyzers.GetLocalizableString("Rule0075RecordGetProcedureArgumentsTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0075RecordGetProcedureArgumentsFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0075RecordGetProcedureArgumentsDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0075"); + + public static readonly DiagnosticDescriptor Rule0076TableRelationTooLong = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0076", + title: LinterCopAnalyzers.GetLocalizableString("Rule0076TableRelationTooLongTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0076TableRelationTooLongFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0076TableRelationTooLongDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0076"); + + public static readonly DiagnosticDescriptor Rule0077MissingParenthesis = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0077", + title: LinterCopAnalyzers.GetLocalizableString("Rule0077MissingParenthesisTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0077MissingParenthesisFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0077MissingParenthesisDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0077"); + + public static readonly DiagnosticDescriptor Rule0078TemporaryRecordsShouldNotTriggerTableTriggers = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0078", + title: LinterCopAnalyzers.GetLocalizableString("Rule0078TemporaryRecordsTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0078TemporaryRecordsFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0078TemporaryRecordsDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0078"); + + public static readonly DiagnosticDescriptor Rule0079NonPublicEventPublisher = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0079", + title: LinterCopAnalyzers.GetLocalizableString("Rule0079NonPublicEventPublisherTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0079NonPublicEventPublisherFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0079NonPublicEventPublisherDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0079"); + + public static readonly DiagnosticDescriptor Rule0080AnalyzeJsonTokenJPath = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0080", + title: LinterCopAnalyzers.GetLocalizableString("Rule0080AnalyzeJsonTokenJPathTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0080AnalyzeJsonTokenJPathFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0080AnalyzeJsonTokenJPathDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0080"); + + public static readonly DiagnosticDescriptor Rule0081UseIsEmptyMethod = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0081", + title: LinterCopAnalyzers.GetLocalizableString("Rule0081UseIsEmptyMethodTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0081UseIsEmptyMethodFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0081UseIsEmptyMethodDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0081"); + + public static readonly DiagnosticDescriptor Rule0082UseFindWithNext = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0082", + title: LinterCopAnalyzers.GetLocalizableString("Rule0082UseFindWithNextTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0082UseFindWithNextFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0082UseFindWithNextDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0082"); + + public static readonly DiagnosticDescriptor Rule0083BuiltInDateTimeMethod = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0083", + title: LinterCopAnalyzers.GetLocalizableString("Rule0083BuiltInDateTimeMethodTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0083BuiltInDateTimeMethodFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0083BuiltInDateTimeMethodDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0083"); } \ No newline at end of file From 48819b8aa77e204828a05b5d89c5d49560c0898f Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Mon, 23 Dec 2024 16:49:48 +0100 Subject: [PATCH 16/28] Add Implicit Primary Key field (#845) --- BusinessCentral.LinterCop.Test/Rule0076.cs | 1 + .../TableRelationImplicitFieldPrimaryKey.al | 17 ++++ .../Design/Rule0076TableRelationTooLong.cs | 97 ++++++++++++------- 3 files changed, 80 insertions(+), 35 deletions(-) create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0076/HasDiagnostic/TableRelationImplicitFieldPrimaryKey.al diff --git a/BusinessCentral.LinterCop.Test/Rule0076.cs b/BusinessCentral.LinterCop.Test/Rule0076.cs index 29149e69..50be59e3 100644 --- a/BusinessCentral.LinterCop.Test/Rule0076.cs +++ b/BusinessCentral.LinterCop.Test/Rule0076.cs @@ -13,6 +13,7 @@ public void Setup() [Test] [TestCase("TableRelationLonger")] + [TestCase("TableRelationImplicitFieldPrimaryKey")] #if !LessThenSpring2024 [TestCase("TableExtRelationLonger")] #endif diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0076/HasDiagnostic/TableRelationImplicitFieldPrimaryKey.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0076/HasDiagnostic/TableRelationImplicitFieldPrimaryKey.al new file mode 100644 index 00000000..f6d68b3a --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0076/HasDiagnostic/TableRelationImplicitFieldPrimaryKey.al @@ -0,0 +1,17 @@ +table 50108 MyTable +{ + fields + { + field(1; MyField; Code[20]) { } + + field(3; [|MySecondField|]; Code[10]) + { + TableRelation = MyTable; + } + } + + keys + { + key(PK; MyField) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0076TableRelationTooLong.cs b/BusinessCentral.LinterCop/Design/Rule0076TableRelationTooLong.cs index 4fb0401b..384e84f4 100644 --- a/BusinessCentral.LinterCop/Design/Rule0076TableRelationTooLong.cs +++ b/BusinessCentral.LinterCop/Design/Rule0076TableRelationTooLong.cs @@ -2,88 +2,115 @@ using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; +using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; using System.Collections.Immutable; namespace BusinessCentral.LinterCop.Design; [DiagnosticAnalyzer] public class Rule0076TableRelationTooLong : DiagnosticAnalyzer { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0076TableRelationTooLong); + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0076TableRelationTooLong); public override void Initialize(AnalysisContext context) => context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Field); - private void AnalyzeSymbol(SymbolAnalysisContext context) + private void AnalyzeSymbol(SymbolAnalysisContext ctx) { - if (context.IsObsoletePendingOrRemoved()) + if (ctx.IsObsoletePendingOrRemoved() || ctx.Symbol is not IFieldSymbol field) return; - if (context.Symbol is not IFieldSymbol currentField) + if (!field.HasLength) return; - var tableRelation = currentField + var tableRelation = field .GetProperty(PropertyKind.TableRelation) ?.GetPropertyValueSyntax(); - if (tableRelation is null) return; - AnalyzeTableRelations(context, currentField, tableRelation); + AnalyzeTableRelations(ctx, field, tableRelation); } - private void AnalyzeTableRelations(SymbolAnalysisContext context, IFieldSymbol currentField, TableRelationPropertyValueSyntax? tableRelation) + private void AnalyzeTableRelations(SymbolAnalysisContext ctx, IFieldSymbol field, TableRelationPropertyValueSyntax? tableRelation) { while (tableRelation is not null) { - if (tableRelation.RelatedTableField is QualifiedNameSyntax relatedField) + var relatedFieldSymbol = ResolveRelatedField(ctx, tableRelation); + + if (relatedFieldSymbol is not null && ShouldReportDiagnostic(field, relatedFieldSymbol)) { - var relatedFieldSymbol = GetRelatedFieldSymbol( - relatedField.Left as IdentifierNameSyntax, - relatedField.Right as IdentifierNameSyntax, - context.Compilation); - - if (relatedFieldSymbol is not null && ShouldReportDiagnostic(currentField, relatedFieldSymbol)) - { - ReportLengthMismatch(context, currentField, relatedFieldSymbol, relatedField); - } + ReportLengthMismatch(ctx, field, relatedFieldSymbol); } tableRelation = tableRelation.ElseExpression?.ElseTableRelationCondition; } } - private static bool ShouldReportDiagnostic(IFieldSymbol currentField, IFieldSymbol? relatedField) => - relatedField?.HasLength == true && - currentField.HasLength && - currentField.Length < relatedField.Length; + private static bool ShouldReportDiagnostic(IFieldSymbol currentField, IFieldSymbol relatedField) => + relatedField.HasLength && currentField.Length < relatedField.Length; - private static void ReportLengthMismatch(SymbolAnalysisContext context, IFieldSymbol currentField, - IFieldSymbol relatedField, QualifiedNameSyntax relatedFieldSyntax) + private static void ReportLengthMismatch(SymbolAnalysisContext ctx, IFieldSymbol currentField, IFieldSymbol relatedField) { - context.ReportDiagnostic(Diagnostic.Create( + ctx.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.Rule0076TableRelationTooLong, currentField.GetLocation(), relatedField.Length, - relatedFieldSyntax.ToString(), + relatedField.ToDisplayString().QuoteIdentifierIfNeeded(), currentField.Length, - currentField.Name)); + currentField.ToDisplayString().QuoteIdentifierIfNeeded())); + } + + private IFieldSymbol? ResolveRelatedField(SymbolAnalysisContext ctx, TableRelationPropertyValueSyntax tableRelation) + { + return tableRelation.RelatedTableField switch + { + QualifiedNameSyntax qualifiedName => + ResolveQualifiedField(qualifiedName, ctx.Compilation), + + IdentifierNameSyntax identifierName => + ResolvePrimaryKeyField(identifierName.Identifier.ValueText, ctx.Compilation), + + _ => null + }; + } + + private IFieldSymbol? ResolveQualifiedField(QualifiedNameSyntax qualifiedName, Compilation compilation) + { + if (qualifiedName.Left is IdentifierNameSyntax tableNameSyntax && + qualifiedName.Right is IdentifierNameSyntax fieldNameSyntax) + { + var tableName = tableNameSyntax.GetIdentifierOrLiteralValue(); + var fieldName = fieldNameSyntax.GetIdentifierOrLiteralValue(); + + if (tableName != null && fieldName != null) + { + return GetFieldFromTable(tableName, fieldName, compilation) + ?? GetFieldFromTableExtension(tableName, fieldName, compilation); + } + } + + return null; } - private IFieldSymbol? GetRelatedFieldSymbol(IdentifierNameSyntax? table, IdentifierNameSyntax? field, Compilation compilation) + private static IFieldSymbol? ResolvePrimaryKeyField(string? tableName, Compilation compilation) { - if (table?.GetIdentifierOrLiteralValue() is not string tableName || - field?.GetIdentifierOrLiteralValue() is not string fieldName) + if (string.IsNullOrEmpty(tableName)) return null; - return GetFieldFromTable(tableName, fieldName, compilation) ?? - GetFieldFromTableExtension(tableName, fieldName, compilation); + var tableSymbols = compilation.GetApplicationObjectTypeSymbolsByNameAcrossModules(SymbolKind.Table, tableName); + + return tableSymbols.FirstOrDefault() is ITableTypeSymbol table && table.PrimaryKey.Fields.Length == 1 + ? table.PrimaryKey.Fields[0] + : null; } private static IFieldSymbol? GetFieldFromTable(string tableName, string fieldName, Compilation compilation) { - var tables = compilation.GetApplicationObjectTypeSymbolsByNameAcrossModules(SymbolKind.Table, tableName); - return tables.FirstOrDefault() is ITableTypeSymbol tableSymbol - ? tableSymbol.Fields.FirstOrDefault(x => x.Name == fieldName) + var tableSymbols = compilation.GetApplicationObjectTypeSymbolsByNameAcrossModules(SymbolKind.Table, tableName); + + return tableSymbols.FirstOrDefault() is ITableTypeSymbol table + ? table.Fields.FirstOrDefault(f => f.Name == fieldName) : null; } From cde2a1de10667f483e9e0b7c82dd0421d27e8a3a Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Mon, 23 Dec 2024 18:12:05 +0100 Subject: [PATCH 17/28] New Rule0084: Use Return Value for Error Handling (#846) --- BusinessCentral.LinterCop.Test/Rule0084.cs | 37 ++++++++++++++++ .../HasDiagnostic/GetBySystemIdMethod.al | 18 ++++++++ .../Rule0084/HasDiagnostic/GetMethod.al | 17 +++++++ .../NoDiagnostic/GetBySystemIdMethod.al | 18 ++++++++ .../Rule0084/NoDiagnostic/GetMethod.al | 17 +++++++ .../Rule0084UseReturnValueForErrorHandling.cs | 44 +++++++++++++++++++ .../LinterCop.ruleset.json | 5 +++ .../LinterCopAnalyzers.Generated.cs | 11 +++++ .../LinterCopAnalyzers.resx | 9 ++++ README.md | 1 + 10 files changed, 177 insertions(+) create mode 100644 BusinessCentral.LinterCop.Test/Rule0084.cs create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0084/HasDiagnostic/GetBySystemIdMethod.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0084/HasDiagnostic/GetMethod.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0084/NoDiagnostic/GetBySystemIdMethod.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0084/NoDiagnostic/GetMethod.al create mode 100644 BusinessCentral.LinterCop/Design/Rule0084UseReturnValueForErrorHandling.cs diff --git a/BusinessCentral.LinterCop.Test/Rule0084.cs b/BusinessCentral.LinterCop.Test/Rule0084.cs new file mode 100644 index 00000000..e7e34374 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/Rule0084.cs @@ -0,0 +1,37 @@ +namespace BusinessCentral.LinterCop.Test; + +public class Rule0084 +{ + private string _testCaseDir = ""; + + [SetUp] + public void Setup() + { + _testCaseDir = Path.Combine(Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + "TestCases", "Rule0084"); + } + + [Test] + [TestCase("GetMethod")] + [TestCase("GetBySystemIdMethod")] + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0084UseReturnValueForErrorHandling.Id); + } + + [Test] + [TestCase("GetMethod")] + [TestCase("GetBySystemIdMethod")] + public async Task NoDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0084UseReturnValueForErrorHandling.Id); + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0084/HasDiagnostic/GetBySystemIdMethod.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0084/HasDiagnostic/GetBySystemIdMethod.al new file mode 100644 index 00000000..0840e5b3 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0084/HasDiagnostic/GetBySystemIdMethod.al @@ -0,0 +1,18 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + MyGuid: Guid; + begin + MyTable.[|GetBySystemId|](MyGuid); + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Code[20]) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0084/HasDiagnostic/GetMethod.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0084/HasDiagnostic/GetMethod.al new file mode 100644 index 00000000..2aa2e742 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0084/HasDiagnostic/GetMethod.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + MyTable.[|Get|](); + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Code[20]) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0084/NoDiagnostic/GetBySystemIdMethod.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0084/NoDiagnostic/GetBySystemIdMethod.al new file mode 100644 index 00000000..f6f577c6 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0084/NoDiagnostic/GetBySystemIdMethod.al @@ -0,0 +1,18 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + MyGuid: Guid; + begin + if MyTable.[|GetBySystemId|](MyGuid) then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Code[20]) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0084/NoDiagnostic/GetMethod.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0084/NoDiagnostic/GetMethod.al new file mode 100644 index 00000000..53446d0c --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0084/NoDiagnostic/GetMethod.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTable: Record MyTable; + begin + if MyTable.[|Get|]() then; + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Code[20]) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0084UseReturnValueForErrorHandling.cs b/BusinessCentral.LinterCop/Design/Rule0084UseReturnValueForErrorHandling.cs new file mode 100644 index 00000000..c59f2b60 --- /dev/null +++ b/BusinessCentral.LinterCop/Design/Rule0084UseReturnValueForErrorHandling.cs @@ -0,0 +1,44 @@ +using System.Collections.Immutable; +using BusinessCentral.LinterCop.Helpers; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; + +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0084UseReturnValueForErrorHandling : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0084UseReturnValueForErrorHandling); + + private static readonly HashSet methodsToCheck = ["Find", "FindFirst", "FindLast", "Get", "GetBySystemId"]; + + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(AnalyzeAssignmentStatement, OperationKind.InvocationExpression); + + private void AnalyzeAssignmentStatement(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) + return; + + if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || + operation.TargetMethod.ContainingSymbol?.Name != "Table" || + !methodsToCheck.Contains(operation.TargetMethod.Name)) + return; + + if (ctx.Operation.Syntax.Parent.Kind == SyntaxKind.ExpressionStatement) + { + var methodName = operation.TargetMethod.Name.ToString(); + var node = operation.Syntax.DescendantNodesAndSelf() + .OfType() + .Where(node => node.Identifier.ValueText == methodName) + .FirstOrDefault(); + + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0084UseReturnValueForErrorHandling, + node.GetLocation(), + methodName)); + } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCop.ruleset.json b/BusinessCentral.LinterCop/LinterCop.ruleset.json index ed389782..fd6c016a 100644 --- a/BusinessCentral.LinterCop/LinterCop.ruleset.json +++ b/BusinessCentral.LinterCop/LinterCop.ruleset.json @@ -416,6 +416,11 @@ "id": "LC0083", "action": "Info", "justification": "Use new Date/Time/DateTime methods for extracting parts." + }, + { + "id": "LC0084", + "action": "Info", + "justification": "Use return value for better error handling." } ] } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs b/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs index e3647cad..bbdcdae9 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs @@ -854,4 +854,15 @@ public static class DiagnosticDescriptors isEnabledByDefault: true, description: LinterCopAnalyzers.GetLocalizableString("Rule0083BuiltInDateTimeMethodDescription"), helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0083"); + + public static readonly DiagnosticDescriptor Rule0084UseReturnValueForErrorHandling = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0084", + title: LinterCopAnalyzers.GetLocalizableString("Rule0084UseReturnValueForErrorHandlingTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0084UseReturnValueForErrorHandlingFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0084UseReturnValueForErrorHandlingDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0084"); + } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx index 603d32a1..8f6353c2 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx @@ -867,4 +867,13 @@ Replace outdated functions for extracting specific parts of Date, Time, and DateTime types (such as day, month, hour, or second) with the new, modernized methods. + + Use return value for better error handling. + + + The return value of the '{0}' method must be used to improve error handling or provide meaningful feedback to the user. + + + Database read methods, like Record.Get(), returns a boolean indicating whether the record was successfully retrieved. Failing to use this return value can lead to uncaught errors, poor error handling, and a lack of actionable feedback for users when something goes wrong. + \ No newline at end of file diff --git a/README.md b/README.md index 188d0c01..04204d0b 100644 --- a/README.md +++ b/README.md @@ -237,5 +237,6 @@ For an example and the default values see: [LinterCop.ruleset.json](./BusinessCe |[LC0081](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0081)|Use `Rec.IsEmpty()` for checking record existence.|Info| |[LC0082](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0082)|Use `Rec.Find('-')` with `Rec.Next()` for checking exactly one record.|Info| |[LC0083](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0083)|Use new Date/Time/DateTime methods for extracting parts.|Info| +|[LC0084](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0084)|Use return value for better error handling.|Info| From 5532894c400b389948006cd02b4b10e8dfed91bb Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Fri, 27 Dec 2024 11:13:04 +0100 Subject: [PATCH 18/28] New Rule LC0085: Use the (CR)LFSeparator from the "Type Helper" codeunit (#848) * new Rule0085: LFSeparator * Add documentation --- BusinessCentral.LinterCop.Test/Rule0085.cs | 39 +++++++++++ .../Rule0085/HasDiagnostic/LFSeparatorChar.al | 9 +++ .../Rule0085/HasDiagnostic/LFSeparatorCode.al | 9 +++ .../Rule0085/HasDiagnostic/LFSeparatorText.al | 9 +++ .../Rule0085/NoDiagnostic/LFSeparatorCode.al | 9 +++ .../Rule0085/NoDiagnostic/LFSeparatorText.al | 9 +++ .../LFSeparatorTextElementAccess2.al | 9 +++ .../Design/Rule0085LFSeparator.cs | 70 +++++++++++++++++++ .../LinterCop.ruleset.json | 5 ++ .../LinterCopAnalyzers.Generated.cs | 10 +++ .../LinterCopAnalyzers.resx | 9 +++ README.md | 3 +- 12 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 BusinessCentral.LinterCop.Test/Rule0085.cs create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorChar.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorCode.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorText.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorCode.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorText.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorTextElementAccess2.al create mode 100644 BusinessCentral.LinterCop/Design/Rule0085LFSeparator.cs diff --git a/BusinessCentral.LinterCop.Test/Rule0085.cs b/BusinessCentral.LinterCop.Test/Rule0085.cs new file mode 100644 index 00000000..cb9e12a9 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/Rule0085.cs @@ -0,0 +1,39 @@ +namespace BusinessCentral.LinterCop.Test; + +public class Rule0085 +{ + private string _testCaseDir = ""; + + [SetUp] + public void Setup() + { + _testCaseDir = Path.Combine(Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + "TestCases", "Rule0085"); + } + + [Test] + [TestCase("LFSeparatorChar")] + [TestCase("LFSeparatorCode")] + [TestCase("LFSeparatorText")] + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0085LFSeparator.Id); + } + + [Test] + [TestCase("LFSeparatorCode")] + [TestCase("LFSeparatorText")] + [TestCase("LFSeparatorTextElementAccess2")] + public async Task NoDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0085LFSeparator.Id); + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorChar.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorChar.al new file mode 100644 index 00000000..201d9454 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorChar.al @@ -0,0 +1,9 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + myChar: Char; + begin + [|myChar := 10|]; + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorCode.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorCode.al new file mode 100644 index 00000000..5fa49395 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorCode.al @@ -0,0 +1,9 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyCode: Code[2]; + begin + [|MyCode[1] := 10|]; + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorText.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorText.al new file mode 100644 index 00000000..75d32547 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorText.al @@ -0,0 +1,9 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyText: Text; + begin + [|MyText[1] := 10|]; + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorCode.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorCode.al new file mode 100644 index 00000000..e2848581 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorCode.al @@ -0,0 +1,9 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyCode: Code[2]; + begin + [|MyCode[2] := 10|]; + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorText.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorText.al new file mode 100644 index 00000000..b5a57b51 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorText.al @@ -0,0 +1,9 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyText: Text; + begin + [|MyText[2] := 10|]; + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorTextElementAccess2.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorTextElementAccess2.al new file mode 100644 index 00000000..b5a57b51 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorTextElementAccess2.al @@ -0,0 +1,9 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyText: Text; + begin + [|MyText[2] := 10|]; + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0085LFSeparator.cs b/BusinessCentral.LinterCop/Design/Rule0085LFSeparator.cs new file mode 100644 index 00000000..61bb3182 --- /dev/null +++ b/BusinessCentral.LinterCop/Design/Rule0085LFSeparator.cs @@ -0,0 +1,70 @@ +using BusinessCentral.LinterCop.Helpers; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Semantics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; +using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; +using System.Collections.Immutable; + +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0085LFSeparator : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0085LFSeparator); + + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction(new Action(this.AnalyzeAssignmentStatement), OperationKind.AssignmentStatement); + + private void AnalyzeAssignmentStatement(OperationAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IAssignmentStatement operation) + return; + + if (IsLFSeparatorAssignment(operation)) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0085LFSeparator, + ctx.Operation.Syntax.GetLocation())); + } + } + + private static bool IsLFSeparatorAssignment(IAssignmentStatement operation) + { + // Right side needs to be := 10; + if (operation.Value.Syntax is not LiteralExpressionSyntax sourceLiteral) + return false; + + if (sourceLiteral.Literal is not Int32SignedLiteralValueSyntax sourceInt) + return false; + + if (sourceInt.GetIdentifierOrLiteralValue() != "10") + return false; + + // Left side needs to be Code[1], Text[1] or a Char + switch (operation.Target.Kind) + { + case OperationKind.FieldAccess: + if (operation.Target.Syntax is not ElementAccessExpressionSyntax elementAccess || + elementAccess.ArgumentList.Arguments.Count != 1 || + elementAccess.ArgumentList.Arguments[0] is not LiteralExpressionSyntax targetLiteral) + return false; + + if (targetLiteral.Literal is not Int32SignedLiteralValueSyntax targetInt) + return false; + + return targetInt.GetIdentifierOrLiteralValue() == "1"; + + case OperationKind.LocalReferenceExpression: + case OperationKind.GlobalReferenceExpression: + if (operation.Target.Syntax.Kind != SyntaxKind.IdentifierName || + operation.Target.GetSymbol() is not IVariableSymbol identifierNameSymbol) + return false; + + return identifierNameSymbol.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.Char; + } + + return false; + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCop.ruleset.json b/BusinessCentral.LinterCop/LinterCop.ruleset.json index fd6c016a..2e76e55d 100644 --- a/BusinessCentral.LinterCop/LinterCop.ruleset.json +++ b/BusinessCentral.LinterCop/LinterCop.ruleset.json @@ -421,6 +421,11 @@ "id": "LC0084", "action": "Info", "justification": "Use return value for better error handling." + }, + { + "id": "LC0085", + "action": "Info", + "justification": "Use the (CR)LFSeparator from the Type Helper codeunit." } ] } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs b/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs index bbdcdae9..0e3ea135 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs @@ -865,4 +865,14 @@ public static class DiagnosticDescriptors description: LinterCopAnalyzers.GetLocalizableString("Rule0084UseReturnValueForErrorHandlingDescription"), helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0084"); + public static readonly DiagnosticDescriptor Rule0085LFSeparator = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0085", + title: LinterCopAnalyzers.GetLocalizableString("Rule0085LFSeparatorTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0085LFSeparatorFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0085LFSeparatorDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0085"); + } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx index 8f6353c2..42569122 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx @@ -876,4 +876,13 @@ Database read methods, like Record.Get(), returns a boolean indicating whether the record was successfully retrieved. Failing to use this return value can lead to uncaught errors, poor error handling, and a lack of actionable feedback for users when something goes wrong. + + Use the (CR)LFSeparator from the "Type Helper" codeunit. + + + Use the (CR)LFSeparator from the "Type Helper" codeunit from the Base Application to define a line feed (LF) or carriage return (CR) variable. + + + Avoid manually creating helper methods or assigning character values (e.g., Char := 10 or Text[1] := 10) to define line feed (LF) or carriage return (CR) variables. Instead, use the LFSeparator and CRLFSeparator constants provided by the "Type Helper" codeunit from the Base Application. + \ No newline at end of file diff --git a/README.md b/README.md index 04204d0b..3a160f51 100644 --- a/README.md +++ b/README.md @@ -238,5 +238,4 @@ For an example and the default values see: [LinterCop.ruleset.json](./BusinessCe |[LC0082](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0082)|Use `Rec.Find('-')` with `Rec.Next()` for checking exactly one record.|Info| |[LC0083](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0083)|Use new Date/Time/DateTime methods for extracting parts.|Info| |[LC0084](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0084)|Use return value for better error handling.|Info| - - +|[LC0085](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0085)|Use the (CR)LFSeparator from the "Type Helper" codeunit.|Info| \ No newline at end of file From 8759c23b410f0d79a4cdf741131137f7e698a3de Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Fri, 27 Dec 2024 13:07:33 +0100 Subject: [PATCH 19/28] Add Element Access Two to Rule0085 (#849) --- BusinessCentral.LinterCop.Test/Rule0085.cs | 5 +- .../Rule0085/HasDiagnostic/LFSeparatorCode.al | 1 + .../Rule0085/HasDiagnostic/LFSeparatorText.al | 1 + ...de.al => LFSeparatorCodeElementAccess3.al} | 4 +- .../LFSeparatorTextElementAccess2.al | 9 --- ...xt.al => LFSeparatorTextElementAccess3.al} | 2 +- .../Design/Rule0085LFSeparator.cs | 63 ++++++++++++------- 7 files changed, 46 insertions(+), 39 deletions(-) rename BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/{LFSeparatorCode.al => LFSeparatorCodeElementAccess3.al} (60%) delete mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorTextElementAccess2.al rename BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/{LFSeparatorText.al => LFSeparatorTextElementAccess3.al} (78%) diff --git a/BusinessCentral.LinterCop.Test/Rule0085.cs b/BusinessCentral.LinterCop.Test/Rule0085.cs index cb9e12a9..4fe73dcb 100644 --- a/BusinessCentral.LinterCop.Test/Rule0085.cs +++ b/BusinessCentral.LinterCop.Test/Rule0085.cs @@ -25,9 +25,8 @@ public async Task HasDiagnostic(string testCase) } [Test] - [TestCase("LFSeparatorCode")] - [TestCase("LFSeparatorText")] - [TestCase("LFSeparatorTextElementAccess2")] + [TestCase("LFSeparatorCodeElementAccess3")] + [TestCase("LFSeparatorTextElementAccess3")] public async Task NoDiagnostic(string testCase) { var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorCode.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorCode.al index 5fa49395..eb36940f 100644 --- a/BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorCode.al +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorCode.al @@ -5,5 +5,6 @@ codeunit 50100 MyCodeunit MyCode: Code[2]; begin [|MyCode[1] := 10|]; + [|MyCode[2] := 10|]; end; } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorText.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorText.al index 75d32547..f0aef32a 100644 --- a/BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorText.al +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/HasDiagnostic/LFSeparatorText.al @@ -5,5 +5,6 @@ codeunit 50100 MyCodeunit MyText: Text; begin [|MyText[1] := 10|]; + [|MyText[2] := 10|]; end; } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorCode.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorCodeElementAccess3.al similarity index 60% rename from BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorCode.al rename to BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorCodeElementAccess3.al index e2848581..e39f1dc3 100644 --- a/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorCode.al +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorCodeElementAccess3.al @@ -2,8 +2,8 @@ codeunit 50100 MyCodeunit { procedure MyProcedure() var - MyCode: Code[2]; + MyCode: Code[3]; begin - [|MyCode[2] := 10|]; + [|MyCode[3] := 10|]; end; } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorTextElementAccess2.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorTextElementAccess2.al deleted file mode 100644 index b5a57b51..00000000 --- a/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorTextElementAccess2.al +++ /dev/null @@ -1,9 +0,0 @@ -codeunit 50100 MyCodeunit -{ - procedure MyProcedure() - var - MyText: Text; - begin - [|MyText[2] := 10|]; - end; -} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorText.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorTextElementAccess3.al similarity index 78% rename from BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorText.al rename to BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorTextElementAccess3.al index b5a57b51..36f3b9a6 100644 --- a/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorText.al +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0085/NoDiagnostic/LFSeparatorTextElementAccess3.al @@ -4,6 +4,6 @@ codeunit 50100 MyCodeunit var MyText: Text; begin - [|MyText[2] := 10|]; + [|MyText[3] := 10|]; end; } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0085LFSeparator.cs b/BusinessCentral.LinterCop/Design/Rule0085LFSeparator.cs index 61bb3182..bba2b578 100644 --- a/BusinessCentral.LinterCop/Design/Rule0085LFSeparator.cs +++ b/BusinessCentral.LinterCop/Design/Rule0085LFSeparator.cs @@ -32,39 +32,54 @@ private void AnalyzeAssignmentStatement(OperationAnalysisContext ctx) private static bool IsLFSeparatorAssignment(IAssignmentStatement operation) { - // Right side needs to be := 10; - if (operation.Value.Syntax is not LiteralExpressionSyntax sourceLiteral) + // Ensure the right side of the assignment is the literal value "10" + if (!IsValidLFSeparatorValue(operation.Value)) return false; - if (sourceLiteral.Literal is not Int32SignedLiteralValueSyntax sourceInt) - return false; + // Validate the left-hand side for specific cases + return IsValidLFSeparatorTarget(operation.Target); + } - if (sourceInt.GetIdentifierOrLiteralValue() != "10") - return false; + private static bool IsValidLFSeparatorValue(IOperation valueOperation) + { + return valueOperation.Syntax is LiteralExpressionSyntax sourceLiteral && + sourceLiteral.Literal is Int32SignedLiteralValueSyntax sourceInt && + sourceInt.GetIdentifierOrLiteralValue() == "10"; + } - // Left side needs to be Code[1], Text[1] or a Char - switch (operation.Target.Kind) + private static bool IsValidLFSeparatorTarget(IOperation targetOperation) + { + return targetOperation.Kind switch { - case OperationKind.FieldAccess: - if (operation.Target.Syntax is not ElementAccessExpressionSyntax elementAccess || - elementAccess.ArgumentList.Arguments.Count != 1 || - elementAccess.ArgumentList.Arguments[0] is not LiteralExpressionSyntax targetLiteral) - return false; - - if (targetLiteral.Literal is not Int32SignedLiteralValueSyntax targetInt) - return false; + // Case: Code[1], Code[2], Text[1], Text[2] + OperationKind.FieldAccess => IsValidTextOrCodeArrayAccess(targetOperation), - return targetInt.GetIdentifierOrLiteralValue() == "1"; + // Case: Char variable + OperationKind.LocalReferenceExpression or OperationKind.GlobalReferenceExpression => + IsValidCharVariable(targetOperation), - case OperationKind.LocalReferenceExpression: - case OperationKind.GlobalReferenceExpression: - if (operation.Target.Syntax.Kind != SyntaxKind.IdentifierName || - operation.Target.GetSymbol() is not IVariableSymbol identifierNameSymbol) - return false; + _ => false + }; + } - return identifierNameSymbol.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.Char; + private static bool IsValidTextOrCodeArrayAccess(IOperation targetOperation) + { + if (targetOperation.Syntax is not ElementAccessExpressionSyntax elementAccess || + elementAccess.ArgumentList.Arguments.Count != 1 || + elementAccess.ArgumentList.Arguments[0] is not LiteralExpressionSyntax indexLiteral || + indexLiteral.Literal is not Int32SignedLiteralValueSyntax indexInt) + { + return false; } - return false; + var indexValue = indexInt.GetIdentifierOrLiteralValue(); + return indexValue == "1" || indexValue == "2"; + } + + private static bool IsValidCharVariable(IOperation targetOperation) + { + return targetOperation.Syntax.Kind == SyntaxKind.IdentifierName && + targetOperation.GetSymbol() is IVariableSymbol variableSymbol && + variableSymbol.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.Char; } } \ No newline at end of file From 84bb67bfea1dc66b2949f9f11a4b119d6f74c22b Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Fri, 27 Dec 2024 14:06:43 +0100 Subject: [PATCH 20/28] Exclude List objects for Rule0003 (#850) --- ...oNotUseObjectIDsInVariablesOrProperties.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/BusinessCentral.LinterCop/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs b/BusinessCentral.LinterCop/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs index 7335ec31..a70faab9 100644 --- a/BusinessCentral.LinterCop/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs +++ b/BusinessCentral.LinterCop/Design/Rule0003DoNotUseObjectIDsInVariablesOrProperties.cs @@ -26,17 +26,27 @@ private void CheckForObjectIDsInVariablesOrProperties(SyntaxNodeAnalysisContext if (ctx.IsObsoletePendingOrRemoved()) return; - string correctName; + string correctName = string.Empty; if (ctx.ContainingSymbol.Kind == SymbolKind.LocalVariable || ctx.ContainingSymbol.Kind == SymbolKind.GlobalVariable) { IVariableSymbol variable = (IVariableSymbol)ctx.ContainingSymbol; if (variable.Type.IsErrorType() || variable.Type.GetNavTypeKindSafe() == NavTypeKind.DotNet) return; - if (variable.Type.GetNavTypeKindSafe() == NavTypeKind.Array) - correctName = ((IArrayTypeSymbol)variable.Type).ElementType.Name.ToString(); - else - correctName = variable.Type.Name; + switch (variable.Type.GetNavTypeKindSafe()) + { + case NavTypeKind.Array: + correctName = ((IArrayTypeSymbol)variable.Type).ElementType.Name.ToString(); + break; + + case NavTypeKind.List: + //TODO: Find a way to access variable.Type.TypeArguments[0].OriginalDefinition + return; + + default: + correctName = variable.Type.Name; + break; + } if (ctx.Node.GetLastToken().ToString().Trim('"').ToUpper() != correctName.ToUpper()) ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0003DoNotUseObjectIDsInVariablesOrProperties, ctx.Node.GetLocation(), new object[] { ctx.Node.ToString().Trim('"'), correctName })); From d449128c849c5b8e465699dd91d81a36ee3ce466 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Fri, 27 Dec 2024 15:19:54 +0100 Subject: [PATCH 21/28] Resolve false positive on Format() method (#851) --- .../Design/Rule0083BuiltInDateTimeMethod.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/BusinessCentral.LinterCop/Design/Rule0083BuiltInDateTimeMethod.cs b/BusinessCentral.LinterCop/Design/Rule0083BuiltInDateTimeMethod.cs index a133f581..426265c0 100644 --- a/BusinessCentral.LinterCop/Design/Rule0083BuiltInDateTimeMethod.cs +++ b/BusinessCentral.LinterCop/Design/Rule0083BuiltInDateTimeMethod.cs @@ -78,18 +78,16 @@ private void AnalyzeInvocation(OperationAnalysisContext ctx) private string? GetFormatReplacement(IInvocationExpression operation) { - string? formatSpecifier = string.Empty; + if (operation.Arguments.Length < 3) + return string.Empty; - if (operation.Arguments.Length >= 3) - formatSpecifier = operation.Arguments[2].Value.ConstantValue.Value?.ToString(); - - return formatSpecifier switch + return operation.Arguments[2].Value.ConstantValue.Value?.ToString() switch { "" => "Hour", "" => "Minute", "" => "Second", "" => "Millisecond", - _ => "" + _ => string.Empty }; } } From 98a30ef4652ae873f69f4dedc11eefcbb7ca7d87 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Fri, 27 Dec 2024 21:15:03 +0100 Subject: [PATCH 22/28] New Rule LC0086: PageStyle DataType (#852) * New Rule0086: PageStyle DataType * Refine Rule with additional validations * Update documentation * Exclude AL Version lowe then 14.0 --- BusinessCentral.LinterCop.Test/Rule0086.cs | 40 ++++++++++ .../TestCases/Rule0086/HasDiagnostic/Label.al | 5 ++ .../TestCases/Rule0086/HasDiagnostic/Page.al | 22 ++++++ .../TestCases/Rule0086/NoDiagnostic/Enum.al | 9 +++ .../TestCases/Rule0086/NoDiagnostic/Label.al | 5 ++ .../TestCases/Rule0086/NoDiagnostic/Page.al | 30 ++++++++ .../Design/Rule0086PageStyleDataType.cs | 74 +++++++++++++++++++ .../LinterCop.ruleset.json | 5 ++ .../LinterCopAnalyzers.Generated.cs | 9 +++ .../LinterCopAnalyzers.resx | 9 +++ README.md | 3 +- 11 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 BusinessCentral.LinterCop.Test/Rule0086.cs create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0086/HasDiagnostic/Label.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0086/HasDiagnostic/Page.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0086/NoDiagnostic/Enum.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0086/NoDiagnostic/Label.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0086/NoDiagnostic/Page.al create mode 100644 BusinessCentral.LinterCop/Design/Rule0086PageStyleDataType.cs diff --git a/BusinessCentral.LinterCop.Test/Rule0086.cs b/BusinessCentral.LinterCop.Test/Rule0086.cs new file mode 100644 index 00000000..6419c327 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/Rule0086.cs @@ -0,0 +1,40 @@ +#if !LessThenFall2024 +namespace BusinessCentral.LinterCop.Test; + +public class Rule0086 +{ + private string _testCaseDir = ""; + + [SetUp] + public void Setup() + { + _testCaseDir = Path.Combine(Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + "TestCases", "Rule0086"); + } + + [Test] + [TestCase("Label")] + [TestCase("Page")] + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0086PageStyleDataType.Id); + } + + [Test] + [TestCase("Enum")] + [TestCase("Label")] + [TestCase("Page")] + public async Task NoDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0086PageStyleDataType.Id); + } +} +#endif \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0086/HasDiagnostic/Label.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0086/HasDiagnostic/Label.al new file mode 100644 index 00000000..2a217f6d --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0086/HasDiagnostic/Label.al @@ -0,0 +1,5 @@ +codeunit 50100 MyCodeunit +{ + var + UnfavorableTok: Label [|'Unfavorable'|], Locked = true; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0086/HasDiagnostic/Page.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0086/HasDiagnostic/Page.al new file mode 100644 index 00000000..3e74a273 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0086/HasDiagnostic/Page.al @@ -0,0 +1,22 @@ +page 50100 MyPage +{ + layout + { + area(Content) + { + field(MyField; MyField) + { + ApplicationArea = All; + StyleExpr = MyFieldStyle; + } + } + } + + var + MyField, MyFieldStyle : Text; + + trigger OnAfterGetRecord() + begin + MyFieldStyle := [|'Unfavorable'|]; + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0086/NoDiagnostic/Enum.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0086/NoDiagnostic/Enum.al new file mode 100644 index 00000000..eb5dff2f --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0086/NoDiagnostic/Enum.al @@ -0,0 +1,9 @@ +enum 50100 MyEnum +{ + Caption = [|'Unfavorable'|], Locked = true; + + value(0; [|Unfavorable|]) + { + Caption = [|'Unfavorable'|], Locked = true; + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0086/NoDiagnostic/Label.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0086/NoDiagnostic/Label.al new file mode 100644 index 00000000..9fd7d67a --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0086/NoDiagnostic/Label.al @@ -0,0 +1,5 @@ +codeunit 50100 MyCodeunit +{ + var + UnfavorableTok: Label [|'Unfavorable'|]; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0086/NoDiagnostic/Page.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0086/NoDiagnostic/Page.al new file mode 100644 index 00000000..9234c32d --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0086/NoDiagnostic/Page.al @@ -0,0 +1,30 @@ +page 50100 MyPage +{ + Caption = [|'Unfavorable'|], Locked = true; + + layout + { + area(Content) + { + group(Unfavorable) + { + Caption = [|'Unfavorable'|], Locked = true; + + field(MyField; MyField) + { + ApplicationArea = All; + Caption = [|'Unfavorable'|], Locked = true; + } + } + + } + } + + var + MyField, MyFieldStyle : Text; + + trigger OnAfterGetRecord() + begin + MyFieldStyle := Format(PageStyle::[|Unfavorable|]); + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0086PageStyleDataType.cs b/BusinessCentral.LinterCop/Design/Rule0086PageStyleDataType.cs new file mode 100644 index 00000000..db8d9ccb --- /dev/null +++ b/BusinessCentral.LinterCop/Design/Rule0086PageStyleDataType.cs @@ -0,0 +1,74 @@ +#if !LessThenFall2024 +using BusinessCentral.LinterCop.Helpers; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; +using System.Collections.Immutable; + +namespace BusinessCentral.LinterCop.Design; + +[DiagnosticAnalyzer] +public class Rule0086PageStyleDataType : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.Rule0086PageStyleDataType); + + public override VersionCompatibility SupportedVersions => VersionCompatibility.Fall2024OrGreater; + + private static readonly IReadOnlyDictionary StyleKindDictionary = + Enum.GetValues(typeof(StyleKind)) + .Cast() + .ToDictionary( + styleKind => styleKind.ToString(), + styleKind => styleKind.ToString(), + StringComparer.Ordinal); + + public override void Initialize(AnalysisContext context) => + context.RegisterSyntaxNodeAction(AnalyzeStringLiteralToken, SyntaxKind.StringLiteralValue); + + private void AnalyzeStringLiteralToken(SyntaxNodeAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved() || ctx.Node is not StringLiteralValueSyntax stringLiteralNode) + return; + + if (ctx.ContainingSymbol is IPropertySymbol { PropertyKind: PropertyKind.Caption } || + ctx.ContainingSymbol is ISymbol { Kind: SymbolKind.Enum or SymbolKind.EnumValue }) + return; + + var labelSyntax = GetLabelSyntax(stringLiteralNode); + if (labelSyntax is not null && IsUnlockedLabel(labelSyntax)) + return; + + var stringLiteralValue = stringLiteralNode.Value.Value?.ToString(); + if (string.IsNullOrEmpty(stringLiteralValue)) + return; + + if (StyleKindDictionary.TryGetValue(stringLiteralValue, out string styleKind)) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0086PageStyleDataType, + ctx.Node.GetLocation(), + stringLiteralValue, + styleKind)); + } + } + + private static LabelSyntax? GetLabelSyntax(StringLiteralValueSyntax stringLiteralNode) + { + if (stringLiteralNode.GetFirstParent(SyntaxKind.Label) is LabelSyntax parentNode) + return parentNode; + + return null; + } + + private static bool IsUnlockedLabel(LabelSyntax labelSyntax) + { + // Check if the label has a "Locked" property set to true + bool isLocked = labelSyntax.Properties?.Values + .Any(prop => string.Equals(prop.Identifier.ValueText, "Locked", StringComparison.OrdinalIgnoreCase)) ?? false; + + // If it's locked, return false (i.e., not unlocked), otherwise true + return !isLocked; + } +} +#endif \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCop.ruleset.json b/BusinessCentral.LinterCop/LinterCop.ruleset.json index 2e76e55d..8bc7974d 100644 --- a/BusinessCentral.LinterCop/LinterCop.ruleset.json +++ b/BusinessCentral.LinterCop/LinterCop.ruleset.json @@ -426,6 +426,11 @@ "id": "LC0085", "action": "Info", "justification": "Use the (CR)LFSeparator from the Type Helper codeunit." + }, + { + "id": "LC0086", + "action": "Info", + "justification": "Use the new PageStyle datatype instead string literals." } ] } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs b/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs index 0e3ea135..3ea4c504 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs @@ -875,4 +875,13 @@ public static class DiagnosticDescriptors description: LinterCopAnalyzers.GetLocalizableString("Rule0085LFSeparatorDescription"), helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0085"); + public static readonly DiagnosticDescriptor Rule0086PageStyleDataType = new( + id: LinterCopAnalyzers.AnalyzerPrefix + "0086", + title: LinterCopAnalyzers.GetLocalizableString("Rule0086PageStyleDataTypeTitle"), + messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0086PageStyleDataTypeFormat"), + category: "Design", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: LinterCopAnalyzers.GetLocalizableString("Rule0086PageStyleDataTypeDescription"), + helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0086"); } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx index 42569122..5a019733 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx @@ -885,4 +885,13 @@ Avoid manually creating helper methods or assigning character values (e.g., Char := 10 or Text[1] := 10) to define line feed (LF) or carriage return (CR) variables. Instead, use the LFSeparator and CRLFSeparator constants provided by the "Type Helper" codeunit from the Base Application. + + Use the new PageStyle datatype instead string literals. + + + Avoid using the string literal '{0}' for page styling. Use the PageStyle datatype instead (PageStyle::{1}). + + + Adopting the use of the new PageStyle datatype allows to more easily get the supported pagestyles via IntelliSense and avoids incorrect behaviour when a typo is made in hardcoded strings or label variables. + \ No newline at end of file diff --git a/README.md b/README.md index 3a160f51..46bbf08d 100644 --- a/README.md +++ b/README.md @@ -238,4 +238,5 @@ For an example and the default values see: [LinterCop.ruleset.json](./BusinessCe |[LC0082](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0082)|Use `Rec.Find('-')` with `Rec.Next()` for checking exactly one record.|Info| |[LC0083](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0083)|Use new Date/Time/DateTime methods for extracting parts.|Info| |[LC0084](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0084)|Use return value for better error handling.|Info| -|[LC0085](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0085)|Use the (CR)LFSeparator from the "Type Helper" codeunit.|Info| \ No newline at end of file +|[LC0085](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0085)|Use the (CR)LFSeparator from the "Type Helper" codeunit.|Info| +|[LC0086](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0086)|Use the new `PageStyle` datatype instead string literals.|Info|14.0| \ No newline at end of file From bd3e29e4924b694f5f5a6301ab63c1eb549696c0 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:37:15 +0100 Subject: [PATCH 23/28] Resolve non-working Rule0023 (#854) --- BusinessCentral.LinterCop.Test/Rule0023.cs | 38 +++++++++++++ .../Rule0023/HasDiagnostic/BrickIsMissing.al | 12 +++++ .../HasDiagnostic/DropDownIsMissing.al | 12 +++++ .../HasDiagnostic/FieldgroupsIsMissing.al | 13 +++++ .../NoDiagnostic/HasBrickAndDropDown.al | 13 +++++ .../Rule0023AlwaysSpecifyFieldgroups.cs | 53 ++++++++++++++----- 6 files changed, 129 insertions(+), 12 deletions(-) create mode 100644 BusinessCentral.LinterCop.Test/Rule0023.cs create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/BrickIsMissing.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/DropDownIsMissing.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/FieldgroupsIsMissing.al create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0023/NoDiagnostic/HasBrickAndDropDown.al diff --git a/BusinessCentral.LinterCop.Test/Rule0023.cs b/BusinessCentral.LinterCop.Test/Rule0023.cs new file mode 100644 index 00000000..e0ddd957 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/Rule0023.cs @@ -0,0 +1,38 @@ +namespace BusinessCentral.LinterCop.Test; + +public class Rule0023 +{ + private string _testCaseDir = ""; + + [SetUp] + public void Setup() + { + _testCaseDir = Path.Combine(Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + "TestCases", "Rule0023"); + } + + //TODO: Resolve "There is no issue reported for LC0023 at [96...107]." for these tests. + // [Test] + // [TestCase("BrickIsMissing")] + // [TestCase("DropDownIsMissing")] + // [TestCase("FieldgroupsIsMissing")] + // public async Task HasDiagnostic(string testCase) + // { + // var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) + // .ConfigureAwait(false); + + // var fixture = RoslynFixtureFactory.Create(); + // fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0023AlwaysSpecifyFieldgroups.Id); + // } + + [Test] + [TestCase("HasBrickAndDropDown")] + public async Task NoDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create(); + fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0023AlwaysSpecifyFieldgroups.Id); + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/BrickIsMissing.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/BrickIsMissing.al new file mode 100644 index 00000000..f179fe09 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/BrickIsMissing.al @@ -0,0 +1,12 @@ +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } + + [|fieldgroups|] + { + fieldgroup(DropDown; MyField) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/DropDownIsMissing.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/DropDownIsMissing.al new file mode 100644 index 00000000..b8ff527e --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/DropDownIsMissing.al @@ -0,0 +1,12 @@ +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } + + [|fieldgroups|] + { + fieldgroup(Brick; MyField) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/FieldgroupsIsMissing.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/FieldgroupsIsMissing.al new file mode 100644 index 00000000..9d455669 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/FieldgroupsIsMissing.al @@ -0,0 +1,13 @@ +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } + + keys + { + key(Key1; MyField) { } + } +[| |] +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0023/NoDiagnostic/HasBrickAndDropDown.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0023/NoDiagnostic/HasBrickAndDropDown.al new file mode 100644 index 00000000..ebd3ad9e --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0023/NoDiagnostic/HasBrickAndDropDown.al @@ -0,0 +1,13 @@ +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } + + [|fieldgroups|] + { + fieldgroup(Brick; MyField) { } + fieldgroup(DropDown; MyField) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0023AlwaysSpecifyFieldgroups.cs b/BusinessCentral.LinterCop/Design/Rule0023AlwaysSpecifyFieldgroups.cs index ca7eac8a..d8656e5f 100644 --- a/BusinessCentral.LinterCop/Design/Rule0023AlwaysSpecifyFieldgroups.cs +++ b/BusinessCentral.LinterCop/Design/Rule0023AlwaysSpecifyFieldgroups.cs @@ -2,6 +2,7 @@ using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; +using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using Microsoft.Dynamics.Nav.CodeAnalysis.Text; using System.Collections.Immutable; @@ -11,7 +12,7 @@ namespace BusinessCentral.LinterCop.Design; public class Rule0023AlwaysSpecifyFieldgroups : DiagnosticAnalyzer { public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create(DiagnosticDescriptors.Rule0023AlwaysSpecifyFieldgroups, DiagnosticDescriptors.Rule0000ErrorInRule); + ImmutableArray.Create(DiagnosticDescriptors.Rule0023AlwaysSpecifyFieldgroups); public override void Initialize(AnalysisContext context) => context.RegisterSymbolAction(new Action(this.CheckFieldgroups), SymbolKind.Table); @@ -24,21 +25,49 @@ private void CheckFieldgroups(SymbolAnalysisContext ctx) if (IsTableOfTypeSetupTable(table)) return; - Location FieldGroupLocation = table.GetLocation(); - if (!table.Keys.IsEmpty) + var fieldGroupLocation = GetFieldGroupLocation(ctx, table); + + CheckFieldGroup(ctx, table, "Brick", fieldGroupLocation); + CheckFieldGroup(ctx, table, "DropDown", fieldGroupLocation); + } + + private static Location GetFieldGroupLocation(SymbolAnalysisContext ctx, ITableTypeSymbol table) + { + var location = table.GetLocation(); + + if (ctx.Symbol.DeclaringSyntaxReference?.GetSyntax(ctx.CancellationToken) is not TableSyntax tableSyntax) + return location; + + if (tableSyntax.FieldGroups is not null) { - FieldGroupLocation = table.Keys.Last().GetLocation(); - var span = FieldGroupLocation.SourceSpan; - FieldGroupLocation = Location.Create(FieldGroupLocation.SourceTree!, new TextSpan(span.End + 9, 1)); // Should result in the blank line right after the keys section + var fieldGroupNode = tableSyntax.FieldGroups + .ChildNodesAndTokens() + .FirstOrDefault(node => node.Kind == SyntaxKind.FieldGroupsKeyword); + + var fieldGroupNodeLocation = fieldGroupNode.GetLocation(); + if (fieldGroupNodeLocation is not null) + return fieldGroupNode.GetLocation()!; } - if (!table.FieldGroups.Any(item => item.Name == "Brick")) - ctx.ReportDiagnostic(Diagnostic.Create( - DiagnosticDescriptors.Rule0023AlwaysSpecifyFieldgroups, FieldGroupLocation, "Brick", table.Name)); + if (tableSyntax.Keys is not null && table.GetLocation().SourceTree is SyntaxTree sourceTree) + { + var startPos = tableSyntax.Keys.Span.End + 2; // Should result in the blank line right after the keys section + return Location.Create(sourceTree, new TextSpan(startPos, 1)); + } + + return location; + } - if (!table.FieldGroups.Any(item => item.Name == "DropDown")) + private static void CheckFieldGroup(SymbolAnalysisContext ctx, ITableTypeSymbol table, string fieldGroupName, Location location) + { + if (!table.FieldGroups.Any(item => item.Name == fieldGroupName && item.Fields.Length > 0)) + { ctx.ReportDiagnostic(Diagnostic.Create( - DiagnosticDescriptors.Rule0023AlwaysSpecifyFieldgroups, FieldGroupLocation, "DropDown", table.Name)); + DiagnosticDescriptors.Rule0023AlwaysSpecifyFieldgroups, + location, + fieldGroupName, + table.Name)); + } } private static bool IsTableOfTypeSetupTable(ITableTypeSymbol table) @@ -57,4 +86,4 @@ private static bool IsTableOfTypeSetupTable(ITableTypeSymbol table) return true; } -} +} \ No newline at end of file From 655949af8093bbf9a3eeb5996a95c879ea74f115 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Tue, 7 Jan 2025 09:34:08 +0100 Subject: [PATCH 24/28] Handle trailing comments (#857) --- BusinessCentral.LinterCop.Test/Rule0017.cs | 7 +++++-- ...nment.al => AssignmentWithLeadingComment.al} | 0 .../AssignmentWithTrailingComment.al | 16 ++++++++++++++++ ...alidate.al => ValidateWithLeadingComment.al} | 0 .../NoDiagnostic/ValidateWithTrailingComment.al | 17 +++++++++++++++++ .../Design/Rule0017WriteToFlowField.cs | 3 ++- 6 files changed, 40 insertions(+), 3 deletions(-) rename BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/{Assignment.al => AssignmentWithLeadingComment.al} (100%) create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/AssignmentWithTrailingComment.al rename BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/{Validate.al => ValidateWithLeadingComment.al} (100%) create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/ValidateWithTrailingComment.al diff --git a/BusinessCentral.LinterCop.Test/Rule0017.cs b/BusinessCentral.LinterCop.Test/Rule0017.cs index 3004fa3c..be42b39b 100644 --- a/BusinessCentral.LinterCop.Test/Rule0017.cs +++ b/BusinessCentral.LinterCop.Test/Rule0017.cs @@ -24,8 +24,11 @@ public async Task HasDiagnostic(string testCase) } [Test] - [TestCase("Assignment")] - [TestCase("Validate")] + [TestCase("AssignmentWithLeadingComment")] + [TestCase("AssignmentWithTrailingComment")] + [TestCase("ValidateWithLeadingComment")] + //TODO: The HasExplainingComment method in the Rule0017WriteToFlowField class doesn't support this scenario currently + // [TestCase("ValidateWithTrailingComment")] public async Task NoDiagnostic(string testCase) { var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/Assignment.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/AssignmentWithLeadingComment.al similarity index 100% rename from BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/Assignment.al rename to BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/AssignmentWithLeadingComment.al diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/AssignmentWithTrailingComment.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/AssignmentWithTrailingComment.al new file mode 100644 index 00000000..8473a42f --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/AssignmentWithTrailingComment.al @@ -0,0 +1,16 @@ +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + field(2; MyField2; Boolean) + { + FieldClass = FlowField; + } + } + + procedure MyProcedure(); + begin + [|Rec.MyField2|] := false; // Comment + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/Validate.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/ValidateWithLeadingComment.al similarity index 100% rename from BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/Validate.al rename to BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/ValidateWithLeadingComment.al diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/ValidateWithTrailingComment.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/ValidateWithTrailingComment.al new file mode 100644 index 00000000..392c337e --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0017/NoDiagnostic/ValidateWithTrailingComment.al @@ -0,0 +1,17 @@ + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) { } + field(2; MyField2; Boolean) + { + FieldClass = FlowField; + } + } + + procedure MyProcedure(); + begin + Rec.Validate([|MyField2|], false); // Comment + end; +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0017WriteToFlowField.cs b/BusinessCentral.LinterCop/Design/Rule0017WriteToFlowField.cs index eaf79701..d20756d4 100644 --- a/BusinessCentral.LinterCop/Design/Rule0017WriteToFlowField.cs +++ b/BusinessCentral.LinterCop/Design/Rule0017WriteToFlowField.cs @@ -76,5 +76,6 @@ operation.Target is ITextIndexAccess textIndexAccess && } private bool HasExplainingComment(IOperation operation) => - operation.Syntax.GetLeadingTrivia().Any(trivia => trivia.IsKind(SyntaxKind.LineCommentTrivia)); + operation.Syntax.GetLeadingTrivia().Any(trivia => trivia.IsKind(SyntaxKind.LineCommentTrivia)) || + operation.Syntax.GetTrailingTrivia().Any(trivia => trivia.IsKind(SyntaxKind.LineCommentTrivia)); } \ No newline at end of file From f0f05ce203554ebfe11f52b463bac3ff872a4d92 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Tue, 7 Jan 2025 11:11:43 +0100 Subject: [PATCH 25/28] Resolve false positive (#859) --- BusinessCentral.LinterCop.Test/Rule0075.cs | 1 + .../NoDiagnostic/RecordGetCode10ToCode20.al | 17 +++++++++++++++++ .../Rule0075RecordGetProcedureArguments.cs | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetCode10ToCode20.al diff --git a/BusinessCentral.LinterCop.Test/Rule0075.cs b/BusinessCentral.LinterCop.Test/Rule0075.cs index fcf11e52..53990ddd 100644 --- a/BusinessCentral.LinterCop.Test/Rule0075.cs +++ b/BusinessCentral.LinterCop.Test/Rule0075.cs @@ -38,6 +38,7 @@ public async Task HasDiagnostic(string testCase) [TestCase("ImplicitConversiontIntegerToEnum")] [TestCase("ImplicitConversiontLabelToCode")] [TestCase("RecordGetBuiltInMethodRecordId")] + [TestCase("RecordGetCode10ToCode20")] [TestCase("RecordGetFieldRecordId")] [TestCase("RecordGetGlobalVariable")] [TestCase("RecordGetLocalVariable")] diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetCode10ToCode20.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetCode10ToCode20.al new file mode 100644 index 00000000..64bc2d63 --- /dev/null +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0075/NoDiagnostic/RecordGetCode10ToCode20.al @@ -0,0 +1,17 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure(MyCode: Code[10]) + var + MyTabe: Record MyTabe; + begin + [|MyTabe.Get(MyCode)|]; + end; +} + +table 50100 MyTabe +{ + fields + { + field(1; MyField; Code[20]) { } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs b/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs index d073ae1c..1c6422e1 100644 --- a/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs +++ b/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs @@ -119,7 +119,7 @@ private bool AreFieldCompatible(IArgument argument, IFieldSymbol field) if (argumentNavType == NavTypeKind.Enum && fieldNavType == NavTypeKind.Enum) return argumentType.OriginalDefinition == fieldType.OriginalDefinition; - if ((argumentNavType == fieldNavType && argumentType.Length == fieldType.Length) || + if ((argumentNavType == fieldNavType && argumentType.Length <= fieldType.Length) || argumentNavType == NavTypeKind.None || argumentNavType == NavTypeKind.Joker) return true; From 0ea4e7f911f368d8f0f8d075b84a1d4d5766d1da Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Tue, 7 Jan 2025 14:12:26 +0100 Subject: [PATCH 26/28] Compare Enums through .Equals (#861) --- .../Design/Rule0075RecordGetProcedureArguments.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs b/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs index 1c6422e1..18139561 100644 --- a/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs +++ b/BusinessCentral.LinterCop/Design/Rule0075RecordGetProcedureArguments.cs @@ -42,7 +42,6 @@ private void AnalyzeAssignmentStatement(OperationAnalysisContext ctx) if (ctx.IsObsoletePendingOrRemoved() || ctx.Operation is not IInvocationExpression operation) return; - if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod || !SemanticFacts.IsSameName(operation.TargetMethod.Name, "Get")) return; @@ -117,7 +116,7 @@ private bool AreFieldCompatible(IArgument argument, IFieldSymbol field) var fieldNavType = fieldType.GetNavTypeKindSafe(); if (argumentNavType == NavTypeKind.Enum && fieldNavType == NavTypeKind.Enum) - return argumentType.OriginalDefinition == fieldType.OriginalDefinition; + return argumentType.OriginalDefinition.Equals(fieldType.OriginalDefinition); if ((argumentNavType == fieldNavType && argumentType.Length <= fieldType.Length) || argumentNavType == NavTypeKind.None || From 7cf4f8ad7de0e22988e8791b2b99107f82fb3c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Maro=C5=84?= Date: Wed, 8 Jan 2025 10:42:12 +0100 Subject: [PATCH 27/28] Refactor Rule0023 to simplify field group location retrieval (#862) * Refactor Rule0023 to simplify field group location retrieval * Fix tests for Rule0023 * Fix wrong class --------- Co-authored-by: Arthur van de Vondervoort --- BusinessCentral.LinterCop.Test/Rule0023.cs | 24 +++++++------- .../Rule0023/HasDiagnostic/BrickIsMissing.al | 4 +-- .../HasDiagnostic/DropDownIsMissing.al | 4 +-- .../HasDiagnostic/FieldgroupsIsMissing.al | 13 -------- .../Rule0023AlwaysSpecifyFieldgroups.cs | 32 ++----------------- 5 files changed, 17 insertions(+), 60 deletions(-) delete mode 100644 BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/FieldgroupsIsMissing.al diff --git a/BusinessCentral.LinterCop.Test/Rule0023.cs b/BusinessCentral.LinterCop.Test/Rule0023.cs index e0ddd957..6943e714 100644 --- a/BusinessCentral.LinterCop.Test/Rule0023.cs +++ b/BusinessCentral.LinterCop.Test/Rule0023.cs @@ -11,19 +11,17 @@ public void Setup() "TestCases", "Rule0023"); } - //TODO: Resolve "There is no issue reported for LC0023 at [96...107]." for these tests. - // [Test] - // [TestCase("BrickIsMissing")] - // [TestCase("DropDownIsMissing")] - // [TestCase("FieldgroupsIsMissing")] - // public async Task HasDiagnostic(string testCase) - // { - // var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) - // .ConfigureAwait(false); + [Test] + [TestCase("BrickIsMissing")] + [TestCase("DropDownIsMissing")] + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al")) + .ConfigureAwait(false); - // var fixture = RoslynFixtureFactory.Create(); - // fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0023AlwaysSpecifyFieldgroups.Id); - // } + var fixture = RoslynFixtureFactory.Create(); + fixture.HasDiagnostic(code, DiagnosticDescriptors.Rule0023AlwaysSpecifyFieldgroups.Id); + } [Test] [TestCase("HasBrickAndDropDown")] @@ -32,7 +30,7 @@ public async Task NoDiagnostic(string testCase) var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al")) .ConfigureAwait(false); - var fixture = RoslynFixtureFactory.Create(); + var fixture = RoslynFixtureFactory.Create(); fixture.NoDiagnosticAtMarker(code, DiagnosticDescriptors.Rule0023AlwaysSpecifyFieldgroups.Id); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/BrickIsMissing.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/BrickIsMissing.al index f179fe09..b1c8a188 100644 --- a/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/BrickIsMissing.al +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/BrickIsMissing.al @@ -1,11 +1,11 @@ -table 50100 MyTable +table 50100 [|MyTable|] { fields { field(1; MyField; Integer) { } } - [|fieldgroups|] + fieldgroups { fieldgroup(DropDown; MyField) { } } diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/DropDownIsMissing.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/DropDownIsMissing.al index b8ff527e..cb53801d 100644 --- a/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/DropDownIsMissing.al +++ b/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/DropDownIsMissing.al @@ -1,11 +1,11 @@ -table 50100 MyTable +table 50100 [|MyTable|] { fields { field(1; MyField; Integer) { } } - [|fieldgroups|] + fieldgroups { fieldgroup(Brick; MyField) { } } diff --git a/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/FieldgroupsIsMissing.al b/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/FieldgroupsIsMissing.al deleted file mode 100644 index 9d455669..00000000 --- a/BusinessCentral.LinterCop.Test/TestCases/Rule0023/HasDiagnostic/FieldgroupsIsMissing.al +++ /dev/null @@ -1,13 +0,0 @@ -table 50100 MyTable -{ - fields - { - field(1; MyField; Integer) { } - } - - keys - { - key(Key1; MyField) { } - } -[| |] -} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0023AlwaysSpecifyFieldgroups.cs b/BusinessCentral.LinterCop/Design/Rule0023AlwaysSpecifyFieldgroups.cs index d8656e5f..0bcd0bf7 100644 --- a/BusinessCentral.LinterCop/Design/Rule0023AlwaysSpecifyFieldgroups.cs +++ b/BusinessCentral.LinterCop/Design/Rule0023AlwaysSpecifyFieldgroups.cs @@ -25,37 +25,9 @@ private void CheckFieldgroups(SymbolAnalysisContext ctx) if (IsTableOfTypeSetupTable(table)) return; - var fieldGroupLocation = GetFieldGroupLocation(ctx, table); - CheckFieldGroup(ctx, table, "Brick", fieldGroupLocation); - CheckFieldGroup(ctx, table, "DropDown", fieldGroupLocation); - } - - private static Location GetFieldGroupLocation(SymbolAnalysisContext ctx, ITableTypeSymbol table) - { - var location = table.GetLocation(); - - if (ctx.Symbol.DeclaringSyntaxReference?.GetSyntax(ctx.CancellationToken) is not TableSyntax tableSyntax) - return location; - - if (tableSyntax.FieldGroups is not null) - { - var fieldGroupNode = tableSyntax.FieldGroups - .ChildNodesAndTokens() - .FirstOrDefault(node => node.Kind == SyntaxKind.FieldGroupsKeyword); - - var fieldGroupNodeLocation = fieldGroupNode.GetLocation(); - if (fieldGroupNodeLocation is not null) - return fieldGroupNode.GetLocation()!; - } - - if (tableSyntax.Keys is not null && table.GetLocation().SourceTree is SyntaxTree sourceTree) - { - var startPos = tableSyntax.Keys.Span.End + 2; // Should result in the blank line right after the keys section - return Location.Create(sourceTree, new TextSpan(startPos, 1)); - } - - return location; + CheckFieldGroup(ctx, table, "Brick", table.GetLocation()); + CheckFieldGroup(ctx, table, "DropDown", table.GetLocation()); } private static void CheckFieldGroup(SymbolAnalysisContext ctx, ITableTypeSymbol table, string fieldGroupName, Location location) From d6b8696c79807357e055aa491aee24e009e021b7 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort <44637996+Arthurvdv@users.noreply.github.com> Date: Wed, 8 Jan 2025 18:27:25 +0100 Subject: [PATCH 28/28] Improve recomendation (#865) --- BusinessCentral.LinterCop/LinterCopAnalyzers.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx index 5a019733..36ad9c2a 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx @@ -853,7 +853,7 @@ Use Rec.Find('-') with Rec.Next() for checking exactly one record. - Use {0}.Find('-') together with {0}.Next() instead of {0}.Count() for performance optimization. Replace {0}.Count() with: {0}.Find('-') and (Rec.Next() {1} 0). + Use {0}.Find('-') together with {0}.Next() instead of {0}.Count() for performance optimization. Replace {0}.Count() with: {0}.Find('-') and ({0}.Next() {1} 0). Instead of relying on Rec.Count(), you should use a combination of Rec.Find('-') and Rec.Next() for faster and more efficient record checks.