Skip to content

Commit

Permalink
Merge pull request #423 from StefanMaron/prerelease
Browse files Browse the repository at this point in the history
Merge from 'Prerelease' into master
  • Loading branch information
Arthurvdv authored Dec 12, 2023
2 parents 66c993d + e50a7c8 commit 68ee6a3
Show file tree
Hide file tree
Showing 9 changed files with 313 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,22 @@
namespace BusinessCentral.LinterCop.Design
{
[DiagnosticAnalyzer]
public class Rule0039PageRunTableMismatch : DiagnosticAnalyzer
public class Rule0039ArgumentDifferentTypeThenExpected : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create<DiagnosticDescriptor>(DiagnosticDescriptors.Rule0039ArgumentDifferentTypeThenExpected);

private static readonly List<PropertyKind> referencePageProviders = new List<PropertyKind>
{
PropertyKind.LookupPageId,
PropertyKind.DrillDownPageId
};

public override void Initialize(AnalysisContext context)
{
context.RegisterOperationAction(new Action<OperationAnalysisContext>(this.AnalyzeRunPageArguments), OperationKind.InvocationExpression);
context.RegisterOperationAction(new Action<OperationAnalysisContext>(this.AnalyzeSetRecordArgument), OperationKind.InvocationExpression);
context.RegisterSymbolAction(new Action<SymbolAnalysisContext>(this.AnalyzeTableReferencePageProvider), SymbolKind.Table);
context.RegisterSymbolAction(new Action<SymbolAnalysisContext>(this.AnalyzeTableExtensionReferencePageProvider), SymbolKind.TableExtension);
}

private void AnalyzeRunPageArguments(OperationAnalysisContext ctx)
Expand Down Expand Up @@ -74,6 +82,43 @@ private void AnalyzeSetRecordArgument(OperationAnalysisContext ctx)
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0039ArgumentDifferentTypeThenExpected, ctx.Operation.Syntax.GetLocation(), new object[] { 1, operand.GetSymbol().GetTypeSymbol().ToString(), pageSourceTable.GetNavTypeKindSafe() + " \"" + pageSourceTable.Name + "\"" }));
}

private void AnalyzeTableReferencePageProvider(SymbolAnalysisContext ctx)
{
if (ctx.Symbol.IsObsoletePending || ctx.Symbol.IsObsoleteRemoved) return;

ITableTypeSymbol table = (ITableTypeSymbol)ctx.Symbol;
foreach (PropertyKind propertyKind in referencePageProviders)
{
IPropertySymbol pageReference = table.GetProperty(propertyKind);
if (pageReference == null) continue;
IPageTypeSymbol page = (IPageTypeSymbol)pageReference.Value;
ITableTypeSymbol pageSourceTable = page.RelatedTable;
if (pageSourceTable == null) continue;

if (!AreTheSameNavObjects(table, pageSourceTable))
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0039ArgumentDifferentTypeThenExpected, pageReference.GetLocation(), new object[] { 1, table.GetTypeSymbol().GetNavTypeKindSafe() + " \"" + table.Name + "\"", pageSourceTable.GetNavTypeKindSafe() + " \"" + pageSourceTable.Name + "\"" }));
}
}

private void AnalyzeTableExtensionReferencePageProvider(SymbolAnalysisContext ctx)
{
if (ctx.Symbol.IsObsoletePending || ctx.Symbol.IsObsoleteRemoved) return;

ITableExtensionTypeSymbol tableExtension = (ITableExtensionTypeSymbol)ctx.Symbol;
ITableTypeSymbol table = (ITableTypeSymbol)tableExtension.Target;
foreach (PropertyKind propertyKind in referencePageProviders)
{
IPropertySymbol pageReference = tableExtension.GetProperty(propertyKind);
if (pageReference == null) continue;
IPageTypeSymbol page = (IPageTypeSymbol)pageReference.Value;
ITableTypeSymbol pageSourceTable = page.RelatedTable;
if (pageSourceTable == null) continue;

if (!AreTheSameNavObjects(table, pageSourceTable))
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0039ArgumentDifferentTypeThenExpected, pageReference.GetLocation(), new object[] { 1, table.GetTypeSymbol().GetNavTypeKindSafe() + " \"" + table.Name + "\"", pageSourceTable.GetNavTypeKindSafe() + " \"" + pageSourceTable.Name + "\"" }));
}
}

