Skip to content

Commit

Permalink
Merge pull request #355 from StefanMaron/prerelease
Browse files Browse the repository at this point in the history
Merge branch 'prerelease' into master
  • Loading branch information
Arthurvdv authored Nov 21, 2023
2 parents 5d93975 + 89ae84c commit baafaff
Show file tree
Hide file tree
Showing 8 changed files with 301 additions and 131 deletions.
15 changes: 1 addition & 14 deletions Design/Rule0021BuiltInMethodImplementThroughCodeunit.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
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;

namespace BusinessCentral.LinterCop.Design
{
[DiagnosticAnalyzer]
public class Rule0021BuiltInMethodImplementThroughCodeunit : DiagnosticAnalyzer
public class BuiltInMethodImplementThroughCodeunit : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create<DiagnosticDescriptor>(
DiagnosticDescriptors.Rule0021ConfirmImplementConfirmManagement,
DiagnosticDescriptors.Rule0022GlobalLanguageImplementTranslationHelper,
DiagnosticDescriptors.Rule0027RunPageImplementPageManagement,
DiagnosticDescriptors.Rule0000ErrorInRule);

public override void Initialize(AnalysisContext context) => context.RegisterOperationAction(new Action<OperationAnalysisContext>(this.CheckBuiltInMethod), OperationKind.InvocationExpression);
Expand All @@ -25,16 +22,6 @@ private void CheckBuiltInMethod(OperationAnalysisContext ctx)
IInvocationExpression operation = (IInvocationExpression)ctx.Operation;
if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return;

if (operation.TargetMethod.ContainingType.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.Page && operation.Arguments.Count() > 1)
{
if (operation.TargetMethod.ReturnValueSymbol.ReturnType.NavTypeKind == NavTypeKind.Action) return; // Page Management Codeunit doesn't support returntype Action
if (operation.Arguments[0].Syntax.GetIdentifierOrLiteralValue() == "0") return; // Allow zero as input for Page.Run(0, <recordVar>)
if (operation.Arguments[0].Syntax.IsKind(SyntaxKind.IdentifierName)) return; // In case the PageID is set by a field from a (setup) record, do not raise diagnostic
if (operation.TargetMethod.Name.ToUpper() == "ENQUEUEBACKGROUNDTASK") return; // do not execute on CurrPage.EnqueueBackgroundTask
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0027RunPageImplementPageManagement, ctx.Operation.Syntax.GetLocation()));
return;
}

switch (operation.TargetMethod.Name.ToUpper())
{
case "CONFIRM":
Expand Down
101 changes: 101 additions & 0 deletions Design/Rule0027RunPageImplementPageManagement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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;

namespace BusinessCentral.LinterCop.Design
{
[DiagnosticAnalyzer]
public class Rule0027RunPageImplementPageManagement : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create<DiagnosticDescriptor>(DiagnosticDescriptors.Rule0027RunPageImplementPageManagement);

public override void Initialize(AnalysisContext context) => context.RegisterOperationAction(new Action<OperationAnalysisContext>(this.CheckRunPageImplementPageManagement), OperationKind.InvocationExpression);

private void CheckRunPageImplementPageManagement(OperationAnalysisContext ctx)
{
if (ctx.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoletePending || ctx.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoleteRemoved) return;
if (ctx.ContainingSymbol.IsObsoletePending || ctx.ContainingSymbol.IsObsoleteRemoved) return;

IInvocationExpression operation = (IInvocationExpression)ctx.Operation;
if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return;

if (operation.TargetMethod.ContainingType.GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Page) return;
if (operation.Arguments.Count() < 2) return;

// do not execute on CurrPage.EnqueueBackgroundTask
if (SemanticFacts.IsSameName(operation.TargetMethod.Name, "EnqueueBackgroundTask")) return;

// Page Management Codeunit doesn't support returntype Action
if (operation.TargetMethod.ReturnValueSymbol.ReturnType.GetNavTypeKindSafe() == NavTypeKind.Action) return;

switch (operation.Arguments[0].Syntax.Kind)
{
case SyntaxKind.LiteralExpression:
if (operation.Arguments[0].Syntax.GetIdentifierOrLiteralValue() == "0")
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0027RunPageImplementPageManagement, ctx.Operation.Syntax.GetLocation()));
break;

case SyntaxKind.OptionAccessExpression:
if (IsSupportedRecord(((IConversionExpression)operation.Arguments[1].Value).Operand))
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0027RunPageImplementPageManagement, ctx.Operation.Syntax.GetLocation()));
break;

