diff --git a/AGENTS.md b/AGENTS.md
index e369f99..f3fc821 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -151,12 +151,15 @@ dotnet test DesignPatterns.slnx -c Release
| DP060 | DI captive dependency(Analyzer;`RegisterDi` registryLifetime > implementationLifetime 警告) |
| DP061 | DI lifetime wasteful(Analyzer;`RegisterDi` implementationLifetime > registryLifetime 提示) |
| DP062 | Singleton captive dependency(Analyzer;Singleton 实现构造函数依赖 Scoped/Transient 服务) |
+| DP063 | Composite tree max depth exceeded(生成器;`[CompositeSchema(MaxDepth)]` 约束被超过 Warning) |
+| DP064 | Composite child type not allowed(生成器;子节点实现类型不在父节点 `AllowedChildTypes` 集合中 Error) |
+| DP065 | Composite node count exceeded(生成器;`[CompositeSchema(MaxNodes)]` 约束被超过 Warning) |
常量:[`DesignPatterns.Diagnostics/DiagnosticIds.cs`](DesignPatterns.Diagnostics/DiagnosticIds.cs)。规则表:[`DesignPatterns.SourceGenerators/AnalyzerReleases.Unshipped.md`](DesignPatterns.SourceGenerators/AnalyzerReleases.Unshipped.md)。
诊断 ID 规范(**本表为唯一登记源**,其他文档不得另立分类):
-- 下一个可用 ID:**DP063**;ID 一经发布不复用、不改语义。
+- 下一个可用 ID:**DP066**;ID 一经发布不复用、不改语义。
- 新增 / 修改诊断必须同步 [`DiagnosticIds.cs`](DesignPatterns.Diagnostics/DiagnosticIds.cs)、[`DesignPatternsDiagnosticDescriptors.cs`](DesignPatterns.Diagnostics/DesignPatternsDiagnosticDescriptors.cs)(经 Compile Link 编入 SourceGenerators / Analyzers)与 [`AnalyzerReleases.Unshipped.md`](DesignPatterns.SourceGenerators/AnalyzerReleases.Unshipped.md)。
- 归属:DP006 / DP023 / DP024 / DP025 / DP033 / DP036 / DP044 / DP060 / DP061 / DP062 属 **Analyzer**;其余属**生成器**。
- 文案:`messageFormat` 须含可操作建议;`description` 供 IDE 悬停;`helpLinkUri` 指向 [`DesignPatterns.Docs` diagnostics 页](https://skymly.github.io/DesignPatterns.Docs/diagnostics)(`#dp###` 片段,见 [`DiagnosticHelpLinks.cs`](DesignPatterns.Diagnostics/DiagnosticHelpLinks.cs))。
diff --git a/DesignPatterns.Diagnostics/DesignPatternsDiagnosticDescriptors.cs b/DesignPatterns.Diagnostics/DesignPatternsDiagnosticDescriptors.cs
index e3587b1..56fd507 100644
--- a/DesignPatterns.Diagnostics/DesignPatternsDiagnosticDescriptors.cs
+++ b/DesignPatterns.Diagnostics/DesignPatternsDiagnosticDescriptors.cs
@@ -335,6 +335,32 @@ public static class DesignPatternsDiagnosticDescriptors
DiagnosticSeverity.Warning,
AnalyzerCategory);
+ // Composite tree schema validation (DP063–DP065)
+
+ public static DiagnosticDescriptor CompositeTreeMaxDepthExceeded { get; } = Create(
+ DiagnosticIds.CompositeTreeMaxDepthExceeded,
+ "Composite tree max depth exceeded",
+ "Composite tree for contract '{0}' has depth {1}, exceeding the maximum depth of {2} declared by [CompositeSchema]. Consider flattening the tree structure or increasing MaxDepth.",
+ "Excessively deep composite trees can cause stack overflows in recursive traversal. Review the tree structure or adjust the MaxDepth constraint.",
+ DiagnosticSeverity.Warning,
+ GeneratorCategory);
+
+ public static DiagnosticDescriptor CompositeChildTypeNotAllowed { get; } = Create(
+ DiagnosticIds.CompositeChildTypeNotAllowed,
+ "Composite child type not allowed by parent",
+ "Composite part '{0}' (type '{1}') is not in the AllowedChildTypes of its parent '{2}' (type '{3}'). Add '{1}' to the parent's AllowedChildTypes or change the ParentKey to a compatible parent.",
+ "Parent-child type compatibility is enforced at compile time when AllowedChildTypes is specified. Ensure child implementation types are declared in the parent's AllowedChildTypes array.",
+ DiagnosticSeverity.Error,
+ GeneratorCategory);
+
+ public static DiagnosticDescriptor CompositeNodeCountExceeded { get; } = Create(
+ DiagnosticIds.CompositeNodeCountExceeded,
+ "Composite node count exceeds limit",
+ "Composite contract '{0}' has {1} parts, exceeding the maximum of {2} declared by [CompositeSchema]. Consider splitting into multiple contracts or increasing MaxNodes.",
+ "Excessive node counts may indicate architectural issues or cause performance problems in tree assembly and traversal.",
+ DiagnosticSeverity.Warning,
+ GeneratorCategory);
+
public static DiagnosticDescriptor RegistryKeyNotRegistered { get; } = Create(
DiagnosticIds.RegistryKeyNotRegistered,
"Registry key is not registered",
diff --git a/DesignPatterns.Diagnostics/DiagnosticIds.cs b/DesignPatterns.Diagnostics/DiagnosticIds.cs
index 96eb269..bbec1d0 100644
--- a/DesignPatterns.Diagnostics/DiagnosticIds.cs
+++ b/DesignPatterns.Diagnostics/DiagnosticIds.cs
@@ -67,4 +67,7 @@ public static class DiagnosticIds
public const string DiLifetimeCaptiveDependency = "DP060";
public const string DiLifetimeWasteful = "DP061";
public const string CaptiveDependency = "DP062";
+ public const string CompositeTreeMaxDepthExceeded = "DP063";
+ public const string CompositeChildTypeNotAllowed = "DP064";
+ public const string CompositeNodeCountExceeded = "DP065";
}
diff --git a/DesignPatterns.SourceGenerators/AnalyzerReleases.Unshipped.md b/DesignPatterns.SourceGenerators/AnalyzerReleases.Unshipped.md
index 3ecf931..5b1724b 100644
--- a/DesignPatterns.SourceGenerators/AnalyzerReleases.Unshipped.md
+++ b/DesignPatterns.SourceGenerators/AnalyzerReleases.Unshipped.md
@@ -64,3 +64,6 @@ DP059 | DesignPatterns.Generators | Info | StateParent parent has no chil
DP060 | DesignPatterns.Analyzers | Warning | DI captive dependency: registry lifetime exceeds implementation lifetime
DP061 | DesignPatterns.Analyzers | Info | DI lifetime mismatch: implementation lifetime exceeds registry lifetime
DP062 | DesignPatterns.Analyzers | Warning | Singleton captive dependency on Scoped/Transient service
+DP063 | DesignPatterns.Generators | Warning | Composite tree max depth exceeded
+DP064 | DesignPatterns.Generators | Error | Composite child type not allowed by parent AllowedChildTypes
+DP065 | DesignPatterns.Generators | Warning | Composite node count exceeds MaxNodes limit
diff --git a/DesignPatterns.SourceGenerators/Generators/CompositePartGenerator.cs b/DesignPatterns.SourceGenerators/Generators/CompositePartGenerator.cs
index 1036688..477e9f2 100644
--- a/DesignPatterns.SourceGenerators/Generators/CompositePartGenerator.cs
+++ b/DesignPatterns.SourceGenerators/Generators/CompositePartGenerator.cs
@@ -41,6 +41,15 @@ public sealed class CompositePartGenerator : IIncrementalGenerator
private static readonly DiagnosticDescriptor MissingBuildableDescriptor =
DesignPatternsDiagnosticDescriptors.CompositePartMissingBuildable;
+ private static readonly DiagnosticDescriptor TreeMaxDepthExceededDescriptor =
+ DesignPatternsDiagnosticDescriptors.CompositeTreeMaxDepthExceeded;
+
+ private static readonly DiagnosticDescriptor ChildTypeNotAllowedDescriptor =
+ DesignPatternsDiagnosticDescriptors.CompositeChildTypeNotAllowed;
+
+ private static readonly DiagnosticDescriptor NodeCountExceededDescriptor =
+ DesignPatternsDiagnosticDescriptors.CompositeNodeCountExceeded;
+
///
public void Initialize(IncrementalGeneratorInitializationContext context)
{
@@ -109,6 +118,7 @@ private static Result Transform(GeneratorAttributeSyntaxC
string? parentKey = null;
var order = 0;
+ EquatableArray allowedChildTypes = default;
foreach (var named in attribute.NamedArguments)
{
if (named.Key == "ParentKey" && named.Value.Value is string parent)
@@ -119,6 +129,10 @@ private static Result Transform(GeneratorAttributeSyntaxC
{
order = orderValue;
}
+ else if (named.Key == "AllowedChildTypes" && !named.Value.IsNull)
+ {
+ allowedChildTypes = ExtractAllowedChildTypes(named.Value);
+ }
}
var contractInfo = new ContractInfo(
@@ -129,6 +143,8 @@ private static Result Transform(GeneratorAttributeSyntaxC
: contract.ContainingNamespace.ToDisplayString(),
contract.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+ var (schemaMaxDepth, schemaMaxNodes, schemaLocation) = ExtractSchemaInfo(contract);
+
return Result.Success(new CompositeRegistration(
key!,
parentKey,
@@ -142,7 +158,11 @@ private static Result Transform(GeneratorAttributeSyntaxC
ImplementsContract(implementation, contract),
HasPublicParameterlessConstructor(implementation),
ImplementsBuildable(implementation, contract),
- new LocationInfo(context.TargetNode.GetLocation())));
+ new LocationInfo(context.TargetNode.GetLocation()),
+ allowedChildTypes,
+ schemaMaxDepth,
+ schemaMaxNodes,
+ schemaLocation));
}
return Result.Empty;
@@ -171,6 +191,7 @@ private static void Execute(
ReportContractMismatches(context, contractRegistrations);
ReportMissingConstructors(context, contractRegistrations);
ReportMissingBuildable(context, contractRegistrations, contract);
+ ReportSchemaViolations(context, contractRegistrations, contract);
var valid = contractRegistrations
.Where(static r => r.ImplementsContract)
@@ -279,6 +300,219 @@ private static void ReportMissingBuildable(
}
}
+ private static void ReportSchemaViolations(
+ SourceProductionContext context,
+ List registrations,
+ ContractInfo contract)
+ {
+ // All registrations for the same contract share the same schema info
+ // (extracted from [CompositeSchema] on the contract type).
+ var schemaMaxDepth = registrations.FirstOrDefault()?.SchemaMaxDepth;
+ var schemaMaxNodes = registrations.FirstOrDefault()?.SchemaMaxNodes;
+ var schemaLocation = registrations.FirstOrDefault()?.SchemaLocation;
+
+ // DP065: Node count exceeded
+ if (schemaMaxNodes is { } maxNodes and > 0 && registrations.Count > maxNodes)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ NodeCountExceededDescriptor,
+ schemaLocation?.ToLocation() ?? Location.None,
+ contract.FullyQualifiedName,
+ registrations.Count,
+ maxNodes));
+ }
+
+ // DP063: Max depth exceeded
+ if (schemaMaxDepth is { } maxDepth and > 0)
+ {
+ var actualDepth = ComputeMaxDepth(registrations);
+ if (actualDepth > maxDepth)
+ {
+ // Report on the deepest leaf node
+ var deepestLeaf = FindDeepestLeaf(registrations, maxDepth);
+ context.ReportDiagnostic(Diagnostic.Create(
+ TreeMaxDepthExceededDescriptor,
+ deepestLeaf?.Location.ToLocation() ?? Location.None,
+ contract.FullyQualifiedName,
+ actualDepth,
+ maxDepth));
+ }
+ }
+
+ // DP064: Child type not allowed by parent
+ ReportDisallowedChildTypes(context, registrations, contract);
+ }
+
+ private static void ReportDisallowedChildTypes(
+ SourceProductionContext context,
+ List registrations,
+ ContractInfo contract)
+ {
+ // Build a key-to-registration lookup that tolerates duplicate keys
+ // (DP010 already reports duplicates; we just skip them here).
+ var entryByKey = new Dictionary(StringComparer.Ordinal);
+ foreach (var reg in registrations)
+ {
+ if (!entryByKey.ContainsKey(reg.Key))
+ {
+ entryByKey[reg.Key] = reg;
+ }
+ }
+
+ foreach (var registration in registrations)
+ {
+ if (registration.ParentKey is null)
+ {
+ continue;
+ }
+
+ if (!entryByKey.TryGetValue(registration.ParentKey, out var parent))
+ {
+ continue; // DP011 already handles unknown parent keys
+ }
+
+ if (parent.AllowedChildTypes.Count == 0)
+ {
+ continue; // No restriction
+ }
+
+ if (!parent.AllowedChildTypes.Contains(registration.ImplementationFullyQualifiedDisplayString))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ ChildTypeNotAllowedDescriptor,
+ registration.Location.ToLocation(),
+ registration.Key,
+ registration.ImplementationName,
+ parent.Key,
+ parent.ImplementationName));
+ }
+ }
+ }
+
+ private static int ComputeMaxDepth(List registrations)
+ {
+ // Build children-by-parent map
+ var childrenByParent = new Dictionary>(StringComparer.Ordinal);
+ var keysWithNoParent = new List();
+
+ foreach (var reg in registrations)
+ {
+ if (reg.ParentKey is null)
+ {
+ keysWithNoParent.Add(reg.Key);
+ }
+ else
+ {
+ if (!childrenByParent.TryGetValue(reg.ParentKey, out var children))
+ {
+ children = new List();
+ childrenByParent[reg.ParentKey] = children;
+ }
+ children.Add(reg.Key);
+ }
+ }
+
+ // Memoized depth computation
+ var depthCache = new Dictionary(StringComparer.Ordinal);
+
+ int Depth(string key)
+ {
+ if (depthCache.TryGetValue(key, out var cached))
+ {
+ return cached;
+ }
+
+ if (!childrenByParent.TryGetValue(key, out var children) || children.Count == 0)
+ {
+ depthCache[key] = 1;
+ return 1;
+ }
+
+ var maxChildDepth = 0;
+ foreach (var child in children)
+ {
+ maxChildDepth = Math.Max(maxChildDepth, Depth(child));
+ }
+
+ var result = 1 + maxChildDepth;
+ depthCache[key] = result;
+ return result;
+ }
+
+ var max = 0;
+ foreach (var root in keysWithNoParent)
+ {
+ max = Math.Max(max, Depth(root));
+ }
+
+ // Also handle keys that are roots but also appear as children (shouldn't happen
+ // after cycle detection, but guard anyway)
+ foreach (var reg in registrations)
+ {
+ max = Math.Max(max, Depth(reg.Key));
+ }
+
+ return max;
+ }
+
+ private static CompositeRegistration? FindDeepestLeaf(List registrations, int maxDepth)
+ {
+ var childrenByParent = new Dictionary>(StringComparer.Ordinal);
+ foreach (var reg in registrations)
+ {
+ if (reg.ParentKey is not null)
+ {
+ if (!childrenByParent.TryGetValue(reg.ParentKey, out var children))
+ {
+ children = new List();
+ childrenByParent[reg.ParentKey] = children;
+ }
+ children.Add(reg.Key);
+ }
+ }
+
+ var depthCache = new Dictionary(StringComparer.Ordinal);
+
+ int Depth(string key)
+ {
+ if (depthCache.TryGetValue(key, out var cached))
+ {
+ return cached;
+ }
+
+ if (!childrenByParent.TryGetValue(key, out var children) || children.Count == 0)
+ {
+ depthCache[key] = 1;
+ return 1;
+ }
+
+ var maxChildDepth = 0;
+ foreach (var child in children)
+ {
+ maxChildDepth = Math.Max(maxChildDepth, Depth(child));
+ }
+
+ var result = 1 + maxChildDepth;
+ depthCache[key] = result;
+ return result;
+ }
+
+ CompositeRegistration? deepest = null;
+ var deepestDepth = 0;
+
+ foreach (var reg in registrations)
+ {
+ var depth = Depth(reg.Key);
+ if (depth > deepestDepth)
+ {
+ deepestDepth = depth;
+ deepest = reg;
+ }
+ }
+
+ return deepest;
+ }
+
private static void EmitGeneratedSources(
SourceProductionContext context,
ContractInfo contract,
@@ -417,6 +651,64 @@ private static bool HasPublicParameterlessConstructor(INamedTypeSymbol implement
implementation.InstanceConstructors.Any(static c =>
c.Parameters.IsEmpty && c.DeclaredAccessibility == Accessibility.Public);
+ private static EquatableArray ExtractAllowedChildTypes(TypedConstant value)
+ {
+ if (value.IsNull || value.Values.IsDefaultOrEmpty)
+ {
+ return default;
+ }
+
+ var types = new List(value.Values.Length);
+ foreach (var element in value.Values)
+ {
+ if (element.Value is INamedTypeSymbol typeSymbol)
+ {
+ types.Add(typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+ }
+ }
+
+ return new EquatableArray(types.ToArray());
+ }
+
+ private static (int? MaxDepth, int? MaxNodes, LocationInfo? Location) ExtractSchemaInfo(INamedTypeSymbol contract)
+ {
+ foreach (var attr in contract.GetAttributes())
+ {
+ if (attr.AttributeClass is null ||
+ attr.AttributeClass.Name != "CompositeSchemaAttribute" ||
+ attr.AttributeClass.ContainingNamespace?.ToDisplayString() != "DesignPatterns.Structural")
+ {
+ continue;
+ }
+
+ int? maxDepth = null;
+ int? maxNodes = null;
+
+ foreach (var named in attr.NamedArguments)
+ {
+ if (named.Key == "MaxDepth" && named.Value.Value is int depth)
+ {
+ maxDepth = depth;
+ }
+ else if (named.Key == "MaxNodes" && named.Value.Value is int nodes)
+ {
+ maxNodes = nodes;
+ }
+ }
+
+ // Also check constructor arguments (positional)
+ for (var i = 0; i < attr.ConstructorArguments.Length; i++)
+ {
+ // CompositeSchemaAttribute has parameterless constructor; all values via named args
+ }
+
+ var location = attr.ApplicationSyntaxReference?.GetSyntax().GetLocation();
+ return (maxDepth, maxNodes, location is not null ? new LocationInfo(location) : null);
+ }
+
+ return (null, null, null);
+ }
+
private static bool ImplementsBuildable(INamedTypeSymbol implementation, INamedTypeSymbol contract)
{
foreach (var iface in implementation.AllInterfaces)
@@ -446,5 +738,9 @@ private sealed record CompositeRegistration(
bool ImplementsContract,
bool HasPublicParameterlessConstructor,
bool ImplementsBuildable,
- LocationInfo Location);
+ LocationInfo Location,
+ EquatableArray AllowedChildTypes,
+ int? SchemaMaxDepth,
+ int? SchemaMaxNodes,
+ LocationInfo? SchemaLocation);
}
diff --git a/DesignPatterns/Structural/CompositePartAttribute.TContract.cs b/DesignPatterns/Structural/CompositePartAttribute.TContract.cs
index 61346b9..d8fc592 100644
--- a/DesignPatterns/Structural/CompositePartAttribute.TContract.cs
+++ b/DesignPatterns/Structural/CompositePartAttribute.TContract.cs
@@ -39,6 +39,13 @@ public CompositePartAttribute(string key)
/// Order among siblings with the same parent. Lower values come first.
///
public int Order { get; set; }
+
+ ///
+ /// Allowed child implementation types, or to allow any
+ /// type implementing the contract. When set, the source generator validates
+ /// that each child's implementation type is in this list.
+ ///
+ public Type[]? AllowedChildTypes { get; set; }
}
#endif
diff --git a/DesignPatterns/Structural/CompositePartAttribute.cs b/DesignPatterns/Structural/CompositePartAttribute.cs
index 51cb59c..8cf0873 100644
--- a/DesignPatterns/Structural/CompositePartAttribute.cs
+++ b/DesignPatterns/Structural/CompositePartAttribute.cs
@@ -44,4 +44,11 @@ public CompositePartAttribute(string key, Type @for)
/// Order among siblings with the same parent. Lower values come first.
///
public int Order { get; set; }
+
+ ///
+ /// Allowed child implementation types, or to allow any
+ /// type implementing the contract. When set, the source generator validates
+ /// that each child's implementation type is in this list.
+ ///
+ public Type[]? AllowedChildTypes { get; set; }
}
diff --git a/DesignPatterns/Structural/CompositeSchemaAttribute.cs b/DesignPatterns/Structural/CompositeSchemaAttribute.cs
new file mode 100644
index 0000000..af3af51
--- /dev/null
+++ b/DesignPatterns/Structural/CompositeSchemaAttribute.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace DesignPatterns.Structural;
+
+///
+/// Declares compile-time tree structure constraints for a composite contract.
+/// Apply to the contract interface or base class to enable schema validation.
+///
+///
+/// When applied, the source generator validates the composite tree structure at compile time:
+/// limits tree depth, limits total node count.
+/// All constraints are opt-in — unmarked contracts behave as before.
+///
+[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+public sealed class CompositeSchemaAttribute : Attribute
+{
+ ///
+ /// Maximum tree depth (root = depth 1). 0 = no limit. Default = 0.
+ ///
+ public int MaxDepth { get; set; }
+
+ ///
+ /// Maximum total node count across all roots. 0 = no limit. Default = 0.
+ ///
+ public int MaxNodes { get; set; }
+}
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
index b16fde5..13eb5f7 100644
--- a/docs/ROADMAP.md
+++ b/docs/ROADMAP.md
@@ -20,7 +20,7 @@
| Event Aggregator | `{Event}EventHandlerRegistry` |
| State | `{StateEnum}TransitionTable`、partial `{Holder}` 便捷方法 |
-新增生成器必须沿用此命名风格(详见 [Decorator.md](Decorator.md))。诊断 ID 续接现有区段,下一个可用 ID 为 **DP056**(DP053–DP055 为 Factory async + pooling 签名/池化校验,DP050–DP052 为 Handler guard 签名校验,DP047–DP049 为 Strategy guard 签名校验,DP044–DP046 为 EventAggregator 源生成器 + Analyzer 诊断,DP042–DP043 为 Decorator DI + async 签名校验,DP040–DP041 为 Composite DI + visitor 覆盖校验,DP037–DP039 为 State entry/exit action 诊断,DP032–DP035 为 State guard 诊断,DP036 为 State 字面量边校验;ID 一经发布不复用,详见 [AGENTS.md](../AGENTS.md))。
+新增生成器必须沿用此命名风格(详见 [Decorator.md](Decorator.md))。诊断 ID 续接现有区段,下一个可用 ID 为 **DP066**(DP063–DP065 为 Composite 树 schema 校验,DP060–DP062 为 DI 生命周期校验,DP056–DP059 为 State hierarchy,DP053–DP055 为 Factory async + pooling 签名/池化校验,DP050–DP052 为 Handler guard 签名校验,DP047–DP049 为 Strategy guard 签名校验,DP044–DP046 为 EventAggregator 源生成器 + Analyzer 诊断,DP042–DP043 为 Decorator DI + async 签名校验,DP040–DP041 为 Composite DI + visitor 覆盖校验,DP037–DP039 为 State entry/exit action 诊断,DP032–DP035 为 State guard 诊断,DP036 为 State 字面量边校验;ID 一经发布不复用,详见 [AGENTS.md](../AGENTS.md))。
诊断 ID 预分配(F2+ 增强项,登记后不提前占用,实现时按序领取):
@@ -106,7 +106,7 @@
| ~~State 层级状态机~~ | ~~`[StateMachine(..., Hierarchical = true)]` 支持嵌套状态 + 通配转换,生成器展平为快表~~ — **已在 v3 实现**(v3.1 运行时 `IStateHierarchy`、v3.2 `[StateParent]` + 展平 + DP056–DP059、v3.3 LCA + action 链合成、v3.4 DI + 示例 + 文档) | ⭐⭐⭐ 与 Stateless 重叠但展示「编译期展平层级」技术 |
| ~~Composite 并行遍历~~ | ~~`TraverseParallel` / `TraverseParallelAsync` / `TraverseForestParallel` / `TraverseForestParallelAsync` + `MaxDegreeOfParallelism` + `MaxParallelDepth`~~ — **Phase 1 已实现**(BFS 同层并行 / DFS 子节点并行递归 + 深度回退 / `AggregateException` / `ConfigureAwait(false)` / `#if` TFM 分裂) | ⭐⭐ 大树场景实用;AOT 友好并行调度 |
| Composite 懒加载 | `[CompositePart(..., LazyChildren = true)]` + `AssembleAsync` + `ICompositeLazyNode` | ⭐⭐ 大树按需展开;Phase 3 独立 RFC |
-| Composite 树 schema 校验 | 编译期校验 max depth / parent-child 类型兼容性 / 节点计数 | ⭐⭐ 结构错误编译期捕获 |
+| ~~Composite 树 schema 校验~~ | ~~编译期校验 max depth / parent-child 类型兼容性 / 节点计数~~ — **已实现**(`[CompositeSchema(MaxDepth, MaxNodes)]` 契约级约束 + `[CompositePart(AllowedChildTypes)]` 节点级约束 + DP063–DP065) | ⭐⭐ 结构错误编译期捕获 |
| Decorator 组合 / 嵌套 | `DecoratorStackBuilder.Compose(otherStack)`,Analyzer 校验栈间类型兼容 | ⭐⭐ 可复用装饰器组合 |
| MSDI keyed services(.NET 8+) | 生成 keyed registration 代码(`#if NET8_0_OR_GREATER`),与 Autofac keyed 对称 | ⭐⭐ 补齐 MSDI keyed 缺口 |
| DI 健康检查 + 生命周期校验 | 生成 `IHealthCheck` 校验注册表键可解析;Analyzer 警告无效 lifetime 组合(Singleton registry + Transient impl) | ⭐⭐ 生产场景刚需 |
@@ -207,3 +207,4 @@ State 转换表 v1 已于 0.1.0-preview4 发布;v2(guard 委托、DI 集成
- F4+ DP062 过度注册修复:按模式分组特性标注类型(`RegistrationCategory`),`RegisterDi` containing type 名匹配正确类别,避免跨模式 lifetime 污染。
- F5 Composite 并行遍历 Phase 1:`TraverseParallel` / `TraverseParallelAsync` / `TraverseForestParallel` / `TraverseForestParallelAsync` + `MaxDegreeOfParallelism` + `MaxParallelDepth` + `AggregateException` + `ConfigureAwait(false)` + `#if` TFM 分裂。设计 RFC:[docs/rfc/CompositeParallelTraversal.md](rfc/CompositeParallelTraversal.md)。
- 发版 `0.2.2`:含 DP062 Singleton captive dependency 诊断(tag `v0.2.2`,稳定版)。
+- F5+ Composite 树 schema 校验:`[CompositeSchema(MaxDepth, MaxNodes)]` 契约级约束 + `[CompositePart(AllowedChildTypes)]` 节点级约束 + DP063(max depth exceeded Warning)/ DP064(child type not allowed Error)/ DP065(node count exceeded Warning)。设计 RFC:[docs/rfc/CompositeTreeSchemaValidation.md](rfc/CompositeTreeSchemaValidation.md)。
diff --git a/docs/rfc/CompositeTreeSchemaValidation.md b/docs/rfc/CompositeTreeSchemaValidation.md
new file mode 100644
index 0000000..c1236cc
--- /dev/null
+++ b/docs/rfc/CompositeTreeSchemaValidation.md
@@ -0,0 +1,287 @@
+# RFC: Composite 树 schema 校验
+
+> **状态**:设计完成,待实现
+> **日期**:2026-07-15
+> **关联**:[Composite.md](../Composite.md)、[ROADMAP.md](../ROADMAP.md)、[CompositeParallelTraversal.md](CompositeParallelTraversal.md)
+
+## 概述
+
+为 Composite 模式新增编译期树结构 schema 校验:**最大深度**、**父子类型兼容性**、**节点计数**。在源生成器阶段从 flat parent-key 映射计算树结构并报告诊断,无需运行时开销。
+
+现有 Composite 诊断(DP010–DP015, DP040–DP041)覆盖键唯一性、父键存在性、循环检测、契约实现、构造函数、DI 注册等基础校验,但**不**校验树的整体结构形态。本 RFC 补齐这一层。
+
+## 设计目标
+
+| 目标 | 说明 |
+|------|------|
+| 编译期捕获结构错误 | 深度过大可能导致栈溢出;类型不匹配的父子关系是设计错误;节点过多暗示架构问题 |
+| 零运行时开销 | 所有校验在源生成器阶段完成,不增加运行时成本 |
+| 向后兼容 | 所有新约束均为 opt-in,不标注则不校验(现有代码行为不变) |
+| 探索价值 | 展示「源生成器从 flat 声明重建树拓扑并在编译期校验」的编译期 + 运行时协同模式 |
+
+---
+
+## 一、API 设计
+
+### 1.1 `CompositeSchemaAttribute`(契约级约束)
+
+新增特性,标注在**契约接口/基类**上,声明该契约的全局树结构约束:
+
+```csharp
+[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+public sealed class CompositeSchemaAttribute : Attribute
+{
+ /// Maximum tree depth (root = depth 1). 0 = no limit. Default = 0.
+ public int MaxDepth { get; set; }
+
+ /// Maximum total node count across all roots. 0 = no limit. Default = 0.
+ public int MaxNodes { get; set; }
+}
+```
+
+**用法示例**:
+
+```csharp
+[CompositeSchema(MaxDepth = 10, MaxNodes = 500)]
+public interface IMenuNode : ICompositeNode { }
+```
+
+### 1.2 `CompositePartAttribute.AllowedChildTypes`(节点级约束)
+
+在现有 `CompositePartAttribute` 上新增可选属性,声明该节点允许的子节点实现类型:
+
+```csharp
+// 新增到现有 CompositePartAttribute
+public Type[]? AllowedChildTypes { get; set; }
+```
+
+**用法示例**:
+
+```csharp
+[CompositePart("root", typeof(IMenuNode))]
+public partial class MenuRoot : IMenuNode, ICompositeBuildable { }
+
+[CompositePart("panel", typeof(IMenuNode), ParentKey = "root", AllowedChildTypes = new[] { typeof(PanelNode), typeof(LeafNode) })]
+public partial class PanelNode : IMenuNode, ICompositeBuildable { }
+
+[CompositePart("leaf", typeof(IMenuNode), ParentKey = "panel")]
+// OK: LeafNode 的父节点 panel 允许 LeafNode
+public partial class LeafNode : IMenuNode, ICompositeBuildable { }
+
+[CompositePart("section", typeof(IMenuNode), ParentKey = "root", AllowedChildTypes = new[] { typeof(SectionNode) })]
+// ERROR: SectionNode 的父节点 root 未在 AllowedChildTypes 中声明 SectionNode
+public partial class SectionNode : IMenuNode, ICompositeBuildable { }
+```
+
+**向后兼容**:`AllowedChildTypes = null`(默认)表示不限制——任何实现契约的类型都可作为子节点,与现有行为一致。
+
+### 1.3 泛型变体同步
+
+`CompositePartAttribute` 也新增 `AllowedChildTypes` 属性。`CompositeSchemaAttribute` 不需要泛型变体(它标注在契约类型本身上,无需泛型参数)。
+
+---
+
+## 二、诊断 ID
+
+| ID | 名称 | 严重性 | 归属 | 触发条件 |
+|----|------|--------|------|----------|
+| DP063 | CompositeTreeMaxDepthExceeded | Warning | SourceGenerators | 树的实际深度超过 `[CompositeSchema(MaxDepth)]` 声明值 |
+| DP064 | CompositeChildTypeNotAllowed | Error | SourceGenerators | 子节点的实现类型不在父节点的 `AllowedChildTypes` 集合中 |
+| DP065 | CompositeNodeCountExceeded | Warning | SourceGenerators | 契约的总节点数超过 `[CompositeSchema(MaxNodes)]` 声明值 |
+
+### 2.1 诊断消息格式
+
+**DP063**:
+- Title: "Composite tree max depth exceeded"
+- Message: "Composite tree for contract '{0}' has depth {1}, exceeding the maximum depth of {2} declared by [CompositeSchema]. Consider flattening the tree structure or increasing MaxDepth."
+- Description: "Excessively deep composite trees can cause stack overflows in recursive traversal. Review the tree structure or adjust the MaxDepth constraint."
+
+**DP064**:
+- Title: "Composite child type not allowed by parent"
+- Message: "Composite part '{0}' (type '{1}') is not in the AllowedChildTypes of its parent '{2}' (type '{3}'). Add '{1}' to the parent's AllowedChildTypes or change the ParentKey to a compatible parent."
+- Description: "Parent-child type compatibility is enforced at compile time when AllowedChildTypes is specified. Ensure child implementation types are declared in the parent's AllowedChildTypes array."
+
+**DP065**:
+- Title: "Composite node count exceeds limit"
+- Message: "Composite contract '{0}' has {1} parts, exceeding the maximum of {2} declared by [CompositeSchema]. Consider splitting into multiple contracts or increasing MaxNodes."
+- Description: "Excessive node counts may indicate architectural issues or cause performance problems in tree assembly and traversal."
+
+### 2.2 诊断报告位置
+
+- **DP063**:报告在**最深的叶子节点**的 `[CompositePart]` 特性位置(指向造成超限的路径终点)
+- **DP064**:报告在**子节点**的 `[CompositePart]` 特性位置(指向不被允许的子节点)
+- **DP065**:报告在**契约类型**声明位置(`[CompositeSchema]` 标注的接口/类)
+
+---
+
+## 三、生成器实现
+
+### 3.1 数据流
+
+现有管线不变。新增校验在 `ReportDiagnostics` 阶段执行(与现有 DP010–DP012 同层):
+
+```
+ForAttributeWithMetadataName("CompositePartAttribute") ← 现有
+ForAttributeWithMetadataName("CompositePartAttribute`1") ← 现有
+ → Transform → CompositeRegistration[] ← 现有
+ → Collect + Combine ← 现有
+ → ReportDiagnostics ← 新增 schema 校验
+ → Emit ← 现有
+```
+
+### 3.2 深度计算
+
+从 flat parent-key 映射计算树深度(复用现有 `BuildParentMap` 基础设施):
+
+```csharp
+// 伪代码
+static int ComputeDepth(string key, IReadOnlyDictionary parentByKey)
+{
+ var depth = 1; // root = depth 1
+ var children = entries.Where(e => e.ParentKey == key);
+ foreach (var child in children)
+ {
+ depth = Math.Max(depth, 1 + ComputeDepth(child.Key, parentByKey));
+ }
+ return depth;
+}
+```
+
+**优化**:使用 memoization 避免重复计算(节点数 N,O(N) 时间 + O(N) 空间)。
+
+**安全**:循环检测已在 DP012 完成,深度计算不会无限递归。
+
+### 3.3 父子类型兼容性校验
+
+```csharp
+// 伪代码
+foreach (var entry in registrations)
+{
+ if (entry.ParentKey is null) continue;
+ if (!entryByKey.TryGetValue(entry.ParentKey, out var parent)) continue; // DP011 已处理
+
+ var allowedTypes = parent.AllowedChildTypes;
+ if (allowedTypes is null || allowedTypes.Length == 0) continue; // 不限制
+
+ if (!allowedTypes.Contains(entry.ImplementationType))
+ {
+ context.ReportDiagnostic(DP064, entry.Location, ...);
+ }
+}
+```
+
+**注意**:`AllowedChildTypes` 存储的是 `Type[]`,在生成器中需要从特性参数提取 `INamedTypeSymbol` 并比较完全限定名。
+
+### 3.4 节点计数
+
+```csharp
+// 伪代码
+var schemaAttr = contractType.GetAttributes()
+ .FirstOrDefault(a => a.AttributeClass?.Name == "CompositeSchemaAttribute");
+
+if (schemaAttr != null)
+{
+ var maxNodes = ExtractIntArgument(schemaAttr, "MaxNodes");
+ if (maxNodes > 0 && registrations.Count > maxNodes)
+ {
+ context.ReportDiagnostic(DP065, contractTypeLocation, ...);
+ }
+}
+```
+
+### 3.5 `CompositeSchemaAttribute` 提取
+
+`CompositeSchemaAttribute` 标注在契约接口上,不在 `[CompositePart]` 的目标类上。生成器需要额外提取:
+
+1. 在 Transform 阶段,从 `INamedTypeSymbol contractType` 的 `GetAttributes()` 查找 `CompositeSchemaAttribute`
+2. 提取 `MaxDepth` 和 `MaxNodes` 值
+3. 存入 `CompositeRegistration` 或单独的 schema 模型
+
+**模型扩展**:在 `CompositeRegistration` 中新增 `int? SchemaMaxDepth` 和 `int? SchemaMaxNodes` 字段(nullable,null = 未标注 `[CompositeSchema]`)。或者使用单独的 `CompositeSchemaInfo` 模型,按契约分组。
+
+选择后者(单独模型),避免每个 `CompositeRegistration` 重复存储:
+
+```csharp
+private sealed record CompositeSchemaInfo(
+ ContractInfo Contract,
+ int? MaxDepth,
+ int? MaxNodes,
+ LocationInfo? Location);
+```
+
+在 Combine 阶段,按 Contract 分组提取 schema 信息,与 registrations 一起传入 ReportDiagnostics。
+
+---
+
+## 四、向后兼容性
+
+| 变更 | 影响 |
+|------|------|
+| 新增 `CompositeSchemaAttribute` | 无影响——未标注时不触发任何新诊断 |
+| `CompositePartAttribute.AllowedChildTypes` 新属性 | 无影响——`null` 默认值表示不限制 |
+| 新增 DP063/DP064/DP065 | 无影响——仅在 opt-in 约束被声明时才报告 |
+
+**无破坏性变更**:所有现有 `[CompositePart]` 代码无需修改即可继续工作。
+
+---
+
+## 五、测试计划
+
+### 5.1 生成器单元测试(Verify + 诊断断言)
+
+| 测试 | 验证点 |
+|------|--------|
+| `ReportsDp063_MaxDepthExceeded` | `[CompositeSchema(MaxDepth = 2)]` + 3 层树 → DP063 Warning |
+| `ReportsDp063_NotExceeded_WhenWithinLimit` | `[CompositeSchema(MaxDepth = 5)]` + 3 层树 → 无 DP063 |
+| `ReportsDp064_ChildTypeNotAllowed` | 父节点 `AllowedChildTypes = [A, B]`,子节点类型 C → DP064 Error |
+| `ReportsDp064_Allowed_WhenTypeInList` | 父节点 `AllowedChildTypes = [A, B]`,子节点类型 B → 无 DP064 |
+| `ReportsDp064_NotChecked_WhenAllowedChildTypesNull` | 父节点未设 `AllowedChildTypes` → 无 DP064(任意类型均可) |
+| `ReportsDp065_NodeCountExceeded` | `[CompositeSchema(MaxNodes = 3)]` + 5 个节点 → DP065 Warning |
+| `ReportsDp065_NotExceeded_WhenWithinLimit` | `[CompositeSchema(MaxNodes = 10)]` + 5 个节点 → 无 DP065 |
+| `NoSchemaAttribute_NoNewDiagnostics` | 无 `[CompositeSchema]` + 深树 + 多节点 → 无 DP063/DP065 |
+| `MaxDepth_ForestMode_ComputesPerTree` | 多根森林,最深树超过限制 → DP063 报告最深树 |
+| `AllowedChildTypes_GenericAttribute` | 泛型 `[CompositePart]` + `AllowedChildTypes` → DP064 正确触发 |
+
+### 5.2 运行时测试
+
+无需新增运行时测试——所有校验在编译期完成,运行时行为不变。
+
+### 5.3 集成测试
+
+新增一个使用 `[CompositeSchema]` + `AllowedChildTypes` 的集成测试项目,验证端到端生成 + 组装 + 遍历流程。
+
+---
+
+## 六、实现范围
+
+| 文件 | 变更 |
+|------|------|
+| `DesignPatterns/Structural/CompositeSchemaAttribute.cs` | **新增** — 契约级特性 |
+| `DesignPatterns/Structural/CompositePartAttribute.cs` | **修改** — 新增 `AllowedChildTypes` 属性 |
+| `DesignPatterns/Structural/CompositePartAttribute.TContract.cs` | **修改** — 新增 `AllowedChildTypes` 属性 |
+| `DesignPatterns.Diagnostics/DiagnosticIds.cs` | **修改** — 新增 DP063/DP064/DP065 常量 |
+| `DesignPatterns.Diagnostics/DesignPatternsDiagnosticDescriptors.cs` | **修改** — 新增 3 个描述符 |
+| `DesignPatterns.SourceGenerators/Generators/CompositePartGenerator.cs` | **修改** — 提取 schema、深度计算、类型校验、节点计数 |
+| `DesignPatterns.SourceGenerators/AnalyzerReleases.Unshipped.md` | **修改** — 新增 DP063/DP064/DP065 条目 |
+| `tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.cs` | **修改** — 新增 10 个测试 |
+| `tests/DesignPatterns.Tests/Structural/CompositeSchemaAttributeTests.cs` | **新增** — 特性构造测试 |
+| `docs/Composite.md` | **修改** — 文档补充 schema 校验说明 |
+| `docs/ROADMAP.md` | **修改** — 标记完成 |
+
+**PR 边界**:跨 Runtime + Diagnostics + SourceGenerators 三个模块。按 AGENTS.md 规范,应拆分为:
+1. **Runtime PR**:`CompositeSchemaAttribute` + `CompositePartAttribute.AllowedChildTypes`
+2. **Diagnostics + SourceGenerators PR**:DP063/DP064/DP065 + 生成器校验逻辑 + 测试
+
+或合并为一个 PR(因为特性 + 诊断 + 校验紧密耦合,拆分后第一个 PR 无法独立验证)。选择合并。
+
+---
+
+## 七、探索价值
+
+本 RFC 展示以下编译期 + 运行时协同技术:
+
+1. **从 flat 声明重建树拓扑**:源生成器从 `ParentKey` 字符串引用重建完整树结构,在编译期计算深度、验证类型兼容性——无需运行时反射或动态分析
+2. **opt-in 约束的渐进式类型安全**:`AllowedChildTypes` 允许用户逐步收紧类型约束,从「任意契约实现」到「特定实现类型集合」,编译期捕获设计错误
+3. **契约级 vs 节点级约束的分层**:`CompositeSchemaAttribute`(契约级全局约束)与 `AllowedChildTypes`(节点级局部约束)正交组合,覆盖不同粒度的校验需求
+
+与 Stateless 等运行时库的对比:本方案将结构校验前移到编译期,运行时零开销;Stateless 等库在运行时动态验证状态机配置,有反射和异常成本。
diff --git a/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.NoSchemaAttributeNoNewDiagnostics.verified.txt b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.NoSchemaAttributeNoNewDiagnostics.verified.txt
new file mode 100644
index 0000000..ad47dbb
--- /dev/null
+++ b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.NoSchemaAttributeNoNewDiagnostics.verified.txt
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp063ForestModeComputesPerTree.verified.txt b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp063ForestModeComputesPerTree.verified.txt
new file mode 100644
index 0000000..5036d71
--- /dev/null
+++ b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp063ForestModeComputesPerTree.verified.txt
@@ -0,0 +1,6 @@
+[
+ {
+ Id: DP063,
+ Message: Composite tree for contract 'TestAssembly.IMenuNode' has depth 3, exceeding the maximum depth of 2 declared by [CompositeSchema]. Consider flattening the tree structure or increasing MaxDepth.
+ }
+]
\ No newline at end of file
diff --git a/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp063MaxDepthExceeded.verified.txt b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp063MaxDepthExceeded.verified.txt
new file mode 100644
index 0000000..5036d71
--- /dev/null
+++ b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp063MaxDepthExceeded.verified.txt
@@ -0,0 +1,6 @@
+[
+ {
+ Id: DP063,
+ Message: Composite tree for contract 'TestAssembly.IMenuNode' has depth 3, exceeding the maximum depth of 2 declared by [CompositeSchema]. Consider flattening the tree structure or increasing MaxDepth.
+ }
+]
\ No newline at end of file
diff --git a/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp063NotExceededWhenWithinLimit.verified.txt b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp063NotExceededWhenWithinLimit.verified.txt
new file mode 100644
index 0000000..ad47dbb
--- /dev/null
+++ b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp063NotExceededWhenWithinLimit.verified.txt
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp064AllowedWhenTypeInList.verified.txt b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp064AllowedWhenTypeInList.verified.txt
new file mode 100644
index 0000000..ad47dbb
--- /dev/null
+++ b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp064AllowedWhenTypeInList.verified.txt
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp064ChildTypeNotAllowed.verified.txt b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp064ChildTypeNotAllowed.verified.txt
new file mode 100644
index 0000000..27e45a7
--- /dev/null
+++ b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp064ChildTypeNotAllowed.verified.txt
@@ -0,0 +1,6 @@
+[
+ {
+ Id: DP064,
+ Message: Composite part 'disallowed' (type 'DisallowedMenu') is not in the AllowedChildTypes of its parent 'root' (type 'RootMenu'). Add 'DisallowedMenu' to the parent's AllowedChildTypes or change the ParentKey to a compatible parent.
+ }
+]
\ No newline at end of file
diff --git a/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp064NotCheckedWhenAllowedChildTypesNull.verified.txt b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp064NotCheckedWhenAllowedChildTypesNull.verified.txt
new file mode 100644
index 0000000..ad47dbb
--- /dev/null
+++ b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp064NotCheckedWhenAllowedChildTypesNull.verified.txt
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp064WithNonGenericAttribute.verified.txt b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp064WithNonGenericAttribute.verified.txt
new file mode 100644
index 0000000..27e45a7
--- /dev/null
+++ b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp064WithNonGenericAttribute.verified.txt
@@ -0,0 +1,6 @@
+[
+ {
+ Id: DP064,
+ Message: Composite part 'disallowed' (type 'DisallowedMenu') is not in the AllowedChildTypes of its parent 'root' (type 'RootMenu'). Add 'DisallowedMenu' to the parent's AllowedChildTypes or change the ParentKey to a compatible parent.
+ }
+]
\ No newline at end of file
diff --git a/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp065NodeCountExceeded.verified.txt b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp065NodeCountExceeded.verified.txt
new file mode 100644
index 0000000..ea037e4
--- /dev/null
+++ b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp065NodeCountExceeded.verified.txt
@@ -0,0 +1,6 @@
+[
+ {
+ Id: DP065,
+ Message: Composite contract 'TestAssembly.IMenuNode' has 5 parts, exceeding the maximum of 3 declared by [CompositeSchema]. Consider splitting into multiple contracts or increasing MaxNodes.
+ }
+]
\ No newline at end of file
diff --git a/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp065NotExceededWhenWithinLimit.verified.txt b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp065NotExceededWhenWithinLimit.verified.txt
new file mode 100644
index 0000000..ad47dbb
--- /dev/null
+++ b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.ReportsDp065NotExceededWhenWithinLimit.verified.txt
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.cs b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.cs
index e0192a4..b034cbc 100644
--- a/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.cs
+++ b/tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.cs
@@ -304,4 +304,457 @@ public sealed class SettingsMenu : IMenuNode, ICompositeBuildable
return Verifier.Verify(SourceGeneratorTestContext.GetGeneratedSources(runResult));
}
+
+ [Fact]
+ public Task ReportsDp063MaxDepthExceeded()
+ {
+ var source = MenuUsings + """
+ namespace TestAssembly;
+
+ [CompositeSchema(MaxDepth = 2)]
+ public interface IMenuNode : ICompositeNode
+ {
+ string Title { get; }
+ }
+ """ + """
+
+ [CompositePart("root")]
+ public sealed class RootMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Root";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("mid", ParentKey = "root")]
+ public sealed class MidMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Mid";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("leaf", ParentKey = "mid")]
+ public sealed class LeafMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Leaf";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+ """;
+
+ var runResult = SourceGeneratorTestContext.Run(
+ ("Menus.cs", source));
+
+ return Verifier.Verify(SourceGeneratorTestContext.GetGeneratorDiagnostics(runResult));
+ }
+
+ [Fact]
+ public Task ReportsDp063NotExceededWhenWithinLimit()
+ {
+ var source = MenuUsings + """
+ namespace TestAssembly;
+
+ [CompositeSchema(MaxDepth = 5)]
+ public interface IMenuNode : ICompositeNode
+ {
+ string Title { get; }
+ }
+ """ + """
+
+ [CompositePart("root")]
+ public sealed class RootMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Root";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("child", ParentKey = "root")]
+ public sealed class ChildMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Child";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+ """;
+
+ var runResult = SourceGeneratorTestContext.Run(
+ ("Menus.cs", source));
+
+ return Verifier.Verify(SourceGeneratorTestContext.GetGeneratorDiagnostics(runResult));
+ }
+
+ [Fact]
+ public Task ReportsDp064ChildTypeNotAllowed()
+ {
+ var source = MenuUsings + """
+ namespace TestAssembly;
+
+ public interface IMenuNode : ICompositeNode
+ {
+ string Title { get; }
+ }
+ """ + """
+
+ [CompositePart("root", AllowedChildTypes = new[] { typeof(AllowedMenu) })]
+ public sealed class RootMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Root";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("allowed", ParentKey = "root")]
+ public sealed class AllowedMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Allowed";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("disallowed", ParentKey = "root")]
+ public sealed class DisallowedMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Disallowed";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+ """;
+
+ var runResult = SourceGeneratorTestContext.Run(
+ ("Menus.cs", source));
+
+ return Verifier.Verify(SourceGeneratorTestContext.GetGeneratorDiagnostics(runResult));
+ }
+
+ [Fact]
+ public Task ReportsDp064AllowedWhenTypeInList()
+ {
+ var source = MenuUsings + """
+ namespace TestAssembly;
+
+ public interface IMenuNode : ICompositeNode
+ {
+ string Title { get; }
+ }
+ """ + """
+
+ [CompositePart("root", AllowedChildTypes = new[] { typeof(ChildMenu) })]
+ public sealed class RootMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Root";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("child", ParentKey = "root")]
+ public sealed class ChildMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Child";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+ """;
+
+ var runResult = SourceGeneratorTestContext.Run(
+ ("Menus.cs", source));
+
+ return Verifier.Verify(SourceGeneratorTestContext.GetGeneratorDiagnostics(runResult));
+ }
+
+ [Fact]
+ public Task ReportsDp064NotCheckedWhenAllowedChildTypesNull()
+ {
+ var source = MenuUsings + MenuInterface + """
+
+ [CompositePart("root")]
+ public sealed class RootMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Root";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("child", ParentKey = "root")]
+ public sealed class ChildMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Child";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+ """;
+
+ var runResult = SourceGeneratorTestContext.Run(
+ ("Menus.cs", source));
+
+ return Verifier.Verify(SourceGeneratorTestContext.GetGeneratorDiagnostics(runResult));
+ }
+
+ [Fact]
+ public Task ReportsDp065NodeCountExceeded()
+ {
+ var source = MenuUsings + """
+ namespace TestAssembly;
+
+ [CompositeSchema(MaxNodes = 3)]
+ public interface IMenuNode : ICompositeNode
+ {
+ string Title { get; }
+ }
+ """ + """
+
+ [CompositePart("root")]
+ public sealed class RootMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Root";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("a", ParentKey = "root")]
+ public sealed class MenuA : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "A";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("b", ParentKey = "root")]
+ public sealed class MenuB : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "B";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("c", ParentKey = "root")]
+ public sealed class MenuC : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "C";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("d", ParentKey = "root")]
+ public sealed class MenuD : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "D";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+ """;
+
+ var runResult = SourceGeneratorTestContext.Run(
+ ("Menus.cs", source));
+
+ return Verifier.Verify(SourceGeneratorTestContext.GetGeneratorDiagnostics(runResult));
+ }
+
+ [Fact]
+ public Task ReportsDp065NotExceededWhenWithinLimit()
+ {
+ var source = MenuUsings + """
+ namespace TestAssembly;
+
+ [CompositeSchema(MaxNodes = 10)]
+ public interface IMenuNode : ICompositeNode
+ {
+ string Title { get; }
+ }
+ """ + """
+
+ [CompositePart("root")]
+ public sealed class RootMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Root";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("a", ParentKey = "root")]
+ public sealed class MenuA : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "A";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+ """;
+
+ var runResult = SourceGeneratorTestContext.Run(
+ ("Menus.cs", source));
+
+ return Verifier.Verify(SourceGeneratorTestContext.GetGeneratorDiagnostics(runResult));
+ }
+
+ [Fact]
+ public Task NoSchemaAttributeNoNewDiagnostics()
+ {
+ // Deep tree, many nodes, but no [CompositeSchema] → no DP063/DP065
+ var source = MenuUsings + MenuInterface + """
+
+ [CompositePart("root")]
+ public sealed class RootMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Root";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("l1", ParentKey = "root")]
+ public sealed class Level1 : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "L1";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("l2", ParentKey = "l1")]
+ public sealed class Level2 : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "L2";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("l3", ParentKey = "l2")]
+ public sealed class Level3 : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "L3";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+ """;
+
+ var runResult = SourceGeneratorTestContext.Run(
+ ("Menus.cs", source));
+
+ return Verifier.Verify(SourceGeneratorTestContext.GetGeneratorDiagnostics(runResult));
+ }
+
+ [Fact]
+ public Task ReportsDp063ForestModeComputesPerTree()
+ {
+ // Two roots: one shallow, one deep → DP063 on the deep one
+ var source = MenuUsings + """
+ namespace TestAssembly;
+
+ [CompositeSchema(MaxDepth = 2)]
+ public interface IMenuNode : ICompositeNode
+ {
+ string Title { get; }
+ }
+ """ + """
+
+ [CompositePart("shallow-root")]
+ public sealed class ShallowRoot : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Shallow";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("deep-root")]
+ public sealed class DeepRoot : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Deep";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("mid", ParentKey = "deep-root")]
+ public sealed class MidMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Mid";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("leaf", ParentKey = "mid")]
+ public sealed class LeafMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Leaf";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+ """;
+
+ var runResult = SourceGeneratorTestContext.Run(
+ ("Menus.cs", source));
+
+ return Verifier.Verify(SourceGeneratorTestContext.GetGeneratorDiagnostics(runResult));
+ }
+
+ [Fact]
+ public Task ReportsDp064WithNonGenericAttribute()
+ {
+ var source = MenuUsings + """
+ namespace TestAssembly;
+
+ public interface IMenuNode : ICompositeNode
+ {
+ string Title { get; }
+ }
+ """ + """
+
+ [CompositePart("root", typeof(IMenuNode), AllowedChildTypes = new[] { typeof(AllowedMenu) })]
+ public sealed class RootMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Root";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("allowed", typeof(IMenuNode), ParentKey = "root")]
+ public sealed class AllowedMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Allowed";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+
+ [CompositePart("disallowed", typeof(IMenuNode), ParentKey = "root")]
+ public sealed class DisallowedMenu : IMenuNode, ICompositeBuildable
+ {
+ private IReadOnlyList _children = System.Array.Empty();
+ public string Title => "Disallowed";
+ public IReadOnlyList Children => _children;
+ public void SetChildren(IReadOnlyList children) => _children = children;
+ }
+ """;
+
+ var runResult = SourceGeneratorTestContext.Run(
+ ("Menus.cs", source));
+
+ return Verifier.Verify(SourceGeneratorTestContext.GetGeneratorDiagnostics(runResult));
+ }
}