private static bool AreTheSameNavObjects(ITableTypeSymbol left, ITableTypeSymbol right)
{
if (left.GetNavTypeKindSafe() != right.GetNavTypeKindSafe()) return false;
Expand Down
38 changes: 38 additions & 0 deletions Design/Rule0040ExplicitlySetRunTrigger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Collections.Immutable;
using Microsoft.Dynamics.Nav.CodeAnalysis;
using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics;
using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols;

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

private static readonly List<string> buildInMethodNames = new List<string>
{
"insert",
"modify",
"modifyall",
"delete",
"deleteall"
};

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

private void AnalyzeRunTriggerParameters(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.Instance?.GetSymbol().GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.Record || operation.Instance?.GetSymbol().GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.RecordRef)) return;
if (!buildInMethodNames.Contains(operation.TargetMethod.Name.ToLowerInvariant())) return;

if (operation.Arguments.Where(args => SemanticFacts.IsSameName(args.Parameter.Name, "RunTrigger")).SingleOrDefault() == null)
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0040ExplicitlySetRunTrigger, ctx.Operation.Syntax.GetLocation()));
}
}
}
62 changes: 62 additions & 0 deletions Design/Rule0041EmptyCaptionLocked.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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
{
[DiagnosticAnalyzer]
public class Rule0041EmptyCaptionLocked : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.Rule0041EmptyCaptionLocked);

// List based on https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/properties/devenv-caption-property
public override void Initialize(AnalysisContext context)
=> context.RegisterSyntaxNodeAction(new Action<SyntaxNodeAnalysisContext>(AnalyzeCaptionProperty), new SyntaxKind[] {
SyntaxKind.TableObject,
SyntaxKind.Field, // TableField
SyntaxKind.PageField,
SyntaxKind.PageGroup,
SyntaxKind.PageObject,
SyntaxKind.RequestPage,
SyntaxKind.PageLabel,
SyntaxKind.PageGroup,
SyntaxKind.PagePart,
SyntaxKind.PageSystemPart,
SyntaxKind.PageAction,
SyntaxKind.PageActionSeparator,
SyntaxKind.PageActionGroup,
SyntaxKind.XmlPortObject,
SyntaxKind.ReportObject,
SyntaxKind.QueryObject,
SyntaxKind.QueryColumn,
SyntaxKind.QueryFilter,
SyntaxKind.ReportColumn,
SyntaxKind.EnumValue,
SyntaxKind.PageCustomAction,
SyntaxKind.PageSystemAction,
SyntaxKind.PageView,
SyntaxKind.ReportLayout,
SyntaxKind.ProfileObject,
SyntaxKind.EnumType,
SyntaxKind.PermissionSet,
SyntaxKind.TableExtensionObject,
SyntaxKind.PageExtensionObject
});

private void AnalyzeCaptionProperty(SyntaxNodeAnalysisContext ctx)
{
if (ctx.ContainingSymbol.IsObsoletePending || ctx.ContainingSymbol.IsObsoleteRemoved) return;
if (ctx.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoletePending || ctx.ContainingSymbol.GetContainingObjectTypeSymbol().IsObsoleteRemoved) return;

if (ctx.Node.IsKind(SyntaxKind.EnumValue) && ctx.ContainingSymbol.Kind == SymbolKind.Enum) return; // Prevent double raising the rule on EnumValue in a EnumObject

LabelPropertyValueSyntax captionProperty = ctx.Node?.GetProperty("Caption")?.Value as LabelPropertyValueSyntax;
if (captionProperty?.Value.LabelText.GetLiteralValue() == null || captionProperty.Value.LabelText.GetLiteralValue().ToString().Trim() != "") return;

if (captionProperty.Value.Properties?.Values.Where(prop => prop.Identifier.Text.ToLowerInvariant() == "locked").FirstOrDefault() != null) return;

ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0041EmptyCaptionLocked, captionProperty.GetLocation()));
}
}
}
29 changes: 29 additions & 0 deletions Design/Rule0042AutoCalcFieldsOnNormalFields.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Microsoft.Dynamics.Nav.CodeAnalysis;
using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;

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