default:
return;
}
}

private static bool IsSupportedRecord(IOperation operation)
{
IRecordTypeSymbol recordTypeSymbol = null;

if (operation.Kind == OperationKind.GlobalReferenceExpression || operation.Kind == OperationKind.LocalReferenceExpression)
recordTypeSymbol = (IRecordTypeSymbol)operation.GetSymbol().GetTypeSymbol();

if (operation.Kind == OperationKind.InvocationExpression)
recordTypeSymbol = (IRecordTypeSymbol)operation.Type.GetTypeSymbol();

if (recordTypeSymbol == null || recordTypeSymbol.Temporary) return false;

if (GetSupportedRecords().ContainsKey(recordTypeSymbol.Id))
return SemanticFacts.IsSameName(recordTypeSymbol.Name, GetSupportedRecords()[recordTypeSymbol.Id]);

return false;
}

private static Dictionary<int, string> GetSupportedRecords()
{
Dictionary<int, string> SupportedRecords = new Dictionary<int, string>
{
{ 36, "Sales Header" },
{ 38, "Purchase Header" },
{ 79, "Company Information" },
{ 80, "Gen. Journal Template" },
{ 81, "Gen. Journal Line" },
{ 91, "User Setup" },
{ 98, "General Ledger Setup" },
{ 112, "Sales Invoice Header" },
{ 131, "Incoming Documents Setup" },
{ 207, "Res. Journal Line" },
{ 210, "Job Journal Line" },
{ 232, "Gen. Journal Batch" },
{ 312, "Purchases & Payables Setup" },
{ 454, "Approval Entry" },
{ 843, "Cash Flow Setup" },
{ 1251, "Text-to-Account Mapping" },
{ 1275, "Doc. Exch. Service Setup" },
{ 5107, "Sales Header Archive" },
{ 5109, "Purchase Header Archive" },
{ 5200, "Employee" },
{ 5405, "Production Order" },
{ 5900, "Service Header" },
{ 5965, "Service Contract Header" },
{ 7152, "Item Analysis View" },
{ 2000000120, "User" }
};
return SupportedRecords;
}
}
}
31 changes: 31 additions & 0 deletions Design/Rule0031RecordInstanceIsolationLevel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Microsoft.Dynamics.Nav.Analyzers.Common.AppSourceCopConfiguration;
using Microsoft.Dynamics.Nav.CodeAnalysis;
using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;

namespace BusinessCentral.LinterCop.Design
{
[DiagnosticAnalyzer]
public class Rule0031RecordInstanceIsolationLevel : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create<DiagnosticDescriptor>(DiagnosticDescriptors.Rule0031RecordInstanceIsolationLevel);

public override void Initialize(AnalysisContext context) => context.RegisterOperationAction(new Action<OperationAnalysisContext>(this.CheckLockTable), OperationKind.InvocationExpression);

private void CheckLockTable(OperationAnalysisContext ctx)
{
// ReadIsolation is supported from runtime versions 11.0 or greater.
var manifest = AppSourceCopConfigurationProvider.GetManifest(ctx.Compilation);
if (manifest.Runtime < RuntimeVersion.Spring2023) return;

if (ctx.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoletePending || ctx.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoleteRemoved) return;
if (ctx.ContainingSymbol.IsObsoletePending || ctx.ContainingSymbol.IsObsoleteRemoved) return;

IInvocationExpression operation = (IInvocationExpression)ctx.Operation;
if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return;

if (SemanticFacts.IsSameName(operation.TargetMethod.Name, "LockTable"))
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0031RecordInstanceIsolationLevel, ctx.Operation.Syntax.GetLocation()));
}
}
}
86 changes: 86 additions & 0 deletions Design/Rule0032ClearCodeunitSingleInstance.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using Microsoft.Dynamics.Nav.CodeAnalysis;
using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics;
using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols;
using System.Collections.Immutable;

