Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Registers `RegisterCompilationStartAction` with an inner `RegisterSymbolAction`
| 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. |
| 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; AC0035 does that for the Rest Client (`ac0035-rest-client-initialize-with-http-client-handler.md`). |
| 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. |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
paths:
- "src/ALCops.ApplicationCop/**/RestClientInitializeWithHttpClientHandler*"
- "src/ALCops.ApplicationCop.Test/Rules/RestClientInitializeWithHttpClientHandler/**"
---

# AC0035: RestClientInitializeWithHttpClientHandler

## Purpose

Reports a `Codeunit "Rest Client"` variable that is used or initialized without a custom `"Http Client Handler"`: `Initialize`/`Create` without a handler argument, with the System Application's default codeunit 2360 `"Http Client Handler"`, or no initialization at all before use. Such a client sends through the System Application, so the external-call permission, outgoing web-service telemetry and test mocking belong to the System Application instead of the calling app. AC0033 checks that the app has a handler at all; this rule checks that each variable receives one.

Registers `RegisterSymbolAction` on Codeunit, Table, TableExtension, Page, PageExtension, Report, ReportExtension, Query and XmlPort; main type `TrackedWalker` (an `OperationWalker` per root, sharing one `RootState`).

## Design decisions

| Decision | Rationale |
|---|---|
| Roots are locals (any method or trigger body) and globals of the object; parameters and return values are never roots | The caller owns a parameter's initialization and the callee of a return value decides what to do with it; reporting them would flag every helper that receives a ready client. |
| Good = an argument bound to an `Interface "Http Client Handler"` parameter whose static type is not codeunit 2360 `"Http Client Handler"` (id and name) | Interface-typed values, factory results, parameters and codeunits from any app cannot be proven to be the default handler; tracing their origin was rejected as unbounded. |
| Silence (root becomes unknown) when it is passed to anything not followable (another object, a method without source, an interface method, an event, a built-in such as `Clear`), assigned from anything but `Root.Create(...)`, assigned to another variable, returned via `exit`, or an invocation on it is invalid | Past those points the rule no longer sees every call on the instance; reporting anyway was rejected because it would flag clients initialized elsewhere. |
| A root passed to a procedure declared inside the same object syntax is followed into the callee, `var` and by-value alike, unlimited depth, cycle-safe | Codeunit variables are reference types, so a by-value callee initializes the caller's instance too. Containment is checked on syntax (same tree, span contains the callee), not by comparing `GetContainingApplicationObjectTypeSymbol()`, which is null for request-page procedures. |
| Location: the first default `Initialize`/`Create` found directly on the root; otherwise the variable's name | The call is where the fix goes. A default call inside a followed callee reports at the root's declaration, because the callee's call also serves other callers. |
| A default call is reported even when a good call exists elsewhere on the same root | `Initialize()` after `Initialize(MyHandler)` replaces the handler; ordering analysis was rejected as too costly for the gain. |
| No ordering analysis and no request-method list: any other member invocation marks the root used | A list of `Get`/`Post`/`Send`... would miss new members; "used before initialization" order is not tracked (a good initialization anywhere counts). |
| Roots discovered on symbols (`LocalVariables`, `GetMembers()`), with a body-text pre-filter only for globals | A declaration can spell the type `Codeunit 2350`, so text cannot find roots; a reference to a global always spells its name, so skipping bodies that do not contain it is sound and saves binding. |
| One symbol action per object instead of a code-block action | A global's initialization and use live in different bodies; a per-object callback analyzes and reports in one place (`sdk-analysis-scope.md`) and binds only objects that declare a root. |
| `DescendKinds` and the member descent are copied from `RequiredInterfaceImplementation` instead of shared through Common | Two call sites do not yet justify a Common helper; extracting one is a follow-up that touches both analyzers. |
| Warning, enabled by default, `Category.Design`, no settings, no CodeFix, no version gate | Same impact class as AC0033; the right handler codeunit cannot be generated; every SDK member used exists at ns2.0 / AL 12.0 (nav-sdk-docs `reference/`). |

## Deliberate non-reports

- Parameters and return values of type `Codeunit "Rest Client"` (not roots).
- A root passed to `Clear` or any other built-in, to another object, an interface method or an event publisher (unknown).
- `Codeunit::"Rest Client"` object references without a variable.
- `array[n] of Codeunit "Rest Client"`, `List of [...]` and interface-typed variables: the variable type is not the codeunit symbol.
- A declared but never used root without an `Initialize`/`Create` call.
- Test and test-runner codeunits, obsolete objects and locals of obsolete procedures.