public override void Initialize(AnalysisContext context) => context.RegisterSyntaxNodeAction(syntaxContext =>
{
if (!syntaxContext.Node.ToString().ToLowerInvariant().Contains("setautocalcfields"))
return;

IInvocationExpression operation = (IInvocationExpression)syntaxContext.SemanticModel.GetOperation(syntaxContext.Node);
IMethodSymbol targetMethod = operation.TargetMethod;
if (targetMethod == null || !SemanticFacts.IsSameName(targetMethod.Name, "setautocalcfields") || targetMethod.MethodKind != MethodKind.BuiltInMethod)
return;

foreach (IArgument obj in operation.Arguments)
{
if ((obj.Value is IConversionExpression conversionExpression2 ? conversionExpression2.Operand : (IOperation)null) is IFieldAccess fieldAccess2 && fieldAccess2.FieldSymbol.FieldClass != FieldClassKind.FlowField && fieldAccess2.Type.NavTypeKind != NavTypeKind.Blob)
syntaxContext.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0042AutoCalcFieldsOnNormalFields, fieldAccess2.Syntax.GetLocation(), (object)fieldAccess2.FieldSymbol.Name));
}
}, SyntaxKind.InvocationExpression);
}
}
74 changes: 74 additions & 0 deletions Design/Rule0043SecretText.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System.Collections.Immutable;
using Microsoft.Dynamics.Nav.CodeAnalysis;
using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics;
using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols;

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

private static readonly string authorization = "Authorization";
private static readonly List<string> buildInMethodNames = new List<string>
{
"add",
"getvalues",
"tryaddwithoutvalidation"
};

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

private void AnalyzeHttpObjects(OperationAnalysisContext ctx)
{
if (!VersionChecker.IsSupported(ctx.ContainingSymbol, VersionCompatibility.Fall2023OrGreater)) return;

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

IInvocationExpression operation = (IInvocationExpression)ctx.Operation;

// We need at least two arguments
if (operation.Arguments.Count() < 2) return;

switch (operation.TargetMethod.MethodKind)
{
case MethodKind.BuiltInMethod:
if (!buildInMethodNames.Contains(operation.TargetMethod.Name.ToLowerInvariant())) return;
if (!(operation.Instance?.GetSymbol().GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.HttpHeaders || operation.Instance?.GetSymbol().GetTypeSymbol().GetNavTypeKindSafe() == NavTypeKind.HttpClient)) return;
break;
case MethodKind.Method:
if (operation.TargetMethod.ContainingType.GetNavTypeKindSafe() != NavTypeKind.Codeunit) return;
ICodeunitTypeSymbol codeunitTypeSymbol = (ICodeunitTypeSymbol)operation.TargetMethod.GetContainingObjectTypeSymbol();
if (!SemanticFacts.IsSameName(((INamespaceSymbol)codeunitTypeSymbol.ContainingSymbol).QualifiedName, "System.RestClient")) return;
if (!SemanticFacts.IsSameName(codeunitTypeSymbol.Name, "Rest Client")) return;
if (!SemanticFacts.IsSameName(operation.TargetMethod.Name, "SetDefaultRequestHeader")) return;
break;
default:
return;
}

if (!IsAuthorizationArgument(operation.Arguments[0])) return;

if (operation.Arguments[1].Parameter.OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.SecretText)
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.Rule0043SecretText, ctx.Operation.Syntax.GetLocation()));
}

private static bool IsAuthorizationArgument(IArgument argument)
{
switch (argument.Syntax.Kind)
{
case SyntaxKind.LiteralExpression:
return SemanticFacts.IsSameName(argument.Value.ConstantValue.Value.ToString(), authorization);
case SyntaxKind.IdentifierName:
IOperation operand = ((IConversionExpression)argument.Value).Operand;
if (operand.GetSymbol().OriginalDefinition.GetTypeSymbol().GetNavTypeKindSafe() != NavTypeKind.Label) return false;
ILabelTypeSymbol label = (ILabelTypeSymbol)operand.GetSymbol().OriginalDefinition.GetTypeSymbol();
return SemanticFacts.IsSameName(label.GetLabelText(), authorization);
default:
return false;
}
}
}
}
20 changes: 20 additions & 0 deletions LinterCop.ruleset.json
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,26 @@
"id": "LC0039",
"action": "Warning",
"justification": "The given argument has a different type from the one expected."
},
{
"id": "LC0040",
"action": "Info",
"justification": "Explicitly set the RunTrigger parameter on build-in methods."
},
{
"id": "LC0041",
"action": "Info",
"justification": "Empty Captions should be Locked."
},
{
"id": "LC0042",
"action": "Warning",
"justification": "AutoCalcFields should only be used for FlowFields or Blob fields."
},
{
"id": "LC0043",
"action": "Info",
"justification": "Use SecretText type to protect credentials and sensitive textual values from being revealed."
}
]
}
Loading

0 comments on commit 68ee6a3

Please sign in to comment.