Repository navigation
Conversation
|
Sorry if I'm rushing through this. Just back from vacation today. Just to consider. |
|
@MODUSCarstenScholling I will do that, these were just the defaults for our internal linters, but it makes more sense to keep these fairly low profile and only ramp them up using rulesets. Hope to get around to fixing this and my other PR sometime this week. |
|
Great work, this is an long requested diagnostic to have, see #533. I ran a code reviews on the PR and checked the findings against the code and the Microsoft docs. Below is what I'd like to discuss before we merge. The biggest point is where the rule lives; most of the other points follow from that. 1. Move the rule to PlatformCopLinterCop is scoped to "non-breaking code smells … where multiple valid options exist". A write inside a TryFunction isn't a style choice. It's a runtime-semantics hazard, so it belongs in PlatformCop: "AL language and runtime semantic correctness, preventing patterns that always fail or behave unpredictably … independent of the Business Central domain model". It isn't ApplicationCop either, because nothing here depends on how tables, pages or permissions are modelled. From Handling errors using try methods:
It's "fails or behaves unpredictably" rather than "always fails", which fits PlatformCop. Concretely:
2. Disable LC0102 (the "potential modification" variant) by defaultLC0102 reports every call to a non-built-in method inside a TryFunction ( 3. False positives: temporary records and non-record built-insThe analyzer classifies by method name and codeunit 50100 MyCodeunit
{
[TryFunction]
local procedure TryBuildBuffer()
var
TempBuffer: Record MyTable temporary;
Names: List of [Text];
Builder: TextBuilder;
begin
TempBuffer.Insert(); // reported, but a temporary record never touches the database
TempBuffer.DeleteAll(); // reported, same
Names.Insert(1, 'x'); // reported: List.Insert is a BuiltInMethod named "Insert"
Builder.Insert(1, 'x'); // reported: TextBuilder.Insert, same
end;
}Records whose table has Common already has what's needed:
4. False negative: calls without parenthesesAL lets you write Walking operations instead of syntax fixes this: 5. Simplify the analyzerA few smaller things in
Put together with 3 and 4, the analyzer could look roughly like this (sketchy and untested): using System.Collections.Immutable;
using ALCops.Common;
using ALCops.Common.Extensions;
using ALCops.Common.Reflection;
using Microsoft.Dynamics.Nav.CodeAnalysis;
using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics;
using Microsoft.Dynamics.Nav.CodeAnalysis.Semantics;
using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax;
namespace ALCops.PlatformCop.Analyzers;
[DiagnosticAnalyzer]
public sealed class TryFunctionDatabaseWrite : DiagnosticAnalyzer
{
private static readonly ImmutableHashSet<string> DatabaseWriteMethods =
RecordMethodClassification.InsertMethods
.Union(RecordMethodClassification.ModifyMethods)
.Union(RecordMethodClassification.DeleteMethods)
.Union(new[] { "Truncate", "AddLink", "CopyLinks", "DeleteLink", "DeleteLinks" });
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(DiagnosticDescriptors.TryFunctionDatabaseWrite);
public override void Initialize(AnalysisContext context) =>
context.RegisterCodeBlockAction(AnalyzeCodeBlock);
private static void AnalyzeCodeBlock(CodeBlockAnalysisContext ctx)
{
if (ctx.IsObsolete()
|| ctx.CodeBlock is not MethodDeclarationSyntax { Body: { } body } method
|| !method.IsTryFunction()
|| ctx.SemanticModel.GetOperation(body, ctx.CancellationToken) is not { } operation)
{
return;
}
new DatabaseWriteWalker(ctx).Visit(operation);
}
private sealed class DatabaseWriteWalker : OperationWalker
{
private readonly CodeBlockAnalysisContext _ctx;
public DatabaseWriteWalker(CodeBlockAnalysisContext ctx) => _ctx = ctx;
// IInvocationExpression covers both MyRec.Insert() and MyRec.Insert (no parentheses).
public override void VisitInvocationExpression(IInvocationExpression operation)
{
_ctx.CancellationToken.ThrowIfCancellationRequested();
if (operation.TargetMethod.MethodKind == EnumProvider.MethodKind.BuiltInMethod
&& DatabaseWriteMethods.Contains(operation.TargetMethod.Name)
// null for non-record receivers (List, TextBuilder, HttpClient, RecordRef, ...)
&& operation.GetReceiverTableType(_ctx.OwningSymbol, out var recordType) is { } table
// temporary records never write to the database
&& !table.IsTemporary()
&& recordType?.IsTemporary() != true)
{
_ctx.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.TryFunctionDatabaseWrite,
operation.Syntax.GetLocation(),
operation.TargetMethod.Name));
}
base.VisitInvocationExpression(operation);
}
}
}6. TestsAdd regression fixtures for the cases above, so they can't come back:
For example: codeunit 50100 TryFuncTemporaryRecord
{
[TryFunction]
local procedure TryFillBuffer()
var
TempMyRecord: Record MyTable temporary;
MyTempTable: Record MyTempTable;
begin
[|TempMyRecord.Insert()|];
[|TempMyRecord.DeleteAll()|];
[|MyTempTable.Insert()|];
end;
}
table 50100 MyTable
{
fields { field(1; MyField; Integer) { } }
}
table 50101 MyTempTable
{
TableType = Temporary;
fields { field(1; MyField; Integer) { } }
}codeunit 50100 TryFuncWithoutParentheses
{
[TryFunction]
local procedure TryWork()
var
MyRecord: Record MyTable;
begin
[|MyRecord.Insert|];
[|MyRecord.DeleteAll|];
end;
}
table 50100 MyTable
{
fields { field(1; MyField; Integer) { } }
}7. Housekeeping
Happy to help with any of this of you need any help, feel free to reach out. Again thank you for this great rule 🤗 |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
82eb054 to
3310a0d
Compare
|
Companion documentation PR for PC0040/PC0041: ALCops/alcops.dev#213. It adds both rule pages and updates the PlatformCop index. |
Summary
Moves the TryFunction database-write diagnostics from LinterCop to PlatformCop and separates direct writes from calls that may write.
PC0040 recognizes calls written with or without parentheses and avoids same-named non-record built-ins such as
List.Insert,TextBuilder.InsertandHttpClient.Delete. Both diagnostics skip obsolete TryFunctions and code outside TryFunctions. Temporary records, temporary tables, and temporary page source tables are not reported as direct writes.RecordRefwrites are intentionally skipped because temporary status is only known at runtime.Validation
netstandard2.1,net8.0andnet10.0, with zero warnings/errors.dotnet format ALCops.sln --verify-no-changes: passed.Validate-Rules.ps1: passed.git diff --check: passed.Documentation
Companion rule pages and PlatformCop index updates: ALCops/alcops.dev#213.