From de9fe123ca6fcfcac347ca618deef2fdef787ba3 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Mon, 6 May 2024 21:32:21 +0200 Subject: [PATCH 01/20] Set AssemblyTitle --- .github/workflows/build.yml | 4 +++- AssemblyInfo.cs | Bin 151 -> 346 bytes 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d234c623..977dfdf0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -166,13 +166,15 @@ jobs: - name: Unzip vsix run: 7z x ALLanguage.vsix "-oms-dynamics-smb.al-latest" extension/bin/Analyzers -r - - name: Set current version + - name: Set AssemblyInfo shell: pwsh if: ${{ matrix.GH_eventName != 'pull_request' }} run: (Get-Content AssemblyInfo.cs) -replace 'Version\("([\d\.]+)"\)]', ("Version(""" + ('${{ needs.setup-matrix.outputs.release-tag-name }}' -replace "v","") + """)]") | Out-File AssemblyInfo.cs + (Get-Content AssemblyInfo.cs) -replace 'AssemblyTitle\("([^"]*)"\)', "AssemblyTitle(`"AL-${{ needs.setup-matrix.outputs.version }}`")" | Out-File AssemblyInfo.cs + - name: Build run: dotnet build /p:FeatureFlags=${{ matrix.featureflags }} --no-restore --configuration Release diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs index 64bec08fc0c0aa8698d868700fbd57809324d18f..5d161cda171d21235f98f27e04668d2dd8b24616 100644 GIT binary patch literal 346 zcmb7=u?~Vj5JX>X;y)NFEi`JWmC#ZcH8#cq6$uG~IiTR@)nRGWj$-fj_GWfw_x-9V zNy$kVv8U0iBId!2o$foC&Z}xEg-~mT3@lDW!j^`;2-UDshs|H@AYNrX#(4S-nJOaF zt92*h9~Dl#PQhcsxrf3pyH%OX`;vvSex0^*QE;QSdD~9D@@_k^%@xRLu6dlCXHp4m NbM3wyfw<2v%@0u@J*EHv literal 151 zcmaFAdw*$hW?s5NaAk2xYOY>TYFbWea!F=>o;4SmL~(v;QF3ZAx_D_`NoH=Uo^yU~ zL1s>BQE+NeSteK+m%e^ Date: Mon, 6 May 2024 21:44:14 +0200 Subject: [PATCH 02/20] Version as ENV variable --- .github/workflows/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 977dfdf0..2da8773d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -169,11 +169,13 @@ jobs: - name: Set AssemblyInfo shell: pwsh if: ${{ matrix.GH_eventName != 'pull_request' }} + env: + AL_VERSION: ${{ needs.setup-matrix.outputs.version }} run: (Get-Content AssemblyInfo.cs) -replace 'Version\("([\d\.]+)"\)]', ("Version(""" + ('${{ needs.setup-matrix.outputs.release-tag-name }}' -replace "v","") + """)]") | Out-File AssemblyInfo.cs - (Get-Content AssemblyInfo.cs) -replace 'AssemblyTitle\("([^"]*)"\)', "AssemblyTitle(`"AL-${{ needs.setup-matrix.outputs.version }}`")" | Out-File AssemblyInfo.cs + (Get-Content AssemblyInfo.cs) -replace 'AssemblyTitle\("([^"]*)"\)', "AssemblyTitle(`"AL-$env:AL_VERSION`")" | Out-File AssemblyInfo.cs - name: Build run: dotnet build /p:FeatureFlags=${{ matrix.featureflags }} --no-restore --configuration Release From 2007e4f0b6037754c404005b8c74ca103dd4745f Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Mon, 6 May 2024 22:09:47 +0200 Subject: [PATCH 03/20] Convert input to String --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2da8773d..74033deb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -170,7 +170,7 @@ jobs: shell: pwsh if: ${{ matrix.GH_eventName != 'pull_request' }} env: - AL_VERSION: ${{ needs.setup-matrix.outputs.version }} + AL_VERSION: "${{ needs.setup-matrix.outputs.version }}" run: (Get-Content AssemblyInfo.cs) -replace 'Version\("([\d\.]+)"\)]', ("Version(""" + ('${{ needs.setup-matrix.outputs.release-tag-name }}' -replace "v","") + """)]") | Out-File AssemblyInfo.cs From 4814e7660c36baad7b08a051ac7436ea241de82b Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Thu, 9 May 2024 10:33:26 +0200 Subject: [PATCH 04/20] Set right input variable --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 74033deb..80af5260 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -170,7 +170,7 @@ jobs: shell: pwsh if: ${{ matrix.GH_eventName != 'pull_request' }} env: - AL_VERSION: "${{ needs.setup-matrix.outputs.version }}" + AL_VERSION: ${{ matrix.version }} run: (Get-Content AssemblyInfo.cs) -replace 'Version\("([\d\.]+)"\)]', ("Version(""" + ('${{ needs.setup-matrix.outputs.release-tag-name }}' -replace "v","") + """)]") | Out-File AssemblyInfo.cs From 4a679bcaf4514fd430dbd12f147cc50184f0a0b7 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Thu, 9 May 2024 10:40:32 +0200 Subject: [PATCH 05/20] Disable Setup .NET step --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 80af5260..524fa87a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -151,8 +151,12 @@ jobs: steps: - uses: actions/checkout@v4 + # Disable Setup .NET + # dotnet-install: .NET Core Runtime with version '8.0.4' is already installed. + # dotnet-install: .NET Core SDK with version '8.0.204' is already installed. - name: Setup .NET uses: actions/setup-dotnet@v4 + if: false with: dotnet-version: 8.0.* From 0c342fad17c0684351f7e5afb99b37d372a4b9d0 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Tue, 14 May 2024 16:44:17 +0200 Subject: [PATCH 06/20] Implement code singing of .dll artifact --- .github/workflows/build.yml | 161 ++++++++++++++++++++++++------------ 1 file changed, 108 insertions(+), 53 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 524fa87a..85f398f0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -135,9 +135,7 @@ jobs: outputs: matrix-combinations: ${{ steps.setup-matrix-combinations.outputs.matrix-combinations }} - release-id: ${{ steps.create-release.outputs.id }} release-tag-name: ${{ steps.create-release.outputs.tag_name }} - release-upload-url: ${{ steps.create-release.outputs.upload_url }} matrix-job: name: Build @@ -184,67 +182,124 @@ jobs: - name: Build run: dotnet build /p:FeatureFlags=${{ matrix.featureflags }} --no-restore --configuration Release - - name: Upload Release Asset - id: upload-release-asset - uses: actions/upload-release-asset@v1 - if: ${{ matrix.GH_eventName != 'pull_request' }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload build artifact + id: upload-build-asset + uses: actions/upload-artifact@v4 with: - upload_url: ${{ needs.setup-matrix.outputs.release-upload-url }} - asset_path: bin/Release/netstandard2.1/BusinessCentral.LinterCop.dll - asset_name: ${{ matrix.assetname }} - asset_content_type: application/octet-stream - - ### Release Asset as Latest - - name: Upload Release Asset (Latest) - id: upload-release-asset-latest - uses: actions/upload-release-asset@v1 + name: ${{ matrix.assetname }} + path: bin/Release/netstandard2.1/BusinessCentral.LinterCop.dll + compression-level: 0 # no compression + + ### Upload Asset as Latest + - name: Upload build artifact (Latest) + id: upload-build-asset-latest + uses: actions/upload-artifact@v4 if: ${{ matrix.GH_eventName != 'pull_request' && matrix.latest }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - upload_url: ${{ needs.setup-matrix.outputs.release-upload-url }} - asset_path: bin/Release/netstandard2.1/BusinessCentral.LinterCop.dll - asset_name: BusinessCentral.LinterCop.dll - asset_content_type: application/octet-stream - - ### Release Asset as Pre-Release - - name: Upload Release Asset (Pre-Release) - id: upload-release-asset-prerelease - uses: actions/upload-release-asset@v1 + name: BusinessCentral.LinterCop.dll + path: bin/Release/netstandard2.1/BusinessCentral.LinterCop.dll + compression-level: 0 # no compression + + ### Upload Asset as Pre-Release + - name: Upload build artifact (Pre-Release) + id: upload-build-asset-prerelease + uses: actions/upload-artifact@v4 if: ${{ matrix.GH_eventName != 'pull_request' && matrix.prerelease }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - upload_url: ${{ needs.setup-matrix.outputs.release-upload-url }} - asset_path: bin/Release/netstandard2.1/BusinessCentral.LinterCop.dll - asset_name: BusinessCentral.LinterCop.AL-PreRelease.dll - asset_content_type: application/octet-stream + name: BusinessCentral.LinterCop.AL-PreRelease.dll + path: bin/Release/netstandard2.1/BusinessCentral.LinterCop.dll + compression-level: 0 # no compression ### Compatibility with previous naming of files ### Release Asset as Current - - name: Upload Release Asset (Current) - id: upload-release-asset-current - uses: actions/upload-release-asset@v1 + - name: Upload build artifact (Current) + id: upload-build-asset-current + uses: actions/upload-artifact@v4 if: ${{ matrix.GH_eventName != 'pull_request' && matrix.latest }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - upload_url: ${{ needs.setup-matrix.outputs.release-upload-url }} - asset_path: bin/Release/netstandard2.1/BusinessCentral.LinterCop.dll - asset_name: BusinessCentral.LinterCop.current.dll - asset_content_type: application/octet-stream - - ### Release Asset as Next - - name: Upload Release Asset (Next) - id: upload-release-asset-next - uses: actions/upload-release-asset@v1 + name: BusinessCentral.LinterCop.current.dll + path: bin/Release/netstandard2.1/BusinessCentral.LinterCop.dll + compression-level: 0 # no compression + + # ### Release Asset as Next + - name: Upload build artifact (Next) + id: upload-build-asset-next + uses: actions/upload-artifact@v4 if: ${{ matrix.GH_eventName != 'pull_request' && matrix.prerelease }} + with: + name: BusinessCentral.LinterCop.next.dll + path: bin/Release/netstandard2.1/BusinessCentral.LinterCop.dll + compression-level: 0 # no compression + + sign-job: + name: Sign + runs-on: windows-latest # Code signing must run on a Windows agent for Authenticode signing (dll/exe) + permissions: + id-token: write # Required for requesting the JWT + contents: write # Required for Uploading Release Assets with GitHub CLI (gh release upload) + needs: + - setup-matrix + - matrix-job + if: github.event_name != 'pull_request' # Exclude this job for validation on the pull-request + steps: + # Disable Setup .NET + # dotnet-install: .NET Core Runtime with version '8.0.4' is already installed. + # dotnet-install: .NET Core SDK with version '8.0.204' is already installed. + - name: Setup .NET + uses: actions/setup-dotnet@v4 + if: false + with: + dotnet-version: 8.0.* + + # Install the code signing tool + - name: Install Sign CLI tool + run: dotnet tool install --tool-path . sign --version 0.9.0-beta.23127.3 + + # Download artifacts + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: DownloadArtifacts + + # Rename artifacts + - name: Rename artifacts + run: | + # Get list of artifacts files + $artifacts = Get-ChildItem -Path DownloadArtifacts -Recurse | ForEach-Object { if (!($_.PSIsContainer)) { $_}} | Where-Object { $_.Name -like 'BusinessCentral.LinterCop*.dll' -and $_.Directory.Name -like 'BusinessCentral.LinterCop*.dll' } + + # Create folder if not exits + if (!(Test-Path BuildArtifacts)) { + New-Item -Path BuildArtifacts -ItemType Directory -Force | Out-Null + } + + # Move the artifacts (BusinessCentral.LinterCop.dll) in every directory to a combined folder and rename the file to the name of it's parent directory + $artifacts | ForEach-Object { Move-Item -Path $_ -Destination (Join-Path 'BuildArtifacts' $_.Directory.Name) } + + - name: "Azure Login" + uses: azure/login@v2 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + # Run the signing command + - name: Sign artifacts + shell: pwsh + run: > + ./sign code azure-key-vault + **/*.dll + --base-directory "${{ github.workspace }}\BuildArtifacts" + --description "BusinessCentral.LinterCop" + --description-url "https://github.com/StefanMaron/BusinessCentral.LinterCop" + --azure-key-vault-managed-identity true + --azure-key-vault-url "${{ secrets.KEY_VAULT_URL }}" + --azure-key-vault-certificate "${{ secrets.KEY_VAULT_CERTIFICATE }}" + + # Publish the signed packages + - name: Upload Release Assets + id: upload-release-assets env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.setup-matrix.outputs.release-upload-url }} - asset_path: bin/Release/netstandard2.1/BusinessCentral.LinterCop.dll - asset_name: BusinessCentral.LinterCop.next.dll - asset_content_type: application/octet-stream + GITHUB_REPO: ${{ github.repository }} + RELEASE_TAG_NAME: ${{ needs.setup-matrix.outputs.release-tag-name }} + run: | + $artifacts = Get-ChildItem -Path BuildArtifacts -Depth 0 -Filter *.dll + $artifacts | ForEach-Object { gh release upload --repo "$env:GITHUB_REPO" "$env:RELEASE_TAG_NAME" "$_" } From cd6131c85a0e6c1005449c04b99715ad8bb611a9 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Tue, 14 May 2024 16:53:24 +0200 Subject: [PATCH 07/20] Add missing if expression to upload-build-asset step --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85f398f0..d7989c80 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -185,6 +185,7 @@ jobs: - name: Upload build artifact id: upload-build-asset uses: actions/upload-artifact@v4 + if: ${{ matrix.GH_eventName != 'pull_request' }} with: name: ${{ matrix.assetname }} path: bin/Release/netstandard2.1/BusinessCentral.LinterCop.dll From 4dfbe449dfb1b365dc1bd564069990e9d0ef04e3 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Sat, 18 May 2024 16:16:35 +0200 Subject: [PATCH 08/20] Add support for XmlPortDataItemAccess --- .../Rule0005VariableCasingShouldNotDifferFromDeclaration.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs b/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs index 4e6c4db4..16ead933 100644 --- a/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs +++ b/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs @@ -25,7 +25,8 @@ public override void Initialize(AnalysisContext context) OperationKind.GlobalReferenceExpression, OperationKind.LocalReferenceExpression, OperationKind.ParameterReferenceExpression, - OperationKind.ReturnValueReferenceExpression + OperationKind.ReturnValueReferenceExpression, + OperationKind.XmlPortDataItemAccess }); context.RegisterSymbolAction(new Action(this.CheckForBuiltInTypeCasingMismatch), new SymbolKind[] { @@ -215,6 +216,9 @@ private void CheckForBuiltInMethodsWithCasingMismatch(OperationAnalysisContext c case OperationKind.ReturnValueReferenceExpression: targetName = ((IReturnValueReferenceExpression)ctx.Operation).ReturnValue.Name; break; + case OperationKind.XmlPortDataItemAccess: + targetName = ((IXmlPortNodeAccess)ctx.Operation).XmlPortNodeSymbol.Name; + break; default: return; } From 3b62847b94d7de8bbe1ff8ef0cebeb0087ce6a42 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Sat, 18 May 2024 20:54:12 +0200 Subject: [PATCH 09/20] Add logic for SplitButton with Repeater Scope --- Design/Rule0016CheckForMissingCaptions.cs | 38 ++++++++++++++++++----- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/Design/Rule0016CheckForMissingCaptions.cs b/Design/Rule0016CheckForMissingCaptions.cs index cc04fa8c..36f24cf7 100644 --- a/Design/Rule0016CheckForMissingCaptions.cs +++ b/Design/Rule0016CheckForMissingCaptions.cs @@ -100,9 +100,9 @@ private void CheckForMissingCaptions(SymbolAnalysisContext context) break; } } - else if (context.Symbol.Kind == SymbolKind.Action) + else if (context.Symbol is IActionSymbol actionSymbol) { - switch (((IActionSymbol)context.Symbol).ActionKind) + switch (actionSymbol.ActionKind) { case ActionKind.Action: if (CaptionIsMissing(context.Symbol, context)) @@ -110,9 +110,34 @@ private void CheckForMissingCaptions(SymbolAnalysisContext context) break; case ActionKind.Group: - if (CaptionIsMissing(context.Symbol, context)) - RaiseCaptionWarning(context); - break; + if (context.Symbol.GetEnumPropertyValue(PropertyKind.ShowAs) == ShowAsKind.SplitButton) + { + // There is one specifc case where a Caption is needed on a Group where the property ShowAs is set to SplitButton + // A) The group is inside a Promoted Area + // B) Has one or more actionrefs + // C) One of the actions of the actionsrefs has Scope set to Repeater + + if (((IActionSymbol)context.Symbol.ContainingSymbol).ActionKind != ActionKind.Area) + break; + + if (!SemanticFacts.IsSameName(context.Symbol.ContainingSymbol.Name, "Promoted")) + break; + + if (!actionSymbol.Actions.Where(a => a.ActionKind == ActionKind.ActionRef) + .Where(a => a.Target.GetEnumPropertyValueOrDefault(PropertyKind.Scope) == PageActionScopeKind.Repeater) + .Any()) + break; + + if (CaptionIsMissing(context.Symbol, context)) + RaiseCaptionWarning(context); + break; + } + else + { + if (CaptionIsMissing(context.Symbol, context)) + RaiseCaptionWarning(context); + break; + } } } else if (context.Symbol.Kind == SymbolKind.EnumValue) @@ -151,9 +176,6 @@ private bool CaptionIsMissing(ISymbol Symbol, SymbolAnalysisContext context) return false; } - if (Symbol.GetEnumPropertyValue(PropertyKind.ShowAs) == ShowAsKind.SplitButton) - return false; - if (SemanticFacts.IsSameName(Symbol.MostSpecificKind, "Group") && PromotedGroupNames.Contains(Symbol.Name.ToLowerInvariant())) return false; From 41271e40aceea3dd9e74249b10b83ee4f11f3638 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Sat, 18 May 2024 20:57:38 +0200 Subject: [PATCH 10/20] Format Document --- Design/Rule0016CheckForMissingCaptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Design/Rule0016CheckForMissingCaptions.cs b/Design/Rule0016CheckForMissingCaptions.cs index 36f24cf7..41189985 100644 --- a/Design/Rule0016CheckForMissingCaptions.cs +++ b/Design/Rule0016CheckForMissingCaptions.cs @@ -124,8 +124,8 @@ private void CheckForMissingCaptions(SymbolAnalysisContext context) break; if (!actionSymbol.Actions.Where(a => a.ActionKind == ActionKind.ActionRef) - .Where(a => a.Target.GetEnumPropertyValueOrDefault(PropertyKind.Scope) == PageActionScopeKind.Repeater) - .Any()) + .Where(a => a.Target.GetEnumPropertyValueOrDefault(PropertyKind.Scope) == PageActionScopeKind.Repeater) + .Any()) break; if (CaptionIsMissing(context.Symbol, context)) From 546e51898f5bfca9995391b396143780b17ec8b4 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Mon, 20 May 2024 22:24:42 +0200 Subject: [PATCH 11/20] Improve rule0005 to extend more keywords --- ...bleCasingShouldNotDifferFromDeclaration.cs | 195 ++++++++++-------- 1 file changed, 112 insertions(+), 83 deletions(-) diff --git a/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs b/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs index 16ead933..ec9f3139 100644 --- a/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs +++ b/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs @@ -1,6 +1,7 @@ using BusinessCentral.LinterCop.AnalysisContextExtension; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; namespace BusinessCentral.LinterCop.Design @@ -11,13 +12,15 @@ public class Rule0005VariableCasingShouldNotDifferFromDeclaration : DiagnosticAn public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration); - private static readonly HashSet _validTokens = new(); + private static readonly HashSet _dataTypeSyntaxKinds = Enum.GetValues(typeof(SyntaxKind)).Cast().Where(x => x.ToString().AsSpan().EndsWith("DataType")).ToHashSet(); + private static readonly string[] _symbolKinds = Enum.GetValues(typeof(SymbolKind)).Cast().Select(x => x.ToString()).ToArray(); private static string[] _navTypeKindStrings; + private static readonly string[] _propertyKindStrings = Enum.GetValues(typeof(PropertyKind)).Cast().Select(x => x.ToString()).ToArray(); + private static readonly string[] _labelPropertyString = LabelPropertyHelper.GetAllLabelProperties(); public override void Initialize(AnalysisContext context) { GenerateNavTypeKindArray(); - GenerateValidTokenArray(); context.RegisterOperationAction(new Action(this.CheckForBuiltInMethodsWithCasingMismatch), new OperationKind[] { OperationKind.InvocationExpression, @@ -59,121 +62,148 @@ private static void GenerateNavTypeKindArray() _navTypeKindStrings = navTypeKinds.ToArray(); } - private static void GenerateValidTokenArray() + private void CheckForBuiltInTypeCasingMismatch(SymbolAnalysisContext ctx) + { + AnalyseTokens(ctx); + AnalyseNodes(ctx); + AnalyzeMemberAccessExpressions(ctx); + AnalyzePropertyNames(ctx); + AnalyzeLabelProperties(ctx); + } + + private void AnalyseTokens(SymbolAnalysisContext ctx) { - _validTokens.Clear(); - var allKinds = Enum.GetValues(typeof(SyntaxKind)).Cast(); - foreach (var kind in allKinds) + IEnumerable descendantTokens = ctx.Symbol.DeclaringSyntaxReference.GetSyntax().DescendantTokens() + .Where(t => t.Kind.IsKeyword()) + .Where(t => !_dataTypeSyntaxKinds.Contains(t.Parent.Kind)); + + foreach (SyntaxToken descendantToken in descendantTokens) { - var kindSpan = kind.ToString().AsSpan(); - - if ((kindSpan.Contains("Keyword", StringComparison.Ordinal) && - !kindSpan.StartsWith("Action") && - !kindSpan.StartsWith("Codeunit") && - !kindSpan.StartsWith("ControlAddIn") && - !kindSpan.StartsWith("DotNet") && - !kindSpan.StartsWith("Enum") && - !kindSpan.StartsWith("Interface") && - !kindSpan.StartsWith("Label") && - !kindSpan.StartsWith("Page") && - !kindSpan.StartsWith("Query") && - !kindSpan.StartsWith("Report") && - !kindSpan.StartsWith("XmlPort")) || - kindSpan.Contains("DataType", StringComparison.Ordinal) - ) - { - _validTokens.Add(kind); - continue; - } + ctx.CancellationToken.ThrowIfCancellationRequested(); - switch (kind) - { - case SyntaxKind.SimpleTypeReference: - case SyntaxKind.OptionAccessExpression: - _validTokens.Add(kind); - continue; - } + SyntaxToken syntaxToken = SyntaxFactory.Token(descendantToken.Kind); + if (syntaxToken.Kind == SyntaxKind.None) + continue; + if (!syntaxToken.ToString().AsSpan().Equals(descendantToken.ToString().AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, descendantToken.GetLocation(), new object[] { syntaxToken, "" })); } } - private void CheckForBuiltInTypeCasingMismatch(SymbolAnalysisContext ctx) + private void AnalyseNodes(SymbolAnalysisContext ctx) { - foreach (var node in ctx.Symbol.DeclaringSyntaxReference.GetSyntax().DescendantNodesAndTokens().Where(n => _validTokens.Contains(n.Kind))) + IEnumerable descendantNodes = ctx.Symbol.DeclaringSyntaxReference.GetSyntax().DescendantNodes() + .Where(n => !n.ToString().AsSpan().StartsWith("array")); + + foreach (SyntaxNode descendantNode in descendantNodes) { ctx.CancellationToken.ThrowIfCancellationRequested(); - var syntaxNodeKindSpan = node.Kind.ToString().AsSpan(); + var syntaxNodeKindSpan = descendantNode.Kind.ToString().AsSpan(); + var syntaxNodeSpan = descendantNode.ToString(); - if (node.IsToken) + if ((descendantNode.IsKind(SyntaxKind.SimpleTypeReference) || + syntaxNodeKindSpan.Contains("DataType", StringComparison.Ordinal)) && + !syntaxNodeKindSpan.StartsWith("Codeunit") && + !syntaxNodeKindSpan.StartsWith("Enum") && + !syntaxNodeKindSpan.StartsWith("Label")) { - var syntaxToken = SyntaxFactory.Token(node.Kind); - if (!syntaxToken.ToString().AsSpan().Equals(node.ToString().AsSpan(), StringComparison.Ordinal)) + var targetName = _navTypeKindStrings.FirstOrDefault(Kind => { - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, node.GetLocation(), new object[] { syntaxToken, "" })); + var kindSpan = Kind.AsSpan(); + return kindSpan.Equals(syntaxNodeSpan.AsSpan(), StringComparison.OrdinalIgnoreCase) && + !kindSpan.Equals(syntaxNodeSpan.AsSpan(), StringComparison.Ordinal); + }); + + if (targetName != null) + { + ctx.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, + descendantNode.GetLocation(), new object[] { targetName, "" })); continue; } } - var syntaxNode = node.AsNode(); - if (syntaxNode == null) - continue; - - if (!node.IsNode) - continue; - - var syntaxNodeAsString = syntaxNode.ToString(); - if (!syntaxNodeAsString.StartsWith("array")) + if (IsValidKind(descendantNode.Kind)) { - if ((syntaxNode.IsKind(SyntaxKind.SimpleTypeReference) || - syntaxNodeKindSpan.Contains("DataType", StringComparison.Ordinal)) && - !syntaxNodeKindSpan.StartsWith("Codeunit") && - !syntaxNodeKindSpan.StartsWith("Enum") && + if (syntaxNodeKindSpan.StartsWith("Codeunit") || + !syntaxNodeKindSpan.StartsWith("Enum") || !syntaxNodeKindSpan.StartsWith("Label")) { var targetName = _navTypeKindStrings.FirstOrDefault(Kind => { var kindSpan = Kind.AsSpan(); - return kindSpan.Equals(syntaxNodeAsString.AsSpan(), StringComparison.OrdinalIgnoreCase) && - !kindSpan.Equals(syntaxNodeAsString.AsSpan(), StringComparison.Ordinal); + var readOnlySpan = syntaxNodeSpan.AsSpan(); + return readOnlySpan.StartsWith(kindSpan, StringComparison.OrdinalIgnoreCase) && + !readOnlySpan.StartsWith(kindSpan, StringComparison.Ordinal); }); - if (targetName != null) { + var firstToken = descendantNode.GetFirstToken(); ctx.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, - node.GetLocation(), new object[] { targetName, "" })); - continue; - } - } - - if (IsValidKind(syntaxNode.Kind)) - { - if (syntaxNodeKindSpan.StartsWith("Codeunit") || - !syntaxNodeKindSpan.StartsWith("Enum") || - !syntaxNodeKindSpan.StartsWith("Label")) - { - var targetName = _navTypeKindStrings.FirstOrDefault(Kind => - { - var kindSpan = Kind.AsSpan(); - var readOnlySpan = syntaxNodeAsString.AsSpan(); - return readOnlySpan.StartsWith(kindSpan, StringComparison.OrdinalIgnoreCase) && - !readOnlySpan.StartsWith(kindSpan, StringComparison.Ordinal); - }); - if (targetName != null) - { - var firstToken = syntaxNode.GetFirstToken(); - ctx.ReportDiagnostic(Diagnostic.Create( - DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, - firstToken.GetLocation(), new object[] { targetName, "" })); - - } + firstToken.GetLocation(), new object[] { targetName, "" })); } } } } } + private void AnalyzeMemberAccessExpressions(SymbolAnalysisContext ctx) + { + IEnumerable descendantTokens = ctx.Symbol.DeclaringSyntaxReference.GetSyntax().DescendantTokens() + .Where(t => t.Parent.Parent.Kind == SyntaxKind.MemberAccessExpression); + + foreach (SyntaxToken token in descendantTokens) + { + ctx.CancellationToken.ThrowIfCancellationRequested(); + + int result = Array.FindIndex(_symbolKinds, t => t.Equals(token.ValueText, StringComparison.OrdinalIgnoreCase)); + if (result == -1) + continue; + + if (!token.ValueText.AsSpan().Equals(_symbolKinds[result].ToString().AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, token.GetLocation(), new object[] { _symbolKinds[result].ToString(), "" })); + } + } + + private void AnalyzePropertyNames(SymbolAnalysisContext ctx) + { + IEnumerable descendantTokens = ctx.Symbol.DeclaringSyntaxReference.GetSyntax().DescendantTokens() + .Where(t => t.Parent.Kind == SyntaxKind.PropertyName); + + foreach (SyntaxToken token in descendantTokens) + { + ctx.CancellationToken.ThrowIfCancellationRequested(); + + int result = Array.FindIndex(_propertyKindStrings, t => t.Equals(token.ValueText, StringComparison.OrdinalIgnoreCase)); + if (result == -1) + continue; + + if (!token.ValueText.AsSpan().Equals(_propertyKindStrings[result].ToString().AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, token.GetLocation(), new object[] { _propertyKindStrings[result].ToString(), "" })); + } + } + + private static void AnalyzeLabelProperties(SymbolAnalysisContext ctx) + { + IEnumerable descendantTokens = ctx.Symbol.DeclaringSyntaxReference.GetSyntax().DescendantTokens() + .Where(n => n.Kind == SyntaxKind.IdentifierToken) + .Where(n => n.Parent.Parent.Parent.Kind == SyntaxKind.Label); + + foreach (SyntaxToken token in descendantTokens) + { + ctx.CancellationToken.ThrowIfCancellationRequested(); + + int result = Array.FindIndex(_labelPropertyString, t => t.Equals(token.ToString(), StringComparison.OrdinalIgnoreCase)); + if (result == -1) + continue; + + if (!token.ToString().AsSpan().Equals(_labelPropertyString[result].ToString().AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, token.GetLocation(), new object[] { _labelPropertyString[result].ToString(), "" })); + } + } + private static bool IsValidKind(SyntaxKind kind) { switch (kind) @@ -223,7 +253,6 @@ private void CheckForBuiltInMethodsWithCasingMismatch(OperationAnalysisContext c return; } - if (OnlyDiffersInCasing(ctx.Operation.Syntax.ToString().AsSpan(), targetName.AsSpan())) { ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, ctx.Operation.Syntax.GetLocation(), new object[] { targetName, "" })); From a7fa8b77b667b42dbfad4afb085ea6f540329d0e Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Mon, 20 May 2024 22:49:11 +0200 Subject: [PATCH 12/20] Add AnalysePageActionAreas to rule0005 --- ...bleCasingShouldNotDifferFromDeclaration.cs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs b/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs index ec9f3139..e6fd29ca 100644 --- a/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs +++ b/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs @@ -1,6 +1,7 @@ using BusinessCentral.LinterCop.AnalysisContextExtension; using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols; using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; using System.Collections.Immutable; @@ -13,10 +14,12 @@ public class Rule0005VariableCasingShouldNotDifferFromDeclaration : DiagnosticAn = ImmutableArray.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration); private static readonly HashSet _dataTypeSyntaxKinds = Enum.GetValues(typeof(SyntaxKind)).Cast().Where(x => x.ToString().AsSpan().EndsWith("DataType")).ToHashSet(); - private static readonly string[] _symbolKinds = Enum.GetValues(typeof(SymbolKind)).Cast().Select(x => x.ToString()).ToArray(); private static string[] _navTypeKindStrings; - private static readonly string[] _propertyKindStrings = Enum.GetValues(typeof(PropertyKind)).Cast().Select(x => x.ToString()).ToArray(); + private static readonly string[] _actionAreaKinds = Enum.GetValues(typeof(ActionAreaKind)).Cast().Select(x => x.ToString()).ToArray(); private static readonly string[] _labelPropertyString = LabelPropertyHelper.GetAllLabelProperties(); + private static readonly string[] _propertyKindStrings = Enum.GetValues(typeof(PropertyKind)).Cast().Select(x => x.ToString()).ToArray(); + private static readonly string[] _symbolKinds = Enum.GetValues(typeof(SymbolKind)).Cast().Select(x => x.ToString()).ToArray(); + public override void Initialize(AnalysisContext context) { @@ -66,6 +69,7 @@ private void CheckForBuiltInTypeCasingMismatch(SymbolAnalysisContext ctx) { AnalyseTokens(ctx); AnalyseNodes(ctx); + AnalysePageActionAreas(ctx); AnalyzeMemberAccessExpressions(ctx); AnalyzePropertyNames(ctx); AnalyzeLabelProperties(ctx); @@ -149,6 +153,24 @@ private void AnalyseNodes(SymbolAnalysisContext ctx) } } + private void AnalysePageActionAreas(SymbolAnalysisContext ctx) + { + IEnumerable descendantNodes = ctx.Symbol.DeclaringSyntaxReference.GetSyntax().DescendantNodes() + .Where(n => n.Parent.Kind == SyntaxKind.PageActionArea); + + foreach (SyntaxNode node in descendantNodes) + { + ctx.CancellationToken.ThrowIfCancellationRequested(); + + int result = Array.FindIndex(_actionAreaKinds, t => t.Equals(node.ToString(), StringComparison.OrdinalIgnoreCase)); + if (result == -1) + continue; + + if (!node.ToString().AsSpan().Equals(_actionAreaKinds[result].ToString().AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, node.GetLocation(), new object[] { _actionAreaKinds[result].ToString(), "" })); + } + } + private void AnalyzeMemberAccessExpressions(SymbolAnalysisContext ctx) { IEnumerable descendantTokens = ctx.Symbol.DeclaringSyntaxReference.GetSyntax().DescendantTokens() From abfb5cc2afe06757a43251696f1c18e455ec47b1 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Tue, 21 May 2024 11:03:25 +0200 Subject: [PATCH 13/20] Improve performance by implementing RegisterSyntaxNodeAction --- ...bleCasingShouldNotDifferFromDeclaration.cs | 165 +++++++++--------- 1 file changed, 87 insertions(+), 78 deletions(-) diff --git a/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs b/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs index e6fd29ca..80dd3cbb 100644 --- a/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs +++ b/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs @@ -15,16 +15,22 @@ public class Rule0005VariableCasingShouldNotDifferFromDeclaration : DiagnosticAn private static readonly HashSet _dataTypeSyntaxKinds = Enum.GetValues(typeof(SyntaxKind)).Cast().Where(x => x.ToString().AsSpan().EndsWith("DataType")).ToHashSet(); private static string[] _navTypeKindStrings; + private static readonly string[] _areaKinds = Enum.GetValues(typeof(AreaKind)).Cast().Select(x => x.ToString()).ToArray(); private static readonly string[] _actionAreaKinds = Enum.GetValues(typeof(ActionAreaKind)).Cast().Select(x => x.ToString()).ToArray(); private static readonly string[] _labelPropertyString = LabelPropertyHelper.GetAllLabelProperties(); private static readonly string[] _propertyKindStrings = Enum.GetValues(typeof(PropertyKind)).Cast().Select(x => x.ToString()).ToArray(); private static readonly string[] _symbolKinds = Enum.GetValues(typeof(SymbolKind)).Cast().Select(x => x.ToString()).ToArray(); - public override void Initialize(AnalysisContext context) { GenerateNavTypeKindArray(); + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeLabel), SyntaxKind.Label); + context.RegisterSyntaxNodeAction(new Action(this.AnalyzePropertyName), SyntaxKind.PropertyName); + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeMemberAccessExpression), SyntaxKind.MemberAccessExpression); + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeAreaSectionName), SyntaxKind.PageArea); + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeActionAreaSectionName), SyntaxKind.PageActionArea); + context.RegisterOperationAction(new Action(this.CheckForBuiltInMethodsWithCasingMismatch), new OperationKind[] { OperationKind.InvocationExpression, OperationKind.FieldAccess, @@ -65,14 +71,90 @@ private static void GenerateNavTypeKindArray() _navTypeKindStrings = navTypeKinds.ToArray(); } + private void AnalyzeLabel(SyntaxNodeAnalysisContext ctx) + { + IEnumerable nodes = ctx.Node.DescendantNodes() + .Where(n => n.Kind == SyntaxKind.IdentifierEqualsLiteral); + + foreach (SyntaxNode node in nodes) + { + ctx.CancellationToken.ThrowIfCancellationRequested(); + + SyntaxToken syntaxToken = ((IdentifierEqualsLiteralSyntax)node).Identifier; + int result = Array.FindIndex(_labelPropertyString, t => t.Equals(syntaxToken.ValueText, StringComparison.OrdinalIgnoreCase)); + if (result == -1) + continue; + + if (!syntaxToken.ValueText.AsSpan().Equals(_labelPropertyString[result].ToString().AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, syntaxToken.GetLocation(), new object[] { _labelPropertyString[result].ToString(), "" })); + } + } + + private void AnalyzePropertyName(SyntaxNodeAnalysisContext ctx) + { + SyntaxToken syntaxToken = ((PropertyNameSyntax)ctx.Node).Identifier; + int result = Array.FindIndex(_propertyKindStrings, t => t.Equals(syntaxToken.ValueText, StringComparison.OrdinalIgnoreCase)); + if (result == -1) + return; + + if (!syntaxToken.ValueText.AsSpan().Equals(_propertyKindStrings[result].ToString().AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, syntaxToken.GetLocation(), new object[] { _propertyKindStrings[result].ToString(), "" })); + } + + private void AnalyzeMemberAccessExpression(SyntaxNodeAnalysisContext ctx) + { + SyntaxNode childNode = ctx.Node.ChildNodes().Where(n => n.Kind == SyntaxKind.IdentifierName).FirstOrDefault(); + if (childNode == null) return; + + SyntaxToken syntaxToken = ((IdentifierNameSyntax)childNode).Identifier; + int result = Array.FindIndex(_symbolKinds, t => t.Equals(syntaxToken.ValueText, StringComparison.OrdinalIgnoreCase)); + if (result == -1) + return; + + if (!syntaxToken.ValueText.AsSpan().Equals(_symbolKinds[result].ToString().AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, syntaxToken.GetLocation(), new object[] { _symbolKinds[result].ToString(), "" })); + } + + private void AnalyzeAreaSectionName(SyntaxNodeAnalysisContext ctx) + { + IEnumerable childNodes = ctx.Node.ChildNodes().Where(n => n.Kind == SyntaxKind.IdentifierName); + + foreach (SyntaxNode childNode in childNodes) + { + ctx.CancellationToken.ThrowIfCancellationRequested(); + + SyntaxToken syntaxToken = ((IdentifierNameSyntax)childNode).Identifier; + int result = Array.FindIndex(_areaKinds, t => t.Equals(syntaxToken.ValueText, StringComparison.OrdinalIgnoreCase)); + if (result == -1) + continue; + + if (!syntaxToken.ValueText.AsSpan().Equals(_areaKinds[result].ToString().AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, childNode.GetLocation(), new object[] { _areaKinds[result].ToString(), "" })); + } + } + + private void AnalyzeActionAreaSectionName(SyntaxNodeAnalysisContext ctx) + { + IEnumerable childNodes = ctx.Node.ChildNodes().Where(n => n.Kind == SyntaxKind.IdentifierName); + + foreach (SyntaxNode childNode in childNodes) + { + ctx.CancellationToken.ThrowIfCancellationRequested(); + + SyntaxToken syntaxToken = ((IdentifierNameSyntax)childNode).Identifier; + int result = Array.FindIndex(_actionAreaKinds, t => t.Equals(syntaxToken.ValueText, StringComparison.OrdinalIgnoreCase)); + if (result == -1) + continue; + + if (!syntaxToken.ValueText.AsSpan().Equals(_actionAreaKinds[result].ToString().AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, childNode.GetLocation(), new object[] { _actionAreaKinds[result].ToString(), "" })); + } + } + private void CheckForBuiltInTypeCasingMismatch(SymbolAnalysisContext ctx) { AnalyseTokens(ctx); AnalyseNodes(ctx); - AnalysePageActionAreas(ctx); - AnalyzeMemberAccessExpressions(ctx); - AnalyzePropertyNames(ctx); - AnalyzeLabelProperties(ctx); } private void AnalyseTokens(SymbolAnalysisContext ctx) @@ -153,79 +235,6 @@ private void AnalyseNodes(SymbolAnalysisContext ctx) } } - private void AnalysePageActionAreas(SymbolAnalysisContext ctx) - { - IEnumerable descendantNodes = ctx.Symbol.DeclaringSyntaxReference.GetSyntax().DescendantNodes() - .Where(n => n.Parent.Kind == SyntaxKind.PageActionArea); - - foreach (SyntaxNode node in descendantNodes) - { - ctx.CancellationToken.ThrowIfCancellationRequested(); - - int result = Array.FindIndex(_actionAreaKinds, t => t.Equals(node.ToString(), StringComparison.OrdinalIgnoreCase)); - if (result == -1) - continue; - - if (!node.ToString().AsSpan().Equals(_actionAreaKinds[result].ToString().AsSpan(), StringComparison.Ordinal)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, node.GetLocation(), new object[] { _actionAreaKinds[result].ToString(), "" })); - } - } - - private void AnalyzeMemberAccessExpressions(SymbolAnalysisContext ctx) - { - IEnumerable descendantTokens = ctx.Symbol.DeclaringSyntaxReference.GetSyntax().DescendantTokens() - .Where(t => t.Parent.Parent.Kind == SyntaxKind.MemberAccessExpression); - - foreach (SyntaxToken token in descendantTokens) - { - ctx.CancellationToken.ThrowIfCancellationRequested(); - - int result = Array.FindIndex(_symbolKinds, t => t.Equals(token.ValueText, StringComparison.OrdinalIgnoreCase)); - if (result == -1) - continue; - - if (!token.ValueText.AsSpan().Equals(_symbolKinds[result].ToString().AsSpan(), StringComparison.Ordinal)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, token.GetLocation(), new object[] { _symbolKinds[result].ToString(), "" })); - } - } - - private void AnalyzePropertyNames(SymbolAnalysisContext ctx) - { - IEnumerable descendantTokens = ctx.Symbol.DeclaringSyntaxReference.GetSyntax().DescendantTokens() - .Where(t => t.Parent.Kind == SyntaxKind.PropertyName); - - foreach (SyntaxToken token in descendantTokens) - { - ctx.CancellationToken.ThrowIfCancellationRequested(); - - int result = Array.FindIndex(_propertyKindStrings, t => t.Equals(token.ValueText, StringComparison.OrdinalIgnoreCase)); - if (result == -1) - continue; - - if (!token.ValueText.AsSpan().Equals(_propertyKindStrings[result].ToString().AsSpan(), StringComparison.Ordinal)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, token.GetLocation(), new object[] { _propertyKindStrings[result].ToString(), "" })); - } - } - - private static void AnalyzeLabelProperties(SymbolAnalysisContext ctx) - { - IEnumerable descendantTokens = ctx.Symbol.DeclaringSyntaxReference.GetSyntax().DescendantTokens() - .Where(n => n.Kind == SyntaxKind.IdentifierToken) - .Where(n => n.Parent.Parent.Parent.Kind == SyntaxKind.Label); - - foreach (SyntaxToken token in descendantTokens) - { - ctx.CancellationToken.ThrowIfCancellationRequested(); - - int result = Array.FindIndex(_labelPropertyString, t => t.Equals(token.ToString(), StringComparison.OrdinalIgnoreCase)); - if (result == -1) - continue; - - if (!token.ToString().AsSpan().Equals(_labelPropertyString[result].ToString().AsSpan(), StringComparison.Ordinal)) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, token.GetLocation(), new object[] { _labelPropertyString[result].ToString(), "" })); - } - } - private static bool IsValidKind(SyntaxKind kind) { switch (kind) From f1fe5b97289530e8b27874bcbbf714fe0e20ec93 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Tue, 21 May 2024 16:59:56 +0200 Subject: [PATCH 14/20] Implement PredefinedActionCategoryNames from SyntaxFacts --- Design/Rule0016CheckForMissingCaptions.cs | 26 ++--------------------- 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/Design/Rule0016CheckForMissingCaptions.cs b/Design/Rule0016CheckForMissingCaptions.cs index 41189985..6fcfc5ac 100644 --- a/Design/Rule0016CheckForMissingCaptions.cs +++ b/Design/Rule0016CheckForMissingCaptions.cs @@ -12,29 +12,7 @@ class Rule0016CheckForMissingCaptions : DiagnosticAnalyzer { public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0016CheckForMissingCaptions); - private static readonly List PromotedGroupNames = new List - { - "category_new", - "category_process", - "category_report", - "category_category4", - "category_category5", - "category_category6", - "category_category7", - "category_category8", - "category_category9", - "category_category10", - "category_category11", - "category_category12", - "category_category13", - "category_category14", - "category_category15", - "category_category16", - "category_category17", - "category_category18", - "category_category19", - "category_category20", - }; + private static readonly HashSet _predefinedActionCategoryNames = SyntaxFacts.PredefinedActionCategoryNames.Select(x => x.Key.ToLowerInvariant()).ToHashSet(); public override void Initialize(AnalysisContext context) => context.RegisterSymbolAction(new Action(this.CheckForMissingCaptions), @@ -176,7 +154,7 @@ private bool CaptionIsMissing(ISymbol Symbol, SymbolAnalysisContext context) return false; } - if (SemanticFacts.IsSameName(Symbol.MostSpecificKind, "Group") && PromotedGroupNames.Contains(Symbol.Name.ToLowerInvariant())) + if (Symbol.Kind == SymbolKind.Action && ((IActionSymbol)Symbol).ActionKind == ActionKind.Group && _predefinedActionCategoryNames.Contains(Symbol.Name.ToLowerInvariant())) return false; if (Symbol.GetBooleanPropertyValue(PropertyKind.ShowCaption) != false) From d50893cf1981276f53ba114f8da7155caf2a6a98 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Wed, 22 May 2024 20:33:01 +0200 Subject: [PATCH 15/20] Implement Trigger Declarations --- ...bleCasingShouldNotDifferFromDeclaration.cs | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs b/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs index 80dd3cbb..eb9187e0 100644 --- a/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs +++ b/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs @@ -14,22 +14,23 @@ public class Rule0005VariableCasingShouldNotDifferFromDeclaration : DiagnosticAn = ImmutableArray.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration); private static readonly HashSet _dataTypeSyntaxKinds = Enum.GetValues(typeof(SyntaxKind)).Cast().Where(x => x.ToString().AsSpan().EndsWith("DataType")).ToHashSet(); - private static string[] _navTypeKindStrings; private static readonly string[] _areaKinds = Enum.GetValues(typeof(AreaKind)).Cast().Select(x => x.ToString()).ToArray(); private static readonly string[] _actionAreaKinds = Enum.GetValues(typeof(ActionAreaKind)).Cast().Select(x => x.ToString()).ToArray(); private static readonly string[] _labelPropertyString = LabelPropertyHelper.GetAllLabelProperties(); + private static readonly string[] _navTypeKindStrings = GenerateNavTypeKindArray(); private static readonly string[] _propertyKindStrings = Enum.GetValues(typeof(PropertyKind)).Cast().Select(x => x.ToString()).ToArray(); private static readonly string[] _symbolKinds = Enum.GetValues(typeof(SymbolKind)).Cast().Select(x => x.ToString()).ToArray(); + private static readonly Dictionary _triggerTypeKinds = GenerateNTriggerTypeKindMappings(); + public override void Initialize(AnalysisContext context) { - GenerateNavTypeKindArray(); - context.RegisterSyntaxNodeAction(new Action(this.AnalyzeLabel), SyntaxKind.Label); context.RegisterSyntaxNodeAction(new Action(this.AnalyzePropertyName), SyntaxKind.PropertyName); context.RegisterSyntaxNodeAction(new Action(this.AnalyzeMemberAccessExpression), SyntaxKind.MemberAccessExpression); context.RegisterSyntaxNodeAction(new Action(this.AnalyzeAreaSectionName), SyntaxKind.PageArea); context.RegisterSyntaxNodeAction(new Action(this.AnalyzeActionAreaSectionName), SyntaxKind.PageActionArea); + context.RegisterSyntaxNodeAction(new Action(this.AnalyzeTriggerDeclaration), SyntaxKind.TriggerDeclaration); context.RegisterOperationAction(new Action(this.CheckForBuiltInMethodsWithCasingMismatch), new OperationKind[] { OperationKind.InvocationExpression, @@ -64,11 +65,28 @@ public override void Initialize(AnalysisContext context) }); } - private static void GenerateNavTypeKindArray() + private static string[] GenerateNavTypeKindArray() { var navTypeKinds = Enum.GetValues(typeof(NavTypeKind)).Cast().Select(s => s.ToString()).ToList(); navTypeKinds.Add("Database"); // for Database::"G/L Entry" (there is no NavTypeKind for this) - _navTypeKindStrings = navTypeKinds.ToArray(); + return navTypeKinds.ToArray(); + } + + private static Dictionary GenerateNTriggerTypeKindMappings() + { + var mappings = new Dictionary(); + + foreach (TriggerTypeKind type in Enum.GetValues(typeof(TriggerTypeKind))) + { + string typeName = type.ToString(); + int index = typeName.IndexOf("On"); + if (index > 0) + { + mappings[type] = typeName.Substring(index); ; + } + } + + return mappings; } private void AnalyzeLabel(SyntaxNodeAnalysisContext ctx) @@ -151,6 +169,22 @@ private void AnalyzeActionAreaSectionName(SyntaxNodeAnalysisContext ctx) } } + private void AnalyzeTriggerDeclaration(SyntaxNodeAnalysisContext ctx) + { + TriggerDeclarationSyntax syntax = ctx.Node as TriggerDeclarationSyntax; + ISymbolWithTriggers symbolWithTriggers = ctx.ContainingSymbol.ContainingSymbol as ISymbolWithTriggers; + + TriggerTypeInfo triggerTypeInfo = symbolWithTriggers.GetTriggerTypeInfo(syntax.Name.Identifier.ValueText); + if (triggerTypeInfo == null) + return; + + if (!_triggerTypeKinds.TryGetValue(triggerTypeInfo.Kind, out string targetName)) + return; + + if (!syntax.Name.Identifier.ValueText.AsSpan().Equals(targetName.AsSpan(), StringComparison.Ordinal)) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0005VariableCasingShouldNotDifferFromDeclaration, syntax.Name.GetLocation(), new object[] { targetName, "" })); + } + private void CheckForBuiltInTypeCasingMismatch(SymbolAnalysisContext ctx) { AnalyseTokens(ctx); From 49776f8ba633b61dbc1125198fcdb7afe1adebb8 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Thu, 23 May 2024 14:42:45 +0200 Subject: [PATCH 16/20] Add TryCatch to investigate InvalidCastException --- Design/Rule0043SecretText.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Design/Rule0043SecretText.cs b/Design/Rule0043SecretText.cs index 11fd2a19..5e7f0229 100644 --- a/Design/Rule0043SecretText.cs +++ b/Design/Rule0043SecretText.cs @@ -10,7 +10,7 @@ namespace BusinessCentral.LinterCop.Design [DiagnosticAnalyzer] public class Rule0043SecretText : DiagnosticAnalyzer { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0043SecretText); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0043SecretText, DiagnosticDescriptors.Rule0000ErrorInRule); private static readonly string authorization = "Authorization"; @@ -89,8 +89,15 @@ private void AnalyzeHttpObjects(OperationAnalysisContext ctx) default: return; } - - if (!IsAuthorizationArgument(operation.Arguments[0])) return; + + try + { + if (!IsAuthorizationArgument(operation.Arguments[0])) return; + } + catch(InvalidCastException) + { + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0000ErrorInRule, context.Symbol.GetLocation(), new Object[] { "Rule0043", "InvalidCastException", "at Line 63" })); + } if (!IsArgumentOfTypeSecretText(operation.Arguments[1])) ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0043SecretText, ctx.Operation.Syntax.GetLocation())); From 422d0aee986d39bb7752e24efc42973342932312 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Thu, 23 May 2024 14:47:07 +0200 Subject: [PATCH 17/20] Resolve typo --- Design/Rule0043SecretText.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Design/Rule0043SecretText.cs b/Design/Rule0043SecretText.cs index 5e7f0229..808c0104 100644 --- a/Design/Rule0043SecretText.cs +++ b/Design/Rule0043SecretText.cs @@ -89,14 +89,14 @@ private void AnalyzeHttpObjects(OperationAnalysisContext ctx) default: return; } - + try { if (!IsAuthorizationArgument(operation.Arguments[0])) return; } - catch(InvalidCastException) + catch (InvalidCastException) { - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0000ErrorInRule, context.Symbol.GetLocation(), new Object[] { "Rule0043", "InvalidCastException", "at Line 63" })); + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0000ErrorInRule, ctx.Operation.Syntax.GetLocation(), new Object[] { "Rule0043", "InvalidCastException", "at Line 63" })); } if (!IsArgumentOfTypeSecretText(operation.Arguments[1])) From 250567aa3e9d95bccf6013172e938d8381c71185 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Thu, 23 May 2024 15:01:16 +0200 Subject: [PATCH 18/20] Exclude tokens with an empty string value --- Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs b/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs index eb9187e0..e296751c 100644 --- a/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs +++ b/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs @@ -195,7 +195,8 @@ private void AnalyseTokens(SymbolAnalysisContext ctx) { IEnumerable descendantTokens = ctx.Symbol.DeclaringSyntaxReference.GetSyntax().DescendantTokens() .Where(t => t.Kind.IsKeyword()) - .Where(t => !_dataTypeSyntaxKinds.Contains(t.Parent.Kind)); + .Where(t => !_dataTypeSyntaxKinds.Contains(t.Parent.Kind)) + .Where(t => !string.IsNullOrEmpty(t.ToString())); foreach (SyntaxToken descendantToken in descendantTokens) { From 79c59d61895bca1244bf8be5b75c4766e1edc135 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Thu, 23 May 2024 17:51:16 +0200 Subject: [PATCH 19/20] Resolve InvalidCastException --- Design/Rule0043SecretText.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Design/Rule0043SecretText.cs b/Design/Rule0043SecretText.cs index 808c0104..0f91765d 100644 --- a/Design/Rule0043SecretText.cs +++ b/Design/Rule0043SecretText.cs @@ -10,7 +10,7 @@ namespace BusinessCentral.LinterCop.Design [DiagnosticAnalyzer] public class Rule0043SecretText : DiagnosticAnalyzer { - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0043SecretText, DiagnosticDescriptors.Rule0000ErrorInRule); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0043SecretText); private static readonly string authorization = "Authorization"; @@ -90,14 +90,7 @@ private void AnalyzeHttpObjects(OperationAnalysisContext ctx) return; } - try - { - if (!IsAuthorizationArgument(operation.Arguments[0])) return; - } - catch (InvalidCastException) - { - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0000ErrorInRule, ctx.Operation.Syntax.GetLocation(), new Object[] { "Rule0043", "InvalidCastException", "at Line 63" })); - } + if (!IsAuthorizationArgument(operation.Arguments[0])) return; if (!IsArgumentOfTypeSecretText(operation.Arguments[1])) ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0043SecretText, ctx.Operation.Syntax.GetLocation())); @@ -115,6 +108,7 @@ private static bool IsAuthorizationArgument(IArgument argument) case SyntaxKind.LiteralExpression: return SemanticFacts.IsSameName(argument.Value.ConstantValue.Value.ToString(), authorization); case SyntaxKind.IdentifierName: + if (argument.Value.Kind != OperationKind.ConversionExpression) return false; IOperation operand = ((IConversionExpression)argument.Value).Operand; if (operand.GetSymbol().OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Label) return false; ILabelTypeSymbol label = (ILabelTypeSymbol)operand.GetSymbol().OriginalDefinition.GetTypeSymbol(); From 9dd053fe4953508f71be261d47cd0b6220d024a2 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Thu, 23 May 2024 18:07:34 +0200 Subject: [PATCH 20/20] Some small bugfixes --- Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs | 3 +++ Design/Rule0043SecretText.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs b/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs index e296751c..9992fbc6 100644 --- a/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs +++ b/Design/Rule0005VariableCasingShouldNotDifferFromDeclaration.cs @@ -172,6 +172,9 @@ private void AnalyzeActionAreaSectionName(SyntaxNodeAnalysisContext ctx) private void AnalyzeTriggerDeclaration(SyntaxNodeAnalysisContext ctx) { TriggerDeclarationSyntax syntax = ctx.Node as TriggerDeclarationSyntax; + if (syntax == null) + return; + ISymbolWithTriggers symbolWithTriggers = ctx.ContainingSymbol.ContainingSymbol as ISymbolWithTriggers; TriggerTypeInfo triggerTypeInfo = symbolWithTriggers.GetTriggerTypeInfo(syntax.Name.Identifier.ValueText); diff --git a/Design/Rule0043SecretText.cs b/Design/Rule0043SecretText.cs index 808c0104..d7c8a05c 100644 --- a/Design/Rule0043SecretText.cs +++ b/Design/Rule0043SecretText.cs @@ -105,7 +105,7 @@ private void AnalyzeHttpObjects(OperationAnalysisContext ctx) private bool IsArgumentOfTypeSecretText(IArgument argument) { - return argument.Parameter.OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.SecretText; + return argument.Parameter?.OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.SecretText; } private static bool IsAuthorizationArgument(IArgument argument)