From de5fcc804360e90cfebda61f93d04268aca1992a Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Sun, 27 Sep 2026 10:59:49 +0200 Subject: [PATCH 1/5] test(AC0033,AC0034): add RequiredInterfaceImplementation wiring and fixtures Co-Authored-By: Claude Fable 5.1 --- .../HasDiagnostic/BothRulesOneObject.al | 28 ++++++++ .../HasDiagnostic/GlobalVariable.al | 14 ++++ .../HasDiagnostic/LocalVariable.al | 17 +++++ .../MultipleVariablesOneDiagnostic.al | 21 ++++++ .../HasDiagnostic/NamespacedUsage.al | 16 +++++ .../HasDiagnostic/PageActionTriggerLocal.al | 38 ++++++++++ .../PageExtensionActionTriggerLocal.al | 48 +++++++++++++ .../HasDiagnostic/PageFieldTriggerLocal.al | 38 ++++++++++ .../HasDiagnostic/Parameter.al | 15 ++++ .../HasDiagnostic/QueryTriggerLocal.al | 37 ++++++++++ .../ReportDataItemTriggerLocal.al | 35 +++++++++ .../HasDiagnostic/ReturnValue.al | 15 ++++ .../TableExtensionTriggerLocal.al | 33 +++++++++ .../HasDiagnostic/TableFieldTriggerLocal.al | 23 ++++++ .../HasDiagnostic/TableTriggerLocal.al | 24 +++++++ .../HasDiagnostic/TwoObjectsTwoDiagnostics.al | 23 ++++++ .../XmlPortElementTriggerLocal.al | 39 ++++++++++ .../NoDiagnostic/HandlerInOtherCodeunit.al | 21 ++++++ .../NoDiagnostic/HandlerInSameCodeunit.al | 18 +++++ .../NoDiagnostic/IdMatchNameMismatch.al | 9 +++ .../NoDiagnostic/NameMatchIdMismatch.al | 9 +++ .../NoDiagnostic/NamespacedHandler.al | 23 ++++++ .../NoDiagnostic/NoUsage.al | 14 ++++ .../NoDiagnostic/ObjectReferenceOnly.al | 16 +++++ .../NoDiagnostic/ObsoletePendingObject.al | 17 +++++ .../NoDiagnostic/TestCodeunit.al | 19 +++++ .../NoDiagnostic/TestRunnerCodeunit.al | 19 +++++ .../RestClientRequiresHttpClientHandler.cs | 72 +++++++++++++++++++ .../HasDiagnostic/BothRulesOneObject.al | 28 ++++++++ .../GlobalVariableFeatureTelemetry.al | 18 +++++ .../HasDiagnostic/GlobalVariableTelemetry.al | 18 +++++ .../HasDiagnostic/LocalVariable.al | 21 ++++++ .../MultipleVariablesOneDiagnostic.al | 19 +++++ .../HasDiagnostic/PageActionTriggerLocal.al | 42 +++++++++++ .../HasDiagnostic/Parameter.al | 19 +++++ .../HasDiagnostic/ReturnValue.al | 19 +++++ .../HasDiagnostic/TwoObjectsTwoDiagnostics.al | 27 +++++++ .../NoDiagnostic/IdMatchNameMismatch.al | 9 +++ .../NoDiagnostic/LoggerInOtherCodeunit.al | 25 +++++++ .../NoDiagnostic/LoggerInSameCodeunit.al | 22 ++++++ .../NoDiagnostic/NameMatchIdMismatch.al | 9 +++ .../NoDiagnostic/NamespacedLogger.al | 27 +++++++ .../NoDiagnostic/NoUsage.al | 20 ++++++ .../NoDiagnostic/ObjectReferenceOnly.al | 20 ++++++ .../NoDiagnostic/ObsoletePendingObject.al | 21 ++++++ .../NoDiagnostic/TestCodeunit.al | 23 ++++++ .../NoDiagnostic/TestRunnerCodeunit.al | 23 ++++++ .../TelemetryRequiresTelemetryLogger.cs | 58 +++++++++++++++ .../ALCops.ApplicationCopAnalyzers.resx | 18 +++++ .../RequiredInterfaceImplementation.cs | 17 +++++ .../DiagnosticDescriptors.cs | 20 ++++++ src/ALCops.ApplicationCop/DiagnosticIds.cs | 2 + src/ALCops.Common/Reflection/EnumProvider.cs | 3 + 53 files changed, 1229 insertions(+) create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/BothRulesOneObject.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/GlobalVariable.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/LocalVariable.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/MultipleVariablesOneDiagnostic.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/NamespacedUsage.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/PageActionTriggerLocal.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/PageExtensionActionTriggerLocal.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/PageFieldTriggerLocal.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/Parameter.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/QueryTriggerLocal.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/ReportDataItemTriggerLocal.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/ReturnValue.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/TableExtensionTriggerLocal.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/TableFieldTriggerLocal.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/TableTriggerLocal.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/TwoObjectsTwoDiagnostics.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/XmlPortElementTriggerLocal.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/HandlerInOtherCodeunit.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/HandlerInSameCodeunit.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/IdMatchNameMismatch.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/NameMatchIdMismatch.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/NamespacedHandler.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/NoUsage.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/ObjectReferenceOnly.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/ObsoletePendingObject.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/TestCodeunit.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/TestRunnerCodeunit.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/RestClientRequiresHttpClientHandler.cs create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/BothRulesOneObject.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/GlobalVariableFeatureTelemetry.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/GlobalVariableTelemetry.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/LocalVariable.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/MultipleVariablesOneDiagnostic.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/PageActionTriggerLocal.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/Parameter.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/ReturnValue.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/TwoObjectsTwoDiagnostics.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/IdMatchNameMismatch.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/LoggerInOtherCodeunit.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/LoggerInSameCodeunit.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/NameMatchIdMismatch.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/NamespacedLogger.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/NoUsage.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/ObjectReferenceOnly.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/ObsoletePendingObject.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/TestCodeunit.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/TestRunnerCodeunit.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/TelemetryRequiresTelemetryLogger.cs create mode 100644 src/ALCops.ApplicationCop/Analyzers/RequiredInterfaceImplementation.cs diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/BothRulesOneObject.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/BothRulesOneObject.al new file mode 100644 index 00000000..928ce083 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/BothRulesOneObject.al @@ -0,0 +1,28 @@ +codeunit 50100 [|MyCodeunit|] +{ + var + RestClient: Codeunit "Rest Client"; + Telemetry: Codeunit Telemetry; +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} + +codeunit 8711 Telemetry +{ +} + +codeunit 8703 "Feature Telemetry" +{ +} + +interface "Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]); +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/GlobalVariable.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/GlobalVariable.al new file mode 100644 index 00000000..4b7732de --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/GlobalVariable.al @@ -0,0 +1,14 @@ +codeunit 50100 [|MyCodeunit|] +{ + var + RestClient: Codeunit "Rest Client"; +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/LocalVariable.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/LocalVariable.al new file mode 100644 index 00000000..728e0f24 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/LocalVariable.al @@ -0,0 +1,17 @@ +codeunit 50100 [|MyCodeunit|] +{ + procedure DoSomething() + var + RestClient: Codeunit "Rest Client"; + begin + end; +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/MultipleVariablesOneDiagnostic.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/MultipleVariablesOneDiagnostic.al new file mode 100644 index 00000000..14d1e7eb --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/MultipleVariablesOneDiagnostic.al @@ -0,0 +1,21 @@ +codeunit 50100 [|MyCodeunit|] +{ + var + RestClient: Codeunit "Rest Client"; + OtherRestClient: Codeunit "Rest Client"; + + procedure DoSomething() + var + LocalRestClient: Codeunit "Rest Client"; + begin + end; +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/NamespacedUsage.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/NamespacedUsage.al new file mode 100644 index 00000000..d97c0f45 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/NamespacedUsage.al @@ -0,0 +1,16 @@ +namespace MyPublisher.MyExtension.MyAppDomain; + +codeunit 50100 [|MyCodeunit|] +{ + var + RestClient: Codeunit MyPublisher.MyExtension.MyAppDomain."Rest Client"; +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/PageActionTriggerLocal.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/PageActionTriggerLocal.al new file mode 100644 index 00000000..22441f59 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/PageActionTriggerLocal.al @@ -0,0 +1,38 @@ +page 50100 [|MyPage|] +{ + SourceTable = MyTable; + + actions + { + area(Processing) + { + action(MyAction) + { + trigger OnAction() + var + RestClient: Codeunit "Rest Client"; + begin + end; + } + } + } +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) + { + } + } +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/PageExtensionActionTriggerLocal.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/PageExtensionActionTriggerLocal.al new file mode 100644 index 00000000..3dafef22 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/PageExtensionActionTriggerLocal.al @@ -0,0 +1,48 @@ +pageextension 50100 [|MyPageExtension|] extends MyPage +{ + actions + { + addlast(Processing) + { + action(MyNewAction) + { + trigger OnAction() + var + RestClient: Codeunit "Rest Client"; + begin + end; + } + } + } +} + +page 50100 MyPage +{ + SourceTable = MyTable; + + actions + { + area(Processing) + { + } + } +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) + { + } + } +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/PageFieldTriggerLocal.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/PageFieldTriggerLocal.al new file mode 100644 index 00000000..849b80a1 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/PageFieldTriggerLocal.al @@ -0,0 +1,38 @@ +page 50100 [|MyPage|] +{ + SourceTable = MyTable; + + layout + { + area(Content) + { + field(MyField; Rec.MyField) + { + trigger OnValidate() + var + RestClient: Codeunit "Rest Client"; + begin + end; + } + } + } +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) + { + } + } +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/Parameter.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/Parameter.al new file mode 100644 index 00000000..07047cc7 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/Parameter.al @@ -0,0 +1,15 @@ +codeunit 50100 [|MyCodeunit|] +{ + procedure DoSomething(RestClient: Codeunit "Rest Client") + begin + end; +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/QueryTriggerLocal.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/QueryTriggerLocal.al new file mode 100644 index 00000000..053733a9 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/QueryTriggerLocal.al @@ -0,0 +1,37 @@ +query 50100 [|MyQuery|] +{ + elements + { + dataitem(MyTable; MyTable) + { + column(MyField; MyField) + { + } + } + } + + trigger OnBeforeOpen() + var + RestClient: Codeunit "Rest Client"; + begin + end; +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) + { + } + } +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/ReportDataItemTriggerLocal.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/ReportDataItemTriggerLocal.al new file mode 100644 index 00000000..541d697f --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/ReportDataItemTriggerLocal.al @@ -0,0 +1,35 @@ +report 50100 [|MyReport|] +{ + ProcessingOnly = true; + + dataset + { + dataitem(MyTable; MyTable) + { + trigger OnAfterGetRecord() + var + RestClient: Codeunit "Rest Client"; + begin + end; + } + } +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) + { + } + } +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/ReturnValue.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/ReturnValue.al new file mode 100644 index 00000000..521f5335 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/ReturnValue.al @@ -0,0 +1,15 @@ +codeunit 50100 [|MyCodeunit|] +{ + procedure Create() Result: Codeunit "Rest Client" + begin + end; +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/TableExtensionTriggerLocal.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/TableExtensionTriggerLocal.al new file mode 100644 index 00000000..a1bbf970 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/TableExtensionTriggerLocal.al @@ -0,0 +1,33 @@ +tableextension 50100 [|MyTableExtension|] extends MyTable +{ + fields + { + modify(MyField) + { + trigger OnAfterValidate() + var + RestClient: Codeunit "Rest Client"; + begin + end; + } + } +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) + { + } + } +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/TableFieldTriggerLocal.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/TableFieldTriggerLocal.al new file mode 100644 index 00000000..d43042f0 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/TableFieldTriggerLocal.al @@ -0,0 +1,23 @@ +table 50100 [|MyTable|] +{ + fields + { + field(1; MyField; Integer) + { + trigger OnValidate() + var + RestClient: Codeunit "Rest Client"; + begin + end; + } + } +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/TableTriggerLocal.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/TableTriggerLocal.al new file mode 100644 index 00000000..2f7c5f45 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/TableTriggerLocal.al @@ -0,0 +1,24 @@ +table 50100 [|MyTable|] +{ + fields + { + field(1; MyField; Integer) + { + } + } + + trigger OnInsert() + var + RestClient: Codeunit "Rest Client"; + begin + end; +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/TwoObjectsTwoDiagnostics.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/TwoObjectsTwoDiagnostics.al new file mode 100644 index 00000000..401fe14c --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/TwoObjectsTwoDiagnostics.al @@ -0,0 +1,23 @@ +codeunit 50100 [|MyFirstCodeunit|] +{ + var + RestClient: Codeunit "Rest Client"; +} + +codeunit 50101 [|MySecondCodeunit|] +{ + procedure DoSomething() + var + RestClient: Codeunit "Rest Client"; + begin + end; +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/XmlPortElementTriggerLocal.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/XmlPortElementTriggerLocal.al new file mode 100644 index 00000000..e24c6dc8 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/HasDiagnostic/XmlPortElementTriggerLocal.al @@ -0,0 +1,39 @@ +xmlport 50100 [|MyXmlPort|] +{ + schema + { + textelement(Root) + { + tableelement(MyTable; MyTable) + { + fieldelement(MyField; MyTable.MyField) + { + trigger OnAfterAssignField() + var + RestClient: Codeunit "Rest Client"; + begin + end; + } + } + } + } +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) + { + } + } +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/HandlerInOtherCodeunit.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/HandlerInOtherCodeunit.al new file mode 100644 index 00000000..5cb1fb29 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/HandlerInOtherCodeunit.al @@ -0,0 +1,21 @@ +codeunit 50100 [|MyCodeunit|] +{ + var + RestClient: Codeunit "Rest Client"; +} + +codeunit 50101 MyHttpClientHandler implements "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + end; +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/HandlerInSameCodeunit.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/HandlerInSameCodeunit.al new file mode 100644 index 00000000..c623f363 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/HandlerInSameCodeunit.al @@ -0,0 +1,18 @@ +codeunit 50100 [|MyCodeunit|] implements "Http Client Handler" +{ + var + RestClient: Codeunit "Rest Client"; + + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + end; +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/IdMatchNameMismatch.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/IdMatchNameMismatch.al new file mode 100644 index 00000000..33782065 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/IdMatchNameMismatch.al @@ -0,0 +1,9 @@ +codeunit 50100 [|MyCodeunit|] +{ + var + RestClient: Codeunit "My Rest Client"; +} + +codeunit 2350 "My Rest Client" +{ +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/NameMatchIdMismatch.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/NameMatchIdMismatch.al new file mode 100644 index 00000000..4a9f75f7 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/NameMatchIdMismatch.al @@ -0,0 +1,9 @@ +codeunit 50100 [|MyCodeunit|] +{ + var + RestClient: Codeunit "Rest Client"; +} + +codeunit 50101 "Rest Client" +{ +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/NamespacedHandler.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/NamespacedHandler.al new file mode 100644 index 00000000..af7012f2 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/NamespacedHandler.al @@ -0,0 +1,23 @@ +namespace MyPublisher.MyExtension.MyAppDomain; + +codeunit 50100 [|MyCodeunit|] +{ + var + RestClient: Codeunit MyPublisher.MyExtension.MyAppDomain."Rest Client"; +} + +codeunit 50101 MyHttpClientHandler implements MyPublisher.MyExtension.MyAppDomain."Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + end; +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/NoUsage.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/NoUsage.al new file mode 100644 index 00000000..96b4fa9f --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/NoUsage.al @@ -0,0 +1,14 @@ +codeunit 50100 [|MyCodeunit|] +{ + var + HttpClient: HttpClient; +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/ObjectReferenceOnly.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/ObjectReferenceOnly.al new file mode 100644 index 00000000..94478475 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/ObjectReferenceOnly.al @@ -0,0 +1,16 @@ +codeunit 50100 [|MyCodeunit|] +{ + procedure DoSomething() + begin + Codeunit.Run(Codeunit::"Rest Client"); + end; +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/ObsoletePendingObject.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/ObsoletePendingObject.al new file mode 100644 index 00000000..98e9a728 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/ObsoletePendingObject.al @@ -0,0 +1,17 @@ +codeunit 50100 [|MyCodeunit|] +{ + ObsoleteState = Pending; + ObsoleteReason = 'Replaced by a newer implementation.'; + + var + RestClient: Codeunit "Rest Client"; +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/TestCodeunit.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/TestCodeunit.al new file mode 100644 index 00000000..e616321c --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/TestCodeunit.al @@ -0,0 +1,19 @@ +codeunit 50100 [|MyCodeunit|] +{ + Subtype = Test; + + trigger OnRun() + var + RestClient: Codeunit "Rest Client"; + begin + end; +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/TestRunnerCodeunit.al b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/TestRunnerCodeunit.al new file mode 100644 index 00000000..0dfb186f --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/NoDiagnostic/TestRunnerCodeunit.al @@ -0,0 +1,19 @@ +codeunit 50100 [|MyCodeunit|] +{ + Subtype = TestRunner; + + trigger OnRun() + var + RestClient: Codeunit "Rest Client"; + begin + end; +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/RestClientRequiresHttpClientHandler.cs b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/RestClientRequiresHttpClientHandler.cs new file mode 100644 index 00000000..726db2e2 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/RestClientRequiresHttpClientHandler.cs @@ -0,0 +1,72 @@ +using RoslynTestKit; + +namespace ALCops.ApplicationCop.Test +{ + public class RestClientRequiresHttpClientHandler : NavCodeAnalysisBase + { + private AnalyzerTestFixture _fixture; + private string _testCasePath; + + [SetUp] + public void Setup() + { + _fixture = RoslynFixtureFactory.Create(); + + _testCasePath = Path.Combine( + Directory.GetParent( + Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + Path.Combine("Rules", nameof(RestClientRequiresHttpClientHandler))); + } + + [Test] + [TestCase("GlobalVariable")] + [TestCase("LocalVariable")] + [TestCase("Parameter")] + [TestCase("ReturnValue")] + [TestCase("PageActionTriggerLocal")] + [TestCase("PageFieldTriggerLocal")] + [TestCase("TableTriggerLocal")] + [TestCase("TableFieldTriggerLocal")] + [TestCase("ReportDataItemTriggerLocal")] + [TestCase("XmlPortElementTriggerLocal")] + [TestCase("QueryTriggerLocal")] + [TestCase("TableExtensionTriggerLocal")] + [TestCase("PageExtensionActionTriggerLocal")] + [TestCase("MultipleVariablesOneDiagnostic")] + [TestCase("TwoObjectsTwoDiagnostics")] + [TestCase("BothRulesOneObject")] + [TestCase("NamespacedUsage")] + public async Task HasDiagnostic(string testCase) + { + SkipTestIfVersionIsTooLow( + ["TableExtensionTriggerLocal", "PageExtensionActionTriggerLocal"], + testCase, + "13.0", + "AL 12 rejects extensions whose target is declared in the same module (AL0334)."); + + var code = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(HasDiagnostic), $"{testCase}.al")) + .ConfigureAwait(false); + + _fixture.HasDiagnosticAtAllMarkers(code, DiagnosticIds.RestClientRequiresHttpClientHandler); + } + + [Test] + [TestCase("HandlerInOtherCodeunit")] + [TestCase("HandlerInSameCodeunit")] + [TestCase("NoUsage")] + [TestCase("TestCodeunit")] + [TestCase("TestRunnerCodeunit")] + [TestCase("ObsoletePendingObject")] + [TestCase("NameMatchIdMismatch")] + [TestCase("IdMatchNameMismatch")] + [TestCase("ObjectReferenceOnly")] + [TestCase("NamespacedHandler")] + public async Task NoDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(NoDiagnostic), $"{testCase}.al")) + .ConfigureAwait(false); + + _fixture.NoDiagnosticAtAllMarkers(code, DiagnosticIds.RestClientRequiresHttpClientHandler); + } + } +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/BothRulesOneObject.al b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/BothRulesOneObject.al new file mode 100644 index 00000000..92e6d05c --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/BothRulesOneObject.al @@ -0,0 +1,28 @@ +codeunit 50100 [|MyCodeunit|] +{ + var + RestClient: Codeunit "Rest Client"; + Telemetry: Codeunit Telemetry; +} + +codeunit 8711 Telemetry +{ +} + +codeunit 8703 "Feature Telemetry" +{ +} + +interface "Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]); +} + +codeunit 2350 "Rest Client" +{ +} + +interface "Http Client Handler" +{ + procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean; +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/GlobalVariableFeatureTelemetry.al b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/GlobalVariableFeatureTelemetry.al new file mode 100644 index 00000000..0b9fa57d --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/GlobalVariableFeatureTelemetry.al @@ -0,0 +1,18 @@ +codeunit 50100 [|MyCodeunit|] +{ + var + FeatureTelemetry: Codeunit "Feature Telemetry"; +} + +codeunit 8711 Telemetry +{ +} + +codeunit 8703 "Feature Telemetry" +{ +} + +interface "Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]); +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/GlobalVariableTelemetry.al b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/GlobalVariableTelemetry.al new file mode 100644 index 00000000..73a68392 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/GlobalVariableTelemetry.al @@ -0,0 +1,18 @@ +codeunit 50100 [|MyCodeunit|] +{ + var + Telemetry: Codeunit Telemetry; +} + +codeunit 8711 Telemetry +{ +} + +codeunit 8703 "Feature Telemetry" +{ +} + +interface "Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]); +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/LocalVariable.al b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/LocalVariable.al new file mode 100644 index 00000000..084db48a --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/LocalVariable.al @@ -0,0 +1,21 @@ +codeunit 50100 [|MyCodeunit|] +{ + procedure DoSomething() + var + Telemetry: Codeunit Telemetry; + begin + end; +} + +codeunit 8711 Telemetry +{ +} + +codeunit 8703 "Feature Telemetry" +{ +} + +interface "Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]); +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/MultipleVariablesOneDiagnostic.al b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/MultipleVariablesOneDiagnostic.al new file mode 100644 index 00000000..561bd534 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/MultipleVariablesOneDiagnostic.al @@ -0,0 +1,19 @@ +codeunit 50100 [|MyCodeunit|] +{ + var + Telemetry: Codeunit Telemetry; + FeatureTelemetry: Codeunit "Feature Telemetry"; +} + +codeunit 8711 Telemetry +{ +} + +codeunit 8703 "Feature Telemetry" +{ +} + +interface "Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]); +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/PageActionTriggerLocal.al b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/PageActionTriggerLocal.al new file mode 100644 index 00000000..add5e1df --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/PageActionTriggerLocal.al @@ -0,0 +1,42 @@ +page 50100 [|MyPage|] +{ + SourceTable = MyTable; + + actions + { + area(Processing) + { + action(MyAction) + { + trigger OnAction() + var + FeatureTelemetry: Codeunit "Feature Telemetry"; + begin + end; + } + } + } +} + +table 50100 MyTable +{ + fields + { + field(1; MyField; Integer) + { + } + } +} + +codeunit 8711 Telemetry +{ +} + +codeunit 8703 "Feature Telemetry" +{ +} + +interface "Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]); +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/Parameter.al b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/Parameter.al new file mode 100644 index 00000000..7f096330 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/Parameter.al @@ -0,0 +1,19 @@ +codeunit 50100 [|MyCodeunit|] +{ + procedure DoSomething(FeatureTelemetry: Codeunit "Feature Telemetry") + begin + end; +} + +codeunit 8711 Telemetry +{ +} + +codeunit 8703 "Feature Telemetry" +{ +} + +interface "Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]); +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/ReturnValue.al b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/ReturnValue.al new file mode 100644 index 00000000..6ae64e3e --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/ReturnValue.al @@ -0,0 +1,19 @@ +codeunit 50100 [|MyCodeunit|] +{ + procedure Create() Result: Codeunit Telemetry + begin + end; +} + +codeunit 8711 Telemetry +{ +} + +codeunit 8703 "Feature Telemetry" +{ +} + +interface "Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]); +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/TwoObjectsTwoDiagnostics.al b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/TwoObjectsTwoDiagnostics.al new file mode 100644 index 00000000..9c66ddbc --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/HasDiagnostic/TwoObjectsTwoDiagnostics.al @@ -0,0 +1,27 @@ +codeunit 50100 [|MyFirstCodeunit|] +{ + var + Telemetry: Codeunit Telemetry; +} + +codeunit 50101 [|MySecondCodeunit|] +{ + procedure DoSomething() + var + Telemetry: Codeunit Telemetry; + begin + end; +} + +codeunit 8711 Telemetry +{ +} + +codeunit 8703 "Feature Telemetry" +{ +} + +interface "Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]); +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/IdMatchNameMismatch.al b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/IdMatchNameMismatch.al new file mode 100644 index 00000000..5e9207ca --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/IdMatchNameMismatch.al @@ -0,0 +1,9 @@ +codeunit 50100 [|MyCodeunit|] +{ + var + Telemetry: Codeunit "My Telemetry"; +} + +codeunit 8711 "My Telemetry" +{ +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/LoggerInOtherCodeunit.al b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/LoggerInOtherCodeunit.al new file mode 100644 index 00000000..7aee231f --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/LoggerInOtherCodeunit.al @@ -0,0 +1,25 @@ +codeunit 50100 [|MyCodeunit|] +{ + var + Telemetry: Codeunit Telemetry; +} + +codeunit 50101 MyTelemetryLogger implements "Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]) + begin + end; +} + +codeunit 8711 Telemetry +{ +} + +codeunit 8703 "Feature Telemetry" +{ +} + +interface "Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]); +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/LoggerInSameCodeunit.al b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/LoggerInSameCodeunit.al new file mode 100644 index 00000000..bf83a17e --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/LoggerInSameCodeunit.al @@ -0,0 +1,22 @@ +codeunit 50100 [|MyCodeunit|] implements "Telemetry Logger" +{ + var + FeatureTelemetry: Codeunit "Feature Telemetry"; + + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]) + begin + end; +} + +codeunit 8711 Telemetry +{ +} + +codeunit 8703 "Feature Telemetry" +{ +} + +interface "Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]); +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/NameMatchIdMismatch.al b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/NameMatchIdMismatch.al new file mode 100644 index 00000000..9a69e308 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/NameMatchIdMismatch.al @@ -0,0 +1,9 @@ +codeunit 50100 [|MyCodeunit|] +{ + var + Telemetry: Codeunit Telemetry; +} + +codeunit 50101 Telemetry +{ +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/NamespacedLogger.al b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/NamespacedLogger.al new file mode 100644 index 00000000..4d1ae9dd --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/NamespacedLogger.al @@ -0,0 +1,27 @@ +namespace MyPublisher.MyExtension.MyAppDomain; + +codeunit 50100 [|MyCodeunit|] +{ + var + Telemetry: Codeunit MyPublisher.MyExtension.MyAppDomain.Telemetry; +} + +codeunit 50101 MyTelemetryLogger implements MyPublisher.MyExtension.MyAppDomain."Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]) + begin + end; +} + +codeunit 8711 Telemetry +{ +} + +codeunit 8703 "Feature Telemetry" +{ +} + +interface "Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]); +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/NoUsage.al b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/NoUsage.al new file mode 100644 index 00000000..08cd6ac0 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/NoUsage.al @@ -0,0 +1,20 @@ +codeunit 50100 [|MyCodeunit|] +{ + procedure DoSomething() + begin + Session.LogMessage('0000ABC', 'Message', Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', 'Test'); + end; +} + +codeunit 8711 Telemetry +{ +} + +codeunit 8703 "Feature Telemetry" +{ +} + +interface "Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]); +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/ObjectReferenceOnly.al b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/ObjectReferenceOnly.al new file mode 100644 index 00000000..44315059 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/ObjectReferenceOnly.al @@ -0,0 +1,20 @@ +codeunit 50100 [|MyCodeunit|] +{ + procedure DoSomething() + begin + Codeunit.Run(Codeunit::Telemetry); + end; +} + +codeunit 8711 Telemetry +{ +} + +codeunit 8703 "Feature Telemetry" +{ +} + +interface "Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]); +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/ObsoletePendingObject.al b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/ObsoletePendingObject.al new file mode 100644 index 00000000..cc2832f8 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/ObsoletePendingObject.al @@ -0,0 +1,21 @@ +codeunit 50100 [|MyCodeunit|] +{ + ObsoleteState = Pending; + ObsoleteReason = 'Replaced by a newer implementation.'; + + var + Telemetry: Codeunit Telemetry; +} + +codeunit 8711 Telemetry +{ +} + +codeunit 8703 "Feature Telemetry" +{ +} + +interface "Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]); +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/TestCodeunit.al b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/TestCodeunit.al new file mode 100644 index 00000000..594abdf3 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/TestCodeunit.al @@ -0,0 +1,23 @@ +codeunit 50100 [|MyCodeunit|] +{ + Subtype = Test; + + trigger OnRun() + var + Telemetry: Codeunit Telemetry; + begin + end; +} + +codeunit 8711 Telemetry +{ +} + +codeunit 8703 "Feature Telemetry" +{ +} + +interface "Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]); +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/TestRunnerCodeunit.al b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/TestRunnerCodeunit.al new file mode 100644 index 00000000..33f76464 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/NoDiagnostic/TestRunnerCodeunit.al @@ -0,0 +1,23 @@ +codeunit 50100 [|MyCodeunit|] +{ + Subtype = TestRunner; + + trigger OnRun() + var + FeatureTelemetry: Codeunit "Feature Telemetry"; + begin + end; +} + +codeunit 8711 Telemetry +{ +} + +codeunit 8703 "Feature Telemetry" +{ +} + +interface "Telemetry Logger" +{ + procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]); +} diff --git a/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/TelemetryRequiresTelemetryLogger.cs b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/TelemetryRequiresTelemetryLogger.cs new file mode 100644 index 00000000..3c82bcfa --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/TelemetryRequiresTelemetryLogger.cs @@ -0,0 +1,58 @@ +using RoslynTestKit; + +namespace ALCops.ApplicationCop.Test +{ + public class TelemetryRequiresTelemetryLogger : NavCodeAnalysisBase + { + private AnalyzerTestFixture _fixture; + private string _testCasePath; + + [SetUp] + public void Setup() + { + _fixture = RoslynFixtureFactory.Create(); + + _testCasePath = Path.Combine( + Directory.GetParent( + Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + Path.Combine("Rules", nameof(TelemetryRequiresTelemetryLogger))); + } + + [Test] + [TestCase("GlobalVariableTelemetry")] + [TestCase("GlobalVariableFeatureTelemetry")] + [TestCase("LocalVariable")] + [TestCase("Parameter")] + [TestCase("ReturnValue")] + [TestCase("PageActionTriggerLocal")] + [TestCase("MultipleVariablesOneDiagnostic")] + [TestCase("TwoObjectsTwoDiagnostics")] + [TestCase("BothRulesOneObject")] + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(HasDiagnostic), $"{testCase}.al")) + .ConfigureAwait(false); + + _fixture.HasDiagnosticAtAllMarkers(code, DiagnosticIds.TelemetryRequiresTelemetryLogger); + } + + [Test] + [TestCase("LoggerInOtherCodeunit")] + [TestCase("LoggerInSameCodeunit")] + [TestCase("NoUsage")] + [TestCase("TestCodeunit")] + [TestCase("TestRunnerCodeunit")] + [TestCase("ObsoletePendingObject")] + [TestCase("NameMatchIdMismatch")] + [TestCase("IdMatchNameMismatch")] + [TestCase("ObjectReferenceOnly")] + [TestCase("NamespacedLogger")] + public async Task NoDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(NoDiagnostic), $"{testCase}.al")) + .ConfigureAwait(false); + + _fixture.NoDiagnosticAtAllMarkers(code, DiagnosticIds.TelemetryRequiresTelemetryLogger); + } + } +} diff --git a/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.resx b/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.resx index 4fffc101..7a2f3126 100644 --- a/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.resx +++ b/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.resx @@ -441,6 +441,24 @@ ALCops: Remove unused permission for table '{0}' + + Rest Client requires an Http Client Handler implementation + + + {0} '{1}' uses Codeunit "Rest Client", but no codeunit in this app implements the "Http Client Handler" interface; requests are sent by the System Application's default handler. + + + Codeunit "Rest Client" delegates the HTTP call to an "Http Client Handler" implementation. Without one in the app, the System Application's default handler sends the request, so the external-call permission, outgoing web service telemetry and test mocking attribute the call to the System Application instead of the calling app. Implement the "Http Client Handler" interface and pass it to Initialize or Create. + + + Telemetry requires a Telemetry Logger implementation + + + {0} '{1}' uses Codeunit "Telemetry" or "Feature Telemetry", but no codeunit in this app implements the "Telemetry Logger" interface; messages from this publisher are dropped. + + + Codeunits "Telemetry" and "Feature Telemetry" route each message to the "Telemetry Logger" registered for the calling app's publisher. When no logger is registered, "Telemetry Loggers Impl." logs warning 0000G7K and discards the message. Implement the "Telemetry Logger" interface and register it in a subscriber to "Telemetry Loggers".OnRegisterTelemetryLogger. + Reserve Enum value zero (0) for empty value diff --git a/src/ALCops.ApplicationCop/Analyzers/RequiredInterfaceImplementation.cs b/src/ALCops.ApplicationCop/Analyzers/RequiredInterfaceImplementation.cs new file mode 100644 index 00000000..8cafb332 --- /dev/null +++ b/src/ALCops.ApplicationCop/Analyzers/RequiredInterfaceImplementation.cs @@ -0,0 +1,17 @@ +using System.Collections.Immutable; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; + +namespace ALCops.ApplicationCop.Analyzers; + +[DiagnosticAnalyzer] +public sealed class RequiredInterfaceImplementation : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create( + DiagnosticDescriptors.RestClientRequiresHttpClientHandler, + DiagnosticDescriptors.TelemetryRequiresTelemetryLogger); + + public override void Initialize(AnalysisContext context) + { + } +} diff --git a/src/ALCops.ApplicationCop/DiagnosticDescriptors.cs b/src/ALCops.ApplicationCop/DiagnosticDescriptors.cs index cf54fa33..e3c1a9a4 100644 --- a/src/ALCops.ApplicationCop/DiagnosticDescriptors.cs +++ b/src/ALCops.ApplicationCop/DiagnosticDescriptors.cs @@ -325,6 +325,26 @@ public static class DiagnosticDescriptors description: ApplicationCopAnalyzers.TableDataAccessUnusedPermissionsDescription, helpLinkUri: GetHelpUri(DiagnosticIds.TableDataAccessUnusedPermissions)); + public static readonly DiagnosticDescriptor RestClientRequiresHttpClientHandler = new( + id: DiagnosticIds.RestClientRequiresHttpClientHandler, + title: ApplicationCopAnalyzers.RestClientRequiresHttpClientHandlerTitle, + messageFormat: ApplicationCopAnalyzers.RestClientRequiresHttpClientHandlerMessageFormat, + category: Category.Design, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: ApplicationCopAnalyzers.RestClientRequiresHttpClientHandlerDescription, + helpLinkUri: GetHelpUri(DiagnosticIds.RestClientRequiresHttpClientHandler)); + + public static readonly DiagnosticDescriptor TelemetryRequiresTelemetryLogger = new( + id: DiagnosticIds.TelemetryRequiresTelemetryLogger, + title: ApplicationCopAnalyzers.TelemetryRequiresTelemetryLoggerTitle, + messageFormat: ApplicationCopAnalyzers.TelemetryRequiresTelemetryLoggerMessageFormat, + category: Category.Design, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: ApplicationCopAnalyzers.TelemetryRequiresTelemetryLoggerDescription, + helpLinkUri: GetHelpUri(DiagnosticIds.TelemetryRequiresTelemetryLogger)); + public static readonly DiagnosticDescriptor ZeroEnumValueReservedForEmpty = new( id: DiagnosticIds.ZeroEnumValueReservedForEmpty, title: ApplicationCopAnalyzers.ZeroEnumValueReservedForEmptyTitle, diff --git a/src/ALCops.ApplicationCop/DiagnosticIds.cs b/src/ALCops.ApplicationCop/DiagnosticIds.cs index da7d14fe..b5abfe92 100644 --- a/src/ALCops.ApplicationCop/DiagnosticIds.cs +++ b/src/ALCops.ApplicationCop/DiagnosticIds.cs @@ -35,4 +35,6 @@ public static class DiagnosticIds public static readonly string UseReturnValueForDatabaseReadMethods = "AC0030"; public static readonly string TableDataAccessRequiresPermissions = "AC0031"; public static readonly string TableDataAccessUnusedPermissions = "AC0032"; + public static readonly string RestClientRequiresHttpClientHandler = "AC0033"; + public static readonly string TelemetryRequiresTelemetryLogger = "AC0034"; } \ No newline at end of file diff --git a/src/ALCops.Common/Reflection/EnumProvider.cs b/src/ALCops.Common/Reflection/EnumProvider.cs index 95458d1f..a8869fb5 100644 --- a/src/ALCops.Common/Reflection/EnumProvider.cs +++ b/src/ALCops.Common/Reflection/EnumProvider.cs @@ -220,11 +220,14 @@ public static class CodeunitSubtypeKind new(() => ParseEnum(nameof(NavCodeAnalysis.CodeunitSubtypeKind.Install))); private static readonly Lazy _test = new(() => ParseEnum(nameof(NavCodeAnalysis.CodeunitSubtypeKind.Test))); + private static readonly Lazy _testRunner = + new(() => ParseEnum(nameof(NavCodeAnalysis.CodeunitSubtypeKind.TestRunner))); private static readonly Lazy _upgrade = new(() => ParseEnum(nameof(NavCodeAnalysis.CodeunitSubtypeKind.Upgrade))); public static NavCodeAnalysis.CodeunitSubtypeKind Install => _install.Value; public static NavCodeAnalysis.CodeunitSubtypeKind Test => _test.Value; + public static NavCodeAnalysis.CodeunitSubtypeKind TestRunner => _testRunner.Value; public static NavCodeAnalysis.CodeunitSubtypeKind Upgrade => _upgrade.Value; } /// From aa87b7f9748b5b315318c47e2d04d0f2483ce7aa Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Sun, 27 Sep 2026 11:03:02 +0200 Subject: [PATCH 2/5] feat(AC0033,AC0034): report Rest Client and Telemetry usage without a required interface implementation Co-Authored-By: Claude Fable 5.1 --- .../RequiredInterfaceImplementation.cs | 158 +++++++++++++++++- 1 file changed, 157 insertions(+), 1 deletion(-) diff --git a/src/ALCops.ApplicationCop/Analyzers/RequiredInterfaceImplementation.cs b/src/ALCops.ApplicationCop/Analyzers/RequiredInterfaceImplementation.cs index 8cafb332..003cc92e 100644 --- a/src/ALCops.ApplicationCop/Analyzers/RequiredInterfaceImplementation.cs +++ b/src/ALCops.ApplicationCop/Analyzers/RequiredInterfaceImplementation.cs @@ -1,4 +1,7 @@ using System.Collections.Immutable; +using ALCops.Common.Extensions; +using ALCops.Common.Reflection; +using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; namespace ALCops.ApplicationCop.Analyzers; @@ -6,12 +9,165 @@ namespace ALCops.ApplicationCop.Analyzers; [DiagnosticAnalyzer] public sealed class RequiredInterfaceImplementation : DiagnosticAnalyzer { + // A variable of one of the trigger codeunits (matched on object id and name) requires a codeunit + // declared in the compiling app that implements the named interface. + private static readonly Pair[] Pairs = + [ + new Pair( + DiagnosticDescriptors.RestClientRequiresHttpClientHandler, + [(2350, "Rest Client")], + "Http Client Handler"), + new Pair( + DiagnosticDescriptors.TelemetryRequiresTelemetryLogger, + [(8711, "Telemetry"), (8703, "Feature Telemetry")], + "Telemetry Logger"), + ]; + + // Member kinds whose own members can declare code (triggers, nested controls, actions, dataitems, nodes). + // Keys are left out on purpose: in a tableextension they enumerate the target table's fields, which would + // attribute base-table field triggers to the extension. + private static readonly ImmutableHashSet DescendKinds = ImmutableHashSet.Create( + EnumProvider.SymbolKind.Field, + EnumProvider.SymbolKind.Control, + EnumProvider.SymbolKind.Action, + EnumProvider.SymbolKind.Change, + EnumProvider.SymbolKind.ReportDataItem, + EnumProvider.SymbolKind.XmlPortNode, + EnumProvider.SymbolKind.RequestPage, + EnumProvider.SymbolKind.RequestPageExtension); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( DiagnosticDescriptors.RestClientRequiresHttpClientHandler, DiagnosticDescriptors.TelemetryRequiresTelemetryLogger); - public override void Initialize(AnalysisContext context) + public override void Initialize(AnalysisContext context) => + context.RegisterCompilationStartAction(OnCompilationStart); + + private static void OnCompilationStart(CompilationStartAnalysisContext context) + { + Compilation compilation = context.Compilation; + CancellationToken cancellationToken = context.CancellationToken; + + // Read-only per-compilation index, built on first use by whichever object needs it. + var implemented = new Lazy( + () => BuildImplementedIndex(compilation, cancellationToken), + LazyThreadSafetyMode.ExecutionAndPublication); + + context.RegisterSymbolAction( + ctx => AnalyzeObject(ctx, implemented), + EnumProvider.SymbolKind.Codeunit, + EnumProvider.SymbolKind.Table, + EnumProvider.SymbolKind.TableExtension, + EnumProvider.SymbolKind.Page, + EnumProvider.SymbolKind.PageExtension, + EnumProvider.SymbolKind.Report, + EnumProvider.SymbolKind.ReportExtension, + EnumProvider.SymbolKind.Query, + EnumProvider.SymbolKind.XmlPort); + } + + private static void AnalyzeObject(SymbolAnalysisContext ctx, Lazy implemented) + { + if (ctx.IsObsolete()) + return; + + if (ctx.Symbol is not IContainerSymbol container) + return; + + if (ctx.Symbol is ICodeunitTypeSymbol codeunit && + (codeunit.Subtype == EnumProvider.CodeunitSubtypeKind.Test || + codeunit.Subtype == EnumProvider.CodeunitSubtypeKind.TestRunner)) + return; + + for (int i = 0; i < Pairs.Length; i++) + { + Pair pair = Pairs[i]; + if (!Uses(container, pair, ctx.CancellationToken) || implemented.Value[i]) + continue; + + ctx.ReportDiagnostic(Diagnostic.Create( + pair.Descriptor, + ctx.Symbol.GetLocation(), + ctx.Symbol.Kind.ToString(), + ctx.Symbol.Name)); + } + } + + private static bool[] BuildImplementedIndex(Compilation compilation, CancellationToken cancellationToken) + { + var implemented = new bool[Pairs.Length]; + + // Declared symbols of the compiling module only: implementations in dependencies do not count. + foreach (ICodeunitTypeSymbol codeunit in compilation.GetDeclaredApplicationObjectSymbols().OfType()) + { + cancellationToken.ThrowIfCancellationRequested(); + + foreach (var implementedInterface in codeunit.ImplementedInterfaces) + { + for (int i = 0; i < Pairs.Length; i++) + { + if (SemanticFacts.IsSameName(implementedInterface.Name, Pairs[i].InterfaceName)) + implemented[i] = true; + } + } + } + + return implemented; + } + + // GetMembers returns the members declared on the container itself (globals, procedures, triggers, + // layout controls, actions, dataitems, nodes, changes); members of a base or related table are not included. + private static bool Uses(IContainerSymbol container, Pair pair, CancellationToken cancellationToken) + { + foreach (ISymbol member in container.GetMembers()) + { + cancellationToken.ThrowIfCancellationRequested(); + + switch (member) + { + case IVariableSymbol variable when IsTriggerCodeunit(variable.Type, pair): + return true; + case IMethodSymbol method when MethodUses(method, pair): + return true; + case IContainerSymbol nested when DescendKinds.Contains(member.Kind) && Uses(nested, pair, cancellationToken): + return true; + } + } + + return false; + } + + private static bool MethodUses(IMethodSymbol method, Pair pair) => + method.LocalVariables.Any(local => IsTriggerCodeunit(local.Type, pair)) || + method.Parameters.Any(parameter => IsTriggerCodeunit(parameter.ParameterType, pair)) || + IsTriggerCodeunit(method.ReturnValueSymbol?.ReturnType, pair); + + // The type of a codeunit variable is the codeunit symbol itself; unresolved types fail the cast. + private static bool IsTriggerCodeunit(ITypeSymbol? type, Pair pair) + { + if (type is null || type.NavTypeKind != EnumProvider.NavTypeKind.Codeunit) + return false; + + if ((type.OriginalDefinition as ICodeunitTypeSymbol ?? type as ICodeunitTypeSymbol) is not { } codeunit) + return false; + + foreach ((int id, string name) in pair.TriggerCodeunits) + { + if (codeunit.Id == id && SemanticFacts.IsSameName(codeunit.Name, name)) + return true; + } + + return false; + } + + private sealed class Pair( + DiagnosticDescriptor descriptor, + (int Id, string Name)[] triggerCodeunits, + string interfaceName) { + public DiagnosticDescriptor Descriptor { get; } = descriptor; + public (int Id, string Name)[] TriggerCodeunits { get; } = triggerCodeunits; + public string InterfaceName { get; } = interfaceName; } } From 75e6e1898e07be11cda4f1406398a2ea56ab8a0c Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Sun, 27 Sep 2026 11:06:00 +0200 Subject: [PATCH 3/5] docs(AC0033,AC0034): add rule doc for RequiredInterfaceImplementation Co-Authored-By: Claude Fable 5.1 --- ...c0034-required-interface-implementation.md | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .claude/rules/diagnostics/ac0033-ac0034-required-interface-implementation.md diff --git a/.claude/rules/diagnostics/ac0033-ac0034-required-interface-implementation.md b/.claude/rules/diagnostics/ac0033-ac0034-required-interface-implementation.md new file mode 100644 index 00000000..94af6283 --- /dev/null +++ b/.claude/rules/diagnostics/ac0033-ac0034-required-interface-implementation.md @@ -0,0 +1,59 @@ +--- +paths: + - "src/ALCops.ApplicationCop/**/RequiredInterfaceImplementation*" + - "src/ALCops.ApplicationCop.Test/Rules/RestClientRequiresHttpClientHandler/**" + - "src/ALCops.ApplicationCop.Test/Rules/TelemetryRequiresTelemetryLogger/**" +--- + +# AC0033 / AC0034: RequiredInterfaceImplementation + +## Purpose + +One analyzer reports two diagnostics: AC0033 `RestClientRequiresHttpClientHandler` (an object declares a `Codeunit "Rest Client"` variable but no codeunit in the app implements `"Http Client Handler"`, so the System Application's default handler sends the request and the permission scope, outgoing web-service telemetry and test mocking belong to the System Application) and AC0034 `TelemetryRequiresTelemetryLogger` (an object declares a `Codeunit Telemetry` or `"Feature Telemetry"` variable but no codeunit in the app implements `"Telemetry Logger"`, so `Telemetry Loggers Impl.` finds no logger for the publisher, logs `0000G7K` and drops the message). + +Registers `RegisterCompilationStartAction` with an inner `RegisterSymbolAction` on Codeunit, Table, TableExtension, Page, PageExtension, Report, ReportExtension, Query and XmlPort; main type `RequiredInterfaceImplementation` with a static pair table. + +**References:** [ALCops/Analyzers discussion #405](https://github.com/ALCops/Analyzers/discussions/405) (Rule 3 there); System Application `Telemetry/src/Logging/TelemetryLoggersImpl.Codeunit.al` (`GetTelemetryLogger`) and the `` of `Telemetry.Codeunit.al`. + +## Design decisions + +| Decision | Rationale | +|---|---| +| One analyzer with a hard-coded pair table (trigger codeunits → required interface → descriptor) | Both rules are the same "uses X, implements no Y" check; a future pair is one row plus one ID. Two separate analyzers would duplicate the walk and the index. | +| Existence check only: some codeunit implements the interface | Tracking whether each variable is passed to `Initialize`/`Create` (or the logger registered via `OnRegisterTelemetryLogger`) needs a per-procedure flow walk; that is a separate follow-up rule, not this one. | +| Coverage counts codeunits declared in the compiling module only (`GetDeclaredApplicationObjectSymbols`), obsolete and test codeunits included | Dependency implementations would need a cross-module scan and make the result depend on what happens to be referenced; a shared-library architecture suppresses via ruleset or pragma. | +| Trigger codeunit matched on object id AND name, per id/name pair (2350 "Rest Client"; 8711 Telemetry; 8703 "Feature Telemetry") | Id alone matches unrelated same-id objects outside the System Application; name alone matches any same-named codeunit. Namespace or module checks were rejected so stubbed or namespaced System Application builds still match. | +| Interface matched by name only (`SemanticFacts.IsSameName`) | The implementing codeunit's `ImplementedInterfaces` points at the dependency interface; comparing module or namespace adds nothing a real app would get wrong. | +| Codeunits with `Subtype = Test` or `TestRunner` are never reported | Test code uses the Rest Client or Telemetry against mocks and is not shipped as the publisher's runtime code. | +| One diagnostic per rule per object, at the object name | The fix is app-wide (add one implementation), so per-variable diagnostics would repeat the same message; an object using both trigger kinds without either implementation gets two diagnostics. | +| The symbol walk runs before the index is consulted | Most objects use neither codeunit; the lazily built index is only forced when an object actually uses one. | +| Warning, enabled by default, `Category.Design` | The app works but attributes its calls or loses its telemetry silently; every ApplicationCop rule uses Design. | +| No settings | Nothing to tune: the pairs are fixed platform facts and the suppression cases are covered by ruleset/pragma. | +| No CodeFix | A generated handler or logger codeunit has no deterministic file, name, id or namespace. | +| No version gate | Every SDK member used is available at ns2.0 / AL 12.0 (`reference/` tables in nav-sdk-docs). | + +## Deliberate non-reports + +- `Codeunit::"Rest Client"` object references without a variable (`Codeunit.Run(...)`): no instance is used. +- `array[n] of Codeunit X`, `List of [Codeunit X]` and interface-typed variables: the variable type is not the codeunit symbol. +- Implementations in a dependency app (current-compilation-only coverage). +- Whether a handler is actually passed to `Initialize`/`Create`, or a logger actually registered in `OnRegisterTelemetryLogger`: existence only. +- Obsolete objects (`IsObsolete()`) and test / test-runner codeunits. + +## Known issues + +- A same-named interface from a different module counts as coverage (name-only interface match); accepted. +- One implementation anywhere in the app satisfies every object, even if only some `Rest Client` instances receive it. + +## SDK facts + +- For `Codeunit "Rest Client"`, `IVariableSymbol.Type` is the `ICodeunitTypeSymbol` itself, no wrapper (`Binder.BindSubTypedDataType`); referenced-app codeunits are `ReferenceCodeunitTypeSymbol : CodeunitTypeSymbol`; unresolved types are error type symbols that fail the cast. Only Page/TestPage variables get wrapper types. (Verified in `../nav-sdk-source` `Binder.cs`, net10.0 and v12.0.) +- `IContainerSymbol.GetMembers()` returns members declared on the container itself; base or related table members are not included, and extension symbols return what the extension adds (docs: nav-sdk-docs `docs/20-symbols/object-type-symbols.md`, `docs/20-symbols/symbol-hierarchy.md`). Controls return controls, actions and triggers; actions return actions and triggers; fields return triggers; changes return added controls, actions, dataitems or triggers; report dataitems return triggers, columns and dataitems; xmlport nodes return child nodes and triggers (`Source*Symbol.cs` in `../nav-sdk-source`). +- The walk descends only into Field, Control, Action, Change, ReportDataItem, XmlPortNode, RequestPage and RequestPageExtension. Keys are excluded because in a tableextension they enumerate the target table's fields, which would attribute base-table field triggers to the extension. +- `Compilation.GetDeclaredApplicationObjectSymbols()` returns the current module's objects only (`Compilation.cs`). + +## Test notes + +- `TableExtensionTriggerLocal` and `PageExtensionActionTriggerLocal` are gated on 13.0: AL 12 rejects extensions whose target is declared in the same module (AL0334). +- Fixtures declare their own stubs with the real System Application ids (2350, 8711, 8703); the harness sets no id ranges, so ids outside 50000-99999 compile. +- Report dataitems and page source tables use an inline `table 50100`; system tables are not available in the harness. From c552c2b9daa6854b250ed4ea56bfa72278df4fee Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Sun, 27 Sep 2026 11:23:54 +0200 Subject: [PATCH 4/5] fix(AC0033,AC0034): inert CodeunitSubtypeKind fallback and a single member walk per object A CodeunitSubtypeKind member missing from the loaded SDK resolved to Normal (0), so the Test/TestRunner exemption would have matched every codeunit. The members now fall back to an out-of-range value, like SymbolKind does. The analyzer also walks an object's declared members once for both rules instead of once per rule. Co-Authored-By: Claude Fable 5.1 --- .../RequiredInterfaceImplementation.cs | 73 +++++++++++++------ src/ALCops.Common/Reflection/EnumProvider.cs | 16 +++- 2 files changed, 62 insertions(+), 27 deletions(-) diff --git a/src/ALCops.ApplicationCop/Analyzers/RequiredInterfaceImplementation.cs b/src/ALCops.ApplicationCop/Analyzers/RequiredInterfaceImplementation.cs index 003cc92e..a9282b39 100644 --- a/src/ALCops.ApplicationCop/Analyzers/RequiredInterfaceImplementation.cs +++ b/src/ALCops.ApplicationCop/Analyzers/RequiredInterfaceImplementation.cs @@ -80,14 +80,16 @@ private static void AnalyzeObject(SymbolAnalysisContext ctx, Lazy implem codeunit.Subtype == EnumProvider.CodeunitSubtypeKind.TestRunner)) return; + var used = new bool[Pairs.Length]; + CollectUsedPairs(container, used, ctx.CancellationToken); + for (int i = 0; i < Pairs.Length; i++) { - Pair pair = Pairs[i]; - if (!Uses(container, pair, ctx.CancellationToken) || implemented.Value[i]) + if (!used[i] || implemented.Value[i]) continue; ctx.ReportDiagnostic(Diagnostic.Create( - pair.Descriptor, + Pairs[i].Descriptor, ctx.Symbol.GetLocation(), ctx.Symbol.Kind.ToString(), ctx.Symbol.Name)); @@ -116,35 +118,51 @@ private static bool[] BuildImplementedIndex(Compilation compilation, Cancellatio return implemented; } - // GetMembers returns the members declared on the container itself (globals, procedures, triggers, - // layout controls, actions, dataitems, nodes, changes); members of a base or related table are not included. - private static bool Uses(IContainerSymbol container, Pair pair, CancellationToken cancellationToken) + // One walk over the object marks every pair whose trigger codeunit is declared somewhere in it and stops + // as soon as all pairs are marked. GetMembers returns the members declared on the container itself + // (globals, procedures, triggers, layout controls, actions, dataitems, nodes, changes); members of a base + // or related table are not included. + private static bool CollectUsedPairs(IContainerSymbol container, bool[] used, CancellationToken cancellationToken) { foreach (ISymbol member in container.GetMembers()) { cancellationToken.ThrowIfCancellationRequested(); - switch (member) + bool allUsed = member switch { - case IVariableSymbol variable when IsTriggerCodeunit(variable.Type, pair): - return true; - case IMethodSymbol method when MethodUses(method, pair): - return true; - case IContainerSymbol nested when DescendKinds.Contains(member.Kind) && Uses(nested, pair, cancellationToken): - return true; - } + IVariableSymbol variable => MarkTriggerCodeunit(variable.Type, used), + IMethodSymbol method => MarkMethod(method, used), + IContainerSymbol nested when DescendKinds.Contains(member.Kind) => CollectUsedPairs(nested, used, cancellationToken), + _ => false, + }; + + if (allUsed) + return true; } return false; } - private static bool MethodUses(IMethodSymbol method, Pair pair) => - method.LocalVariables.Any(local => IsTriggerCodeunit(local.Type, pair)) || - method.Parameters.Any(parameter => IsTriggerCodeunit(parameter.ParameterType, pair)) || - IsTriggerCodeunit(method.ReturnValueSymbol?.ReturnType, pair); + private static bool MarkMethod(IMethodSymbol method, bool[] used) + { + foreach (IVariableSymbol local in method.LocalVariables) + { + if (MarkTriggerCodeunit(local.Type, used)) + return true; + } + + foreach (IParameterSymbol parameter in method.Parameters) + { + if (MarkTriggerCodeunit(parameter.ParameterType, used)) + return true; + } + return MarkTriggerCodeunit(method.ReturnValueSymbol?.ReturnType, used); + } + + // Marks the pair whose trigger codeunit matches the type; returns true once every pair is marked. // The type of a codeunit variable is the codeunit symbol itself; unresolved types fail the cast. - private static bool IsTriggerCodeunit(ITypeSymbol? type, Pair pair) + private static bool MarkTriggerCodeunit(ITypeSymbol? type, bool[] used) { if (type is null || type.NavTypeKind != EnumProvider.NavTypeKind.Codeunit) return false; @@ -152,13 +170,22 @@ private static bool IsTriggerCodeunit(ITypeSymbol? type, Pair pair) if ((type.OriginalDefinition as ICodeunitTypeSymbol ?? type as ICodeunitTypeSymbol) is not { } codeunit) return false; - foreach ((int id, string name) in pair.TriggerCodeunits) + for (int i = 0; i < Pairs.Length; i++) { - if (codeunit.Id == id && SemanticFacts.IsSameName(codeunit.Name, name)) - return true; + if (used[i]) + continue; + + foreach ((int id, string name) in Pairs[i].TriggerCodeunits) + { + if (codeunit.Id == id && SemanticFacts.IsSameName(codeunit.Name, name)) + { + used[i] = true; + break; + } + } } - return false; + return Array.TrueForAll(used, static u => u); } private sealed class Pair( diff --git a/src/ALCops.Common/Reflection/EnumProvider.cs b/src/ALCops.Common/Reflection/EnumProvider.cs index a8869fb5..1da1cd57 100644 --- a/src/ALCops.Common/Reflection/EnumProvider.cs +++ b/src/ALCops.Common/Reflection/EnumProvider.cs @@ -216,14 +216,22 @@ public static class BinaryOperationKind /// public static class CodeunitSubtypeKind { + // default(CodeunitSubtypeKind) is Normal (0), the subtype of every ordinary codeunit, so a member missing + // from the loaded SDK must not resolve to it: a Subtype == Test guard would then match every codeunit. + // An out-of-range value never equals a real subtype. + private const NavCodeAnalysis.CodeunitSubtypeKind Unresolved = (NavCodeAnalysis.CodeunitSubtypeKind)int.MaxValue; + + private static NavCodeAnalysis.CodeunitSubtypeKind Parse(string name) => + ParseEnum(name, Unresolved); + private static readonly Lazy _install = - new(() => ParseEnum(nameof(NavCodeAnalysis.CodeunitSubtypeKind.Install))); + new(() => Parse(nameof(NavCodeAnalysis.CodeunitSubtypeKind.Install))); private static readonly Lazy _test = - new(() => ParseEnum(nameof(NavCodeAnalysis.CodeunitSubtypeKind.Test))); + new(() => Parse(nameof(NavCodeAnalysis.CodeunitSubtypeKind.Test))); private static readonly Lazy _testRunner = - new(() => ParseEnum(nameof(NavCodeAnalysis.CodeunitSubtypeKind.TestRunner))); + new(() => Parse(nameof(NavCodeAnalysis.CodeunitSubtypeKind.TestRunner))); private static readonly Lazy _upgrade = - new(() => ParseEnum(nameof(NavCodeAnalysis.CodeunitSubtypeKind.Upgrade))); + new(() => Parse(nameof(NavCodeAnalysis.CodeunitSubtypeKind.Upgrade))); public static NavCodeAnalysis.CodeunitSubtypeKind Install => _install.Value; public static NavCodeAnalysis.CodeunitSubtypeKind Test => _test.Value; From f1867fc39bf23f8c1014a846a491d34f6bb2e575 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Sun, 27 Sep 2026 11:56:53 +0200 Subject: [PATCH 5/5] fix(AC0033,AC0034): consistent codeunit wording and precise telemetry consequence in messages Both messages now name the object as 'the "X" codeunit' like the other ApplicationCop messages, and AC0034 states what actually happens without a registered logger: the message is discarded and only warning 0000G7K is logged. Co-Authored-By: Claude Fable 5.1 --- .../ALCops.ApplicationCopAnalyzers.resx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.resx b/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.resx index 7a2f3126..82a9f3d2 100644 --- a/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.resx +++ b/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.resx @@ -445,19 +445,19 @@ Rest Client requires an Http Client Handler implementation - {0} '{1}' uses Codeunit "Rest Client", but no codeunit in this app implements the "Http Client Handler" interface; requests are sent by the System Application's default handler. + {0} '{1}' uses the "Rest Client" codeunit, but no codeunit in this app implements the "Http Client Handler" interface; requests are sent by the System Application's default handler. - Codeunit "Rest Client" delegates the HTTP call to an "Http Client Handler" implementation. Without one in the app, the System Application's default handler sends the request, so the external-call permission, outgoing web service telemetry and test mocking attribute the call to the System Application instead of the calling app. Implement the "Http Client Handler" interface and pass it to Initialize or Create. + The "Rest Client" codeunit delegates the HTTP call to an "Http Client Handler" implementation. Without one in the app, the System Application's default handler sends the request, so the external-call permission, outgoing web service telemetry and test mocking attribute the call to the System Application instead of the calling app. Implement the "Http Client Handler" interface and pass it to Initialize or Create. Telemetry requires a Telemetry Logger implementation - {0} '{1}' uses Codeunit "Telemetry" or "Feature Telemetry", but no codeunit in this app implements the "Telemetry Logger" interface; messages from this publisher are dropped. + {0} '{1}' uses the "Telemetry" or "Feature Telemetry" codeunit, but no codeunit in this app implements the "Telemetry Logger" interface; its telemetry messages are discarded and only warning 0000G7K is logged. - Codeunits "Telemetry" and "Feature Telemetry" route each message to the "Telemetry Logger" registered for the calling app's publisher. When no logger is registered, "Telemetry Loggers Impl." logs warning 0000G7K and discards the message. Implement the "Telemetry Logger" interface and register it in a subscriber to "Telemetry Loggers".OnRegisterTelemetryLogger. + The "Telemetry" and "Feature Telemetry" codeunits route each message to the "Telemetry Logger" registered for the calling app's publisher. When no logger is registered, the message is not forwarded anywhere: "Telemetry Loggers Impl." logs warning 0000G7K and discards it. Implement the "Telemetry Logger" interface and register it in a subscriber to "Telemetry Loggers".OnRegisterTelemetryLogger. Reserve Enum value zero (0) for empty value