namespace BusinessCentral.LinterCop.Design
{
[DiagnosticAnalyzer]
public class Rule0032ClearCodeunitSingleInstance : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create<DiagnosticDescriptor>(DiagnosticDescriptors.Rule0032ClearCodeunitSingleInstance);

public override void Initialize(AnalysisContext context)
{
context.RegisterOperationAction(new Action<OperationAnalysisContext>(this.ClearCodeunit), OperationKind.InvocationExpression);
context.RegisterOperationAction(new Action<OperationAnalysisContext>(this.ClearAllCodeunit), OperationKind.InvocationExpression);
}

private void ClearCodeunit(OperationAnalysisContext ctx)
{
if (ctx.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoletePending || ctx.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoleteRemoved) return;
if (ctx.ContainingSymbol.IsObsoletePending || ctx.ContainingSymbol.IsObsoleteRemoved) return;

IInvocationExpression operation = (IInvocationExpression)ctx.Operation;
if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return;

if (!SemanticFacts.IsSameName(operation.TargetMethod.Name, "Clear")) return;
if (operation.Arguments.Count() < 1) return;

IOperation operand = ((IConversionExpression)operation.Arguments[0].Value).Operand;
if (operand.GetSymbol().GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Codeunit) return;

if (IsSingleInstanceCodeunitWithGlobalVars((ICodeunitTypeSymbol)operand.GetSymbol().GetTypeSymbol()))
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0032ClearCodeunitSingleInstance, ctx.Operation.Syntax.GetLocation(), new Object[] { operand.GetSymbol().Name, operand.GetSymbol().GetTypeSymbol().Name }));
}

private void ClearAllCodeunit(OperationAnalysisContext ctx)
{
if (ctx.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoletePending || ctx.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoleteRemoved) return;
if (ctx.ContainingSymbol.IsObsoletePending || ctx.ContainingSymbol.IsObsoleteRemoved) return;

if (ctx.ContainingSymbol.GetContainingObjectTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Codeunit) return;

IInvocationExpression operation = (IInvocationExpression)ctx.Operation;
if (operation.TargetMethod.MethodKind != MethodKind.BuiltInMethod) return;
if (!SemanticFacts.IsSameName(operation.TargetMethod.Name, "ClearAll")) return;

IEnumerable<ISymbol> localVariables = ((IMethodSymbol)ctx.ContainingSymbol.OriginalDefinition).LocalVariables
.Where(var => var.OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.Codeunit)
.Where(var => var.OriginalDefinition.GetTypeSymbol().OriginalDefinition != ctx.ContainingSymbol.GetContainingObjectTypeSymbol().OriginalDefinition);
IEnumerable<ISymbol> globalVariables = ctx.ContainingSymbol.GetContainingObjectTypeSymbol()
.GetMembers()
.Where(members => members.Kind == SymbolKind.GlobalVariable)
.Where(var => var.OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.Codeunit)
.Where(var => var.OriginalDefinition.GetTypeSymbol().OriginalDefinition != ctx.ContainingSymbol.GetContainingObjectTypeSymbol().OriginalDefinition);

if (HasSingleInstanceCodeunitWithGlobalVars(localVariables, out ISymbol codeunit) || HasSingleInstanceCodeunitWithGlobalVars(globalVariables, out codeunit))
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0032ClearCodeunitSingleInstance, ctx.Operation.Syntax.GetLocation(), new Object[] { codeunit.Name, codeunit.GetTypeSymbol().Name }));
}

private static bool HasSingleInstanceCodeunitWithGlobalVars(IEnumerable<ISymbol> variables, out ISymbol codeunit)
{
foreach (ISymbol variable in variables.Where(var => var.OriginalDefinition.ContainingType.GetNavTypeKindSafe() == NavTypeKind.Codeunit))
if (IsSingleInstanceCodeunitWithGlobalVars((ICodeunitTypeSymbol)variable.OriginalDefinition.GetTypeSymbol()))
{
codeunit = variable;
return true;
}

codeunit = null;
return false;
}

