Add DP024 analyzer for unregistered handler implementations#8
Conversation
1、Introduce UnregisteredHandlerAnalyzer aligned with DP006 and DP023 2、Recognize generic HandlerOrderAttribute metadata in attribute detection 3、Add analyzer tests and document DP024 in ChainOfResponsibility and ROADMAP
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Unqualified HandlerOrder attribute matching
- HandlerOrder matching now compares fully qualified metadata names for both generic and non-generic attributes.
- ✅ Fixed: HandlerAnalysisConstants never referenced
- UnregisteredHandlerAnalyzer now uses HandlerAnalysisConstants for HandlerOrder attribute identification.
Or push these changes by commenting:
@cursor push 2478fc492e
Preview (2478fc492e)
diff --git a/DesignPatterns.Analyzers/UnregisteredHandlerAnalyzer.cs b/DesignPatterns.Analyzers/UnregisteredHandlerAnalyzer.cs
--- a/DesignPatterns.Analyzers/UnregisteredHandlerAnalyzer.cs
+++ b/DesignPatterns.Analyzers/UnregisteredHandlerAnalyzer.cs
@@ -143,13 +143,21 @@
return false;
}
- var metadataName = attributeClass.MetadataName;
- if (metadataName == "HandlerOrderAttribute")
+ return GetFullMetadataName(attributeClass) == HandlerAnalysisConstants.HandlerOrderMetadataName ||
+ GetFullMetadataName(attributeClass.OriginalDefinition) == HandlerAnalysisConstants.HandlerOrderGenericMetadataName;
+ }
+
+ private static string GetFullMetadataName(INamedTypeSymbol typeSymbol)
+ {
+ var metadataName = typeSymbol.MetadataName;
+ for (var containingType = typeSymbol.ContainingType; containingType is not null; containingType = containingType.ContainingType)
{
- return true;
+ metadataName = containingType.MetadataName + "+" + metadataName;
}
- return attributeClass.OriginalDefinition.MetadataName == "HandlerOrderAttribute`1";
+ return typeSymbol.ContainingNamespace.IsGlobalNamespace
+ ? metadataName
+ : typeSymbol.ContainingNamespace.ToDisplayString() + "." + metadataName;
}
private static INamedTypeSymbol? TryGetContextFromAttribute(AttributeData attribute)
diff --git a/tests/DesignPatterns.Analyzers.Tests/UnregisteredHandlerAnalyzerTests.cs b/tests/DesignPatterns.Analyzers.Tests/UnregisteredHandlerAnalyzerTests.cs
--- a/tests/DesignPatterns.Analyzers.Tests/UnregisteredHandlerAnalyzerTests.cs
+++ b/tests/DesignPatterns.Analyzers.Tests/UnregisteredHandlerAnalyzerTests.cs
@@ -112,4 +112,113 @@
Assert.DoesNotContain(diagnostics, diagnostic => diagnostic.Id == "DP024");
}
+
+ [Fact]
+ public async Task DoesNotTreatForeignHandlerOrderAsRegisteredContext()
+ {
+ const string source = """
+ using System;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using DesignPatterns.Behavioral;
+
+ namespace Other
+ {
+ [AttributeUsage(AttributeTargets.Class)]
+ public sealed class HandlerOrderAttribute : Attribute
+ {
+ public HandlerOrderAttribute(int order, Type contextType) { }
+ }
+ }
+
+ namespace TestAssembly
+ {
+ public sealed class RequestContext
+ {
+ }
+
+ [Other.HandlerOrder(10, typeof(RequestContext))]
+ public sealed class LoggingHandler : IHandler<RequestContext>
+ {
+ public ValueTask InvokeAsync(
+ RequestContext context,
+ HandlerDelegate<RequestContext> next,
+ CancellationToken cancellationToken = default) =>
+ default;
+ }
+
+ public sealed class AuditHandler : IHandler<RequestContext>
+ {
+ public ValueTask InvokeAsync(
+ RequestContext context,
+ HandlerDelegate<RequestContext> next,
+ CancellationToken cancellationToken = default) =>
+ default;
+ }
+ }
+ """;
+
+ var diagnostics = await AnalyzerTestContext.RunAnalyzersAsync(
+ source,
+ new UnregisteredHandlerAnalyzer());
+
+ Assert.DoesNotContain(diagnostics, diagnostic => diagnostic.Id == "DP024");
+ }
+
+ [Fact]
+ public async Task ReportsWhenOnlyForeignHandlerOrderIsPresentOnImplementation()
+ {
+ const string source = """
+ using System;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using DesignPatterns.Behavioral;
+
+ namespace Other
+ {
+ [AttributeUsage(AttributeTargets.Class)]
+ public sealed class HandlerOrderAttribute<TContext> : Attribute
+ {
+ public HandlerOrderAttribute(int order) { }
+ }
+ }
+
+ namespace TestAssembly
+ {
+ public sealed class RequestContext
+ {
+ }
+
+ [DesignPatterns.Behavioral.HandlerOrder<RequestContext>(10)]
+ public sealed class LoggingHandler : IHandler<RequestContext>
+ {
+ public ValueTask InvokeAsync(
+ RequestContext context,
+ HandlerDelegate<RequestContext> next,
+ CancellationToken cancellationToken = default) =>
+ default;
+ }
+
+ [Other.HandlerOrder<RequestContext>(20)]
+ public sealed class AuditHandler : IHandler<RequestContext>
+ {
+ public ValueTask InvokeAsync(
+ RequestContext context,
+ HandlerDelegate<RequestContext> next,
+ CancellationToken cancellationToken = default) =>
+ default;
+ }
+ }
+ """;
+
+ var diagnostics = await AnalyzerTestContext.RunAnalyzersAsync(
+ source,
+ new UnregisteredHandlerAnalyzer());
+
+ Assert.Contains(
+ diagnostics,
+ diagnostic =>
+ diagnostic.Id == "DP024" &&
+ diagnostic.GetMessage().Contains("AuditHandler", StringComparison.Ordinal));
+ }
}You can send follow-ups to the cloud agent here.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 30fe19e. Configure here.
| } | ||
|
|
||
| return attributeClass.OriginalDefinition.MetadataName == "HandlerOrderAttribute`1"; | ||
| } |
There was a problem hiding this comment.
Unqualified HandlerOrder attribute matching
Low Severity
IsHandlerOrderAttribute treats any attribute type whose short MetadataName is HandlerOrderAttribute (or generic HandlerOrderAttribute1) as the design-patterns attribute, without checking namespace. Sibling unregistered analyzers compare fully qualified metadata names, so another HandlerOrderAttribute` in the compilation can skew registered contexts or suppress or trigger DP024 incorrectly.
Reviewed by Cursor Bugbot for commit 30fe19e. Configure here.
| { | ||
| internal const string HandlerOrderMetadataName = "DesignPatterns.Behavioral.HandlerOrderAttribute"; | ||
| internal const string HandlerOrderGenericMetadataName = "DesignPatterns.Behavioral.HandlerOrderAttribute`1"; | ||
| } |
There was a problem hiding this comment.
HandlerAnalysisConstants never referenced
Low Severity
HandlerAnalysisConstants defines fully qualified HandlerOrder metadata name strings but nothing in the analyzers project references them. The new analyzer duplicates the generator’s naming approach in a dead file instead of sharing or using those constants.
Reviewed by Cursor Bugbot for commit 30fe19e. Configure here.



Summary
Closes #6
Test plan
DesignPatterns.Analyzers.Tests (15 tests)
Full solution dotnet test
CI green
Note
Low Risk
Compile-time Info diagnostic and documentation only; no runtime or generator behavior changes.
Overview
Adds DP024 (
UnregisteredHandlerAnalyzer), an Info-level compile-time check aligned with DP006/DP023: when a context type already has at least one[HandlerOrder]in the compilation, any other concreteIHandler<TContext>class without a matching[HandlerOrder]/[HandlerOrder<TContext>]is flagged.The analyzer discovers “registered” contexts by scanning the assembly for handler-order attributes, resolves context types from both generic and non-generic attribute shapes, and treats generic
HandlerOrderAttribute1via **OriginalDefinition.MetadataName`**. New analyzer tests cover report, suppressed when attributed, and no report when no handler is registered for that context. Docs and diagnostic tables are updated through DP024.Reviewed by Cursor Bugbot for commit 30fe19e. Configure here.