@@ -16,6 +16,7 @@ public sealed class PluginDependencyAnalyzer : DiagnosticAnalyzer
1616 public const string HasSetterOrInitId = "XrmTools002" ;
1717 public const string PluginAttributeCombinationId = "XrmTools003" ;
1818 public const string PluginAttributeOrderId = "XrmTools004" ;
19+ public const string InvalidStepStageModeId = "XrmTools006" ;
1920
2021 private static readonly DiagnosticDescriptor MissingDependencyAttributeRule =
2122 new (
@@ -61,8 +62,19 @@ public sealed class PluginDependencyAnalyzer : DiagnosticAnalyzer
6162 description : "[Plugin] must appear before [Step]/[CustomApi]. For step plugins, [Image] attributes must appear after [Step]." ,
6263 helpLinkUri : "https://github.com/rezanid/xrmtools/wiki/Analyzers#xrmtools004---invalid-plugin-attribute-order" ) ;
6364
65+ private static readonly DiagnosticDescriptor InvalidStepStageModeRule =
66+ new (
67+ InvalidStepStageModeId ,
68+ "Step attribute has invalid stage and mode combination" ,
69+ "Step attribute cannot use ExecutionMode.Asynchronous with {0}" ,
70+ "Usage" ,
71+ DiagnosticSeverity . Warning ,
72+ isEnabledByDefault : true ,
73+ description : "Dataverse plug-in steps registered in PreValidation or PreOperation must run synchronously." ,
74+ helpLinkUri : "https://github.com/rezanid/xrmtools/wiki/Analyzers#xrmtools006---invalid-step-stage-and-mode" ) ;
75+
6476 public override ImmutableArray < DiagnosticDescriptor > SupportedDiagnostics =>
65- [ MissingDependencyAttributeRule , HasSetterOrInitRule , PluginAttributeCombinationRule , PluginAttributeOrderRule ] ;
77+ [ MissingDependencyAttributeRule , HasSetterOrInitRule , PluginAttributeCombinationRule , PluginAttributeOrderRule , InvalidStepStageModeRule ] ;
6678
6779 public override void Initialize ( AnalysisContext context )
6880 {
@@ -87,13 +99,15 @@ public override void Initialize(AnalysisContext context)
8799 var customApiAttrSymbol = compilationStart . Compilation . GetTypeByMetadataName ( "XrmTools.Meta.Attributes.CustomApiAttribute" ) ;
88100 var stepAttrSymbol = compilationStart . Compilation . GetTypeByMetadataName ( "XrmTools.Meta.Attributes.StepAttribute" ) ;
89101 var imageAttrSymbol = compilationStart . Compilation . GetTypeByMetadataName ( "XrmTools.Meta.Attributes.ImageAttribute" ) ;
102+ var stageEnumSymbol = compilationStart . Compilation . GetTypeByMetadataName ( "XrmTools.Meta.Attributes.Stages" ) ;
103+ var executionModeEnumSymbol = compilationStart . Compilation . GetTypeByMetadataName ( "XrmTools.Meta.Attributes.ExecutionMode" ) ;
90104
91105 compilationStart . RegisterSyntaxNodeAction (
92106 c => AnalyzeProperty ( c , pluginSymbol , dependencyAttrSymbol ) ,
93107 SyntaxKind . PropertyDeclaration ) ;
94108
95109 compilationStart . RegisterSymbolAction (
96- c => AnalyzePluginTypeAttributes ( c , pluginSymbol , pluginAttrSymbol , customApiAttrSymbol , stepAttrSymbol , imageAttrSymbol ) ,
110+ c => AnalyzePluginTypeAttributes ( c , pluginSymbol , pluginAttrSymbol , customApiAttrSymbol , stepAttrSymbol , imageAttrSymbol , stageEnumSymbol , executionModeEnumSymbol ) ,
97111 SymbolKind . NamedType ) ;
98112 } ) ;
99113 }
@@ -104,7 +118,9 @@ private static void AnalyzePluginTypeAttributes(
104118 INamedTypeSymbol ? pluginAttrSymbol ,
105119 INamedTypeSymbol ? customApiAttrSymbol ,
106120 INamedTypeSymbol ? stepAttrSymbol ,
107- INamedTypeSymbol ? imageAttrSymbol )
121+ INamedTypeSymbol ? imageAttrSymbol ,
122+ INamedTypeSymbol ? stageEnumSymbol ,
123+ INamedTypeSymbol ? executionModeEnumSymbol )
108124 {
109125 if ( pluginAttrSymbol is null || customApiAttrSymbol is null || stepAttrSymbol is null )
110126 return ;
@@ -142,6 +158,8 @@ private static void AnalyzePluginTypeAttributes(
142158 // Keep going; XOR rule may also be violated and should be reported too.
143159 }
144160
161+ ReportInvalidStepStageMode ( context , typeSymbol , attrs , stepAttrSymbol , stageEnumSymbol , executionModeEnumSymbol ) ;
162+
145163 if ( hasCustomApi == hasStep )
146164 {
147165 var location = typeSymbol . Locations . FirstOrDefault ( ) ;
@@ -209,6 +227,124 @@ private static bool HasOrderIssue(
209227 return false ;
210228 }
211229
230+ private static void ReportInvalidStepStageMode (
231+ SymbolAnalysisContext context ,
232+ INamedTypeSymbol typeSymbol ,
233+ ImmutableArray < AttributeData > attributes ,
234+ INamedTypeSymbol ? stepAttrSymbol ,
235+ INamedTypeSymbol ? stageEnumSymbol ,
236+ INamedTypeSymbol ? executionModeEnumSymbol )
237+ {
238+ if ( stepAttrSymbol is null || stageEnumSymbol is null || executionModeEnumSymbol is null )
239+ return ;
240+
241+ for ( int i = 0 ; i < attributes . Length ; i ++ )
242+ {
243+ var attribute = attributes [ i ] ;
244+ if ( ! SymbolEqualityComparer . Default . Equals ( attribute . AttributeClass , stepAttrSymbol ) )
245+ continue ;
246+
247+ if ( ! TryGetStepStageAndMode ( attribute , stageEnumSymbol , executionModeEnumSymbol , out var stage , out var mode ) )
248+ continue ;
249+
250+ if ( mode != 1 || ( stage != 10 && stage != 20 ) )
251+ continue ;
252+
253+ var location = attribute . ApplicationSyntaxReference ? . GetSyntax ( context . CancellationToken ) . GetLocation ( )
254+ ?? typeSymbol . Locations . FirstOrDefault ( ) ;
255+ if ( location is null )
256+ continue ;
257+
258+ context . ReportDiagnostic ( Diagnostic . Create (
259+ InvalidStepStageModeRule ,
260+ location ,
261+ GetStageDisplayName ( stage ) ) ) ;
262+ }
263+ }
264+
265+ private static bool TryGetStepStageAndMode (
266+ AttributeData attribute ,
267+ INamedTypeSymbol stageEnumSymbol ,
268+ INamedTypeSymbol executionModeEnumSymbol ,
269+ out int stage ,
270+ out int mode )
271+ {
272+ stage = default ;
273+ mode = default ;
274+
275+ var hasStage = false ;
276+ var hasMode = false ;
277+ var constructor = attribute . AttributeConstructor ;
278+
279+ if ( constructor is not null )
280+ {
281+ var parameterCount = constructor . Parameters . Length ;
282+ var argumentCount = attribute . ConstructorArguments . Length ;
283+ var count = parameterCount < argumentCount ? parameterCount : argumentCount ;
284+
285+ for ( int i = 0 ; i < count ; i ++ )
286+ {
287+ var parameter = constructor . Parameters [ i ] ;
288+ var argument = attribute . ConstructorArguments [ i ] ;
289+
290+ if ( ! TryGetEnumValue ( argument , out var value ) )
291+ continue ;
292+
293+ if ( ! hasStage && SymbolEqualityComparer . Default . Equals ( parameter . Type , stageEnumSymbol ) )
294+ {
295+ stage = value ;
296+ hasStage = true ;
297+ continue ;
298+ }
299+
300+ if ( ! hasMode && SymbolEqualityComparer . Default . Equals ( parameter . Type , executionModeEnumSymbol ) )
301+ {
302+ mode = value ;
303+ hasMode = true ;
304+ }
305+ }
306+ }
307+
308+ for ( int i = 0 ; i < attribute . NamedArguments . Length ; i ++ )
309+ {
310+ var namedArgument = attribute . NamedArguments [ i ] ;
311+ if ( namedArgument . Key != "Mode" )
312+ continue ;
313+
314+ if ( ! SymbolEqualityComparer . Default . Equals ( namedArgument . Value . Type , executionModeEnumSymbol ) )
315+ continue ;
316+
317+ if ( ! TryGetEnumValue ( namedArgument . Value , out mode ) )
318+ continue ;
319+
320+ hasMode = true ;
321+ }
322+
323+ return hasStage && hasMode ;
324+ }
325+
326+ private static bool TryGetEnumValue ( TypedConstant value , out int intValue )
327+ {
328+ if ( value . Value is int typedValue )
329+ {
330+ intValue = typedValue ;
331+ return true ;
332+ }
333+
334+ intValue = default ;
335+ return false ;
336+ }
337+
338+ private static string GetStageDisplayName ( int stage )
339+ {
340+ return stage switch
341+ {
342+ 10 => "Stages.PreValidation" ,
343+ 20 => "Stages.PreOperation" ,
344+ _ => $ "stage value { stage } ",
345+ } ;
346+ }
347+
212348 private static void AnalyzeProperty (
213349 SyntaxNodeAnalysisContext context ,
214350 INamedTypeSymbol pluginSymbol ,
@@ -314,7 +450,7 @@ private static bool ContainsRequireInvocation(
314450 return false ;
315451 }
316452
317- private static bool HasSetterOrInit ( PropertyDeclarationSyntax propDecl , out Location location )
453+ private static bool HasSetterOrInit ( PropertyDeclarationSyntax propDecl , out Location ? location )
318454 {
319455 location = null ;
320456
0 commit comments