From 66bad3c4e34962c70723c899baaec57291308d3b Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Sun, 1 Feb 2026 14:38:55 +0100 Subject: [PATCH 1/3] Add "Xml Documentation" to DocumentationCop --- src/ALCops.Common/Reflection/EnumProvider.cs | 6 ++ .../HasDiagnostic/DuplicateParameter.al | 12 +++ .../HasDiagnostic/DuplicateReturns.al | 12 +++ .../HasDiagnostic/Parameter.al | 28 +++++ .../HasDiagnostic/Return.al | 19 ++++ .../HasDiagnostic/TryFunction.al | 11 ++ .../NoDiagnostic/NoDocumentationComment.al | 8 ++ .../NoDiagnostic/Parameter.al | 11 ++ .../NoDiagnostic/Return.al | 11 ++ .../NoDiagnostic/TryFunction.al | 12 +++ .../XmlDocumentationProcedureConsistency.cs | 48 +++++++++ .../ALCops.DocumentationCopAnalyzers.cs | 27 +++++ .../ALCops.DocumentationCopAnalyzers.resx | 9 ++ .../XmlDocumentationProcedureConsistency.cs | 102 ++++++++++++++++++ .../DiagnosticDescriptors.cs | 10 ++ src/ALCops.DocumentationCop/DiagnosticIds.cs | 1 + 16 files changed, 327 insertions(+) create mode 100644 src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/DuplicateParameter.al create mode 100644 src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/DuplicateReturns.al create mode 100644 src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/Parameter.al create mode 100644 src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/Return.al create mode 100644 src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/TryFunction.al create mode 100644 src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/NoDiagnostic/NoDocumentationComment.al create mode 100644 src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/NoDiagnostic/Parameter.al create mode 100644 src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/NoDiagnostic/Return.al create mode 100644 src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/NoDiagnostic/TryFunction.al create mode 100644 src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/XmlDocumentationProcedureConsistency.cs create mode 100644 src/ALCops.DocumentationCop/Analyzers/XmlDocumentationProcedureConsistency.cs diff --git a/src/ALCops.Common/Reflection/EnumProvider.cs b/src/ALCops.Common/Reflection/EnumProvider.cs index 673d639..3b7b0b6 100644 --- a/src/ALCops.Common/Reflection/EnumProvider.cs +++ b/src/ALCops.Common/Reflection/EnumProvider.cs @@ -1606,6 +1606,10 @@ public static class SyntaxKind new(() => ParseEnum(nameof(NavCodeAnalysis.SyntaxKind.WhileKeyword))); private static readonly Lazy _whileStatement = new(() => ParseEnum(nameof(NavCodeAnalysis.SyntaxKind.WhileStatement))); + private static readonly Lazy _xmlElement = + new(() => ParseEnum(nameof(NavCodeAnalysis.SyntaxKind.XmlElement))); + private static readonly Lazy _xmlNameAttribute = + new(() => ParseEnum(nameof(NavCodeAnalysis.SyntaxKind.XmlNameAttribute))); private static readonly Lazy _xmlPortKeyword = new(() => ParseEnum(nameof(NavCodeAnalysis.SyntaxKind.XmlPortKeyword))); private static readonly Lazy _xmlPortObject = @@ -1749,7 +1753,9 @@ public static class SyntaxKind public static NavCodeAnalysis.SyntaxKind VarSection => _varSection.Value; public static NavCodeAnalysis.SyntaxKind WhileKeyword => _whileKeyword.Value; public static NavCodeAnalysis.SyntaxKind WhileStatement => _whileStatement.Value; + public static NavCodeAnalysis.SyntaxKind XmlElement => _xmlElement.Value; public static NavCodeAnalysis.SyntaxKind XmlPortKeyword => _xmlPortKeyword.Value; + public static NavCodeAnalysis.SyntaxKind XmlNameAttribute => _xmlNameAttribute.Value; public static NavCodeAnalysis.SyntaxKind XmlPortObject => _xmlPortObject.Value; } diff --git a/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/DuplicateParameter.al b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/DuplicateParameter.al new file mode 100644 index 0000000..65712bf --- /dev/null +++ b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/DuplicateParameter.al @@ -0,0 +1,12 @@ +codeunit 50100 MyCodeunit +{ + /// + /// Duplicate documentation parameter. + /// + /// The parameter documentation. + /// [|The duplicate parameter documentation.|] + procedure MyProcedure(Value: Boolean) + begin + + end; +} \ No newline at end of file diff --git a/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/DuplicateReturns.al b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/DuplicateReturns.al new file mode 100644 index 0000000..e5600c8 --- /dev/null +++ b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/DuplicateReturns.al @@ -0,0 +1,12 @@ +codeunit 50100 MyCodeunit +{ + /// + /// Duplicate documentation returns. + /// + /// A Value. + /// [|A Value (duplicate).|] + procedure MyProcedure() Value: Boolean + begin + + end; +} \ No newline at end of file diff --git a/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/Parameter.al b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/Parameter.al new file mode 100644 index 0000000..3fbf0d6 --- /dev/null +++ b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/Parameter.al @@ -0,0 +1,28 @@ +codeunit 50100 MyCodeunit +{ + /// + /// Documentation comment parameter but no procedure parameter. + /// + /// [|The value.|] + procedure NoParameter() + begin + + end; + + /// + /// Procedure parameter but no documentation comment parameter. + /// + procedure ParameterButNoComment([|Value: Boolean|]) + begin + + end; + + /// + /// Parameter name mismatch. + /// + /// [|The value.|] + procedure NameMissmatch([|Value: Boolean|]) + begin + + end; +} \ No newline at end of file diff --git a/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/Return.al b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/Return.al new file mode 100644 index 0000000..2fedd71 --- /dev/null +++ b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/Return.al @@ -0,0 +1,19 @@ +codeunit 50100 MyCodeunit +{ + /// + /// Documentation comment with returns but no return value. + /// + /// [|Some value.|] + procedure DoesNotReturn() + begin + + end; + + /// + /// Return value but no documentation comment returns. + /// + procedure DoesReturnButNoComment() [|ReturnValue: Boolean|] + begin + + end; +} \ No newline at end of file diff --git a/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/TryFunction.al b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/TryFunction.al new file mode 100644 index 0000000..d892208 --- /dev/null +++ b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/HasDiagnostic/TryFunction.al @@ -0,0 +1,11 @@ +codeunit 50100 MyCodeunit +{ + /// + /// A method with TryFunction attribute has a implicit (boolean) return value. + /// + [[|TryFunction|]] + procedure MyTryFunction() + begin + + end; +} \ No newline at end of file diff --git a/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/NoDiagnostic/NoDocumentationComment.al b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/NoDiagnostic/NoDocumentationComment.al new file mode 100644 index 0000000..5e6ae06 --- /dev/null +++ b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/NoDiagnostic/NoDocumentationComment.al @@ -0,0 +1,8 @@ +codeunit 50100 MyCodeunit +{ + [|// just a normal procedure without a (structured) documentation comment + procedure NoDocumentationComment(Param: Boolean) ReturnValue: Boolean + begin + + end;|] +} \ No newline at end of file diff --git a/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/NoDiagnostic/Parameter.al b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/NoDiagnostic/Parameter.al new file mode 100644 index 0000000..a4f24c6 --- /dev/null +++ b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/NoDiagnostic/Parameter.al @@ -0,0 +1,11 @@ +codeunit 50100 MyCodeunit +{ + [|/// + /// Has a valid documentation comment. + /// + /// The parameter. + procedure ValidWithParameter(Param: Boolean) + begin + + end;|] +} \ No newline at end of file diff --git a/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/NoDiagnostic/Return.al b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/NoDiagnostic/Return.al new file mode 100644 index 0000000..51f1e83 --- /dev/null +++ b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/NoDiagnostic/Return.al @@ -0,0 +1,11 @@ +codeunit 50100 MyCodeunit +{ + [|/// + /// Has a valid documentation comment. + /// + /// The return value. + procedure ValidWithReturn() ReturnValue: Boolean + begin + + end;|] +} \ No newline at end of file diff --git a/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/NoDiagnostic/TryFunction.al b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/NoDiagnostic/TryFunction.al new file mode 100644 index 0000000..34d7a98 --- /dev/null +++ b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/NoDiagnostic/TryFunction.al @@ -0,0 +1,12 @@ +codeunit 50100 MyCodeunit +{ + /// + /// A method with TryFunction attribute has a implicit (boolean) return value. + /// + /// Returns success (true/false) + [[|TryFunction|]] + procedure MyTryFunction() + begin + + end; +} \ No newline at end of file diff --git a/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/XmlDocumentationProcedureConsistency.cs b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/XmlDocumentationProcedureConsistency.cs new file mode 100644 index 0000000..e47d5fc --- /dev/null +++ b/src/ALCops.DocumentationCop.Test/Rules/XmlDocumentationProcedureConsistency/XmlDocumentationProcedureConsistency.cs @@ -0,0 +1,48 @@ +using RoslynTestKit; + +namespace ALCops.DocumentationCop.Test +{ + public class XmlDocumentationProcedureConsistency : 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(XmlDocumentationProcedureConsistency))); + } + + [Test] + [TestCase("Return")] + [TestCase("Parameter")] + [TestCase("DuplicateParameter")] + [TestCase("DuplicateReturns")] + [TestCase("TryFunction")] + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(HasDiagnostic), $"{testCase}.al")) + .ConfigureAwait(false); + + _fixture.HasDiagnosticAtAllMarkers(code, DiagnosticIds.XmlDocumentationProcedureConsistency); + } + + [Test] + [TestCase("Return")] + [TestCase("Parameter")] + [TestCase("NoDocumentationComment")] + [TestCase("TryFunction")] + public async Task NoDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(NoDiagnostic), $"{testCase}.al")) + .ConfigureAwait(false); + + _fixture.NoDiagnosticAtAllMarkers(code, DiagnosticIds.XmlDocumentationProcedureConsistency); + } + } +} \ No newline at end of file diff --git a/src/ALCops.DocumentationCop/ALCops.DocumentationCopAnalyzers.cs b/src/ALCops.DocumentationCop/ALCops.DocumentationCopAnalyzers.cs index 6f39896..2c26b2e 100644 --- a/src/ALCops.DocumentationCop/ALCops.DocumentationCopAnalyzers.cs +++ b/src/ALCops.DocumentationCop/ALCops.DocumentationCopAnalyzers.cs @@ -164,5 +164,32 @@ internal static string WriteToFlowFieldRequiresCommentTitle { return ResourceManager.GetString("WriteToFlowFieldRequiresCommentTitle", resourceCulture); } } + + /// + /// Looks up a localized string similar to The XML documentation for a procedure must accurately reflect its signature.. + /// + internal static string XmlDocumentationProcedureConsistencyDescription { + get { + return ResourceManager.GetString("XmlDocumentationProcedureConsistencyDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The XML documentation does not match the procedure signature.. + /// + internal static string XmlDocumentationProcedureConsistencyMessageFormat { + get { + return ResourceManager.GetString("XmlDocumentationProcedureConsistencyMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to XML documentation must match the procedure signature. + /// + internal static string XmlDocumentationProcedureConsistencyTitle { + get { + return ResourceManager.GetString("XmlDocumentationProcedureConsistencyTitle", resourceCulture); + } + } } } diff --git a/src/ALCops.DocumentationCop/ALCops.DocumentationCopAnalyzers.resx b/src/ALCops.DocumentationCop/ALCops.DocumentationCopAnalyzers.resx index d8c31b9..3d4a5d3 100644 --- a/src/ALCops.DocumentationCop/ALCops.DocumentationCopAnalyzers.resx +++ b/src/ALCops.DocumentationCop/ALCops.DocumentationCopAnalyzers.resx @@ -153,4 +153,13 @@ FlowFields are calculated fields and are not intended to be written to. Writing to a FlowField, whether accidental or intentional, can lead to runtime errors. + + XML documentation must match the procedure signature + + + The XML documentation does not match the procedure signature. + + + The XML documentation for a procedure must accurately reflect its signature. + \ No newline at end of file diff --git a/src/ALCops.DocumentationCop/Analyzers/XmlDocumentationProcedureConsistency.cs b/src/ALCops.DocumentationCop/Analyzers/XmlDocumentationProcedureConsistency.cs new file mode 100644 index 0000000..784be05 --- /dev/null +++ b/src/ALCops.DocumentationCop/Analyzers/XmlDocumentationProcedureConsistency.cs @@ -0,0 +1,102 @@ +using System.Collections.Immutable; +using ALCops.Common.Extensions; +using ALCops.Common.Reflection; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; +using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; + +namespace ALCops.DocumentationCop.Analyzers; + +[DiagnosticAnalyzer] +public sealed class XmlDocumentationProcedureConsistency : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create( + DiagnosticDescriptors.XmlDocumentationProcedureConsistency); + + public override void Initialize(AnalysisContext context) => + context.RegisterSyntaxNodeAction( + AnalyzeDocumentationComments, + EnumProvider.SyntaxKind.MethodDeclaration); + + private void AnalyzeDocumentationComments(SyntaxNodeAnalysisContext ctx) + { + if (ctx.IsObsolete() || ctx.Node is not MethodDeclarationSyntax methodDeclarationSyntax) + return; + + var docCommentTrivia = methodDeclarationSyntax.GetLeadingTrivia().FirstOrDefault(trivia => trivia.Kind == EnumProvider.SyntaxKind.SingleLineDocumentationCommentTrivia); + if (docCommentTrivia.IsKind(EnumProvider.SyntaxKind.None)) + return; // no documentation comment exists + + Dictionary docCommentParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + XmlElementSyntax? docCommentReturns = null; + + var docCommentStructure = (DocumentationCommentTriviaSyntax)docCommentTrivia.GetStructure(); + var docCommentElements = docCommentStructure.Content.Where(xmlNode => xmlNode.Kind == EnumProvider.SyntaxKind.XmlElement); + + // evaluate documentation comment syntax + foreach (XmlElementSyntax element in docCommentElements.Cast()) + { + switch (element.StartTag.Name.LocalName.Text.ToLowerInvariant()) + { + case "param": + var nameAttribute = (XmlNameAttributeSyntax)element.StartTag.Attributes.First(att => att.IsKind(EnumProvider.SyntaxKind.XmlNameAttribute)); + var parameterName = nameAttribute.Identifier.GetText().ToString(); + if (!docCommentParameters.ContainsKey(parameterName)) + docCommentParameters.Add(parameterName, element); + else + // report diagnostic for duplicate parameter documentation + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.XmlDocumentationProcedureConsistency, element.GetLocation())); + break; + case "returns": + if (docCommentReturns is not null) + // report diagnostic for duplicate returns documentation + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.XmlDocumentationProcedureConsistency, element.GetLocation())); + docCommentReturns = element; + break; + } + } + + // excess documentation comment return value + if (docCommentReturns is not null && methodDeclarationSyntax.ReturnValue is null) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.XmlDocumentationProcedureConsistency, docCommentReturns.GetLocation())); + + // return value without documentation comment + if (docCommentReturns is null && (methodDeclarationSyntax.ReturnValue is not null)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.XmlDocumentationProcedureConsistency, methodDeclarationSyntax.ReturnValue.GetLocation())); + + // method with TryFunction decorator without return in documentation comment + if (docCommentReturns is null) + { + var tryFunctionAttribute = + methodDeclarationSyntax.Attributes + .FirstOrDefault(attr => + string.Equals( + attr.Name.Identifier.ValueText?.UnquoteIdentifier(), + "TryFunction", + StringComparison.OrdinalIgnoreCase)); + + if (tryFunctionAttribute is not null) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.XmlDocumentationProcedureConsistency, + tryFunctionAttribute.Name.GetLocation())); + } + } + + // check documentation comment parameters against method syntax + foreach (var docCommentParameter in docCommentParameters) + { + if (!methodDeclarationSyntax.ParameterList.Parameters.Any(param => (param.Name.Identifier.ValueText?.UnquoteIdentifier() ?? string.Empty).Equals(docCommentParameter.Key, StringComparison.OrdinalIgnoreCase))) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.XmlDocumentationProcedureConsistency, docCommentParameter.Value.GetLocation())); + } + + // check method parameters against documentation comment syntax + foreach (var methodParameter in methodDeclarationSyntax.ParameterList.Parameters) + { + if (!docCommentParameters.Any(docParam => docParam.Key.Equals(methodParameter.Name.Identifier.ValueText?.UnquoteIdentifier(), StringComparison.OrdinalIgnoreCase))) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.XmlDocumentationProcedureConsistency, methodParameter.GetLocation())); + } + } +} \ No newline at end of file diff --git a/src/ALCops.DocumentationCop/DiagnosticDescriptors.cs b/src/ALCops.DocumentationCop/DiagnosticDescriptors.cs index 8204d64..cf23b60 100644 --- a/src/ALCops.DocumentationCop/DiagnosticDescriptors.cs +++ b/src/ALCops.DocumentationCop/DiagnosticDescriptors.cs @@ -45,6 +45,16 @@ public static class DiagnosticDescriptors description: DocumentationCopAnalyzers.WriteToFlowFieldRequiresCommentDescription, helpLinkUri: GetHelpUri(DiagnosticIds.WriteToFlowFieldRequiresComment)); + public static readonly DiagnosticDescriptor XmlDocumentationProcedureConsistency = new( + id: DiagnosticIds.XmlDocumentationProcedureConsistency, + title: DocumentationCopAnalyzers.XmlDocumentationProcedureConsistencyTitle, + messageFormat: DocumentationCopAnalyzers.XmlDocumentationProcedureConsistencyMessageFormat, + category: Category.Design, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: DocumentationCopAnalyzers.XmlDocumentationProcedureConsistencyDescription, + helpLinkUri: GetHelpUri(DiagnosticIds.XmlDocumentationProcedureConsistency)); + public static string GetHelpUri(string identifier) { return string.Format(CultureInfo.InvariantCulture, "https://alcops.dev/docs/analyzers/documentationcop/{0}/", identifier.ToLower()); diff --git a/src/ALCops.DocumentationCop/DiagnosticIds.cs b/src/ALCops.DocumentationCop/DiagnosticIds.cs index 10fc39c..4b25f7c 100644 --- a/src/ALCops.DocumentationCop/DiagnosticIds.cs +++ b/src/ALCops.DocumentationCop/DiagnosticIds.cs @@ -6,4 +6,5 @@ public static class DiagnosticIds public static readonly string WriteToFlowFieldRequiresComment = "DC0002"; public static readonly string EmptyStatementRequiresComment = "DC0003"; public static readonly string PublicProcedureRequiresDocumentation = "DC0004"; + public static readonly string XmlDocumentationProcedureConsistency = "DC0005"; } \ No newline at end of file From 878b50fa91beab8c64663643d294e17581e768d9 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Mon, 2 Feb 2026 17:16:54 +0100 Subject: [PATCH 2/3] Add "TableDataAccessRequiresPermissions" to ApplicationCop --- .../HasDiagnostic/ProcedureCalls.al | 47 +++ .../HasDiagnostic/Queries.al | 33 ++ .../HasDiagnostic/Reports.al | 34 ++ .../HasDiagnostic/XmlPorts.al | 93 ++++++ .../NoDiagnostic/IntegerTable.al | 10 + .../MultiplePermissionsDifferentType.al | 31 ++ .../NoDiagnostic/PageExtensionSourceTable.al | 25 ++ .../NoDiagnostic/PageSourceTable.al | 52 ++++ .../PermissionPropertyWithComment.al | 32 ++ .../PermissionPropertyWithPragma.al | 45 +++ .../NoDiagnostic/PermissionsAsObjectId.al | 19 ++ ...cedureCallsInherentPermissionsAttribute.al | 41 +++ ...ocedureCallsInherentPermissionsProperty.al | 41 +++ .../ProcedureCallsPermissionsProperty.al | 42 +++ ...eCallsPermissionsPropertyFullyQualified.al | 44 +++ .../NoDiagnostic/QueryInherentPermissions.al | 33 ++ .../NoDiagnostic/QueryPermissionsProperty.al | 34 ++ .../NoDiagnostic/ReportInherentPermissions.al | 35 +++ .../NoDiagnostic/ReportPermissionsProperty.al | 35 +++ .../XMLPortWithTableElementProps.al | 41 +++ .../XmlPortInherentPermissions.al | 92 ++++++ .../XmlPortPermissionsProperty.al | 96 ++++++ .../TableDataAccessRequiresPermissions.cs | 86 ++++++ .../ALCops.ApplicationCopAnalyzers.cs | 36 +++ .../ALCops.ApplicationCopAnalyzers.resx | 12 + .../TableDataAccessRequiresPermissions.cs | 292 ++++++++++++++++++ .../UseReturnValueForDatabaseReadMethods.cs | 11 +- .../DiagnosticDescriptors.cs | 10 + src/ALCops.ApplicationCop/DiagnosticIds.cs | 1 + src/ALCops.Common/Reflection/EnumProvider.cs | 55 +++- .../Analyzers/AnalyzeCountMethod.cs | 2 +- 31 files changed, 1455 insertions(+), 5 deletions(-) create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/ProcedureCalls.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/Queries.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/Reports.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/XmlPorts.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/IntegerTable.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/MultiplePermissionsDifferentType.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PageExtensionSourceTable.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PageSourceTable.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionPropertyWithComment.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionPropertyWithPragma.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionsAsObjectId.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsInherentPermissionsAttribute.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsInherentPermissionsProperty.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsPermissionsProperty.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsPermissionsPropertyFullyQualified.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/QueryInherentPermissions.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/QueryPermissionsProperty.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ReportInherentPermissions.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ReportPermissionsProperty.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XMLPortWithTableElementProps.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XmlPortInherentPermissions.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XmlPortPermissionsProperty.al create mode 100644 src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/TableDataAccessRequiresPermissions.cs create mode 100644 src/ALCops.ApplicationCop/Analyzers/TableDataAccessRequiresPermissions.cs diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/ProcedureCalls.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/ProcedureCalls.al new file mode 100644 index 0000000..48b7185 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/ProcedureCalls.al @@ -0,0 +1,47 @@ +codeunit 50000 MyCodeunit +{ + + trigger OnRun() + var + MyTable: Record MyTable; + begin + // read + [|MyTable.Find();|] + [|MyTable.FindFirst();|] + [|MyTable.FindLast();|] + [|MyTable.FindSet();|] + [|MyTable.Get(1);|] + [|if MyTable.IsEmpty() then;|] + + // insert + [|MyTable.Insert();|] + + // modify + [|MyTable.Modify();|] + [|MyTable.ModifyAll(MyField2, 2);|] + [|MyTable.Rename(1);|] + + // delete + [|MyTable.Delete();|] + [|MyTable.DeleteAll();|] + end; +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/Queries.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/Queries.al new file mode 100644 index 0000000..d8ad897 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/Queries.al @@ -0,0 +1,33 @@ +query 50000 MyQuery +{ + + elements + { + [|dataitem(DataItemName; MyTable)|] + { + column(ColumnName; MyField) + { + + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/Reports.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/Reports.al new file mode 100644 index 0000000..5f0dbbe --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/Reports.al @@ -0,0 +1,34 @@ +report 50000 MyReport +{ + ProcessingOnly = true; + + dataset + { + [|dataitem(DataItemName; MyTable)|] + { + column(ColumnName; MyField) + { + + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/XmlPorts.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/XmlPorts.al new file mode 100644 index 0000000..d76362f --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/HasDiagnostic/XmlPorts.al @@ -0,0 +1,93 @@ +xmlport 50000 MyXmlport +{ + Direction = Import; + Permissions = tabledata MyTable = r; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField2) + { + + } + } + } + } +} +xmlport 50001 MyXmlport2 +{ + Direction = Export; + Permissions = tabledata MyTable = m; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField2) + { + + } + } + } + } +} +xmlport 50002 MyXmlport3 +{ + Direction = Both; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField2) + { + + } + } + } + } +} + +xmlport 50003 MyXmlport4 +{ + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField2) + { + + } + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/IntegerTable.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/IntegerTable.al new file mode 100644 index 0000000..151fad6 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/IntegerTable.al @@ -0,0 +1,10 @@ +codeunit 50000 MyCodeunit +{ + trigger OnRun() + var + Integer: Record Integer; + begin + [|Integer.FindFirst();|] + end; +} + diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/MultiplePermissionsDifferentType.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/MultiplePermissionsDifferentType.al new file mode 100644 index 0000000..1f61cb9 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/MultiplePermissionsDifferentType.al @@ -0,0 +1,31 @@ +codeunit 50000 Test +{ + Permissions = + tabledata MyTableOne = r, + tabledata MyTableTwo = i; + + procedure Test() + var + MyTableOne: Record MyTableOne; + MyTableTwo: Record MyTableTwo; + begin + [|MyTableOne.FindFirst();|] + [|MyTableTwo.Insert();|] + end; +} + +table 50000 MyTableOne +{ + fields + { + field(1; MyField; Integer) { } + } +} + +table 50001 MyTableTwo +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PageExtensionSourceTable.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PageExtensionSourceTable.al new file mode 100644 index 0000000..c0327d4 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PageExtensionSourceTable.al @@ -0,0 +1,25 @@ +pageextension 50000 MyPageExtension extends MyPage +{ + trigger OnOpenPage() + var + MyTable: Record MyTable; + begin + [|MyTable.FindFirst();|] + [|MyTable.Insert();|] + [|MyTable.Modify();|] + [|MyTable.Delete();|] + end; +} + +page 50000 MyPage +{ + SourceTable = MyTable; +} + +table 50000 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PageSourceTable.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PageSourceTable.al new file mode 100644 index 0000000..4c6fa81 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PageSourceTable.al @@ -0,0 +1,52 @@ +page 50000 MyPage +{ + PageType = Card; + ApplicationArea = All; + UsageCategory = Administration; + SourceTable = MyTable; + + layout + { + area(Content) + { + group(GroupName) + { + field(Name; MyField) + { + ApplicationArea = All; + + } + } + } + } + + trigger OnOpenPage() + var + MyTable: Record MyTable; + begin + [|Rec.FindFirst();|] + [|MyTable.FindFirst();|] + [|MyTable.Insert();|] + [|MyTable.Modify();|] + [|MyTable.Delete();|] + end; +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionPropertyWithComment.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionPropertyWithComment.al new file mode 100644 index 0000000..c95f832 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionPropertyWithComment.al @@ -0,0 +1,32 @@ +codeunit 50000 CommentTestCodeunit +{ + Permissions = + tabledata MyTableOne = r, + // single line comment + tabledata MyTableTwo = r; + + trigger OnRun() + var + MyTableOne: Record MyTableOne; + MyTableTwo: Record MyTableTwo; + begin + MyTableOne.FindFirst(); + [|MyTableTwo.FindFirst();|] + end; +} + +table 50000 MyTableOne +{ + fields + { + field(1; MyField; Integer) { } + } +} + +table 50001 MyTableTwo +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionPropertyWithPragma.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionPropertyWithPragma.al new file mode 100644 index 0000000..c3e587f --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionPropertyWithPragma.al @@ -0,0 +1,45 @@ +codeunit 50000 PragmaTestCodeunit +{ + // example from issue #923 (pragma in permissions property) + + Permissions = + tabledata MyTableOne = r, +#pragma warning disable AA0123 + tabledata MyTableTwo = r, +#pragma warning restore AA0123 + tabledata MyTableThree = r; + + trigger OnRun() + var + MyTableOne: Record MyTableOne; + MyTableTwo: Record MyTableTwo; + MyTableThree: Record MyTableThree; + begin + MyTableOne.FindFirst(); + [|MyTableTwo.FindFirst();|] + [|MyTableThree.FindFirst();|] + end; +} +table 50000 MyTableOne +{ + fields + { + field(1; MyField; Integer) { } + } +} + +table 50001 MyTableTwo +{ + fields + { + field(1; MyField; Integer) { } + } +} + +table 50002 MyTableThree +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionsAsObjectId.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionsAsObjectId.al new file mode 100644 index 0000000..6fe0ee5 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/PermissionsAsObjectId.al @@ -0,0 +1,19 @@ +codeunit 50000 MyCodeunit +{ + Permissions = tabledata 50000 = r; + + trigger OnRun() + var + MyTable: Record MyTable; + begin + [|MyTable.FindFirst();|] + end; +} + +table 50000 MyTable +{ + fields + { + field(1; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsInherentPermissionsAttribute.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsInherentPermissionsAttribute.al new file mode 100644 index 0000000..14d4e16 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsInherentPermissionsAttribute.al @@ -0,0 +1,41 @@ +codeunit 50000 MyCodeunit +{ + + [InherentPermissions(PermissionObjectType::TableData, Database::MyTable, 'RIMD')] + local procedure Test() + var + MyTable: Record MyTable; + begin + [|MyTable.Insert();|] + [|MyTable.Modify();|] + [|MyTable.Rename(1);|] + [|MyTable.ModifyAll(MyField2, 2);|] + [|MyTable.Find();|] + [|MyTable.FindFirst();|] + [|MyTable.FindLast();|] + [|MyTable.FindSet();|] + [|if MyTable.IsEmpty() then;|] + [|MyTable.Delete();|] + [|MyTable.Insert();|] + [|MyTable.DeleteAll();|] + end; +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsInherentPermissionsProperty.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsInherentPermissionsProperty.al new file mode 100644 index 0000000..87ae3c6 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsInherentPermissionsProperty.al @@ -0,0 +1,41 @@ +codeunit 50000 MyCodeunit +{ + + trigger OnRun() + var + MyTable: Record MyTable; + begin + [|MyTable.Insert();|] + [|MyTable.Modify();|] + [|MyTable.Rename(1);|] + [|MyTable.ModifyAll(MyField2, 2);|] + [|MyTable.Find();|] + [|MyTable.FindFirst();|] + [|MyTable.FindLast();|] + [|MyTable.FindSet();|] + [|if MyTable.IsEmpty() then;|] + [|MyTable.Delete();|] + [|MyTable.Insert();|] + [|MyTable.DeleteAll();|] + end; +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + InherentPermissions = rimd; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsPermissionsProperty.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsPermissionsProperty.al new file mode 100644 index 0000000..0205912 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsPermissionsProperty.al @@ -0,0 +1,42 @@ +codeunit 50000 MyCodeunit +{ + + Permissions = tabledata MyTable = rimd; + + trigger OnRun() + var + MyTable: Record MyTable; + begin + [|MyTable.Insert();|] + [|MyTable.Modify();|] + [|MyTable.Rename(1);|] + [|MyTable.ModifyAll(MyField2, 2);|] + [|MyTable.Find();|] + [|MyTable.FindFirst();|] + [|MyTable.FindLast();|] + [|MyTable.FindSet();|] + [|if MyTable.IsEmpty() then;|] + [|MyTable.Delete();|] + [|MyTable.Insert();|] + [|MyTable.DeleteAll();|] + end; +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsPermissionsPropertyFullyQualified.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsPermissionsPropertyFullyQualified.al new file mode 100644 index 0000000..f55d0b6 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ProcedureCallsPermissionsPropertyFullyQualified.al @@ -0,0 +1,44 @@ +namespace MyNameSpace; + +codeunit 50000 MyCodeunit +{ + + Permissions = tabledata MyNameSpace.MyTable = rimd; + + trigger OnRun() + var + MyTable: Record MyNameSpace.MyTable; + begin + [|MyTable.Insert();|] + [|MyTable.Modify();|] + [|MyTable.Rename(1);|] + [|MyTable.ModifyAll(MyField2, 2);|] + [|MyTable.Find();|] + [|MyTable.FindFirst();|] + [|MyTable.FindLast();|] + [|MyTable.FindSet();|] + [|if MyTable.IsEmpty() then;|] + [|MyTable.Delete();|] + [|MyTable.Insert();|] + [|MyTable.DeleteAll();|] + end; +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/QueryInherentPermissions.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/QueryInherentPermissions.al new file mode 100644 index 0000000..bc2aee6 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/QueryInherentPermissions.al @@ -0,0 +1,33 @@ +query 50000 MyQuery +{ + elements + { + [|dataitem(DataItemName; MyTable)|] + { + column(ColumnName; MyField) + { + + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + InherentPermissions = r; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/QueryPermissionsProperty.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/QueryPermissionsProperty.al new file mode 100644 index 0000000..67648ae --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/QueryPermissionsProperty.al @@ -0,0 +1,34 @@ +query 50000 MyQuery +{ + Permissions = tabledata MyTable = r; + + elements + { + [|dataitem(DataItemName; MyTable)|] + { + column(ColumnName; MyField) + { + + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ReportInherentPermissions.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ReportInherentPermissions.al new file mode 100644 index 0000000..56d50b3 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ReportInherentPermissions.al @@ -0,0 +1,35 @@ +report 50000 MyReport +{ + ProcessingOnly = true; + + dataset + { + [|dataitem(DataItemName; MyTable)|] + { + column(ColumnName; MyField) + { + + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + InherentPermissions = r; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ReportPermissionsProperty.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ReportPermissionsProperty.al new file mode 100644 index 0000000..1dbd82b --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/ReportPermissionsProperty.al @@ -0,0 +1,35 @@ +report 50000 MyReport +{ + ProcessingOnly = true; + Permissions = tabledata MyTable = r; + + dataset + { + [|dataitem(DataItemName; MyTable)|] + { + column(ColumnName; MyField) + { + + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XMLPortWithTableElementProps.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XMLPortWithTableElementProps.al new file mode 100644 index 0000000..ed61e05 --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XMLPortWithTableElementProps.al @@ -0,0 +1,41 @@ +xmlport 50000 MyXmlport +{ + Direction = Import; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + AutoReplace = false; // modify permissions + AutoSave = false; // insert permissions + AutoUpdate = false; //modify permissions + + fieldattribute(NodeName3; NodeName2.MyField) + { + + } + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XmlPortInherentPermissions.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XmlPortInherentPermissions.al new file mode 100644 index 0000000..d7076ed --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XmlPortInherentPermissions.al @@ -0,0 +1,92 @@ +xmlport 50000 MyXmlport +{ + Direction = Import; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField) + { + + } + } + } + } +} +xmlport 50001 MyXmlport2 +{ + Direction = Export; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField) + { + + } + } + } + } +} +xmlport 50002 MyXmlport3 +{ + Direction = Both; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField) + { + + } + } + } + } +} + +xmlport 50003 MyXmlport4 +{ + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField) + { + + } + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + InherentPermissions = rim; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XmlPortPermissionsProperty.al b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XmlPortPermissionsProperty.al new file mode 100644 index 0000000..c385b2d --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/NoDiagnostic/XmlPortPermissionsProperty.al @@ -0,0 +1,96 @@ +xmlport 50000 MyXmlport +{ + Direction = Import; + Permissions = tabledata MyTable = im; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField) + { + + } + } + } + } +} +xmlport 50001 MyXmlport2 +{ + Direction = Export; + Permissions = tabledata MyTable = r; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField) + { + + } + } + } + } +} +xmlport 50002 MyXmlport3 +{ + Direction = Both; + Permissions = tabledata MyTable = rim; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField) + { + + } + } + } + } +} + +xmlport 50003 MyXmlport4 +{ + + Permissions = tabledata MyTable = rim; + + schema + { + textelement(NodeName1) + { + [|tableelement(NodeName2; MyTable)|] + { + fieldattribute(NodeName3; NodeName2.MyField) + { + + } + } + } + } +} + +table 50000 MyTable +{ + Caption = '', Locked = true; + + fields + { + field(1; MyField; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + field(2; MyField2; Integer) + { + Caption = '', Locked = true; + DataClassification = ToBeClassified; + } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/TableDataAccessRequiresPermissions.cs b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/TableDataAccessRequiresPermissions.cs new file mode 100644 index 0000000..45de38b --- /dev/null +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/TableDataAccessRequiresPermissions.cs @@ -0,0 +1,86 @@ +using ALCops.ApplicationCop.CodeFixes; +using RoslynTestKit; + +namespace ALCops.ApplicationCop.Test +{ + public class TableDataAccessRequiresPermissions : NavCodeAnalysisBase + { + private AnalyzerTestFixture _fixture; + private static readonly Analyzers.TableDataAccessRequiresPermissions _analyzer = new(); + private string _testCasePath; + + [SetUp] + public void Setup() + { + _fixture = RoslynFixtureFactory.Create(); + + _testCasePath = Path.Combine( + Directory.GetParent( + Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + Path.Combine("Rules", nameof(TableDataAccessRequiresPermissions))); + } + + [Test] + [TestCase("ProcedureCalls")] + [TestCase("XmlPorts")] + [TestCase("Queries")] + [TestCase("Reports")] + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(HasDiagnostic), $"{testCase}.al")) + .ConfigureAwait(false); + + _fixture.HasDiagnosticAtAllMarkers(code, DiagnosticIds.TableDataAccessRequiresPermissions); + } + + [Test] + [TestCase("ProcedureCallsPermissionsProperty")] + [TestCase("XmlPortPermissionsProperty")] + [TestCase("QueryPermissionsProperty")] + [TestCase("XmlPortInherentPermissions")] + [TestCase("QueryInherentPermissions")] + [TestCase("ReportPermissionsProperty")] + [TestCase("ReportInherentPermissions")] + [TestCase("ProcedureCallsInherentPermissionsProperty")] + [TestCase("ProcedureCallsInherentPermissionsAttribute")] + [TestCase("PageSourceTable")] + [TestCase("PageExtensionSourceTable")] + [TestCase("ProcedureCallsPermissionsPropertyFullyQualified")] + // [TestCase("IntegerTable")] + [TestCase("XMLPortWithTableElementProps")] + [TestCase("PermissionsAsObjectId")] + [TestCase("PermissionPropertyWithPragma")] + [TestCase("PermissionPropertyWithComment")] + [TestCase("MultiplePermissionsDifferentType")] + public async Task NoDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(NoDiagnostic), $"{testCase}.al")) + .ConfigureAwait(false); + + _fixture.NoDiagnosticAtAllMarkers(code, DiagnosticIds.TableDataAccessRequiresPermissions); + } + + // [Test] + // [TestCase("PageRunModelPageIdentifierAndRecord")] + // [TestCase("PageRunModelPageIdentifierAndRecordWithPageFIeld")] + // [TestCase("PageRunPageIdentifierAndRecord")] + // [TestCase("PageRunPageIdentifierAndRecordWithPageField")] + // [TestCase("PageRunZeroIdentifierAndRecord")] + // public async Task HasFix(string testCase) + // { + // var currentCode = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(HasFix), testCase, "current.al")) + // .ConfigureAwait(false); + + // var expectedCode = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(HasFix), testCase, "expected.al")) + // .ConfigureAwait(false); + + // var fixture = RoslynFixtureFactory.Create( + // new CodeFixTestFixtureConfig + // { + // AdditionalAnalyzers = [_analyzer] + // }); + + // fixture.TestCodeFix(currentCode, expectedCode, DiagnosticDescriptors.NotBlankRequiredOnPrimaryKeyField); + // } + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.cs b/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.cs index d14da45..551aaf8 100644 --- a/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.cs +++ b/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.cs @@ -759,6 +759,42 @@ internal static string RunPageImplementPageManagementTitle { } } + /// + /// Looks up a localized string similar to ALCops: Add missing permissions. + /// + internal static string TableDataAccessRequiresPermissionsCodeAction { + get { + return ResourceManager.GetString("TableDataAccessRequiresPermissionsCodeAction", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Code that reads from or writes to table data must declare the required Permissions on the containing object. This ensures the code can run using indirect permissions, which is required when table access is restricted by the user's license. . + /// + internal static string TableDataAccessRequiresPermissionsDescription { + get { + return ResourceManager.GetString("TableDataAccessRequiresPermissionsDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The object does not declare permission "{0}" for tabledata "{1}". + /// + internal static string TableDataAccessRequiresPermissionsMessageFormat { + get { + return ResourceManager.GetString("TableDataAccessRequiresPermissionsMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Table data access requires explicit object permissions. + /// + internal static string TableDataAccessRequiresPermissionsTitle { + get { + return ResourceManager.GetString("TableDataAccessRequiresPermissionsTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to In projects where company data isolation matters, table objects must explicitly define the DataPerCompany property. Relying on implicit defaults makes the data scope unclear and can lead to incorrect assumptions about whether data is shared across companies or stored per company.. /// diff --git a/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.resx b/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.resx index d9d7f39..9ab6f5f 100644 --- a/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.resx +++ b/src/ALCops.ApplicationCop/ALCops.ApplicationCopAnalyzers.resx @@ -351,6 +351,18 @@ ALCops: Refactor to use "Page Management" codeunit + + Table data access requires explicit object permissions + + + The object does not declare permission "{0}" for tabledata "{1}" + + + Code that reads from or writes to table data must declare the required Permissions on the containing object. This ensures the code can run using indirect permissions, which is required when table access is restricted by the user's license. + + + ALCops: Add missing permissions + DataPerCompany must be explicitly set on table objects diff --git a/src/ALCops.ApplicationCop/Analyzers/TableDataAccessRequiresPermissions.cs b/src/ALCops.ApplicationCop/Analyzers/TableDataAccessRequiresPermissions.cs new file mode 100644 index 0000000..ded600f --- /dev/null +++ b/src/ALCops.ApplicationCop/Analyzers/TableDataAccessRequiresPermissions.cs @@ -0,0 +1,292 @@ +using System.Collections.Immutable; +using ALCops.Common.Extensions; +using ALCops.Common.Reflection; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; +using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; +using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities; + +namespace ALCops.ApplicationCop.Analyzers; + +[DiagnosticAnalyzer] +public class TableDataAccessRequiresPermissions : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create( + DiagnosticDescriptors.TableDataAccessRequiresPermissions); + + private static readonly ImmutableDictionary MethodPermissionMap = + ImmutableDictionary.CreateRange( + StringComparer.OrdinalIgnoreCase, + new[] + { + // read + new KeyValuePair("Find", 'r'), + new KeyValuePair("FindFirst", 'r'), + new KeyValuePair("FindLast", 'r'), + new KeyValuePair("FindSet", 'r'), + new KeyValuePair("Get", 'r'), + new KeyValuePair("IsEmpty", 'r'), + + // insert + new KeyValuePair("Insert", 'i'), + + // modify + new KeyValuePair("Modify", 'm'), + new KeyValuePair("ModifyAll", 'm'), + new KeyValuePair("Rename", 'm'), + + // delete + new KeyValuePair("Delete", 'd'), + new KeyValuePair("DeleteAll", 'd'), + }); + + public override void Initialize(AnalysisContext context) + { + context.RegisterOperationAction( + AnalyzeInvocation, + EnumProvider.OperationKind.InvocationExpression); + + context.RegisterSymbolAction( + CheckReportDataItemObjectPermission, + EnumProvider.SymbolKind.ReportDataItem); + + context.RegisterSymbolAction( + CheckQueryDataItemObjectPermission, + EnumProvider.SymbolKind.QueryDataItem); + + context.RegisterSymbolAction( + CheckXmlportNodeObjectPermission, + EnumProvider.SymbolKind.XmlPortNode); + } + + private void AnalyzeInvocation(OperationAnalysisContext ctx) + { + if (ctx.IsObsolete() || ctx.Operation is not IInvocationExpression invocation) + return; + + if (invocation.TargetMethod.MethodKind != EnumProvider.MethodKind.BuiltInMethod) + return; + + if (invocation.Instance?.Type is not IRecordTypeSymbol recordType || recordType.Temporary) + return; + + if (recordType.OriginalDefinition is not ITableTypeSymbol tableType) + return; + + if (TargetTableIsPageSourceTable(ctx, tableType)) + return; + + var permission = GetRequiredPermission(invocation.TargetMethod.Name); + if (permission is null) + return; + + var inherentPermissions = GetInherentPermissionsAttributes(ctx); + if (ProcedureHasInherentPermission(inherentPermissions, recordType, permission.Value)) + return; + + var objectPermissions = ctx.ContainingSymbol + .GetContainingApplicationObjectTypeSymbol() + ?.GetProperty(EnumProvider.PropertyKind.Permissions); + + CheckProcedureInvocation( + objectPermissions, + recordType, + permission.Value, + ctx.ReportDiagnostic, + invocation.Syntax.GetLocation(), + tableType); + } + + private void CheckXmlportNodeObjectPermission(SymbolAnalysisContext ctx) + { + if (ctx.IsObsolete()) + return; + + if (((IXmlPortNodeSymbol)ctx.Symbol.OriginalDefinition).SourceTypeKind != EnumProvider.XmlPortSourceTypeKind.Table) return; + + string direction = ""; + + IXmlPortTypeSymbol xmlPort = (IXmlPortTypeSymbol)ctx.Symbol.GetContainingObjectTypeSymbol(); + + IPropertySymbol? objectPermissions = xmlPort.GetProperty(EnumProvider.PropertyKind.Permissions); + ITypeSymbol targetSymbol = ((IXmlPortNodeSymbol)ctx.Symbol.OriginalDefinition).GetTypeSymbol(); + var directionProperty = xmlPort.Properties.FirstOrDefault(property => property.Name == "Direction"); + + if (directionProperty is null) + direction = EnumProvider.DirectionKind.Both.ToString(); + else + direction = directionProperty.ValueText; + + bool? AutoReplace = (bool?)ctx.Symbol.Properties.FirstOrDefault(property => property.PropertyKind == EnumProvider.PropertyKind.AutoReplace)?.Value; // modify permissions + bool? AutoUpdate = (bool?)ctx.Symbol.Properties.FirstOrDefault(property => property.PropertyKind == EnumProvider.PropertyKind.AutoUpdate)?.Value; // modify permissions + bool? AutoSave = (bool?)ctx.Symbol.Properties.FirstOrDefault(property => property.PropertyKind == EnumProvider.PropertyKind.AutoSave)?.Value; // insert permissions + + AutoReplace ??= true; + AutoUpdate ??= true; + AutoSave ??= true; + + direction = direction.ToLowerInvariant(); + + if (direction == "import" || direction == "both") + { + if (AutoReplace == true || AutoUpdate == true) + CheckProcedureInvocation(objectPermissions, targetSymbol, 'm', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); + if (AutoSave == true) + CheckProcedureInvocation(objectPermissions, targetSymbol, 'i', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); + } + if (direction == "export" || direction == "both") + CheckProcedureInvocation(objectPermissions, targetSymbol, 'r', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); + } + + private void CheckQueryDataItemObjectPermission(SymbolAnalysisContext ctx) + { + if (ctx.IsObsolete()) return; + + IPropertySymbol? objectPermissions = ctx.Symbol.GetContainingApplicationObjectTypeSymbol()?.GetProperty(EnumProvider.PropertyKind.Permissions); + ITypeSymbol targetSymbol = ((IQueryDataItemSymbol)ctx.Symbol).GetTypeSymbol(); + CheckProcedureInvocation(objectPermissions, targetSymbol, 'r', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); + } + + private void CheckReportDataItemObjectPermission(SymbolAnalysisContext ctx) + { + if (ctx.IsObsolete()) return; + if (ctx.Symbol.GetBooleanPropertyValue(EnumProvider.PropertyKind.UseTemporary) == true) return; + if (((ITableTypeSymbol)((IRecordTypeSymbol)((IReportDataItemSymbol)ctx.Symbol).GetTypeSymbol()).OriginalDefinition).TableType == EnumProvider.TableTypeKind.Temporary) return; + + IPropertySymbol? objectPermissions = ctx.Symbol.GetContainingApplicationObjectTypeSymbol()?.GetProperty(EnumProvider.PropertyKind.Permissions); + ITypeSymbol targetSymbol = ((IReportDataItemSymbol)ctx.Symbol).GetTypeSymbol(); + CheckProcedureInvocation(objectPermissions, targetSymbol, 'r', ctx.ReportDiagnostic, ctx.Symbol.GetLocation(), (ITableTypeSymbol)targetSymbol.OriginalDefinition); + } + + + + private static bool ProcedureHasInherentPermission(IEnumerable inherentPermissions, ITypeSymbol variableType, char requestedPermission) + { + //[InherentPermissions(PermissionObjectType::TableData, Database::"SomeTable", 'r'),InherentPermissions(PermissionObjectType::TableData, Database::"SomeOtherTable", 'w')] + + if (inherentPermissions is null || inherentPermissions.Count() == 0) return false; + + foreach (var inherentPermission in inherentPermissions) + { + var inherentPermissionAsString = inherentPermission.DeclaringSyntaxReference?.GetSyntax().ToString(); + + var permissions = inherentPermissionAsString?.Split(new[] { '[', ']', '(', ')', ',' }, StringSplitOptions.RemoveEmptyEntries); + if (permissions?[1].Trim() != "PermissionObjectType::TableData") continue; + + var typeAndObjectName = permissions[2].Trim(); + var permissionValue = permissions[3].Trim().Trim(new[] { '\'', ' ' }).ToLowerInvariant(); + + var typeParts = typeAndObjectName.Split(new[] { "::" }, StringSplitOptions.RemoveEmptyEntries); + if (typeParts.Length < 2) continue; + + var objectName = typeParts[1].Trim().Trim('"'); + if (objectName.ToLowerInvariant() != variableType.Name.ToLowerInvariant()) + if (objectName.UnquoteIdentifier().ToLowerInvariant() != (variableType.OriginalDefinition.ContainingNamespace?.QualifiedName.ToLowerInvariant() + "." + variableType.Name.ToLowerInvariant())) + continue; + + if (permissionValue.Contains(requestedPermission.ToString().ToLowerInvariant()[0])) + { + return true; + } + } + return false; + } + + private void CheckProcedureInvocation(IPropertySymbol? objectPermissions, ITypeSymbol variableType, char requestedPermission, Action ReportDiagnostic, Microsoft.Dynamics.Nav.CodeAnalysis.Text.Location location, ITableTypeSymbol targetTable) + { + if (targetTable.Id > 2000000000) + return; + + if (TableHasInherentPermission(targetTable, requestedPermission)) + return; + + if (objectPermissions is null) + { + ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.TableDataAccessRequiresPermissions, location, requestedPermission, variableType.Name)); + return; + } + + bool permissionContainRequestedObject = false; + var permissions = objectPermissions.GetPropertyValueSyntax(); + foreach (var permission in permissions.PermissionProperties) + { + if (!permission.ObjectType.IsKind(EnumProvider.SyntaxKind.TableDataKeyword)) + continue; // ensure permission is tabledata + + var identifier = permission.ObjectReference.Identifier; + switch (identifier.Kind) + { + case var _ when identifier.Kind == EnumProvider.SyntaxKind.IdentifierName: + string? name = ((IdentifierNameSyntax)identifier).Identifier.ValueText?.UnquoteIdentifier(); + if (name is not null && name.Equals(variableType.Name, StringComparison.OrdinalIgnoreCase)) + permissionContainRequestedObject = true; + break; + case var _ when identifier.Kind == EnumProvider.SyntaxKind.ObjectId: + int objectId = Convert.ToInt32(((ObjectIdSyntax)identifier).Value.ValueText); + if (objectId == targetTable.Id) + permissionContainRequestedObject = true; + break; + case var _ when identifier.Kind == EnumProvider.SyntaxKind.QualifiedName: + string qualifier = ((QualifiedNameSyntax)identifier).Left.GetText().ToString(); + string? onlyName = ((QualifiedNameSyntax)identifier).Right.Identifier.ValueText?.UnquoteIdentifier(); + if (onlyName is not null && qualifier.Equals(variableType.OriginalDefinition.ContainingNamespace?.QualifiedName, StringComparison.OrdinalIgnoreCase) && onlyName.Equals(variableType.Name, StringComparison.OrdinalIgnoreCase)) + permissionContainRequestedObject = true; + break; + } + if (permissionContainRequestedObject) + { + var permissionsText = permission.Permissions.ValueText; + if (permissionsText is null || !permissionsText.ToLowerInvariant().Contains(requestedPermission)) + ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.TableDataAccessRequiresPermissions, location, requestedPermission, variableType.Name)); + break; // analysed the permissions for the requested object, break the foreach loop + } + } + if (!permissionContainRequestedObject) + { + ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.TableDataAccessRequiresPermissions, location, requestedPermission, variableType.Name)); + } + } + + private bool TableHasInherentPermission(ITableTypeSymbol table, char requestedPermission) + { + IPropertySymbol? permissionProperty = table.GetProperty(EnumProvider.PropertyKind.InherentPermissions); + // InherentPermissions = RIMD; + char[]? permissions = permissionProperty?.Value.ToString()?.ToLowerInvariant().Split(new[] { '=' }, 2)[0].Trim().ToCharArray(); + + if (permissions is not null && permissions.Contains(requestedPermission.ToString().ToLowerInvariant()[0])) + return true; + + return false; + } + + + private static char? GetRequiredPermission(string methodName) + { + return MethodPermissionMap.TryGetValue(methodName, out var p) ? p : null; + } + + private static bool TargetTableIsPageSourceTable(OperationAnalysisContext ctx, ITableTypeSymbol targetTable) + { + IPageBaseTypeSymbol? page = ctx.ContainingSymbol.GetContainingApplicationObjectTypeSymbol() switch + { + IPageBaseTypeSymbol p => p, + IApplicationObjectExtensionTypeSymbol ext => ext.Target?.OriginalDefinition as IPageBaseTypeSymbol, + _ => null + }; + + if (page is null || page.RelatedTable is null) + return false; + + return page.RelatedTable.OriginalDefinition.Equals(targetTable); + } + + private static IEnumerable GetInherentPermissionsAttributes(OperationAnalysisContext ctx) + { + if (ctx.ContainingSymbol is not IMethodSymbol method) + return Enumerable.Empty(); + + return method.Attributes.Where(a => a.AttributeKind == EnumProvider.AttributeKind.InherentPermissions); + } +} \ No newline at end of file diff --git a/src/ALCops.ApplicationCop/Analyzers/UseReturnValueForDatabaseReadMethods.cs b/src/ALCops.ApplicationCop/Analyzers/UseReturnValueForDatabaseReadMethods.cs index 1894497..7f094c5 100644 --- a/src/ALCops.ApplicationCop/Analyzers/UseReturnValueForDatabaseReadMethods.cs +++ b/src/ALCops.ApplicationCop/Analyzers/UseReturnValueForDatabaseReadMethods.cs @@ -3,6 +3,7 @@ using ALCops.Common.Reflection; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; namespace ALCops.ApplicationCop.Analyzers; @@ -33,9 +34,13 @@ private void AnalyzeInvocation(OperationAnalysisContext ctx) if (ctx.IsObsolete() || ctx.Operation is not IInvocationExpression invocation) return; - if (invocation.TargetMethod.MethodKind != EnumProvider.MethodKind.BuiltInMethod || - invocation.Instance?.Type.OriginalDefinition.Kind != EnumProvider.SymbolKind.Table || - !DatabaseReadMethods.Contains(invocation.TargetMethod.Name)) + if (invocation.TargetMethod.MethodKind != EnumProvider.MethodKind.BuiltInMethod) + return; + + if (invocation.Instance?.Type is not IRecordTypeSymbol) + return; + + if (!DatabaseReadMethods.Contains(invocation.TargetMethod.Name)) return; if (ctx.Operation.Syntax.Parent.Kind == EnumProvider.SyntaxKind.ExpressionStatement) diff --git a/src/ALCops.ApplicationCop/DiagnosticDescriptors.cs b/src/ALCops.ApplicationCop/DiagnosticDescriptors.cs index 2567b25..d30860f 100644 --- a/src/ALCops.ApplicationCop/DiagnosticDescriptors.cs +++ b/src/ALCops.ApplicationCop/DiagnosticDescriptors.cs @@ -225,6 +225,16 @@ public static class DiagnosticDescriptors description: ApplicationCopAnalyzers.RunPageImplementPageManagementDescription, helpLinkUri: GetHelpUri(DiagnosticIds.RunPageImplementPageManagement)); + public static readonly DiagnosticDescriptor TableDataAccessRequiresPermissions = new( + id: DiagnosticIds.TableDataAccessRequiresPermissions, + title: ApplicationCopAnalyzers.TableDataAccessRequiresPermissionsTitle, + messageFormat: ApplicationCopAnalyzers.TableDataAccessRequiresPermissionsMessageFormat, + category: Category.Design, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: ApplicationCopAnalyzers.TableDataAccessRequiresPermissionsDescription, + helpLinkUri: GetHelpUri(DiagnosticIds.TableDataAccessRequiresPermissions)); + public static readonly DiagnosticDescriptor TableDataPerCompanyDeclaration = new( id: DiagnosticIds.TableDataPerCompanyDeclaration, title: ApplicationCopAnalyzers.TableDataPerCompanyDeclarationTitle, diff --git a/src/ALCops.ApplicationCop/DiagnosticIds.cs b/src/ALCops.ApplicationCop/DiagnosticIds.cs index 65efa6c..1d5f3c5 100644 --- a/src/ALCops.ApplicationCop/DiagnosticIds.cs +++ b/src/ALCops.ApplicationCop/DiagnosticIds.cs @@ -32,4 +32,5 @@ public static class DiagnosticIds public static readonly string TableFieldToolTipShouldBeDefined = "AC0028"; public static readonly string DuplicateToolTipBetweenPageAndTable = "AC0029"; public static readonly string UseReturnValueForDatabaseReadMethods = "AC0030"; + public static readonly string TableDataAccessRequiresPermissions = "AC0031"; } \ No newline at end of file diff --git a/src/ALCops.Common/Reflection/EnumProvider.cs b/src/ALCops.Common/Reflection/EnumProvider.cs index 3b7b0b6..11ddd07 100644 --- a/src/ALCops.Common/Reflection/EnumProvider.cs +++ b/src/ALCops.Common/Reflection/EnumProvider.cs @@ -150,6 +150,8 @@ public static class AttributeKind new(() => ParseEnum(nameof(NavCodeAnalysis.InternalSyntax.AttributeKind.FilterPageHandler))); private static readonly Lazy _hyperlinkHandler = new(() => ParseEnum(nameof(NavCodeAnalysis.InternalSyntax.AttributeKind.HyperlinkHandler))); + private static readonly Lazy _inherentPermissions = + new(() => ParseEnum(nameof(NavCodeAnalysis.InternalSyntax.AttributeKind.InherentPermissions))); private static readonly Lazy _integrationEvent = new(() => ParseEnum(nameof(NavCodeAnalysis.InternalSyntax.AttributeKind.IntegrationEvent))); private static readonly Lazy _internalEvent = @@ -183,6 +185,7 @@ public static class AttributeKind public static NavCodeAnalysis.InternalSyntax.AttributeKind EventSubscriber => _eventSubscriber.Value; public static NavCodeAnalysis.InternalSyntax.AttributeKind FilterPageHandler => _filterPageHandler.Value; public static NavCodeAnalysis.InternalSyntax.AttributeKind HyperlinkHandler => _hyperlinkHandler.Value; + public static NavCodeAnalysis.InternalSyntax.AttributeKind InherentPermissions => _inherentPermissions.Value; public static NavCodeAnalysis.InternalSyntax.AttributeKind IntegrationEvent => _integrationEvent.Value; public static NavCodeAnalysis.InternalSyntax.AttributeKind InternalEvent => _internalEvent.Value; public static NavCodeAnalysis.InternalSyntax.AttributeKind MessageHandler => _messageHandler.Value; @@ -327,6 +330,12 @@ public static class DirectionKind { public static readonly Lazy> CanonicalNames = CreateEnumDictionary(); + + + private static readonly Lazy _both = + new(() => ParseEnum(nameof(NavCodeAnalysis.DirectionKind.Both))); + + public static NavCodeAnalysis.DirectionKind Both => _both.Value; } /// @@ -880,6 +889,12 @@ public static class PropertyKind new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.ApplicationArea))); private static readonly Lazy _autoIncrement = new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.AutoIncrement))); + private static readonly Lazy _autoReplace = + new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.AutoReplace))); + private static readonly Lazy _autoSave = + new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.AutoSave))); + private static readonly Lazy _autoUpdate = + new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.AutoUpdate))); private static readonly Lazy _caption = new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.Caption))); private static readonly Lazy _captionClass = @@ -908,6 +923,8 @@ public static class PropertyKind new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.ObsoleteState))); private static readonly Lazy _oDataKeyFields = new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.ODataKeyFields))); + private static readonly Lazy _permissions = + new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.Permissions))); private static readonly Lazy _scope = new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.Scope))); private static readonly Lazy _showAs = @@ -916,6 +933,8 @@ public static class PropertyKind new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.ShowCaption))); private static readonly Lazy _singleInstance = new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.SingleInstance))); + private static readonly Lazy _sourceTable = + new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.SourceTable))); private static readonly Lazy _sourceTableTemporary = new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.SourceTableTemporary))); private static readonly Lazy _subtype = @@ -924,6 +943,8 @@ public static class PropertyKind new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.TableRelation))); private static readonly Lazy _toolTip = new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.ToolTip))); + private static readonly Lazy _useTemporary = + new(() => ParseEnum(nameof(NavCodeAnalysis.PropertyKind.UseTemporary))); public static NavCodeAnalysis.PropertyKind Access => _access.Value; @@ -931,6 +952,9 @@ public static class PropertyKind public static NavCodeAnalysis.PropertyKind AllowInCustomizations => _allowInCustomizations.Value; public static NavCodeAnalysis.PropertyKind ApplicationArea => _applicationArea.Value; public static NavCodeAnalysis.PropertyKind AutoIncrement => _autoIncrement.Value; + public static NavCodeAnalysis.PropertyKind AutoReplace => _autoReplace.Value; + public static NavCodeAnalysis.PropertyKind AutoSave => _autoSave.Value; + public static NavCodeAnalysis.PropertyKind AutoUpdate => _autoUpdate.Value; public static NavCodeAnalysis.PropertyKind Caption => _caption.Value; public static NavCodeAnalysis.PropertyKind CaptionClass => _captionClass.Value; public static NavCodeAnalysis.PropertyKind CaptionML => _captionMl.Value; @@ -945,14 +969,17 @@ public static class PropertyKind public static NavCodeAnalysis.PropertyKind NotBlank => _notBlank.Value; public static NavCodeAnalysis.PropertyKind ObsoleteState => _obsoleteState.Value; public static NavCodeAnalysis.PropertyKind ODataKeyFields => _oDataKeyFields.Value; + public static NavCodeAnalysis.PropertyKind Permissions => _permissions.Value; public static NavCodeAnalysis.PropertyKind Scope => _scope.Value; public static NavCodeAnalysis.PropertyKind ShowAs => _showAs.Value; public static NavCodeAnalysis.PropertyKind ShowCaption => _showCaption.Value; public static NavCodeAnalysis.PropertyKind SingleInstance => _singleInstance.Value; + public static NavCodeAnalysis.PropertyKind SourceTable => _sourceTable.Value; public static NavCodeAnalysis.PropertyKind SourceTableTemporary => _sourceTableTemporary.Value; public static NavCodeAnalysis.PropertyKind Subtype => _subtype.Value; public static NavCodeAnalysis.PropertyKind TableRelation => _tableRelation.Value; public static NavCodeAnalysis.PropertyKind ToolTip => _toolTip.Value; + public static NavCodeAnalysis.PropertyKind UseTemporary => _useTemporary.Value; } /// @@ -1209,6 +1236,20 @@ public static class UsageCategoryKind CreateEnumDictionary(); } + /// + /// XmlPortSourceTypeKind enum values + /// + public static class XmlPortSourceTypeKind + { + public static readonly Lazy> CanonicalNames = + CreateEnumDictionary(); + + private static readonly Lazy _table = + new(() => ParseEnum(nameof(NavCodeAnalysis.XmlPortSourceTypeKind.Table))); + + public static NavCodeAnalysis.XmlPortSourceTypeKind Table => _table.Value; + } + /// /// XmlVersionNoKind enum values /// @@ -1268,8 +1309,12 @@ public static class SymbolKind new(() => ParseEnum(nameof(NavCodeAnalysis.SymbolKind.ProfileExtension))); private static readonly Lazy _query = new(() => ParseEnum(nameof(NavCodeAnalysis.SymbolKind.Query))); + private static readonly Lazy _queryDataItem = + new(() => ParseEnum(nameof(NavCodeAnalysis.SymbolKind.QueryDataItem))); private static readonly Lazy _report = new(() => ParseEnum(nameof(NavCodeAnalysis.SymbolKind.Report))); + private static readonly Lazy _reportDataItem = + new(() => ParseEnum(nameof(NavCodeAnalysis.SymbolKind.ReportDataItem))); private static readonly Lazy _reportExtension = new(() => ParseEnum(nameof(NavCodeAnalysis.SymbolKind.ReportExtension))); private static readonly Lazy _table = @@ -1280,6 +1325,8 @@ public static class SymbolKind new(() => ParseEnum(nameof(NavCodeAnalysis.SymbolKind.Undefined))); private static readonly Lazy _xmlPort = new(() => ParseEnum(nameof(NavCodeAnalysis.SymbolKind.XmlPort))); + private static readonly Lazy _xmlPortNode = + new(() => ParseEnum(nameof(NavCodeAnalysis.SymbolKind.XmlPortNode))); public static NavCodeAnalysis.SymbolKind Action => _action.Value; public static NavCodeAnalysis.SymbolKind Class => _class.Value; @@ -1302,12 +1349,15 @@ public static class SymbolKind public static NavCodeAnalysis.SymbolKind Profile => _profile.Value; public static NavCodeAnalysis.SymbolKind ProfileExtension => _profileExtension.Value; public static NavCodeAnalysis.SymbolKind Query => _query.Value; + public static NavCodeAnalysis.SymbolKind QueryDataItem => _queryDataItem.Value; public static NavCodeAnalysis.SymbolKind Report => _report.Value; + public static NavCodeAnalysis.SymbolKind ReportDataItem => _reportDataItem.Value; public static NavCodeAnalysis.SymbolKind ReportExtension => _reportExtension.Value; public static NavCodeAnalysis.SymbolKind Table => _table.Value; public static NavCodeAnalysis.SymbolKind TableExtension => _tableExtension.Value; public static NavCodeAnalysis.SymbolKind Undefined => _undefined.Value; public static NavCodeAnalysis.SymbolKind XmlPort => _xmlPort.Value; + public static NavCodeAnalysis.SymbolKind XmlPortNode => _xmlPortNode.Value; } /// @@ -1580,6 +1630,8 @@ public static class SyntaxKind new(() => ParseEnum(nameof(NavCodeAnalysis.SyntaxKind.SubtypedDataType))); private static readonly Lazy _systemKeyword = new(() => ParseEnum(nameof(NavCodeAnalysis.SyntaxKind.SystemKeyword))); + private static readonly Lazy _tableDataKeyword = + new(() => ParseEnum(nameof(NavCodeAnalysis.SyntaxKind.TableDataKeyword))); private static readonly Lazy _tableExtensionObject = new(() => ParseEnum(nameof(NavCodeAnalysis.SyntaxKind.TableExtensionObject))); private static readonly Lazy _tableKeyword = @@ -1689,7 +1741,7 @@ public static class SyntaxKind public static NavCodeAnalysis.SyntaxKind NotEqualsToken => _notEqualsToken.Value; public static NavCodeAnalysis.SyntaxKind NotKeyword => _notKeyword.Value; public static NavCodeAnalysis.SyntaxKind ObjectNameReference => _objectId.Value; - public static NavCodeAnalysis.SyntaxKind ObjectId => _objectNameReference.Value; + public static NavCodeAnalysis.SyntaxKind ObjectId => _objectId.Value; public static NavCodeAnalysis.SyntaxKind ObjectReference => _objectReference.Value; public static NavCodeAnalysis.SyntaxKind OpenBraceToken => _openBraceToken.Value; public static NavCodeAnalysis.SyntaxKind OpenParenToken => _openParenToken.Value; @@ -1740,6 +1792,7 @@ public static class SyntaxKind public static NavCodeAnalysis.SyntaxKind SingleLineDocumentationCommentTrivia => _singleLineDocumentationCommentTrivia.Value; public static NavCodeAnalysis.SyntaxKind SubtypedDataType => _subtypedDataType.Value; public static NavCodeAnalysis.SyntaxKind SystemKeyword => _systemKeyword.Value; + public static NavCodeAnalysis.SyntaxKind TableDataKeyword => _tableDataKeyword.Value; public static NavCodeAnalysis.SyntaxKind TableExtensionObject => _tableExtensionObject.Value; public static NavCodeAnalysis.SyntaxKind TableKeyword => _tableKeyword.Value; public static NavCodeAnalysis.SyntaxKind TableObject => _tableObject.Value; diff --git a/src/ALCops.LinterCop/Analyzers/AnalyzeCountMethod.cs b/src/ALCops.LinterCop/Analyzers/AnalyzeCountMethod.cs index a72bae6..f72e626 100644 --- a/src/ALCops.LinterCop/Analyzers/AnalyzeCountMethod.cs +++ b/src/ALCops.LinterCop/Analyzers/AnalyzeCountMethod.cs @@ -47,7 +47,7 @@ private void AnalyzeCountInvocation(OperationAnalysisContext ctx) invocation.TargetMethod.ContainingSymbol?.Name != "Table") return; - if (invocation.Instance?.GetSymbol() is not IVariableSymbol { Type: IRecordTypeSymbol recordTypeSymbol } || recordTypeSymbol.Temporary) + if (invocation.Instance?.Type is not IRecordTypeSymbol recordTypeSymbol || recordTypeSymbol.Temporary) return; if (invocation.Syntax.Parent is not BinaryExpressionSyntax binaryExpression) From 9779b58f25df7bebed8bb8d5beec7db17527c6c7 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Mon, 2 Feb 2026 17:24:49 +0100 Subject: [PATCH 3/3] Refactor TableDataAccessRequiresPermissions analyzer and add version check for table extensions --- .../TableDataAccessRequiresPermissions.cs | 6 ++++++ .../Analyzers/TableDataAccessRequiresPermissions.cs | 10 +++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/TableDataAccessRequiresPermissions.cs b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/TableDataAccessRequiresPermissions.cs index 45de38b..77b1cea 100644 --- a/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/TableDataAccessRequiresPermissions.cs +++ b/src/ALCops.ApplicationCop.Test/Rules/TableDataAccessRequiresPermissions/TableDataAccessRequiresPermissions.cs @@ -54,6 +54,12 @@ public async Task HasDiagnostic(string testCase) [TestCase("MultiplePermissionsDifferentType")] public async Task NoDiagnostic(string testCase) { + SkipTestIfVersionIsTooLow( + ["PageExtensionSourceTable"], + testCase, + "13.0", + "No support for tableextensions when target itself is already declared in the same module"); + var code = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(NoDiagnostic), $"{testCase}.al")) .ConfigureAwait(false); diff --git a/src/ALCops.ApplicationCop/Analyzers/TableDataAccessRequiresPermissions.cs b/src/ALCops.ApplicationCop/Analyzers/TableDataAccessRequiresPermissions.cs index ded600f..ccce8f5 100644 --- a/src/ALCops.ApplicationCop/Analyzers/TableDataAccessRequiresPermissions.cs +++ b/src/ALCops.ApplicationCop/Analyzers/TableDataAccessRequiresPermissions.cs @@ -19,8 +19,7 @@ public class TableDataAccessRequiresPermissions : DiagnosticAnalyzer private static readonly ImmutableDictionary MethodPermissionMap = ImmutableDictionary.CreateRange( StringComparer.OrdinalIgnoreCase, - new[] - { + [ // read new KeyValuePair("Find", 'r'), new KeyValuePair("FindFirst", 'r'), @@ -40,7 +39,7 @@ public class TableDataAccessRequiresPermissions : DiagnosticAnalyzer // delete new KeyValuePair("Delete", 'd'), new KeyValuePair("DeleteAll", 'd'), - }); + ]); public override void Initialize(AnalysisContext context) { @@ -99,6 +98,8 @@ private void AnalyzeInvocation(OperationAnalysisContext ctx) tableType); } + + private void CheckXmlportNodeObjectPermission(SymbolAnalysisContext ctx) { if (ctx.IsObsolete()) @@ -249,7 +250,7 @@ private void CheckProcedureInvocation(IPropertySymbol? objectPermissions, ITypeS } } - private bool TableHasInherentPermission(ITableTypeSymbol table, char requestedPermission) + private static bool TableHasInherentPermission(ITableTypeSymbol table, char requestedPermission) { IPropertySymbol? permissionProperty = table.GetProperty(EnumProvider.PropertyKind.InherentPermissions); // InherentPermissions = RIMD; @@ -261,7 +262,6 @@ private bool TableHasInherentPermission(ITableTypeSymbol table, char requestedPe return false; } - private static char? GetRequiredPermission(string methodName) { return MethodPermissionMap.TryGetValue(methodName, out var p) ? p : null;