Skip to content

Commit 20f38d6

Browse files
authored
Merge pull request #20 from delegateas/pluginstepanalyzers
Add diagnostic analyzer for plugin step configuration rules
2 parents 9e028ac + da16f6f commit 20f38d6

4 files changed

Lines changed: 665 additions & 0 deletions

File tree

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
7+
namespace DataverseAnalyzer;
8+
9+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
10+
public sealed class PluginStepConfigurationAnalyzer : DiagnosticAnalyzer
11+
{
12+
private static readonly Lazy<DiagnosticDescriptor> LazyRuleFilteredAttributesOnCreate = new(() => new DiagnosticDescriptor(
13+
"CT0008",
14+
Resources.CT0008_Title,
15+
Resources.CT0008_MessageFormat,
16+
"Usage",
17+
DiagnosticSeverity.Error,
18+
isEnabledByDefault: true,
19+
description: Resources.CT0008_Description));
20+
21+
private static readonly Lazy<DiagnosticDescriptor> LazyRulePreImageOnCreate = new(() => new DiagnosticDescriptor(
22+
"CT0009",
23+
Resources.CT0009_Title,
24+
Resources.CT0009_MessageFormat,
25+
"Usage",
26+
DiagnosticSeverity.Error,
27+
isEnabledByDefault: true,
28+
description: Resources.CT0009_Description));
29+
30+
private static readonly Lazy<DiagnosticDescriptor> LazyRulePostImageOnDelete = new(() => new DiagnosticDescriptor(
31+
"CT0010",
32+
Resources.CT0010_Title,
33+
Resources.CT0010_MessageFormat,
34+
"Usage",
35+
DiagnosticSeverity.Error,
36+
isEnabledByDefault: true,
37+
description: Resources.CT0010_Description));
38+
39+
public static DiagnosticDescriptor RuleFilteredAttributesOnCreate => LazyRuleFilteredAttributesOnCreate.Value;
40+
41+
public static DiagnosticDescriptor RulePreImageOnCreate => LazyRulePreImageOnCreate.Value;
42+
43+
public static DiagnosticDescriptor RulePostImageOnDelete => LazyRulePostImageOnDelete.Value;
44+
45+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
46+
RuleFilteredAttributesOnCreate,
47+
RulePreImageOnCreate,
48+
RulePostImageOnDelete);
49+
50+
public override void Initialize(AnalysisContext context)
51+
{
52+
if (context is null)
53+
{
54+
throw new ArgumentNullException(nameof(context));
55+
}
56+
57+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
58+
context.EnableConcurrentExecution();
59+
60+
context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
61+
}
62+
63+
private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
64+
{
65+
var invocation = (InvocationExpressionSyntax)context.Node;
66+
67+
if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
68+
return;
69+
70+
var methodName = memberAccess.Name.Identifier.ValueText;
71+
72+
if (methodName == "AddFilteredAttributes")
73+
AnalyzeAddFilteredAttributes(context, invocation, memberAccess);
74+
else if (methodName == "AddImage")
75+
AnalyzeAddImage(context, invocation, memberAccess);
76+
}
77+
78+
private static void AnalyzeAddFilteredAttributes(
79+
SyntaxNodeAnalysisContext context,
80+
InvocationExpressionSyntax invocation,
81+
MemberAccessExpressionSyntax memberAccess)
82+
{
83+
var operation = FindEventOperation(memberAccess.Expression);
84+
85+
if (operation == "Create")
86+
{
87+
var diagnostic = Diagnostic.Create(
88+
RuleFilteredAttributesOnCreate,
89+
invocation.GetLocation());
90+
context.ReportDiagnostic(diagnostic);
91+
}
92+
}
93+
94+
private static void AnalyzeAddImage(
95+
SyntaxNodeAnalysisContext context,
96+
InvocationExpressionSyntax invocation,
97+
MemberAccessExpressionSyntax memberAccess)
98+
{
99+
var operation = FindEventOperation(memberAccess.Expression);
100+
var imageType = GetImageTypeFromArguments(invocation);
101+
102+
if (operation == "Create" && imageType is "PreImage" or "Both")
103+
{
104+
var diagnostic = Diagnostic.Create(
105+
RulePreImageOnCreate,
106+
invocation.GetLocation(),
107+
imageType);
108+
context.ReportDiagnostic(diagnostic);
109+
}
110+
else if (operation == "Delete" && imageType is "PostImage" or "Both")
111+
{
112+
var diagnostic = Diagnostic.Create(
113+
RulePostImageOnDelete,
114+
invocation.GetLocation(),
115+
imageType);
116+
context.ReportDiagnostic(diagnostic);
117+
}
118+
}
119+
120+
private static string? FindEventOperation(ExpressionSyntax expression)
121+
{
122+
var current = expression;
123+
124+
while (current is not null)
125+
{
126+
if (current is InvocationExpressionSyntax inv)
127+
{
128+
var methodName = GetMethodName(inv);
129+
if (methodName == "RegisterPluginStep")
130+
return GetOperationFromRegisterPluginStep(inv);
131+
132+
current = GetReceiverExpression(inv);
133+
}
134+
else if (current is MemberAccessExpressionSyntax ma)
135+
{
136+
current = ma.Expression;
137+
}
138+
else
139+
{
140+
break;
141+
}
142+
}
143+
144+
return null;
145+
}
146+
147+
private static string? GetMethodName(InvocationExpressionSyntax invocation)
148+
{
149+
return invocation.Expression switch
150+
{
151+
MemberAccessExpressionSyntax ma => ma.Name switch
152+
{
153+
GenericNameSyntax gns => gns.Identifier.ValueText,
154+
IdentifierNameSyntax ins => ins.Identifier.ValueText,
155+
_ => null,
156+
},
157+
IdentifierNameSyntax id => id.Identifier.ValueText,
158+
GenericNameSyntax gn => gn.Identifier.ValueText,
159+
_ => null,
160+
};
161+
}
162+
163+
private static ExpressionSyntax? GetReceiverExpression(InvocationExpressionSyntax invocation)
164+
{
165+
return invocation.Expression switch
166+
{
167+
MemberAccessExpressionSyntax ma => ma.Expression,
168+
_ => null,
169+
};
170+
}
171+
172+
private static string? GetOperationFromRegisterPluginStep(InvocationExpressionSyntax invocation)
173+
{
174+
foreach (var arg in invocation.ArgumentList.Arguments)
175+
{
176+
var argText = arg.Expression.ToString();
177+
178+
if (argText.IndexOf("Create", StringComparison.Ordinal) >= 0)
179+
return "Create";
180+
if (argText.IndexOf("Delete", StringComparison.Ordinal) >= 0)
181+
return "Delete";
182+
if (argText.IndexOf("Update", StringComparison.Ordinal) >= 0)
183+
return "Update";
184+
}
185+
186+
return null;
187+
}
188+
189+
private static string? GetImageTypeFromArguments(InvocationExpressionSyntax invocation)
190+
{
191+
if (invocation.ArgumentList.Arguments.Count == 0)
192+
return null;
193+
194+
var firstArg = invocation.ArgumentList.Arguments[0].Expression;
195+
var argText = firstArg.ToString();
196+
197+
if (argText.IndexOf("PreImage", StringComparison.Ordinal) >= 0)
198+
return "PreImage";
199+
if (argText.IndexOf("PostImage", StringComparison.Ordinal) >= 0)
200+
return "PostImage";
201+
if (argText.IndexOf("Both", StringComparison.Ordinal) >= 0)
202+
return "Both";
203+
204+
return null;
205+
}
206+
}

