Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ dotnet test DesignPatterns.slnx -c Release
| DP023 | 未注册工厂(Analyzer + CodeFix) |
| DP024 | 未注册 Handler(Analyzer + CodeFix) |
| DP025 | 未知注册表键字面量(Analyzer + CodeFix;Strategy/Factory `Get`/`TryGet`/`Create`/`TryCreate`) |
| DP033 | 跨程序集重复 strategy key(Analyzer;多供应商宿主引用冲突) |
| DP026–DP031 | State 转换表(生成器;重复边、非法 enum、holder、孤立态 Info) |
| DP010–DP015 | CompositePart(生成器) |
| DP016–DP019 | Decorator(生成器) |
Expand Down
7 changes: 7 additions & 0 deletions DesignPatterns.Analyzers/AnalyzerSymbolHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ internal static bool ImplementsContract(INamedTypeSymbol typeSymbol, INamedTypeS

internal static INamedTypeSymbol? TryGetContractTypeFromAttribute(AttributeData attribute)
{
if (attribute.AttributeClass is INamedTypeSymbol attributeClass &&
attributeClass.TypeArguments.Length == 1 &&
attributeClass.TypeArguments[0] is INamedTypeSymbol typeArgumentContract)
{
return typeArgumentContract;
}

if (attribute.AttributeClass?.IsGenericType == true)
{
return attribute.AttributeClass.TypeArguments.Length == 1
Expand Down
136 changes: 136 additions & 0 deletions DesignPatterns.Analyzers/CrossAssemblyRegistryKeyAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using DesignPatterns.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace DesignPatterns.Analyzers;

/// <summary>
/// Reports duplicate strategy keys for the same contract when multiple referenced provider assemblies are in the compilation.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class CrossAssemblyRegistryKeyAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule =
DesignPatternsDiagnosticDescriptors.PluginRegistryDuplicateKeyAcrossAssemblies;

/// <inheritdoc />
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(Rule);

/// <inheritdoc />
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationAction(AnalyzeCompilation);
}

private static void AnalyzeCompilation(CompilationAnalysisContext context)
{
var registrations = CollectStrategyRegistrations(context.Compilation);
if (registrations.IsEmpty)
{
return;
}

ReportDuplicates(context, registrations);
}

private static void ReportDuplicates(
CompilationAnalysisContext context,
ImmutableArray<StrategyRegistration> registrations)
{
foreach (var group in registrations.GroupBy(
static registration => (
Contract: GetContractIdentity(registration.Contract),
registration.Key),
EqualityComparer<(string Contract, string Key)>.Default))
{
var assemblyNames = string.Join(
", ",
group
.Select(static registration => registration.Assembly.Name ?? string.Empty)
.Distinct(StringComparer.Ordinal)
.OrderBy(static name => name, StringComparer.Ordinal));

if (group.Select(static registration => registration.Assembly).Distinct(SymbolEqualityComparer.Default).Count() <= 1)
{
continue;
}

foreach (var registration in group)
{
context.ReportDiagnostic(Diagnostic.Create(
Rule,
registration.Location,
group.Key.Key,
group.Key.Contract,
assemblyNames));
}
}
}

private static string GetContractIdentity(INamedTypeSymbol contract) =>
contract.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);

private static ImmutableArray<StrategyRegistration> CollectStrategyRegistrations(Compilation compilation)
{
var builder = ImmutableArray.CreateBuilder<StrategyRegistration>();

foreach (var assembly in AnalyzerSymbolHelper.GetAssembliesInCompilation(compilation))
{
foreach (var typeSymbol in AnalyzerSymbolHelper.GetAllTypes(assembly.GlobalNamespace))
{
foreach (var attribute in typeSymbol.GetAttributes())
{
if (!StrategyAnalysisConstants.IsRegisterStrategyAttribute(attribute.AttributeClass))
{
continue;
}

var contract = AnalyzerSymbolHelper.TryGetContractTypeFromAttribute(attribute);
var key = AnalyzerSymbolHelper.TryGetKeyFromAttribute(attribute);
if (contract is null || key is null)
{
continue;
}

var location = attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation()
?? typeSymbol.Locations.FirstOrDefault()
?? Location.None;

builder.Add(new StrategyRegistration(contract, key, assembly, location));
}
}
}

return builder.ToImmutable();
}

private sealed class StrategyRegistration
{
public StrategyRegistration(
INamedTypeSymbol contract,
string key,
IAssemblySymbol assembly,
Location location)
{
Contract = contract;
Key = key;
Assembly = assembly;
Location = location;
}

public INamedTypeSymbol Contract { get; }

public string Key { get; }

public IAssemblySymbol Assembly { get; }

public Location Location { get; }
}
}
16 changes: 16 additions & 0 deletions DesignPatterns.Analyzers/StrategyAnalysisConstants.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
using Microsoft.CodeAnalysis;