## Known issues

- A same-named codeunit 2360 from another module would count as the default handler (id and name match only); accepted.
- A call that fails to bind (for example a root passed where an `Integer` is expected) makes the root unknown, so a default initialization reachable only through that call is not reported; the compiler error is the signal there.

## SDK facts

- `RestClient.Initialize;` without parentheses in statement position binds to an `IInvocationExpression` whose `Syntax` is the `MemberAccessExpressionSyntax` (verified with the `LocalInitializeWithoutParentheses` fixture, net10.0 SDK).
- `ISymbol.GetLocation()` on a local or global variable is the name identifier, not the whole declaration (verified by dumping fixture diagnostics).
- `IArgument.Parameter` is null when no parameter could be matched (docs: nav-sdk-docs `docs/40-operations/invocation-and-arguments.md`); arguments bound to an interface parameter are wrapped in `IConversionExpression`, so values are compared after `UnwrapConversions()`.
- The SDK has no `SymbolEqualityComparer`; `ISymbol.Equals` is the only equality contract (docs: nav-sdk-docs `docs/20-symbols/symbol-hierarchy.md`).

## Test notes

- `ThisReceiverProcedureCall` is gated on 14.0 (the `this` keyword).
- Fixtures declare their own System Application stubs with the real ids (2350 `"Rest Client"`, 2360 `"Http Client Handler"`, 2358 `"Http Authentication Anonymous"`); no stub implements both interfaces, so `Initialize`/`Create` overloads resolve unambiguously. A user procedure named `Run` collides with the built-in codeunit `Run` (AL0440).
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
codeunit 50100 MyCodeunit
{
var
RestClient: Codeunit "Rest Client";

procedure Setup()
begin
[|RestClient.Initialize()|];
end;

procedure DoSomething()
begin
RestClient.Get('https://example.com');
end;
}

codeunit 2350 "Rest Client"
{
procedure Initialize()
begin
end;

procedure Initialize(HttpClientHandler: Interface "Http Client Handler")
begin
end;

procedure Initialize(HttpAuthentication: Interface "Http Authentication")
begin
end;

procedure Initialize(HttpClientHandler: Interface "Http Client Handler"; HttpAuthentication: Interface "Http Authentication")
begin
end;

procedure Create(): Codeunit "Rest Client"
begin
end;

procedure Create(HttpClientHandler: Interface "Http Client Handler"): Codeunit "Rest Client"
begin
end;

procedure Create(HttpAuthentication: Interface "Http Authentication"): Codeunit "Rest Client"
begin
end;

procedure Create(HttpClientHandler: Interface "Http Client Handler"; HttpAuthentication: Interface "Http Authentication"): Codeunit "Rest Client"
begin
end;

procedure Get(RequestUri: Text)
begin
end;

procedure SetBaseAddress(Url: Text)
begin
end;
}

interface "Http Client Handler"
{
procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean;
}

