Skip to content

Commit b89932a

Browse files
committed
Diagnostics
1 parent afa020b commit b89932a

File tree

5 files changed

+127
-4
lines changed

5 files changed

+127
-4
lines changed

src/Foundatio.Mediator.SourceGenerator/CallSiteAnalyzer.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using Foundatio.Mediator.Models;
2-
using Foundatio.Mediator.Utility;
32
using Microsoft.CodeAnalysis;
43
using Microsoft.CodeAnalysis.CSharp;
54
using Microsoft.CodeAnalysis.CSharp.Syntax;

src/Foundatio.Mediator.SourceGenerator/HandlerGenerator.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public static void Execute(SourceProductionContext context, List<HandlerInfo> ha
1111
if (handlers.Count == 0)
1212
return;
1313

14+
Validate(context, handlers);
15+
1416
foreach (var handler in handlers)
1517
{
1618
try
@@ -572,4 +574,31 @@ public static string GetHandlerMethodName(HandlerInfo handler)
572574
{
573575
return handler.IsAsync ? "HandleAsync" : "Handle";
574576
}
577+
578+
private static void Validate(SourceProductionContext context, List<HandlerInfo> handlers)
579+
{
580+
// TODO: Error for invoke or invokeasync call with more than one handler
581+
// TODO: Error for no handler found for invoke or invokeasync call
582+
// TODO: Error for calling sync invoke on async handler, tell them to use InvokeAsync instead
583+
// TODO: Error for calling sync invoke on handler that has async middleware, tell them to use InvokeAsync instead
584+
// TODO: Error for calling sync invoke on handler that returns tuple, tell them to use InvokeAsync instead because it needs to publish cascading messages
585+
586+
var processedMiddleware = new HashSet<MiddlewareInfo>();
587+
588+
foreach (var handler in handlers)
589+
{
590+
foreach (var middleware in handler.Middleware)
591+
{
592+
if (processedMiddleware.Contains(middleware))
593+
continue;
594+
595+
processedMiddleware.Add(middleware);
596+
597+
foreach (var diagnostic in middleware.Diagnostics)
598+
{
599+
context.ReportDiagnostic(diagnostic.ToDiagnostic());
600+
}
601+
}
602+
}
603+
}
575604
}

src/Foundatio.Mediator.SourceGenerator/MiddlewareAnalyzer.cs

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,91 @@ public static bool IsMatch(SyntaxNode node)
4242
if (beforeMethods.Count == 0 && afterMethods.Count == 0 && finallyMethods.Count == 0)
4343
return null;
4444

45-
// TODO: Diagnostic if multiple methods for the same lifecycle stage
46-
// TODO: Diagnostic if there are mixed static and instance methods
47-
// TODO: Diagnostic if all message types are not the same
45+
var diagnostics = new List<DiagnosticInfo>();
46+
47+
if (beforeMethods.Count > 1)
48+
{
49+
diagnostics.Add(new DiagnosticInfo
50+
{
51+
Identifier = "FMED001",
52+
Title = "Multiple Before Methods in Middleware",
53+
Message = $"Middleware '{classSymbol.Name}' has multiple Before methods. Only one Before/BeforeAsync method is allowed per middleware class.",
54+
Severity = DiagnosticSeverity.Error,
55+
Location = LocationInfo.CreateFrom(classDeclaration)
56+
});
57+
}
58+
59+
if (afterMethods.Count > 1)
60+
{
61+
diagnostics.Add(new DiagnosticInfo
62+
{
63+
Identifier = "FMED002",
64+
Title = "Multiple After Methods in Middleware",
65+
Message = $"Middleware '{classSymbol.Name}' has multiple After methods. Only one After/AfterAsync method is allowed per middleware class.",
66+
Severity = DiagnosticSeverity.Error,
67+
Location = LocationInfo.CreateFrom(classDeclaration)
68+
});
69+
}
70+
71+
if (finallyMethods.Count > 1)
72+
{
73+
diagnostics.Add(new DiagnosticInfo
74+
{
75+
Identifier = "FMED003",
76+
Title = "Multiple Finally Methods in Middleware",
77+
Message = $"Middleware '{classSymbol.Name}' has multiple Finally methods. Only one Finally/FinallyAsync method is allowed per middleware class.",
78+
Severity = DiagnosticSeverity.Error,
79+
Location = LocationInfo.CreateFrom(classDeclaration)
80+
});
81+
}
4882

4983
var beforeMethod = beforeMethods.FirstOrDefault();
5084
var afterMethod = afterMethods.FirstOrDefault();
5185
var finallyMethod = finallyMethods.FirstOrDefault();
5286

87+
var allMethods = new[] { beforeMethod, afterMethod, finallyMethod }.Where(m => m != null).ToList();
88+
89+
// Validate mixed static and instance methods
90+
if (allMethods.Any() && !classSymbol.IsStatic)
91+
{
92+
var staticMethods = allMethods.Where(m => m!.IsStatic).ToList();
93+
var instanceMethods = allMethods.Where(m => !m!.IsStatic).ToList();
94+
95+
if (staticMethods.Any() && instanceMethods.Any())
96+
{
97+
diagnostics.Add(new DiagnosticInfo
98+
{
99+
Identifier = "FMED004",
100+
Title = "Mixed Static and Instance Middleware Methods",
101+
Message = $"Middleware '{classSymbol.Name}' has both static and instance methods. All middleware methods must be either static or instance methods consistently.",
102+
Severity = DiagnosticSeverity.Error,
103+
Location = LocationInfo.CreateFrom(classDeclaration)
104+
});
105+
}
106+
}
107+
108+
// Validate all message types are the same
109+
if (allMethods.Count > 1)
110+
{
111+
var messageTypes = allMethods
112+
.Where(m => m!.Parameters.Length > 0)
113+
.Select(m => m!.Parameters[0].Type)
114+
.Distinct(SymbolEqualityComparer.Default)
115+
.ToList();
116+
117+
if (messageTypes.Count > 1)
118+
{
119+
diagnostics.Add(new DiagnosticInfo
120+
{
121+
Identifier = "FMED005",
122+
Title = "Middleware Message Type Mismatch",
123+
Message = $"Middleware '{classSymbol.Name}' handles different message types. All middleware methods in the same class must handle the same message type.",
124+
Severity = DiagnosticSeverity.Error,
125+
Location = LocationInfo.CreateFrom(classDeclaration)
126+
});
127+
}
128+
}
129+
53130
ITypeSymbol? messageType = beforeMethod?.Parameters[0].Type
54131
?? afterMethod?.Parameters[0].Type
55132
?? finallyMethod?.Parameters[0].Type;
@@ -84,6 +161,7 @@ public static bool IsMatch(SyntaxNode node)
84161
FinallyMethod = finallyMethod != null ? CreateMiddlewareMethodInfo(finallyMethod, context.SemanticModel.Compilation) : null,
85162
IsStatic = isStatic,
86163
Order = order,
164+
Diagnostics = new(diagnostics.ToArray()),
87165
};
88166
}
89167

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Foundatio.Mediator.Models;
2+
using Microsoft.CodeAnalysis;
3+
4+
internal readonly record struct DiagnosticInfo
5+
{
6+
public string Identifier { get; init; }
7+
public string Title { get; init; }
8+
public string Message { get; init; }
9+
public DiagnosticSeverity Severity { get; init; }
10+
public LocationInfo? Location { get; init; }
11+
12+
public Diagnostic ToDiagnostic()
13+
{
14+
return Diagnostic.Create(new DiagnosticDescriptor(Identifier, Title, Message, "Usage", Severity, true), Location?.ToLocation());
15+
}
16+
}

src/Foundatio.Mediator.SourceGenerator/Models/MiddlewareInfo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ internal readonly record struct MiddlewareInfo
1313
public bool IsStatic { get; init; }
1414
public bool IsAsync => BeforeMethod?.IsAsync == true || AfterMethod?.IsAsync == true || FinallyMethod?.IsAsync == true;
1515
public int? Order { get; init; }
16+
public EquatableArray<DiagnosticInfo> Diagnostics { get; init; }
1617
}
1718

1819
internal readonly record struct MiddlewareMethodInfo

0 commit comments

Comments
 (0)