Skip to content

Commit 6517810

Browse files
authored
Merge pull request #164 from rezanid/157-fix-exception-missing-in-logs
Do not allow async+preoperation or prevalidation combos
2 parents c14d6d9 + de0c37b commit 6517810

8 files changed

Lines changed: 286 additions & 16 deletions

File tree

src/Analyzers/XrmTools.Analyzers.Test/XrmToolsAnalyzersUnitTests.cs

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,28 @@ public sealed class PluginAttribute : System.Attribute { }
3030
[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
3131
public sealed class CustomApiAttribute : System.Attribute { }
3232
33+
public enum Stages
34+
{
35+
PreValidation = 10,
36+
PreOperation = 20,
37+
MainOperation = 30,
38+
PostOperation = 40,
39+
}
40+
41+
public enum ExecutionMode
42+
{
43+
Synchronous = 0,
44+
Asynchronous = 1,
45+
}
46+
3347
[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
34-
public sealed class StepAttribute : System.Attribute { }
48+
public sealed class StepAttribute : System.Attribute
49+
{
50+
public StepAttribute(string messageName, Stages stage, ExecutionMode mode) { }
51+
public StepAttribute(string messageName, string primaryEntityName, Stages stage, ExecutionMode mode) { }
52+
public StepAttribute(string messageName, string primaryEntityName, string filteringAttributes, Stages stage, ExecutionMode mode) { }
53+
public ExecutionMode Mode { get; set; }
54+
}
3555
3656
[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
3757
public sealed class ImageAttribute : System.Attribute { }
@@ -273,7 +293,7 @@ public sealed class MyPlugin : PluginBase
273293
public async Task Plugin_PluginAttributeMustComeBeforeStep_ReportsXrmTools004()
274294
{
275295
var test = Preamble + @"
276-
[XrmTools.Meta.Attributes.Step]
296+
[XrmTools.Meta.Attributes.Step(""Create"", XrmTools.Meta.Attributes.Stages.PostOperation, XrmTools.Meta.Attributes.ExecutionMode.Synchronous)]
277297
[XrmTools.Meta.Attributes.Plugin]
278298
public sealed class {|#0:MyPlugin|} : PluginBase
279299
{
@@ -311,7 +331,7 @@ public async Task Plugin_ImageBeforeStep_ReportsXrmTools004()
311331
var test = Preamble + @"
312332
[XrmTools.Meta.Attributes.Plugin]
313333
[XrmTools.Meta.Attributes.Image]
314-
[XrmTools.Meta.Attributes.Step]
334+
[XrmTools.Meta.Attributes.Step(""Create"", XrmTools.Meta.Attributes.Stages.PostOperation, XrmTools.Meta.Attributes.ExecutionMode.Synchronous)]
315335
public sealed class {|#0:MyPlugin|} : PluginBase
316336
{
317337
}
@@ -329,7 +349,7 @@ public async Task Plugin_ImageAfterStep_IsAllowed()
329349
{
330350
var test = Preamble + @"
331351
[XrmTools.Meta.Attributes.Plugin]
332-
[XrmTools.Meta.Attributes.Step]
352+
[XrmTools.Meta.Attributes.Step(""Create"", XrmTools.Meta.Attributes.Stages.PostOperation, XrmTools.Meta.Attributes.ExecutionMode.Synchronous)]
333353
[XrmTools.Meta.Attributes.Image]
334354
public sealed class MyPlugin : PluginBase
335355
{
@@ -344,13 +364,99 @@ public async Task Plugin_MultipleStepsAndImages_ImageAfterFirstStep_IsAllowed()
344364
{
345365
var test = Preamble + @"
346366
[XrmTools.Meta.Attributes.Plugin]
347-
[XrmTools.Meta.Attributes.Step]
367+
[XrmTools.Meta.Attributes.Step(""Create"", XrmTools.Meta.Attributes.Stages.PostOperation, XrmTools.Meta.Attributes.ExecutionMode.Synchronous)]
348368
[XrmTools.Meta.Attributes.Image]
349-
[XrmTools.Meta.Attributes.Step]
369+
[XrmTools.Meta.Attributes.Step(""Update"", ""account"", XrmTools.Meta.Attributes.Stages.PostOperation, XrmTools.Meta.Attributes.ExecutionMode.Synchronous)]
350370
[XrmTools.Meta.Attributes.Image]
351371
public sealed class MyPlugin : PluginBase
352372
{
353373
}
374+
";
375+
376+
await VerifyCS.VerifyAnalyzerAsync(test);
377+
}
378+
379+
[Fact]
380+
public async Task Plugin_PreValidationAsyncStep_ReportsXrmTools006()
381+
{
382+
var test = Preamble + @"
383+
[XrmTools.Meta.Attributes.Plugin]
384+
[{|#0:XrmTools.Meta.Attributes.Step(""Create"", XrmTools.Meta.Attributes.Stages.PreValidation, XrmTools.Meta.Attributes.ExecutionMode.Asynchronous)|}]
385+
public sealed class MyPlugin : PluginBase
386+
{
387+
}
388+
";
389+
390+
var expected = new DiagnosticResult(PluginDependencyAnalyzer.InvalidStepStageModeId, DiagnosticSeverity.Warning)
391+
.WithLocation(0)
392+
.WithArguments("Stages.PreValidation");
393+
394+
await VerifyCS.VerifyAnalyzerAsync(test, expected);
395+
}
396+
397+
[Fact]
398+
public async Task Plugin_PreOperationAsyncStep_WithPrimaryEntityConstructor_ReportsXrmTools006()
399+
{
400+
var test = Preamble + @"
401+
[XrmTools.Meta.Attributes.Plugin]
402+
[{|#0:XrmTools.Meta.Attributes.Step(""Update"", ""account"", XrmTools.Meta.Attributes.Stages.PreOperation, XrmTools.Meta.Attributes.ExecutionMode.Asynchronous)|}]
403+
public sealed class MyPlugin : PluginBase
404+
{
405+
}
406+
";
407+
408+
var expected = new DiagnosticResult(PluginDependencyAnalyzer.InvalidStepStageModeId, DiagnosticSeverity.Warning)
409+
.WithLocation(0)
410+
.WithArguments("Stages.PreOperation");
411+
412+
await VerifyCS.VerifyAnalyzerAsync(test, expected);
413+
}
414+
415+
[Fact]
416+
public async Task Plugin_PreValidationAsyncStep_WithFilteringAttributesConstructor_ReportsXrmTools006()
417+
{
418+
var test = Preamble + @"
419+
[XrmTools.Meta.Attributes.Plugin]
420+
[{|#0:XrmTools.Meta.Attributes.Step(""Update"", ""account"", ""name"", XrmTools.Meta.Attributes.Stages.PreValidation, XrmTools.Meta.Attributes.ExecutionMode.Asynchronous)|}]
421+
public sealed class MyPlugin : PluginBase
422+
{
423+
}
424+
";
425+
426+
var expected = new DiagnosticResult(PluginDependencyAnalyzer.InvalidStepStageModeId, DiagnosticSeverity.Warning)
427+
.WithLocation(0)
428+
.WithArguments("Stages.PreValidation");
429+
430+
await VerifyCS.VerifyAnalyzerAsync(test, expected);
431+
}
432+
433+
[Fact]
434+
public async Task Plugin_PreOperationAsyncStep_WithNamedModeOverride_ReportsXrmTools006()
435+
{
436+
var test = Preamble + @"
437+
[XrmTools.Meta.Attributes.Plugin]
438+
[{|#0:XrmTools.Meta.Attributes.Step(""Update"", XrmTools.Meta.Attributes.Stages.PreOperation, XrmTools.Meta.Attributes.ExecutionMode.Synchronous, Mode = XrmTools.Meta.Attributes.ExecutionMode.Asynchronous)|}]
439+
public sealed class MyPlugin : PluginBase
440+
{
441+
}
442+
";
443+
444+
var expected = new DiagnosticResult(PluginDependencyAnalyzer.InvalidStepStageModeId, DiagnosticSeverity.Warning)
445+
.WithLocation(0)
446+
.WithArguments("Stages.PreOperation");
447+
448+
await VerifyCS.VerifyAnalyzerAsync(test, expected);
449+
}
450+
451+
[Fact]
452+
public async Task Plugin_PostOperationAsyncStep_IsAllowed()
453+
{
454+
var test = Preamble + @"
455+
[XrmTools.Meta.Attributes.Plugin]
456+
[XrmTools.Meta.Attributes.Step(""Create"", XrmTools.Meta.Attributes.Stages.PostOperation, XrmTools.Meta.Attributes.ExecutionMode.Asynchronous)]
457+
public sealed class MyPlugin : PluginBase
458+
{
459+
}
354460
";
355461

356462
await VerifyCS.VerifyAnalyzerAsync(test);

src/Analyzers/XrmTools.Analyzers/PluginDependencyAnalyzer.cs

Lines changed: 140 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace XrmTools.Tests.Logging;
2+
using System;
3+
using FluentAssertions;
4+
using Xunit;
5+
using XrmTools.Logging.Compatibility;
6+
7+
public class OutputLoggerTests
8+
{
9+
[Fact]
10+
public void AppendException_Should_Include_Exception_Details()
11+
{
12+
var logRecord = "[Error] TestCategory: Failed to register plugin assembly.";
13+
var exception = new InvalidOperationException("Boom");
14+
15+
var result = OutputLogger.AppendException(logRecord, exception);
16+
17+
result.Should().StartWith(logRecord + Environment.NewLine);
18+
result.Should().Contain(exception.ToString());
19+
}
20+
}

src/XrmTools.Meta.Attributes.Package/XrmTools.Meta.Attributes.Package.msbuildproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<Title>XrmTools.Meta.Attributes</Title>
99
<Description>Attributes to be used by Xrm Tools for source generation, registration and other features in Visual Studio. This is a source only package with no assemblies included.</Description>
1010
<PackageId>XrmTools.Meta.Attributes</PackageId>
11-
<VersionPrefix>1.1.4</VersionPrefix>
11+
<VersionPrefix>1.1.5</VersionPrefix>
1212
<Authors>Reza Niroomand</Authors>
1313
<PackageIcon>XrmTools_256.png</PackageIcon>
1414
<PackageTags>Power Platform Apps XRM Tools</PackageTags>

src/XrmTools.Meta.Attributes.SourceGenerator/XrmTools.Meta.Attributes.SourceGenerator.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<IsPackable>false</IsPackable>
77
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
88
<RootNamespace>XrmTools.Meta.Attributes.SourceGenerator</RootNamespace>
9+
<VersionPrefix>1.1.5</VersionPrefix>
910
</PropertyGroup>
1011

1112
<ItemGroup>

0 commit comments

Comments
 (0)