src/DataverseAnalyzer/Resources.Designer.cs

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/DataverseAnalyzer/Resources.resx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,31 @@
127127
<data name="CT0007_Description" xml:space="preserve">
128128
<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>
129129
</data>
130+
<data name="CT0008_Title" xml:space="preserve">
131+
<value>AddFilteredAttributes not allowed on Create operations</value>
132+
</data>
133+
<data name="CT0008_MessageFormat" xml:space="preserve">
134+
<value>AddFilteredAttributes cannot be used with Create operations because it has no effect</value>
135+
</data>
136+
<data name="CT0008_Description" xml:space="preserve">
137+
<value>Create operations do not support filtered attributes. It has no effect on whether the step gets triggered</value>
138+
</data>
139+
<data name="CT0009_Title" xml:space="preserve">
140+
<value>PreImage not allowed on Create operations</value>
141+
</data>
142+
<data name="CT0009_MessageFormat" xml:space="preserve">
143+
<value>AddImage with {0} cannot be used with Create operations because no previous record state exists</value>
144+
</data>
145+
<data name="CT0009_Description" xml:space="preserve">
146+
<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>
147+
</data>
148+
<data name="CT0010_Title" xml:space="preserve">
149+
<value>PostImage not allowed on Delete operations</value>
150+
</data>
151+
<data name="CT0010_MessageFormat" xml:space="preserve">
152+
<value>AddImage with {0} cannot be used with Delete operations because no record state exists after deletion</value>
153+
</data>
154+
<data name="CT0010_Description" xml:space="preserve">
155+
<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>
156+
</data>
130157
</root>

0 commit comments

Comments
 (0)