Skip to content

Commit 95019dd

Browse files
committed
* Fix: XPC4003 has been reduced to Warning to allow initial build to succeed
* Add: New rule XPC4006 (Error) to enforce handler signature correctness once generated types exist
1 parent 1bcaae8 commit 95019dd

File tree

9 files changed

+263
-10
lines changed

9 files changed

+263
-10
lines changed

XrmPluginCore.SourceGenerator.Tests/DiagnosticTests/DiagnosticReportingTests.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,8 @@ public void Process() { }
211211
.ToArray();
212212

213213
errorDiagnostics.Should().NotBeEmpty("XPC4003 should be reported when handler is missing PreImage parameter");
214-
errorDiagnostics.Should().OnlyContain(d => d.Severity == DiagnosticSeverity.Error);
214+
// XPC4003 is Warning when generated types don't exist yet (allows initial build to succeed)
215+
errorDiagnostics.Should().OnlyContain(d => d.Severity == DiagnosticSeverity.Warning);
215216
}
216217

217218
[Fact]
@@ -265,7 +266,8 @@ public void Process() { }
265266
.ToArray();
266267

267268
errorDiagnostics.Should().NotBeEmpty("XPC4003 should be reported when handler is missing PostImage parameter");
268-
errorDiagnostics.Should().OnlyContain(d => d.Severity == DiagnosticSeverity.Error);
269+
// XPC4003 is Warning when generated types don't exist yet (allows initial build to succeed)
270+
errorDiagnostics.Should().OnlyContain(d => d.Severity == DiagnosticSeverity.Warning);
269271
}
270272

271273
[Fact]
@@ -320,7 +322,8 @@ public void Process() { }
320322
.ToArray();
321323

322324
errorDiagnostics.Should().NotBeEmpty("XPC4003 should be reported when handler is missing both image parameters");
323-
errorDiagnostics.Should().OnlyContain(d => d.Severity == DiagnosticSeverity.Error);
325+
// XPC4003 is Warning when generated types don't exist yet (allows initial build to succeed)
326+
errorDiagnostics.Should().OnlyContain(d => d.Severity == DiagnosticSeverity.Warning);
324327
}
325328

326329
[Fact]
@@ -376,7 +379,8 @@ public void Process(PostImage post, PreImage pre) { }
376379
.ToArray();
377380

378381
errorDiagnostics.Should().NotBeEmpty("XPC4003 should be reported when handler has wrong parameter order");
379-
errorDiagnostics.Should().OnlyContain(d => d.Severity == DiagnosticSeverity.Error);
382+
// XPC4003 is Warning when generated types don't exist yet (allows initial build to succeed)
383+
errorDiagnostics.Should().OnlyContain(d => d.Severity == DiagnosticSeverity.Warning);
380384
}
381385

382386
[Fact]