namespace DesignPatterns.Analyzers;

internal static class StrategyAnalysisConstants
{
internal const string RegisterStrategyMetadataName = "DesignPatterns.Behavioral.RegisterStrategyAttribute";
internal const string RegisterStrategyGenericMetadataName = "DesignPatterns.Behavioral.RegisterStrategyAttribute`1";

internal static bool IsRegisterStrategyAttribute(INamedTypeSymbol? attributeClass)
{
if (attributeClass is null)
{
return false;
}

return attributeClass.OriginalDefinition.MetadataName switch
{
"RegisterStrategyAttribute" or "RegisterStrategyAttribute`1" => true,
_ => false,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Attribute match ignores declaring namespace

Low Severity

IsRegisterStrategyAttribute treats any attribute whose OriginalDefinition.MetadataName is RegisterStrategyAttribute or RegisterStrategyAttribute`1 as a DesignPatterns strategy registration. Elsewhere (IsKeyedRegistrationAttribute, UnregisteredStrategyAnalyzer, generators) matching uses the fully qualified DesignPatterns.Behavioral metadata names.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0590bc7. Configure here.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@ public static class DesignPatternsDiagnosticDescriptors
DiagnosticSeverity.Info,
AnalyzerCategory);

public static DiagnosticDescriptor PluginRegistryDuplicateKeyAcrossAssemblies { get; } = Create(
DiagnosticIds.PluginRegistryDuplicateKeyAcrossAssemblies,
"Duplicate registry key across assemblies",
"Strategy key '{0}' for contract '{1}' is registered in multiple assemblies ({2}). Use unique keys per provider assembly or reference only one provider per contract dimension.",
"Plugin provider assemblies must not register the same strategy key for the same contract when both are referenced by the host.",
DiagnosticSeverity.Error,
AnalyzerCategory);

// State transition table (DP026–DP031)

public static DiagnosticDescriptor StateTransitionDuplicateEdge { get; } = Create(
Expand Down
1 change: 1 addition & 0 deletions DesignPatterns.Diagnostics/DiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public static class DiagnosticIds
public const string RegisterFactoryUnregisteredImplementation = "DP023";
public const string HandlerOrderUnregisteredImplementation = "DP024";
public const string RegistryKeyNotRegistered = "DP025";
public const string PluginRegistryDuplicateKeyAcrossAssemblies = "DP033";
public const string StateTransitionDuplicateEdge = "DP026";
public const string StateTransitionInvalidStateMember = "DP027";
public const string StateTransitionInvalidTriggerMember = "DP028";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ DP006 | DesignPatterns.Analyzers | Info | Strategy implementation missing
DP023 | DesignPatterns.Analyzers | Info | Factory implementation missing RegisterFactory attribute
DP024 | DesignPatterns.Analyzers | Info | Handler implementation missing HandlerOrder attribute
DP025 | DesignPatterns.Analyzers | Info | Registry key is not registered for contract
DP033 | DesignPatterns.Analyzers | Error | Duplicate strategy key across referenced provider assemblies
DP026 | DesignPatterns.Generators | Error | Duplicate state transition edge
DP027 | DesignPatterns.Generators | Error | Transition state is not a declared enum member
DP028 | DesignPatterns.Generators | Error | Transition trigger is not a declared enum member
Expand Down
2 changes: 1 addition & 1 deletion docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ Nuke `UnitTest` 会依次运行 `tests/` 下四个测试项目,并将 TRX 写
| 单元 | `tests/DesignPatterns.Tests` | 运行时 API、特性校验 |
| 集成 | `tests/DesignPatterns.Tests/Integration/` | 生成器产出 → 运行时(Chain/Strategy/Composite/Singleton/Decorator/Factory/EventAggregator 单元测试在 Behavioral) |
| 快照 | `tests/DesignPatterns.SourceGenerators.Tests` | 生成源码与 DP 诊断(Verify) |
| Analyzer | `tests/DesignPatterns.Analyzers.Tests` | DP006、DP023、DP024、DP025 与 CodeFix(Strategy/Factory/Handler/Registry key) |
| Analyzer | `tests/DesignPatterns.Analyzers.Tests` | DP006、DP023、DP024、DP025、DP033 与 CodeFix(Strategy/Factory/Handler/Registry key) |
| DI 扩展 | `tests/DesignPatterns.Extensions.DependencyInjection.Tests` | DI 注册与解析 |
| AppSettings 扩展 | `tests/DesignPatterns.Extensions.AppSettings.Tests`(net8.0)、`tests/DesignPatterns.Extensions.AppSettings.Tests.Net48`(net48 编译) | AppSettings 桥接 |
| Configuration 扩展 | `tests/DesignPatterns.Extensions.Configuration.Tests`(net8.0)、`tests/DesignPatterns.Extensions.Configuration.Tests.Net48`(net48 编译) | `IConfiguration` 桥接 |
Expand Down
5 changes: 4 additions & 1 deletion docs/PluginAssemblies.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ FCErrorRegistry.RegisterAutofac(builder);

v1 **不**提供 `[RegisterStrategyBundle]`;若样板过多再评估(见 #108)。

同一实现需在 Autofac 中暴露多个接口时,在模块中手动补充 `builder.RegisterType<Impl>().As<IContract>().As<IAdditional>()`;仅当样例摩擦证明有必要时再评估生成器 `AdditionalContracts` 参数。

---

## 6. 配置选型
Expand All @@ -138,7 +140,8 @@ v1 **不**提供 `[RegisterStrategyBundle]`;若样板过多再评估(见 #10
|----|------|
| DP003 | 同一编译单元内重复 key(已有) |
| DP025 | 未知字面量 key(已有) |
| **DP033** | 跨程序集重复 key 合并 — **暂不实现**:`partial` 无法跨程序集,重复 key 表现为多个同名 Registry 类型或宿主歧义,应在 **引用图 / 部署** 层保证单供应商维度 |
| **DP033** | 宿主同时引用多个供应商程序集时,**同一契约**出现**相同 strategy key**(Analyzer Error) |
| **DP034** | 供应商已引用但 key 未出现在样例/配置文档 — 可选 Info,**未实现** |

---

Expand Down
2 changes: 1 addition & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
| Factory | `{Contract}Keys`、`{Contract}Registry` |
| State | `{StateEnum}TransitionTable`、partial `{Holder}` 便捷方法 |

新增生成器必须沿用此命名风格(详见 [Decorator.md](Decorator.md))。诊断 ID 续接现有区段,下一个可用 ID 为 **DP032**
新增生成器必须沿用此命名风格(详见 [Decorator.md](Decorator.md))。诊断 ID 续接现有区段,下一个可用 ID 为 **DP034**(DP032 预留给 App.config 静态分析,**未实现**;DP033 为跨程序集重复 strategy key)

---

Expand Down
Loading
Loading