Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 206 additions & 0 deletions src/DataverseAnalyzer/PluginStepConfigurationAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace DataverseAnalyzer;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class PluginStepConfigurationAnalyzer : DiagnosticAnalyzer
{
private static readonly Lazy<DiagnosticDescriptor> LazyRuleFilteredAttributesOnCreate = new(() => new DiagnosticDescriptor(
"CT0008",
Resources.CT0008_Title,
Resources.CT0008_MessageFormat,
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: Resources.CT0008_Description));

private static readonly Lazy<DiagnosticDescriptor> LazyRulePreImageOnCreate = new(() => new DiagnosticDescriptor(
"CT0009",
Resources.CT0009_Title,
Resources.CT0009_MessageFormat,
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: Resources.CT0009_Description));

private static readonly Lazy<DiagnosticDescriptor> LazyRulePostImageOnDelete = new(() => new DiagnosticDescriptor(
"CT0010",
Resources.CT0010_Title,
Resources.CT0010_MessageFormat,
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: Resources.CT0010_Description));

public static DiagnosticDescriptor RuleFilteredAttributesOnCreate => LazyRuleFilteredAttributesOnCreate.Value;

public static DiagnosticDescriptor RulePreImageOnCreate => LazyRulePreImageOnCreate.Value;

public static DiagnosticDescriptor RulePostImageOnDelete => LazyRulePostImageOnDelete.Value;

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
RuleFilteredAttributesOnCreate,
RulePreImageOnCreate,
RulePostImageOnDelete);

public override void Initialize(AnalysisContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}

context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
}

private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
{
var invocation = (InvocationExpressionSyntax)context.Node;

if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
return;

var methodName = memberAccess.Name.Identifier.ValueText;

if (methodName == "AddFilteredAttributes")
AnalyzeAddFilteredAttributes(context, invocation, memberAccess);
else if (methodName == "AddImage")
AnalyzeAddImage(context, invocation, memberAccess);
}

private static void AnalyzeAddFilteredAttributes(
SyntaxNodeAnalysisContext context,
InvocationExpressionSyntax invocation,
MemberAccessExpressionSyntax memberAccess)
{
var operation = FindEventOperation(memberAccess.Expression);

if (operation == "Create")
{
var diagnostic = Diagnostic.Create(
RuleFilteredAttributesOnCreate,
invocation.GetLocation());
context.ReportDiagnostic(diagnostic);
}
}

private static void AnalyzeAddImage(
SyntaxNodeAnalysisContext context,
InvocationExpressionSyntax invocation,
MemberAccessExpressionSyntax memberAccess)
{
var operation = FindEventOperation(memberAccess.Expression);
var imageType = GetImageTypeFromArguments(invocation);

if (operation == "Create" && imageType is "PreImage" or "Both")
{
var diagnostic = Diagnostic.Create(
RulePreImageOnCreate,
invocation.GetLocation(),
imageType);
context.ReportDiagnostic(diagnostic);
}
else if (operation == "Delete" && imageType is "PostImage" or "Both")
{
var diagnostic = Diagnostic.Create(
RulePostImageOnDelete,
invocation.GetLocation(),
imageType);
context.ReportDiagnostic(diagnostic);
}
}

private static string? FindEventOperation(ExpressionSyntax expression)
{
var current = expression;

while (current is not null)
{
if (current is InvocationExpressionSyntax inv)
{
var methodName = GetMethodName(inv);
if (methodName == "RegisterPluginStep")
return GetOperationFromRegisterPluginStep(inv);

current = GetReceiverExpression(inv);
}
else if (current is MemberAccessExpressionSyntax ma)
{
current = ma.Expression;
}
else
{
break;
}
}

return null;
}

private static string? GetMethodName(InvocationExpressionSyntax invocation)
{
return invocation.Expression switch
{
MemberAccessExpressionSyntax ma => ma.Name switch
{
GenericNameSyntax gns => gns.Identifier.ValueText,
IdentifierNameSyntax ins => ins.Identifier.ValueText,
_ => null,
},
IdentifierNameSyntax id => id.Identifier.ValueText,
GenericNameSyntax gn => gn.Identifier.ValueText,
_ => null,
};
}

private static ExpressionSyntax? GetReceiverExpression(InvocationExpressionSyntax invocation)
{
return invocation.Expression switch
{
MemberAccessExpressionSyntax ma => ma.Expression,
_ => null,
};
}

private static string? GetOperationFromRegisterPluginStep(InvocationExpressionSyntax invocation)
{
foreach (var arg in invocation.ArgumentList.Arguments)
{
var argText = arg.Expression.ToString();

if (argText.IndexOf("Create", StringComparison.Ordinal) >= 0)
return "Create";
if (argText.IndexOf("Delete", StringComparison.Ordinal) >= 0)
return "Delete";
if (argText.IndexOf("Update", StringComparison.Ordinal) >= 0)
return "Update";
}

return null;
}

private static string? GetImageTypeFromArguments(InvocationExpressionSyntax invocation)
{
if (invocation.ArgumentList.Arguments.Count == 0)
return null;

var firstArg = invocation.ArgumentList.Arguments[0].Expression;
var argText = firstArg.ToString();

if (argText.IndexOf("PreImage", StringComparison.Ordinal) >= 0)
return "PreImage";
if (argText.IndexOf("PostImage", StringComparison.Ordinal) >= 0)
return "PostImage";
if (argText.IndexOf("Both", StringComparison.Ordinal) >= 0)
return "Both";

return null;
}
}
18 changes: 18 additions & 0 deletions src/DataverseAnalyzer/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions src/DataverseAnalyzer/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,31 @@
<data name="CT0007_Description" xml:space="preserve">
<value>Entity types should use the type-safe ContainsAttributes method with lambda expressions instead of the string-based Contains method to enable compile-time checking of attribute names.</value>
</data>
<data name="CT0008_Title" xml:space="preserve">
<value>AddFilteredAttributes not allowed on Create operations</value>
</data>
<data name="CT0008_MessageFormat" xml:space="preserve">
<value>AddFilteredAttributes cannot be used with Create operations because it has no effect</value>
</data>
<data name="CT0008_Description" xml:space="preserve">
<value>Create operations do not support filtered attributes. It has no effect on whether the step gets triggered</value>
</data>
<data name="CT0009_Title" xml:space="preserve">
<value>PreImage not allowed on Create operations</value>
</data>
<data name="CT0009_MessageFormat" xml:space="preserve">
<value>AddImage with {0} cannot be used with Create operations because no previous record state exists</value>
</data>
<data name="CT0009_Description" xml:space="preserve">
<value>Create operations cannot have pre-images because the record does not exist before the create operation. ImageType.Both is also invalid as it includes PreImage.</value>
</data>
<data name="CT0010_Title" xml:space="preserve">
<value>PostImage not allowed on Delete operations</value>
</data>
<data name="CT0010_MessageFormat" xml:space="preserve">
<value>AddImage with {0} cannot be used with Delete operations because no record state exists after deletion</value>
</data>
<data name="CT0010_Description" xml:space="preserve">
<value>Delete operations cannot have post-images because the record no longer exists after the delete operation. ImageType.Both is also invalid as it includes PostImage.</value>
</data>
</root>
Loading