Skip to content

Commit

Permalink
Add rules for best pratices on API Pages
Browse files Browse the repository at this point in the history
  • Loading branch information
Arthurvdv committed Jul 6, 2024
1 parent aa8c791 commit 33027ba
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -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<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create<DiagnosticDescriptor>(DiagnosticDescriptors.Rule0060PropertyApplicationAreaOnApiPage);

public override void Initialize(AnalysisContext context)
=> context.RegisterSymbolAction(new Action<SymbolAnalysisContext>(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<IControlSymbol> 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()));
}
}
}
Original file line number Diff line number Diff line change
@@ -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<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create<DiagnosticDescriptor>(DiagnosticDescriptors.Rule0061SetODataKeyFieldsWithSystemIdField);

public override void Initialize(AnalysisContext context)
=> context.RegisterSymbolAction(new Action<SymbolAnalysisContext>(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));
}
}
}
Original file line number Diff line number Diff line change
@@ -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<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create<DiagnosticDescriptor>(DiagnosticDescriptors.Rule0062MandatoryFieldMissingOnApiPage);

private static readonly Dictionary<string, string> _mandatoryFields = new Dictionary<string, string>
{
{ "SystemId", "id" },
{ "SystemModifiedAt", "lastModifiedDateTime" }
};

public override void Initialize(AnalysisContext context)
=> context.RegisterSymbolAction(new Action<SymbolAnalysisContext>(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<IControlSymbol> pageFields = pageTypeSymbol.FlattenedControls
.Where(e => e.ControlKind == ControlKind.Field)
.Where(e => e.RelatedFieldSymbol != null);

IEnumerable<KeyValuePair<string, string>> missingMandatoryFields = _mandatoryFields
.Where(mf => !pageFields.Any(pf => mf.Key == pf.RelatedFieldSymbol?.Name && mf.Value == pf.Name));

foreach (KeyValuePair<string, string> field in missingMandatoryFields)
{
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0062MandatoryFieldMissingOnApiPage, pageTypeSymbol.GetLocation(), new object[] { field.Key, field.Value }));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create<DiagnosticDescriptor>(DiagnosticDescriptors.Rule0063GiveFieldMoreDescriptiveName);
private static readonly Dictionary<string, string> _descriptiveNames = new Dictionary<string, string>
{
{ "SystemId", "id" },
{ "Name", "displayName" },
{ "SystemModifiedAt", "lastModifiedDateTime" }
};

public override void Initialize(AnalysisContext context)
=> context.RegisterSymbolAction(new Action<SymbolAnalysisContext>(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<IControlSymbol> 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;
}
}
}
20 changes: 20 additions & 0 deletions BusinessCentral.LinterCop/LinterCop.ruleset.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
]
}
4 changes: 4 additions & 0 deletions BusinessCentral.LinterCop/LinterCopAnalyzers.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
36 changes: 36 additions & 0 deletions BusinessCentral.LinterCop/LinterCopAnalyzers.resx
Original file line number Diff line number Diff line change
Expand Up @@ -645,4 +645,40 @@
<data name="Rule0059SingleQuoteEscapingIssueDetectedDescription" xml:space="preserve">
<value>Single quote escaping issue detected: Use %1 or '''' for correct text escaping.</value>
</data>
<data name="Rule0060PropertyApplicationAreaOnApiPageTitle" xml:space="preserve">
<value>The 'ApplicationArea' property is not applicable to API pages.</value>
</data>
<data name="Rule0060PropertyApplicationAreaOnApiPageFormat" xml:space="preserve">
<value>The 'ApplicationArea' property is not applicable to API pages.</value>
</data>
<data name="Rule0060PropertyApplicationAreaOnApiPageDescription" xml:space="preserve">
<value>The 'ApplicationArea' property is not applicable to API pages"</value>
</data>
<data name="Rule0061SetODataKeyFieldsWithSystemIdFieldTitle" xml:space="preserve">
<value>Pages of type API must have the 'ODataKeyFields' property set to the 'SystemId' field.</value>
</data>
<data name="Rule0061SetODataKeyFieldsWithSystemIdFieldFormat" xml:space="preserve">
<value>Pages of type API must have the 'ODataKeyFields' property set to the 'SystemId' field.</value>
</data>
<data name="Rule0061SetODataKeyFieldsWithSystemIdFieldDescription" xml:space="preserve">
<value>Pages of type API must have the 'ODataKeyFields' property set to the 'SystemId' field.</value>
</data>
<data name="Rule0062MandatoryFieldMissingOnApiPageTitle" xml:space="preserve">
<value>"Field 'Rec.{0}' exposed with the name '{1}' should always be included on API Pages."</value>
</data>
<data name="Rule0062MandatoryFieldMissingOnApiPageFormat" xml:space="preserve">
<value>"Field 'Rec.{0}' exposed with the name '{1}' should always be included on API Pages."</value>
</data>
<data name="Rule0062MandatoryFieldMissingOnApiPageDescription" xml:space="preserve">
<value>"Field 'Rec.{0}' exposed with the name '{1}' should always be included on API Pages."</value>
</data>
<data name="Rule0063GiveFieldMoreDescriptiveNameTitle" xml:space="preserve">
<value>Consider naming field with a more descriptive name: '{0}'.</value>
</data>
<data name="Rule0063GiveFieldMoreDescriptiveNameFormat" xml:space="preserve">
<value>Consider naming field with a more descriptive name: '{0}'.</value>
</data>
<data name="Rule0063GiveFieldMoreDescriptiveNameDescription" xml:space="preserve">
<value>Consider naming field with a more descriptive name: '{0}'.</value>
</data>
</root>
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|
|[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|

0 comments on commit 33027ba

Please sign in to comment.