interface "Http Authentication"
{
procedure IsAuthenticationRequired(): Boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
codeunit 50100 MyCodeunit
{
var
[|RestClient|]: Codeunit "Rest Client";

procedure DoSomething()
begin
RestClient.Get('https://example.com');
end;
}

codeunit 2350 "Rest Client"
{
procedure Initialize()
begin
end;

procedure Initialize(HttpClientHandler: Interface "Http Client Handler")
begin
end;

procedure Initialize(HttpAuthentication: Interface "Http Authentication")
begin
end;

procedure Initialize(HttpClientHandler: Interface "Http Client Handler"; HttpAuthentication: Interface "Http Authentication")
begin
end;

procedure Create(): Codeunit "Rest Client"
begin
end;

procedure Create(HttpClientHandler: Interface "Http Client Handler"): Codeunit "Rest Client"
begin
end;

procedure Create(HttpAuthentication: Interface "Http Authentication"): Codeunit "Rest Client"
begin
end;

procedure Create(HttpClientHandler: Interface "Http Client Handler"; HttpAuthentication: Interface "Http Authentication"): Codeunit "Rest Client"
begin
end;

procedure Get(RequestUri: Text)
begin
end;

procedure SetBaseAddress(Url: Text)
begin
end;
}

interface "Http Client Handler"
{
procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean;
}

interface "Http Authentication"
{
procedure IsAuthenticationRequired(): Boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
codeunit 50100 MyCodeunit
{
procedure DoSomething()
var
RestClient: Codeunit "Rest Client";
begin
[|RestClient.Initialize()|];
RestClient.Get('https://example.com');
end;
}

codeunit 2350 "Rest Client"
{
procedure Initialize()
begin
end;

procedure Initialize(HttpClientHandler: Interface "Http Client Handler")
begin
end;

procedure Initialize(HttpAuthentication: Interface "Http Authentication")
begin
end;

procedure Initialize(HttpClientHandler: Interface "Http Client Handler"; HttpAuthentication: Interface "Http Authentication")
begin
end;

procedure Create(): Codeunit "Rest Client"
begin
end;

procedure Create(HttpClientHandler: Interface "Http Client Handler"): Codeunit "Rest Client"
begin
end;

procedure Create(HttpAuthentication: Interface "Http Authentication"): Codeunit "Rest Client"
begin
end;

procedure Create(HttpClientHandler: Interface "Http Client Handler"; HttpAuthentication: Interface "Http Authentication"): Codeunit "Rest Client"
begin
end;

procedure Get(RequestUri: Text)
begin
end;

procedure SetBaseAddress(Url: Text)
begin
end;
}

interface "Http Client Handler"
{
procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean;
}

interface "Http Authentication"
{
procedure IsAuthenticationRequired(): Boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
codeunit 50100 MyCodeunit
{
procedure DoSomething()
var
RestClient: Codeunit "Rest Client";
Auth: Codeunit "Http Authentication Anonymous";
begin
RestClient := [|RestClient.Create(Auth)|];
RestClient.Get('https://example.com');
end;
}

codeunit 2350 "Rest Client"
{
procedure Initialize()
begin
end;

procedure Initialize(HttpClientHandler: Interface "Http Client Handler")
begin
end;

procedure Initialize(HttpAuthentication: Interface "Http Authentication")
begin
end;

procedure Initialize(HttpClientHandler: Interface "Http Client Handler"; HttpAuthentication: Interface "Http Authentication")
begin
end;

procedure Create(): Codeunit "Rest Client"
begin
end;

procedure Create(HttpClientHandler: Interface "Http Client Handler"): Codeunit "Rest Client"
begin
end;

procedure Create(HttpAuthentication: Interface "Http Authentication"): Codeunit "Rest Client"
begin
end;

procedure Create(HttpClientHandler: Interface "Http Client Handler"; HttpAuthentication: Interface "Http Authentication"): Codeunit "Rest Client"
begin
end;

procedure Get(RequestUri: Text)
begin
end;

procedure SetBaseAddress(Url: Text)
begin
end;
}

interface "Http Client Handler"
{
procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean;
}

interface "Http Authentication"
{
procedure IsAuthenticationRequired(): Boolean;
}

codeunit 2358 "Http Authentication Anonymous" implements "Http Authentication"
{
procedure IsAuthenticationRequired(): Boolean
begin
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
codeunit 50100 MyCodeunit
{
procedure DoSomething()
var
RestClient: Codeunit "Rest Client";
begin
RestClient := [|RestClient.Create()|];
RestClient.Get('https://example.com');
end;
}

codeunit 2350 "Rest Client"
{
procedure Initialize()
begin
end;

procedure Initialize(HttpClientHandler: Interface "Http Client Handler")
begin
end;

procedure Initialize(HttpAuthentication: Interface "Http Authentication")
begin
end;

procedure Initialize(HttpClientHandler: Interface "Http Client Handler"; HttpAuthentication: Interface "Http Authentication")
begin
end;

procedure Create(): Codeunit "Rest Client"
begin
end;

procedure Create(HttpClientHandler: Interface "Http Client Handler"): Codeunit "Rest Client"
begin
end;

procedure Create(HttpAuthentication: Interface "Http Authentication"): Codeunit "Rest Client"
begin
end;

procedure Create(HttpClientHandler: Interface "Http Client Handler"; HttpAuthentication: Interface "Http Authentication"): Codeunit "Rest Client"
begin
end;

procedure Get(RequestUri: Text)
begin
end;

procedure SetBaseAddress(Url: Text)
begin
end;
}

interface "Http Client Handler"
{
procedure Send(CurrHttpClientInstance: HttpClient; HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean;
}

interface "Http Authentication"
{
procedure IsAuthenticationRequired(): Boolean;
}
Loading
Loading