From 33027ba8d8d2a8acb4da4619da9716e93913aa61 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Sat, 6 Jul 2024 14:35:44 +0200 Subject: [PATCH] Add rules for best pratices on API Pages --- ...0RemovePropertyApplicationAreaOnApiPage.cs | 37 +++++++++++ ...e0061SetODataKeyFieldsWithSystemIdField.cs | 41 ++++++++++++ .../Rule0062MandatoryFieldMissingOnApiPage.cs | 48 ++++++++++++++ .../Rule0063GiveFieldMoreDescriptiveName.cs | 66 +++++++++++++++++++ .../LinterCop.ruleset.json | 20 ++++++ .../LinterCopAnalyzers.Generated.cs | 4 ++ .../LinterCopAnalyzers.resx | 36 ++++++++++ README.md | 6 +- 8 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 BusinessCentral.LinterCop/Design/Rule0060RemovePropertyApplicationAreaOnApiPage.cs create mode 100644 BusinessCentral.LinterCop/Design/Rule0061SetODataKeyFieldsWithSystemIdField.cs create mode 100644 BusinessCentral.LinterCop/Design/Rule0062MandatoryFieldMissingOnApiPage.cs create mode 100644 BusinessCentral.LinterCop/Design/Rule0063GiveFieldMoreDescriptiveName.cs diff --git a/BusinessCentral.LinterCop/Design/Rule0060RemovePropertyApplicationAreaOnApiPage.cs b/BusinessCentral.LinterCop/Design/Rule0060RemovePropertyApplicationAreaOnApiPage.cs new file mode 100644 index 00000000..98b2ae4a --- /dev/null +++ b/BusinessCentral.LinterCop/Design/Rule0060RemovePropertyApplicationAreaOnApiPage.cs @@ -0,0 +1,37 @@ +using BusinessCentral.LinterCop.AnalysisContextExtension; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; + +namespace BusinessCentral.LinterCop.Design +{ + [DiagnosticAnalyzer] + public class Rule0060PropertyApplicationAreaOnApiPage : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0060PropertyApplicationAreaOnApiPage); + + public override void Initialize(AnalysisContext context) + => context.RegisterSymbolAction(new Action(this.AnalyzePropertyApplicationAreaOnApiPage), SymbolKind.Page); + + private void AnalyzePropertyApplicationAreaOnApiPage(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) return; + + if (ctx.Symbol is not IPageTypeSymbol pageTypeSymbol) + return; + + if (pageTypeSymbol.PageType != PageTypeKind.API) + return; + + if (pageTypeSymbol.GetProperty(PropertyKind.ApplicationArea) is IPropertySymbol propertyApplicationArea) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0060PropertyApplicationAreaOnApiPage, propertyApplicationArea.GetLocation())); + + IEnumerable pageFields = pageTypeSymbol.FlattenedControls + .Where(e => e.ControlKind == ControlKind.Field) + .Where(e => e.GetProperty(PropertyKind.ApplicationArea) is not null); + + foreach (IControlSymbol pageField in pageFields) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0060PropertyApplicationAreaOnApiPage, pageField.GetProperty(PropertyKind.ApplicationArea).GetLocation())); + } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0061SetODataKeyFieldsWithSystemIdField.cs b/BusinessCentral.LinterCop/Design/Rule0061SetODataKeyFieldsWithSystemIdField.cs new file mode 100644 index 00000000..37f4620b --- /dev/null +++ b/BusinessCentral.LinterCop/Design/Rule0061SetODataKeyFieldsWithSystemIdField.cs @@ -0,0 +1,41 @@ +using BusinessCentral.LinterCop.AnalysisContextExtension; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Text; +using System.Collections.Immutable; + +namespace BusinessCentral.LinterCop.Design +{ + [DiagnosticAnalyzer] + public class Rule0061SetODataKeyFieldsWithSystemIdField : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0061SetODataKeyFieldsWithSystemIdField); + + public override void Initialize(AnalysisContext context) + => context.RegisterSymbolAction(new Action(this.AnalyzeODataKeyFieldsPropertyOnApiPage), SymbolKind.Page); + + private void AnalyzeODataKeyFieldsPropertyOnApiPage(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) return; + + if (ctx.Symbol is not IPageTypeSymbol pageTypeSymbol) + return; + + if (pageTypeSymbol.PageType != PageTypeKind.API) + return; + + if (pageTypeSymbol.GetBooleanPropertyValue(PropertyKind.SourceTableTemporary).GetValueOrDefault()) + return; + + IPropertySymbol property = pageTypeSymbol.GetProperty(PropertyKind.ODataKeyFields); + + // Set the location of the diagnostic on the property itself (if exists) + Location location = pageTypeSymbol.GetLocation(); + if (property != null) + location = property.GetLocation(); + + if (property == null || property.Value == null || property.ValueText != "2000000000") + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0061SetODataKeyFieldsWithSystemIdField, location)); + } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0062MandatoryFieldMissingOnApiPage.cs b/BusinessCentral.LinterCop/Design/Rule0062MandatoryFieldMissingOnApiPage.cs new file mode 100644 index 00000000..aa87c72a --- /dev/null +++ b/BusinessCentral.LinterCop/Design/Rule0062MandatoryFieldMissingOnApiPage.cs @@ -0,0 +1,48 @@ +using BusinessCentral.LinterCop.AnalysisContextExtension; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; + +namespace BusinessCentral.LinterCop.Design +{ + [DiagnosticAnalyzer] + public class Rule0062MandatoryFieldMissingOnApiPage : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0062MandatoryFieldMissingOnApiPage); + + private static readonly Dictionary _mandatoryFields = new Dictionary + { + { "SystemId", "id" }, + { "SystemModifiedAt", "lastModifiedDateTime" } + }; + + public override void Initialize(AnalysisContext context) + => context.RegisterSymbolAction(new Action(this.AnalyzeRule0062MandatoryFieldOnApiPage), SymbolKind.Page); + + private void AnalyzeRule0062MandatoryFieldOnApiPage(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) return; + + if (ctx.Symbol is not IPageTypeSymbol pageTypeSymbol) + return; + + if (pageTypeSymbol.PageType != PageTypeKind.API) + return; + + if (pageTypeSymbol.GetBooleanPropertyValue(PropertyKind.SourceTableTemporary).GetValueOrDefault()) + return; + + IEnumerable pageFields = pageTypeSymbol.FlattenedControls + .Where(e => e.ControlKind == ControlKind.Field) + .Where(e => e.RelatedFieldSymbol != null); + + IEnumerable> missingMandatoryFields = _mandatoryFields + .Where(mf => !pageFields.Any(pf => mf.Key == pf.RelatedFieldSymbol?.Name && mf.Value == pf.Name)); + + foreach (KeyValuePair field in missingMandatoryFields) + { + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0062MandatoryFieldMissingOnApiPage, pageTypeSymbol.GetLocation(), new object[] { field.Key, field.Value })); + } + } + } +} \ No newline at end of file diff --git a/BusinessCentral.LinterCop/Design/Rule0063GiveFieldMoreDescriptiveName.cs b/BusinessCentral.LinterCop/Design/Rule0063GiveFieldMoreDescriptiveName.cs new file mode 100644 index 00000000..776dcdb3 --- /dev/null +++ b/BusinessCentral.LinterCop/Design/Rule0063GiveFieldMoreDescriptiveName.cs @@ -0,0 +1,66 @@ +using BusinessCentral.LinterCop.AnalysisContextExtension; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; + +namespace BusinessCentral.LinterCop.Design +{ + [DiagnosticAnalyzer] + public class Rule0063GiveFieldMoreDescriptiveName : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0063GiveFieldMoreDescriptiveName); + private static readonly Dictionary _descriptiveNames = new Dictionary + { + { "SystemId", "id" }, + { "Name", "displayName" }, + { "SystemModifiedAt", "lastModifiedDateTime" } + }; + + public override void Initialize(AnalysisContext context) + => context.RegisterSymbolAction(new Action(this.AnalyzePropertyApplicationAreaOnFieldsOfApiPage), SymbolKind.Page); + + private void AnalyzePropertyApplicationAreaOnFieldsOfApiPage(SymbolAnalysisContext ctx) + { + if (ctx.IsObsoletePendingOrRemoved()) return; + + if (ctx.Symbol is not IPageTypeSymbol pageTypeSymbol) + return; + + if (pageTypeSymbol.PageType != PageTypeKind.API) + return; + + IEnumerable pageFields = pageTypeSymbol.FlattenedControls + .Where(e => e.ControlKind == ControlKind.Field) + .Where(e => e.RelatedFieldSymbol != null); + + foreach (IControlSymbol field in pageFields) + { + ctx.CancellationToken.ThrowIfCancellationRequested(); + string descriptiveName = GetDescriptiveName(field); + if (!string.IsNullOrEmpty(descriptiveName) && field.Name != descriptiveName) + { + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0063GiveFieldMoreDescriptiveName, field.GetLocation(), new object[] { descriptiveName })); + } + } + } + + private static string GetDescriptiveName(IControlSymbol field) + { + if (_descriptiveNames.ContainsKey(field.RelatedFieldSymbol.Name)) + return _descriptiveNames[field.RelatedFieldSymbol.Name]; + + if (field.RelatedFieldSymbol.Name.Contains("No.") + && field.Name.Contains("no", StringComparison.OrdinalIgnoreCase) + && !field.Name.Contains("number", StringComparison.OrdinalIgnoreCase)) + return ReplaceNoWithNumber(field.Name); + + return null; + } + public static string ReplaceNoWithNumber(string input) + { + input = input.Replace("No", "Number"); + input = input.Replace("no", "number"); + return input; + } + } +} diff --git a/BusinessCentral.LinterCop/LinterCop.ruleset.json b/BusinessCentral.LinterCop/LinterCop.ruleset.json index 00e76747..e4edd08e 100644 --- a/BusinessCentral.LinterCop/LinterCop.ruleset.json +++ b/BusinessCentral.LinterCop/LinterCop.ruleset.json @@ -296,6 +296,26 @@ "id": "LC0059", "action": "Warning", "justification": "Single quote escaping issue detected." + }, + { + "id": "LC0060", + "action": "Info", + "justification": " The ApplicationArea property is not applicable to API pages." + }, + { + "id": "LC0061", + "action": "Info", + "justification": "Pages of type API must have the ODataKeyFields property set to the SystemId field." + }, + { + "id": "LC0062", + "action": "Info", + "justification": "Mandatory field is missing on API page." + }, + { + "id": "LC0063", + "action": "Info", + "justification": "Consider naming field with a more descriptive name." } ] } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs b/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs index 4a0bcb98..b6a33d17 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs @@ -66,5 +66,9 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor Rule0057EnumValueWithEmptyCaption = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0057", (LocalizableString)new LocalizableResourceString("Rule0057EnumValueWithEmptyCaptionTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0057EnumValueWithEmptyCaptionFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0057EnumValueWithEmptyCaptionDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0057"); public static readonly DiagnosticDescriptor Rule0058PageVariableMethodOnTemporaryTable = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0058", (LocalizableString)new LocalizableResourceString("Rule0058PageVariableMethodOnTemporaryTableTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0058PageVariableMethodOnTemporaryTableFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0058PageVariableMethodOnTemporaryTableDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0058"); public static readonly DiagnosticDescriptor Rule0059SingleQuoteEscapingIssueDetected = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0059", (LocalizableString)new LocalizableResourceString("Rule0059SingleQuoteEscapingIssueDetectedTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0059SingleQuoteEscapingIssueDetectedFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0059SingleQuoteEscapingIssueDetectedDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0059"); + public static readonly DiagnosticDescriptor Rule0060PropertyApplicationAreaOnApiPage = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0060", (LocalizableString)new LocalizableResourceString("Rule0060PropertyApplicationAreaOnApiPageTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0060PropertyApplicationAreaOnApiPageFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0060PropertyApplicationAreaOnApiPageDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0060"); + public static readonly DiagnosticDescriptor Rule0061SetODataKeyFieldsWithSystemIdField = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0061", (LocalizableString)new LocalizableResourceString("Rule0061SetODataKeyFieldsWithSystemIdFieldTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0061SetODataKeyFieldsWithSystemIdFieldFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0061SetODataKeyFieldsWithSystemIdFieldDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0061"); + public static readonly DiagnosticDescriptor Rule0062MandatoryFieldMissingOnApiPage = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0062", (LocalizableString)new LocalizableResourceString("Rule0062MandatoryFieldMissingOnApiPageTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0062MandatoryFieldMissingOnApiPageFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0062MandatoryFieldMissingOnApiPageDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0062"); + public static readonly DiagnosticDescriptor Rule0063GiveFieldMoreDescriptiveName = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0063", (LocalizableString)new LocalizableResourceString("Rule0063GiveFieldMoreDescriptiveNameTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0063GiveFieldMoreDescriptiveNameFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0063GiveFieldMoreDescriptiveNameDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0063"); } } \ No newline at end of file diff --git a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx index 60865632..752425e7 100644 --- a/BusinessCentral.LinterCop/LinterCopAnalyzers.resx +++ b/BusinessCentral.LinterCop/LinterCopAnalyzers.resx @@ -645,4 +645,40 @@ Single quote escaping issue detected: Use %1 or '''' for correct text escaping. + + The 'ApplicationArea' property is not applicable to API pages. + + + The 'ApplicationArea' property is not applicable to API pages. + + + The 'ApplicationArea' property is not applicable to API pages" + + + Pages of type API must have the 'ODataKeyFields' property set to the 'SystemId' field. + + + Pages of type API must have the 'ODataKeyFields' property set to the 'SystemId' field. + + + Pages of type API must have the 'ODataKeyFields' property set to the 'SystemId' field. + + + "Field 'Rec.{0}' exposed with the name '{1}' should always be included on API Pages." + + + "Field 'Rec.{0}' exposed with the name '{1}' should always be included on API Pages." + + + "Field 'Rec.{0}' exposed with the name '{1}' should always be included on API Pages." + + + Consider naming field with a more descriptive name: '{0}'. + + + Consider naming field with a more descriptive name: '{0}'. + + + Consider naming field with a more descriptive name: '{0}'. + \ No newline at end of file diff --git a/README.md b/README.md index 17766ac5..8b5beb7a 100644 --- a/README.md +++ b/README.md @@ -212,4 +212,8 @@ For an example and the default values see: [LinterCop.ruleset.json](LinterCop.ru |[LC0056](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0056)|Empty Enum values should not have a specified `Caption` property.|Info| |[LC0057](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0057)|Enum values must have non-empty a `Caption` to be selectable in the client|Info| |[LC0058](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0058)|PageVariable.SetRecord(): You cannot use a temporary record for the Record parameter.|Warning| -|[LC0059](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0058)|Single quote escaping issue detected.|Warning| \ No newline at end of file +|[LC0059](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0058)|Single quote escaping issue detected.|Warning| +|[LC0060](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0060)|The `ApplicationArea` property is not applicable to API pages.|Info| +|[LC0061](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0061)|Pages of type API must have the `ODataKeyFields` property set to the SystemId field.|Info| +|[LC0062](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0062)|Mandatory field is missing on API page.|Info| +|[LC0063](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0063)|Consider naming field with a more descriptive name.|Info| \ No newline at end of file