private static bool IsSingleInstanceCodeunitWithGlobalVars(ICodeunitTypeSymbol codeunitTypeSymbol)
{
IPropertySymbol singleInstanceProperty = codeunitTypeSymbol.GetProperty(PropertyKind.SingleInstance);
if (singleInstanceProperty == null || !(bool)singleInstanceProperty.Value) return false;

var globalVariables = codeunitTypeSymbol.GetMembers().Where(members => members.Kind == SymbolKind.GlobalVariable);
var globalVariablesNonRecordTypes = globalVariables.Where(vars => vars.GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Record);

bool globalVariablesExists = globalVariablesNonRecordTypes.Count() != 0;
return globalVariablesExists;
}
}
}
17 changes: 16 additions & 1 deletion LinterCop.ruleset.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
{
"id": "LC0027",
"action": "Info",
"justification": "Page.Run(Modal) must be implemented through the Page Management codeunit from the Base Application."
"justification": "Utilize the Page Management codeunit for launching page."
},
{
"id": "LC0028",
Expand All @@ -146,6 +146,21 @@
"id": "LC0029",
"action": "Info",
"justification": "Use CompareDateTime method in Type Helper codeunit for DateTime variable comparisons."
},
{
"id": "LC0030",
"action": "Info",
"justification": "Set Access property to Internal for Install/Upgrade codeunits."
},
{
"id": "LC0031",
"action": "Info",
"justification": "Set ReadIsolation property instead of LockTable method."
},
{
"id": "LC0032",
"action": "Warning",
"justification": "Clear(All) does not affect or change values for global variables in single instance codeunits."
}
]
}
2 changes: 2 additions & 0 deletions LinterCopAnalyzers.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,7 @@ public static class DiagnosticDescriptors
public static readonly DiagnosticDescriptor Rule0028CodeNavigabilityOnEventSubscribers = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0028", (LocalizableString)new LocalizableResourceString("Rule0028CodeNavigabilityOnEventSubscribersTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0028CodeNavigabilityOnEventSubscribersFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0028CodeNavigabilityOnEventSubscribersDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0028");
public static readonly DiagnosticDescriptor Rule0029CompareDateTimeThroughCodeunit = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0029", (LocalizableString)new LocalizableResourceString("Rule0029CompareDateTimeThroughCodeunitTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0029CompareDateTimeThroughCodeunitFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0029CompareDateTimeThroughCodeunitDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0029");
public static readonly DiagnosticDescriptor Rule0030AccessInternalForInstallAndUpgradeCodeunits = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0030", (LocalizableString)new LocalizableResourceString("Rule0030AccessInternalForInstallAndUpgradeCodeunitsTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0030AccessInternalForInstallAndUpgradeCodeunitsFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0030AccessInternalForInstallAndUpgradeCodeunitsDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0030");
public static readonly DiagnosticDescriptor Rule0031RecordInstanceIsolationLevel = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0031", (LocalizableString)new LocalizableResourceString("Rule0031RecordInstanceIsolationLevelTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0031RecordInstanceIsolationLevelFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Info, true, (LocalizableString)new LocalizableResourceString("Rule0031RecordInstanceIsolationLevelDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0031");
public static readonly DiagnosticDescriptor Rule0032ClearCodeunitSingleInstance = new DiagnosticDescriptor(LinterCopAnalyzers.AnalyzerPrefix + "0032", (LocalizableString)new LocalizableResourceString("Rule0032ClearCodeunitSingleInstanceTitle", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), (LocalizableString)new LocalizableResourceString("Rule0032ClearCodeunitSingleInstanceFormat", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "Design", DiagnosticSeverity.Warning, true, (LocalizableString)new LocalizableResourceString("Rule0032ClearCodeunitSingleInstanceDescription", LinterCopAnalyzers.ResourceManager, typeof(LinterCopAnalyzers)), "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0032");
}
}
Loading

0 comments on commit baafaff

Please sign in to comment.