XrmPluginCore.SourceGenerator/AnalyzerReleases.Shipped.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,17 @@ XPC5000 | XrmPluginCore.SourceGenerator | Error | XPC5000 Failed to generate
2020
Rule ID | Category | Severity | Notes
2121
--------|----------|----------|-------
2222
XPC4005 | XrmPluginCore.SourceGenerator | Info | XPC4005 Consider using modern image registration API
23+
24+
## Release 1.2.2
25+
26+
### New Rules
27+
28+
Rule ID | Category | Severity | Notes
29+
--------|----------|----------|-------
30+
XPC4006 | XrmPluginCore.SourceGenerator | Error | XPC4006 Handler signature mismatch (generated types exist)
31+
32+
### Changed Rules
33+
34+
Rule ID | New Category | New Severity | Old Category | Old Severity | Notes
35+
--------|--------------|--------------|--------------|--------------|-------|-------
36+
XPC4003 | XrmPluginCore.SourceGenerator | Warning | XrmPluginCore.SourceGenerator | Error | Handler signature does not match registered images (generated types don't exist)

XrmPluginCore.SourceGenerator/AnalyzerReleases.Unshipped.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,12 @@
33
Rule ID | Category | Severity | Notes
44
--------|----------|----------|-------
55

6+
### Removed Rules
7+
8+
Rule ID | Category | Severity | Notes
9+
--------|----------|----------|-------
10+
11+
### Changed Rules
12+
13+
Rule ID | New Category | New Severity | Old Category | Old Severity | Notes
14+
--------|--------------|--------------|--------------|--------------|-------

XrmPluginCore.SourceGenerator/Analyzers/HandlerSignatureMismatchAnalyzer.cs

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@
99
namespace XrmPluginCore.SourceGenerator.Analyzers;
1010

1111
/// <summary>
12-
/// Analyzer that reports an error when a handler method signature does not match the registered images.
12+
/// Analyzer that reports when a handler method signature does not match the registered images.
13+
/// Reports XPC4003 (Warning) when generated types don't exist yet, XPC4006 (Error) when they do.
1314
/// </summary>
1415
[DiagnosticAnalyzer(LanguageNames.CSharp)]
1516
public class HandlerSignatureMismatchAnalyzer : DiagnosticAnalyzer
1617
{
1718
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
18-
ImmutableArray.Create(DiagnosticDescriptors.HandlerSignatureMismatch);
19+
ImmutableArray.Create(
20+
DiagnosticDescriptors.HandlerSignatureMismatch,
21+
DiagnosticDescriptors.HandlerSignatureMismatchError);
1922

2023
public override void Initialize(AnalysisContext context)
2124
{
@@ -92,6 +95,19 @@ private void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
9295
// Build expected signature description
9396
var expectedSignature = SyntaxFactoryHelper.BuildSignatureDescription(hasPreImage, hasPostImage);
9497

98+
// Determine if generated types exist to choose appropriate diagnostic severity
99+
var generatedTypesExist = DoGeneratedTypesExist(
100+
context,
101+
invocation,
102+
genericName,
103+
hasPreImage,
104+
hasPostImage);
105+
106+
// Choose diagnostic: XPC4006 (Error) if types exist, XPC4003 (Warning) if they don't
107+
var descriptor = generatedTypesExist
108+
? DiagnosticDescriptors.HandlerSignatureMismatchError
109+
: DiagnosticDescriptors.HandlerSignatureMismatch;
110+
95111
// Create diagnostic properties for the code fix
96112
var properties = ImmutableDictionary.CreateBuilder<string, string>();
97113
properties.Add("ServiceType", serviceType.Name);
@@ -100,7 +116,7 @@ private void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
100116
properties.Add("HasPostImage", hasPostImage.ToString());
101117

102118
var diagnostic = Diagnostic.Create(
103-
DiagnosticDescriptors.HandlerSignatureMismatch,
119+
descriptor,
104120
handlerArgument.GetLocation(),
105121
properties.ToImmutable(),
106122
methodName,
@@ -109,6 +125,97 @@ private void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
109125
context.ReportDiagnostic(diagnostic);
110126
}
111127

128+
private static bool DoGeneratedTypesExist(
129+
SyntaxNodeAnalysisContext context,
130+
InvocationExpressionSyntax invocation,
131+
GenericNameSyntax genericName,
132+
bool hasPreImage,
133+
bool hasPostImage)
134+
{
135+
// Get plugin class info
136+
var pluginClass = invocation.Ancestors().OfType<ClassDeclarationSyntax>().FirstOrDefault();
137+
if (pluginClass == null)
138+
{
139+
return false;
140+
}
141+
142+
var pluginClassName = pluginClass.Identifier.Text;
143+
var pluginNamespace = GetNamespace(pluginClass);
144+
145+
// Get entity type name
146+
var entityTypeSyntax = genericName.TypeArgumentList.Arguments[0];
147+
var entityTypeInfo = context.SemanticModel.GetTypeInfo(entityTypeSyntax);
148+
var entityTypeName = entityTypeInfo.Type?.Name ?? "Unknown";
149+
150+
// Get operation and stage from arguments
151+
var arguments = invocation.ArgumentList.Arguments;
152+
var operation = ExtractEnumValue(arguments[0].Expression);
153+
var stage = ExtractEnumValue(arguments[1].Expression);
154+
155+
// Build expected namespace: {Namespace}.PluginRegistrations.{PluginClass}.{Entity}{Op}{Stage}
156+
var expectedNamespace = $"{pluginNamespace}.PluginRegistrations.{pluginClassName}.{entityTypeName}{operation}{stage}";
157+
158+
// Check if the required generated types exist
159+
var compilation = context.SemanticModel.Compilation;
160+
161+
if (hasPreImage)
162+
{
163+
var preImageType = compilation.GetTypeByMetadataName($"{expectedNamespace}.PreImage");
164+
if (preImageType == null)
165+
{
166+
return false;
167+
}
168+
}
169+
170+
if (hasPostImage)
171+
{
172+
var postImageType = compilation.GetTypeByMetadataName($"{expectedNamespace}.PostImage");
173+
if (postImageType == null)
174+
{
175+
return false;
176+
}
177+
}
178+
179+
return true;
180+
}
181+
182+
private static string GetNamespace(SyntaxNode node)
183+
{
184+
while (node != null)
185+
{
186+
if (node is NamespaceDeclarationSyntax namespaceDecl)
187+
{
188+
return namespaceDecl.Name.ToString();
189+
}
190+
191+
if (node is FileScopedNamespaceDeclarationSyntax fileScopedNs)
192+
{
193+
return fileScopedNs.Name.ToString();
194+
}
195+
196+
node = node.Parent;
197+
}
198+
199+
return string.Empty;
200+
}
201+
202+
private static string ExtractEnumValue(ExpressionSyntax expression)
203+
{
204+
// Handle direct enum access like EventOperation.Update
205+
if (expression is MemberAccessExpressionSyntax memberAccess)
206+
{
207+
return memberAccess.Name.Identifier.Text;
208+
}
209+
210+
// Handle string literal for custom messages
211+
if (expression is LiteralExpressionSyntax literal)
212+
{
213+
return literal.Token.ValueText;
214+
}
215+
216+
return "Unknown";
217+
}
218+
112219
private static bool SignatureMatches(IMethodSymbol method, bool hasPreImage, bool hasPostImage)
113220
{
114221
var parameters = method.Parameters;

XrmPluginCore.SourceGenerator/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
### v1.2.2 - 27 November 2025
2+
* Fix: XPC4003 has been reduced to Warning to allow initial build to succeed
3+
* Add: New rule XPC4006 (Error) to enforce handler signature correctness once generated types exist
4+
15
### v1.0.1 - 27 November 2025
26
* Fix: Analyzer for XPC4005 to correctly identify lambda expressions in AddImage calls
37

XrmPluginCore.SourceGenerator/DiagnosticDescriptors.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ internal static class DiagnosticDescriptors
5858
title: "Handler signature does not match registered images",
5959
messageFormat: "Handler method '{0}' does not have expected signature. Expected parameters in order: {1}. PreImage must be the first parameter, followed by PostImage if both are used.",
6060
category: Category,
61-
defaultSeverity: DiagnosticSeverity.Error,
61+
defaultSeverity: DiagnosticSeverity.Warning,
6262
isEnabledByDefault: true,
6363
helpLinkUri: "https://github.com/delegateas/XrmPluginCore/blob/main/XrmPluginCore.SourceGenerator/rules/XPC4003.md");
6464

@@ -80,6 +80,15 @@ internal static class DiagnosticDescriptors
8080
isEnabledByDefault: true,
8181
helpLinkUri: "https://github.com/delegateas/XrmPluginCore/blob/main/XrmPluginCore.SourceGenerator/rules/XPC4005.md");
8282

83+
public static readonly DiagnosticDescriptor HandlerSignatureMismatchError = new(
84+
id: "XPC4006",
85+
title: "Handler signature does not match registered images",
86+
messageFormat: "Handler method '{0}' does not have expected signature. Expected parameters in order: {1}. PreImage must be the first parameter, followed by PostImage if both are used.",
87+
category: Category,
88+
defaultSeverity: DiagnosticSeverity.Error,
89+
isEnabledByDefault: true,
90+
helpLinkUri: "https://github.com/delegateas/XrmPluginCore/blob/main/XrmPluginCore.SourceGenerator/rules/XPC4006.md");
91+
8392
public static readonly DiagnosticDescriptor GenerationError = new(
8493
id: "XPC5000",
8594
title: "Failed to generate wrapper classes",

XrmPluginCore.SourceGenerator/rules/XPC4003.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
## Severity
44

5-
Error
5+
Warning
66

77
## Description
88

9-
This rule reports when a handler method's signature does not match the images registered with `WithPreImage()`, `WithPostImage()`, or `AddImage()`. The handler method must accept parameters in a specific order: `PreImage` first (if registered), then `PostImage` (if registered).
9+
This rule reports when a handler method's signature does not match the images registered with `WithPreImage()`, `WithPostImage()`, or `AddImage()`, **and** the generated `PreImage`/`PostImage` types don't exist yet. The handler method must accept parameters in a specific order: `PreImage` first (if registered), then `PostImage` (if registered).
10+
11+
This is a **warning** to allow the initial build to succeed so that the source generator can create the wrapper types. Once the types exist, this diagnostic escalates to [XPC4006](XPC4006.md) (Error).
1012

1113
## ❌ Examples of violations
1214

@@ -126,7 +128,13 @@ public interface IAccountService
126128

127129
Visual Studio and other IDEs supporting Roslyn analyzers will offer a code fix to update the method signature to match the registered images.
128130

131+
## Relationship with XPC4006
132+
133+
- **XPC4003 (Warning)**: Reported when the generated types don't exist yet. This allows the initial build to succeed so the types can be generated.
134+
- **XPC4006 (Error)**: Reported when the generated types exist but the signature is still wrong. This prevents runtime failures.
135+
129136
## See also
130137

138+
- [XPC4006: Handler signature does not match registered images (types exist)](XPC4006.md)
131139
- [XPC4002: Handler method not found](XPC4002.md)
132140
- [XPC4004: Image registration without method reference](XPC4004.md)
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# XPC4006: Handler signature does not match registered images (types exist)
2+
3+
## Severity
4+
5+
Error
6+
7+
## Description
8+
9+
This rule reports when a handler method signature does not match the registered images **and** the generated `PreImage`/`PostImage` types already exist. This is the escalated version of [XPC4003](XPC4003.md) that becomes an error once the types have been generated.
10+
11+
When you register images with `WithPreImage()` or `WithPostImage()`, the source generator creates type-safe wrapper classes. Your handler method must accept these wrapper types as parameters in the correct order.
12+
13+
## ❌ Example of violation
14+
15+
```csharp
16+
using MyNamespace.PluginRegistrations.AccountPlugin.AccountUpdatePostOperation;
17+
18+
public class AccountPlugin : Plugin
19+
{
20+
public AccountPlugin()
21+
{
22+
RegisterStep<Account, IAccountService>(
23+
EventOperation.Update,
24+
ExecutionStage.PostOperation,
25+
nameof(IAccountService.HandleUpdate))
26+
.WithPreImage(x => x.Name);
27+
}
28+
}
29+
30+
public interface IAccountService
31+
{
32+
// XPC4006: Missing PreImage parameter
33+
void HandleUpdate();
34+
}
35+
```
36+
37+
## ✅ How to fix
38+
39+
Update the handler method signature to match the registered images:
40+
41+
```csharp
42+
using MyNamespace.PluginRegistrations.AccountPlugin.AccountUpdatePostOperation;
43+
44+
public class AccountPlugin : Plugin
45+
{
46+
public AccountPlugin()
47+
{
48+
RegisterStep<Account, IAccountService>(
49+
EventOperation.Update,
50+
ExecutionStage.PostOperation,
51+
nameof(IAccountService.HandleUpdate))
52+
.WithPreImage(x => x.Name);
53+
}
54+
}
55+
56+
public interface IAccountService
57+
{
58+
// Correct: PreImage parameter matches the registered image
59+
void HandleUpdate(PreImage preImage);
60+
}
61+
```
62+
63+
## Parameter Order
64+
65+
When both images are registered, they must appear in this order:
66+
1. `PreImage` (first)
67+
2. `PostImage` (second)
68+
69+
```csharp
70+
// Both images registered
71+
.WithPreImage(x => x.Name)
72+
.WithPostImage(x => x.AccountNumber);
73+
74+
// Handler must accept them in order: PreImage first, PostImage second
75+
void HandleUpdate(PreImage preImage, PostImage postImage);
76+
```
77+
78+
## Why this is an Error
79+
80+
This rule is an error (not a warning) because:
81+
82+
1. **The generated types exist**: The source generator has already created the `PreImage`/`PostImage` wrapper classes
83+
2. **Runtime failure**: Without the correct signature, the plugin execution will fail at runtime
84+
3. **Fix is available**: You can add the correct using statement and update the method signature
85+
86+
## Relationship with XPC4003
87+
88+
- **XPC4003 (Warning)**: Reported when the generated types don't exist yet. This allows the initial build to succeed so the types can be generated.
89+
- **XPC4006 (Error)**: Reported when the generated types exist but the signature is still wrong. This prevents runtime failures.
90+
91+
## See also
92+
93+
- [XPC4003: Handler signature does not match registered images (types don't exist)](XPC4003.md)
94+
- [XPC4004: Image registration without method reference](XPC4004.md)

XrmPluginCore/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
### v1.2.2 - 27 November 2025
2+
* Fix: XPC4003 has been reduced to Warning to allow initial build to succeed
3+
* Add: New rule XPC4006 (Error) to enforce handler signature correctness once generated types exist
4+
15
### v1.2.1 - 27 November 2025
26
* Fix: Analyzer for XPC4005 to correctly identify lambda expressions in AddImage calls
37

0 commit comments

Comments
 (0)