Skip to content

Add DP024 analyzer for unregistered handler implementations#8

Merged
Skymly merged 2 commits into
mainfrom
feature/analyzers/unregistered-handler
Jun 4, 2026
Merged

Add DP024 analyzer for unregistered handler implementations#8
Skymly merged 2 commits into
mainfrom
feature/analyzers/unregistered-handler

Conversation

@Skymly

@Skymly Skymly commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds DP024 UnregisteredHandlerAnalyzer (Info) when a class implements IHandler without [HandlerOrder] while that context is already used in the compilation.
  • Fixes generic HandlerOrderAttribute detection via OriginalDefinition metadata name.

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 concrete IHandler<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 HandlerOrderAttribute1 via **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.

Skymly added 2 commits June 4, 2026 20:20
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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

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.

Create PR

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";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 30fe19e. Configure here.

{
internal const string HandlerOrderMetadataName = "DesignPatterns.Behavioral.HandlerOrderAttribute";
internal const string HandlerOrderGenericMetadataName = "DesignPatterns.Behavioral.HandlerOrderAttribute`1";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 30fe19e. Configure here.

@Skymly
Skymly merged commit c0e27b0 into main Jun 4, 2026
4 checks passed
@Skymly
Skymly deleted the feature/analyzers/unregistered-handler branch June 16, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add DP024 analyzer for unregistered handler implementations

1 participant