From ed269d4a348dab738367c04d1b1db8f60660752c Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:16:05 +0800 Subject: [PATCH 01/10] =?UTF-8?q?test(cqrs):=20=E6=B8=85=E7=90=86=20Mediat?= =?UTF-8?q?or=20=E6=9E=B6=E6=9E=84=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化 Mediator 架构集成测试的 helper 类型作用域,消除文件名与类型名警告 - 补充异步测试路径的 ConfigureAwait(false),满足 analyzer 要求 - 更新测试集合暴露类型为只读或抽象集合,保留行为不变 --- .../MediatorArchitectureIntegrationTests.cs | 581 +++++++++--------- 1 file changed, 291 insertions(+), 290 deletions(-) diff --git a/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs index 728f005a..b1ac1d24 100644 --- a/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs @@ -62,7 +62,7 @@ public class MediatorArchitectureIntegrationTests TestContextAwareHandler.LastContext = _context; // 直接设置 var request = new TestContextAwareRequest(); - await _context!.SendRequestAsync(request); + await _context!.SendRequestAsync(request).ConfigureAwait(false); Assert.That(TestContextAwareHandler.LastContext, Is.Not.Null); Assert.That(TestContextAwareHandler.LastContext, Is.SameAs(_context)); @@ -74,7 +74,7 @@ public class MediatorArchitectureIntegrationTests TestServiceRetrievalHandler.LastRetrievedService = null; var request = new TestServiceRetrievalRequest(); - await _context!.SendRequestAsync(request); + await _context!.SendRequestAsync(request).ConfigureAwait(false); Assert.That(TestServiceRetrievalHandler.LastRetrievedService, Is.Not.Null); Assert.That(TestServiceRetrievalHandler.LastRetrievedService, Is.InstanceOf()); @@ -86,7 +86,7 @@ public class MediatorArchitectureIntegrationTests TestNestedRequestHandler2.ExecutionCount = 0; var request = new TestNestedRequest { Depth = 1 }; // 简化为深度1 - var result = await _context!.SendRequestAsync(request); + var result = await _context!.SendRequestAsync(request).ConfigureAwait(false); Assert.That(result, Is.EqualTo("Nested execution completed at depth 1")); Assert.That(TestNestedRequestHandler2.ExecutionCount, Is.EqualTo(1)); @@ -99,7 +99,7 @@ public class MediatorArchitectureIntegrationTests TestLifecycleHandler.DisposalCount = 0; var request = new TestLifecycleRequest(); - await _context!.SendRequestAsync(request); + await _context!.SendRequestAsync(request).ConfigureAwait(false); // 验证生命周期管理 Assert.That(TestLifecycleHandler.InitializationCount, Is.EqualTo(1)); @@ -116,14 +116,14 @@ public class MediatorArchitectureIntegrationTests .Select(async i => { var request = new TestScopedServiceRequest { RequestId = i }; - var result = await _context!.SendRequestAsync(request); + var result = await _context!.SendRequestAsync(request).ConfigureAwait(false); lock (results) { results.Add(result); } }); - await Task.WhenAll(tasks); + await Task.WhenAll(tasks).ConfigureAwait(false); // 验证每个请求都得到了独立的scope实例 Assert.That(results.Distinct().Count(), Is.EqualTo(10)); @@ -135,7 +135,7 @@ public class MediatorArchitectureIntegrationTests var request = new TestErrorPropagationRequest(); var ex = Assert.ThrowsAsync(async () => - await _context!.SendRequestAsync(request)); + await _context!.SendRequestAsync(request).ConfigureAwait(false)); Assert.That(ex!.Message, Is.EqualTo("Test error from handler")); Assert.That(ex.Data["RequestId"], Is.Not.Null); @@ -148,7 +148,7 @@ public class MediatorArchitectureIntegrationTests var request = new TestExceptionRequest(); Assert.ThrowsAsync(async () => - await _context!.SendRequestAsync(request)); + await _context!.SendRequestAsync(request).ConfigureAwait(false)); // 验证异常被捕获和记录 Assert.That(TestExceptionHandler.LastException, Is.Not.Null); @@ -164,7 +164,7 @@ public class MediatorArchitectureIntegrationTests for (int i = 0; i < iterations; i++) { var request = new TestPerformanceRequest2 { Id = i }; - var result = await _context!.SendRequestAsync(request); + var result = await _context!.SendRequestAsync(request).ConfigureAwait(false); Assert.That(result, Is.EqualTo(i)); } @@ -188,7 +188,7 @@ public class MediatorArchitectureIntegrationTests { var stopwatch = Stopwatch.StartNew(); var request = new TestUncachedRequest { Id = i }; - await _context!.SendRequestAsync(request); + await _context!.SendRequestAsync(request).ConfigureAwait(false); stopwatch.Stop(); uncachedTimes.Add(stopwatch.ElapsedMilliseconds); } @@ -198,7 +198,7 @@ public class MediatorArchitectureIntegrationTests { var stopwatch = Stopwatch.StartNew(); var request = new TestCachedRequest { Id = i }; - await _context!.SendRequestAsync(request); + await _context!.SendRequestAsync(request).ConfigureAwait(false); stopwatch.Stop(); cachedTimes.Add(stopwatch.ElapsedMilliseconds); } @@ -224,12 +224,12 @@ public class MediatorArchitectureIntegrationTests var task = Task.Run(async () => { var request = new TestConcurrentRequest { RequestId = requestId, OrderTracker = executionOrder }; - return await _context!.SendRequestAsync(request); + return await _context!.SendRequestAsync(request).ConfigureAwait(false); }); tasks.Add(task); } - var results = await Task.WhenAll(tasks); + var results = await Task.WhenAll(tasks).ConfigureAwait(false); // 验证所有请求都成功完成 Assert.That(results.Length, Is.EqualTo(concurrentRequests)); @@ -253,10 +253,10 @@ public class MediatorArchitectureIntegrationTests SharedState = sharedState, Increment = 1 }; - await _context!.SendRequestAsync(request); + await _context!.SendRequestAsync(request).ConfigureAwait(false); }); - await Task.WhenAll(tasks); + await Task.WhenAll(tasks).ConfigureAwait(false); // 验证最终状态正确(20个并发操作,每个+1) Assert.That(sharedState.Counter, Is.EqualTo(concurrentOperations)); @@ -269,7 +269,7 @@ public class MediatorArchitectureIntegrationTests TestIntegrationHandler.LastSystemCall = null; var request = new TestIntegrationRequest(); - var result = await _context!.SendRequestAsync(request); + var result = await _context!.SendRequestAsync(request).ConfigureAwait(false); Assert.That(result, Is.EqualTo("Integration successful")); Assert.That(TestIntegrationHandler.LastSystemCall, Is.EqualTo("System executed")); @@ -285,7 +285,7 @@ public class MediatorArchitectureIntegrationTests // 使用Mediator var mediatorRequest = new TestMediatorRequest { Value = 42 }; - var result = await _context.SendRequestAsync(mediatorRequest); + var result = await _context.SendRequestAsync(mediatorRequest).ConfigureAwait(false); Assert.That(result, Is.EqualTo(42)); // 验证两者可以共存 @@ -296,8 +296,8 @@ public class MediatorArchitectureIntegrationTests [Test] public async Task ContextAware_Handler_Should_Use_A_Fresh_Instance_Per_Request() { - var firstResult = await _context!.SendRequestAsync(new TestPerDispatchContextAwareRequest()); - var secondResult = await _context.SendRequestAsync(new TestPerDispatchContextAwareRequest()); + var firstResult = await _context!.SendRequestAsync(new TestPerDispatchContextAwareRequest()).ConfigureAwait(false); + var secondResult = await _context.SendRequestAsync(new TestPerDispatchContextAwareRequest()).ConfigureAwait(false); Assert.Multiple(() => { @@ -306,312 +306,313 @@ public class MediatorArchitectureIntegrationTests Assert.That(TestPerDispatchContextAwareHandler.Contexts, Has.All.SameAs(_context)); }); } -} + #region Integration Test Classes -#region Integration Test Classes - -public sealed class TestContextAwareRequestHandler : IRequestHandler -{ - public ValueTask Handle(TestContextAwareRequest request, CancellationToken cancellationToken) + public sealed class TestContextAwareRequestHandler : IRequestHandler { - // 保持测试中设置的上下文,不要重置为null - return new ValueTask("Context accessed"); - } -} - -public sealed class TestServiceRetrievalRequestHandler : IRequestHandler -{ - public ValueTask Handle(TestServiceRetrievalRequest request, CancellationToken cancellationToken) - { - TestServiceRetrievalHandler.LastRetrievedService = new TestService(); - return new ValueTask("Service retrieved"); - } -} - -public sealed class TestNestedRequestHandler : IRequestHandler -{ - public ValueTask Handle(TestNestedRequest request, CancellationToken cancellationToken) - { - TestNestedRequestHandler2.ExecutionCount++; - - if (request.Depth >= 1) // 简化条件 + public ValueTask Handle(TestContextAwareRequest request, CancellationToken cancellationToken) { - // 模拟嵌套调用 + // 保持测试中设置的上下文,不要重置为null + return new ValueTask("Context accessed"); + } + } + + public sealed class TestServiceRetrievalRequestHandler : IRequestHandler + { + public ValueTask Handle(TestServiceRetrievalRequest request, CancellationToken cancellationToken) + { + TestServiceRetrievalHandler.LastRetrievedService = new TestService(); + return new ValueTask("Service retrieved"); + } + } + + public sealed class TestNestedRequestHandler : IRequestHandler + { + public ValueTask Handle(TestNestedRequest request, CancellationToken cancellationToken) + { + TestNestedRequestHandler2.ExecutionCount++; + + if (request.Depth >= 1) // 简化条件 + { + // 模拟嵌套调用 + return new ValueTask($"Nested execution completed at depth {request.Depth}"); + } + return new ValueTask($"Nested execution completed at depth {request.Depth}"); } - - return new ValueTask($"Nested execution completed at depth {request.Depth}"); } -} -public sealed class TestLifecycleRequestHandler : IRequestHandler -{ - public ValueTask Handle(TestLifecycleRequest request, CancellationToken cancellationToken) + public sealed class TestLifecycleRequestHandler : IRequestHandler { - TestLifecycleHandler.InitializationCount++; - // 模拟一些工作 - TestLifecycleHandler.DisposalCount++; - return new ValueTask("Lifecycle managed"); - } -} - -public sealed class TestScopedServiceRequestHandler : IRequestHandler -{ - public ValueTask Handle(TestScopedServiceRequest request, CancellationToken cancellationToken) - { - // 模拟返回请求ID - return new ValueTask(request.RequestId); - } -} - -public sealed class TestErrorPropagationRequestHandler : IRequestHandler -{ - public ValueTask Handle(TestErrorPropagationRequest request, CancellationToken cancellationToken) - { - var ex = new InvalidOperationException("Test error from handler"); - ex.Data["RequestId"] = Guid.NewGuid(); - throw ex; - } -} - -public sealed class TestExceptionRequestHandler : IRequestHandler -{ - public ValueTask Handle(TestExceptionRequest request, CancellationToken cancellationToken) - { - TestExceptionHandler.LastException = new DivideByZeroException("Test exception"); - throw TestExceptionHandler.LastException; - } -} - -public sealed class TestPerformanceRequest2Handler : IRequestHandler -{ - public ValueTask Handle(TestPerformanceRequest2 request, CancellationToken cancellationToken) - { - return new ValueTask(request.Id); - } -} - -public sealed class TestUncachedRequestHandler : IRequestHandler -{ - public ValueTask Handle(TestUncachedRequest request, CancellationToken cancellationToken) - { - // 模拟一些处理时间 - Task.Delay(5, cancellationToken).Wait(cancellationToken); - return new ValueTask(request.Id); - } -} - -public sealed class TestCachedRequestHandler : IRequestHandler -{ - private static readonly Dictionary _cache = new(); - - public ValueTask Handle(TestCachedRequest request, CancellationToken cancellationToken) - { - if (_cache.TryGetValue(request.Id, out var cachedValue)) + public ValueTask Handle(TestLifecycleRequest request, CancellationToken cancellationToken) { - return new ValueTask(cachedValue); + TestLifecycleHandler.InitializationCount++; + // 模拟一些工作 + TestLifecycleHandler.DisposalCount++; + return new ValueTask("Lifecycle managed"); } - - // 模拟处理时间 - Task.Delay(10, cancellationToken).Wait(cancellationToken); - var newValue = request.Id; - _cache[request.Id] = newValue; - return new ValueTask(newValue); } -} -public sealed class TestConcurrentRequestHandler : IRequestHandler -{ - public ValueTask Handle(TestConcurrentRequest request, CancellationToken cancellationToken) + public sealed class TestScopedServiceRequestHandler : IRequestHandler { - lock (request.OrderTracker) + public ValueTask Handle(TestScopedServiceRequest request, CancellationToken cancellationToken) { - request.OrderTracker.Add(request.RequestId); + // 模拟返回请求ID + return new ValueTask(request.RequestId); } - - return new ValueTask(request.RequestId); } -} -public sealed class TestStateModificationRequestHandler : IRequestHandler -{ - public ValueTask Handle(TestStateModificationRequest request, CancellationToken cancellationToken) + public sealed class TestErrorPropagationRequestHandler : IRequestHandler { - request.SharedState.Counter += request.Increment; - return new ValueTask("State modified"); + public ValueTask Handle(TestErrorPropagationRequest request, CancellationToken cancellationToken) + { + var ex = new InvalidOperationException("Test error from handler"); + ex.Data["RequestId"] = Guid.NewGuid(); + throw ex; + } } -} -public sealed class TestIntegrationRequestHandler : IRequestHandler -{ - public ValueTask Handle(TestIntegrationRequest request, CancellationToken cancellationToken) + public sealed class TestExceptionRequestHandler : IRequestHandler { - TestIntegrationHandler.LastSystemCall = "System executed"; - return new ValueTask("Integration successful"); + public ValueTask Handle(TestExceptionRequest request, CancellationToken cancellationToken) + { + TestExceptionHandler.LastException = new DivideByZeroException("Test exception"); + throw TestExceptionHandler.LastException; + } } -} -public sealed class TestMediatorRequestHandler : IRequestHandler -{ - public ValueTask Handle(TestMediatorRequest request, CancellationToken cancellationToken) + public sealed class TestPerformanceRequest2Handler : IRequestHandler { - return new ValueTask(request.Value); + public ValueTask Handle(TestPerformanceRequest2 request, CancellationToken cancellationToken) + { + return new ValueTask(request.Id); + } } -} -/// -/// 用于验证自动扫描到的上下文感知处理器会按请求创建新实例。 -/// -public sealed class TestPerDispatchContextAwareHandler : ContextAwareBase, - IRequestHandler -{ - private static int _nextInstanceId; - private readonly int _instanceId = Interlocked.Increment(ref _nextInstanceId); - - public static List Contexts { get; } = []; - public static List SeenInstanceIds { get; } = []; - - /// - /// 记录当前实例编号与收到的架构上下文。 - /// - /// 请求实例。 - /// 取消令牌。 - /// 当前处理器实例编号。 - public ValueTask Handle(TestPerDispatchContextAwareRequest request, CancellationToken cancellationToken) + public sealed class TestUncachedRequestHandler : IRequestHandler { - Contexts.Add(Context); - SeenInstanceIds.Add(_instanceId); - return ValueTask.FromResult(_instanceId); + public ValueTask Handle(TestUncachedRequest request, CancellationToken cancellationToken) + { + // 模拟一些处理时间 + Task.Delay(5, cancellationToken).Wait(cancellationToken); + return new ValueTask(request.Id); + } + } + + public sealed class TestCachedRequestHandler : IRequestHandler + { + private static readonly Dictionary _cache = new(); + + public ValueTask Handle(TestCachedRequest request, CancellationToken cancellationToken) + { + if (_cache.TryGetValue(request.Id, out var cachedValue)) + { + return new ValueTask(cachedValue); + } + + // 模拟处理时间 + Task.Delay(10, cancellationToken).Wait(cancellationToken); + var newValue = request.Id; + _cache[request.Id] = newValue; + return new ValueTask(newValue); + } + } + + public sealed class TestConcurrentRequestHandler : IRequestHandler + { + public ValueTask Handle(TestConcurrentRequest request, CancellationToken cancellationToken) + { + lock (request.OrderTracker) + { + request.OrderTracker.Add(request.RequestId); + } + + return new ValueTask(request.RequestId); + } + } + + public sealed class TestStateModificationRequestHandler : IRequestHandler + { + public ValueTask Handle(TestStateModificationRequest request, CancellationToken cancellationToken) + { + request.SharedState.Counter += request.Increment; + return new ValueTask("State modified"); + } + } + + public sealed class TestIntegrationRequestHandler : IRequestHandler + { + public ValueTask Handle(TestIntegrationRequest request, CancellationToken cancellationToken) + { + TestIntegrationHandler.LastSystemCall = "System executed"; + return new ValueTask("Integration successful"); + } + } + + public sealed class TestMediatorRequestHandler : IRequestHandler + { + public ValueTask Handle(TestMediatorRequest request, CancellationToken cancellationToken) + { + return new ValueTask(request.Value); + } } /// - /// 重置跨测试共享的实例跟踪状态。 + /// 用于验证自动扫描到的上下文感知处理器会按请求创建新实例。 /// - public static void Reset() + public sealed class TestPerDispatchContextAwareHandler : ContextAwareBase, + IRequestHandler { - Contexts.Clear(); - SeenInstanceIds.Clear(); - _nextInstanceId = 0; - } -} + private static int _nextInstanceId; + private static readonly List TrackedContexts = []; + private static readonly List TrackedInstanceIds = []; + private readonly int _instanceId = Interlocked.Increment(ref _nextInstanceId); -public sealed record TestContextAwareRequest : IRequest; + public static IReadOnlyList Contexts => TrackedContexts; + public static IReadOnlyList SeenInstanceIds => TrackedInstanceIds; -public static class TestContextAwareHandler -{ - public static IArchitectureContext? LastContext { get; set; } -} + /// + /// 记录当前实例编号与收到的架构上下文。 + /// + /// 请求实例。 + /// 取消令牌。 + /// 当前处理器实例编号。 + public ValueTask Handle(TestPerDispatchContextAwareRequest request, CancellationToken cancellationToken) + { + TrackedContexts.Add(Context); + TrackedInstanceIds.Add(_instanceId); + return ValueTask.FromResult(_instanceId); + } -public sealed record TestServiceRetrievalRequest : IRequest; - -public static class TestServiceRetrievalHandler -{ - public static object? LastRetrievedService { get; set; } -} - -public class TestService -{ - public string Id { get; } = Guid.NewGuid().ToString(); -} - -public sealed record TestNestedRequest : IRequest -{ - public int Depth { get; init; } -} - -public static class TestNestedRequestHandler2 -{ - public static int ExecutionCount { get; set; } -} - -// 生命周期相关类 -public sealed record TestLifecycleRequest : IRequest; - -public static class TestLifecycleHandler -{ - public static int InitializationCount { get; set; } - public static int DisposalCount { get; set; } -} - -public sealed record TestScopedServiceRequest : IRequest -{ - public int RequestId { get; init; } -} - -// 错误处理相关类 -public sealed record TestErrorPropagationRequest : IRequest; - -public static class TestExceptionHandler -{ - public static Exception? LastException { get; set; } -} - -public sealed record TestExceptionRequest : IRequest; - -// 性能测试相关类 -public sealed record TestPerformanceRequest2 : IRequest -{ - public int Id { get; init; } -} - -public sealed record TestUncachedRequest : IRequest -{ - public int Id { get; init; } -} - -public sealed record TestCachedRequest : IRequest -{ - public int Id { get; init; } -} - -// 并发测试相关类 -public class SharedState -{ - public int Counter { get; set; } -} - -public sealed record TestConcurrentRequest : IRequest -{ - public int RequestId { get; init; } - public List OrderTracker { get; init; } = new(); -} - -public sealed record TestStateModificationRequest : IRequest -{ - public SharedState SharedState { get; init; } = null!; - public int Increment { get; init; } -} - -// 集成测试相关类 -public static class TestIntegrationHandler -{ - public static string? LastSystemCall { get; set; } -} - -public sealed record TestIntegrationRequest : IRequest; - -public sealed record TestMediatorRequest : IRequest -{ - public int Value { get; init; } -} - -/// -/// 用于验证每次请求分发都会获得新的上下文感知处理器实例。 -/// -public sealed record TestPerDispatchContextAwareRequest : IRequest; - -// 传统命令用于混合测试 -public class TestTraditionalCommand : ICommand -{ - public bool Executed { get; private set; } - - public void Execute() => Executed = true; - - public void SetContext(IArchitectureContext context) - { + /// + /// 重置跨测试共享的实例跟踪状态。 + /// + public static void Reset() + { + TrackedContexts.Clear(); + TrackedInstanceIds.Clear(); + _nextInstanceId = 0; + } } - public IArchitectureContext GetContext() => null!; -} + public sealed record TestContextAwareRequest : IRequest; -#endregion + public static class TestContextAwareHandler + { + public static IArchitectureContext? LastContext { get; set; } + } + + public sealed record TestServiceRetrievalRequest : IRequest; + + public static class TestServiceRetrievalHandler + { + public static object? LastRetrievedService { get; set; } + } + + public class TestService + { + public string Id { get; } = Guid.NewGuid().ToString(); + } + + public sealed record TestNestedRequest : IRequest + { + public int Depth { get; init; } + } + + public static class TestNestedRequestHandler2 + { + public static int ExecutionCount { get; set; } + } + + // 生命周期相关类 + public sealed record TestLifecycleRequest : IRequest; + + public static class TestLifecycleHandler + { + public static int InitializationCount { get; set; } + public static int DisposalCount { get; set; } + } + + public sealed record TestScopedServiceRequest : IRequest + { + public int RequestId { get; init; } + } + + // 错误处理相关类 + public sealed record TestErrorPropagationRequest : IRequest; + + public static class TestExceptionHandler + { + public static Exception? LastException { get; set; } + } + + public sealed record TestExceptionRequest : IRequest; + + // 性能测试相关类 + public sealed record TestPerformanceRequest2 : IRequest + { + public int Id { get; init; } + } + + public sealed record TestUncachedRequest : IRequest + { + public int Id { get; init; } + } + + public sealed record TestCachedRequest : IRequest + { + public int Id { get; init; } + } + + // 并发测试相关类 + public class SharedState + { + public int Counter { get; set; } + } + + public sealed record TestConcurrentRequest : IRequest + { + public int RequestId { get; init; } + public ICollection OrderTracker { get; init; } = new List(); + } + + public sealed record TestStateModificationRequest : IRequest + { + public SharedState SharedState { get; init; } = null!; + public int Increment { get; init; } + } + + // 集成测试相关类 + public static class TestIntegrationHandler + { + public static string? LastSystemCall { get; set; } + } + + public sealed record TestIntegrationRequest : IRequest; + + public sealed record TestMediatorRequest : IRequest + { + public int Value { get; init; } + } + + /// + /// 用于验证每次请求分发都会获得新的上下文感知处理器实例。 + /// + public sealed record TestPerDispatchContextAwareRequest : IRequest; + + // 传统命令用于混合测试 + public class TestTraditionalCommand : ICommand + { + public bool Executed { get; private set; } + + public void Execute() => Executed = true; + + public void SetContext(IArchitectureContext context) + { + } + + public IArchitectureContext GetContext() => null!; + } + + #endregion +} From 121df440c35e4e9c5d100f6298815f3a489d5bd2 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:18:16 +0800 Subject: [PATCH 02/10] =?UTF-8?q?test(cqrs):=20=E6=B8=85=E7=90=86=20Mediat?= =?UTF-8?q?or=20=E9=AB=98=E7=BA=A7=E6=B5=8B=E8=AF=95=E5=91=8A=E8=AD=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 Mediator 高级测试中的异步等待告警 - 修复 验证请求异常参数名告警 - 优化 测试辅助类型区域的文件名告警处理 --- .../Mediator/MediatorAdvancedFeaturesTests.cs | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs index b89bc871..344a0203 100644 --- a/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs @@ -52,7 +52,7 @@ public class MediatorAdvancedFeaturesTests var request = new TestValidatedRequest { Value = -1 }; // 无效值 Assert.ThrowsAsync(async () => - await _context!.SendRequestAsync(request)); + await _context!.SendRequestAsync(request).ConfigureAwait(false)); } [Test] @@ -62,7 +62,7 @@ public class MediatorAdvancedFeaturesTests TestRetryBehavior.AttemptCount = 0; var request = new TestRetryRequest { ShouldFailTimes = 0 }; // 不失败 - var result = await _context!.SendRequestAsync(request); + var result = await _context!.SendRequestAsync(request).ConfigureAwait(false); Assert.That(result, Is.EqualTo("Success")); Assert.That(TestRetryBehavior.AttemptCount, Is.EqualTo(1)); @@ -82,7 +82,7 @@ public class MediatorAdvancedFeaturesTests tasks.Add(_context!.SendRequestAsync(request).AsTask()); } - var results = await Task.WhenAll(tasks); + var results = await Task.WhenAll(tasks).ConfigureAwait(false); stopwatch.Stop(); // 验证所有请求都成功处理 @@ -102,7 +102,7 @@ public class MediatorAdvancedFeaturesTests for (int i = 0; i < requestCount; i++) { var request = new TestMemoryRequest { Data = new string('x', 1000) }; - await _context!.SendRequestAsync(request); + await _context!.SendRequestAsync(request).ConfigureAwait(false); // 定期强制GC来测试内存泄漏 if (i % 100 == 0) @@ -126,7 +126,7 @@ public class MediatorAdvancedFeaturesTests TestTransientErrorHandler.ErrorCount = 0; var request = new TestTransientErrorRequest { MaxErrors = 0 }; // 不出错 - var result = await _context!.SendRequestAsync(request); + var result = await _context!.SendRequestAsync(request).ConfigureAwait(false); Assert.That(result, Is.EqualTo("Success")); Assert.That(TestTransientErrorHandler.ErrorCount, Is.EqualTo(0)); @@ -140,7 +140,8 @@ public class MediatorAdvancedFeaturesTests { try { - await _context!.SendRequestAsync(new TestCircuitBreakerRequest { ShouldFail = true }); + await _context!.SendRequestAsync(new TestCircuitBreakerRequest { ShouldFail = true }) + .ConfigureAwait(false); } catch (Exception) { @@ -151,7 +152,8 @@ public class MediatorAdvancedFeaturesTests // 验证断路器已打开,后续请求应该快速失败 var stopwatch = Stopwatch.StartNew(); Assert.ThrowsAsync(async () => - await _context!.SendRequestAsync(new TestCircuitBreakerRequest { ShouldFail = false })); + await _context!.SendRequestAsync(new TestCircuitBreakerRequest { ShouldFail = false }) + .ConfigureAwait(false)); stopwatch.Stop(); // 验证快速失败(应该在很短时间内完成) @@ -172,7 +174,7 @@ public class MediatorAdvancedFeaturesTests // 执行saga foreach (var request in requests) { - await _context!.SendRequestAsync(request); + await _context!.SendRequestAsync(request).ConfigureAwait(false); } // 验证所有步骤都成功执行 @@ -192,10 +194,10 @@ public class MediatorAdvancedFeaturesTests }; // 执行saga,第二步会失败 - await _context!.SendRequestAsync(requests[0]); + await _context!.SendRequestAsync(requests[0]).ConfigureAwait(false); Assert.ThrowsAsync(async () => - await _context.SendRequestAsync(requests[1])); + await _context.SendRequestAsync(requests[1]).ConfigureAwait(false)); // 验证回滚机制被触发 Assert.That(sagaData.CompletedSteps, Is.EqualTo(new[] { 1 })); // 只有第一步完成 @@ -206,7 +208,7 @@ public class MediatorAdvancedFeaturesTests [Test] public async Task Request_Chaining_With_Dependencies_Should_Work_Correctly() { - var chainResult = await _context!.SendRequestAsync(new TestChainStartRequest()); + var chainResult = await _context!.SendRequestAsync(new TestChainStartRequest()).ConfigureAwait(false); Assert.That(chainResult, Is.EqualTo("Chain completed: Step1 -> Step2 -> Step3")); } @@ -218,7 +220,7 @@ public class MediatorAdvancedFeaturesTests var request = new TestExternalServiceRequest { TimeoutMs = 1000 }; Assert.ThrowsAsync(async () => - await _context!.SendRequestAsync(request, cts.Token)); + await _context!.SendRequestAsync(request, cts.Token).ConfigureAwait(false)); } [Test] @@ -227,13 +229,14 @@ public class MediatorAdvancedFeaturesTests var testData = new List(); var request = new TestDatabaseRequest { Data = "test data", Storage = testData }; - var result = await _context!.SendRequestAsync(request); + var result = await _context!.SendRequestAsync(request).ConfigureAwait(false); Assert.That(result, Is.EqualTo("Data saved successfully")); Assert.That(testData, Contains.Item("test data")); } } +#pragma warning disable MA0048 #region Advanced Test Classes public sealed class TestRetryRequestHandler : IRequestHandler @@ -329,7 +332,7 @@ public sealed class TestChainStartRequestHandler : IRequestHandler Handle(TestChainStartRequest request, CancellationToken cancellationToken) { // 模拟链式调用 - await Task.Delay(10, cancellationToken); + await Task.Delay(10, cancellationToken).ConfigureAwait(false); return "Chain completed: Step1 -> Step2 -> Step3"; } } @@ -338,7 +341,7 @@ public sealed class TestExternalServiceRequestHandler : IRequestHandler Handle(TestExternalServiceRequest request, CancellationToken cancellationToken) { - await Task.Delay(request.TimeoutMs, cancellationToken); + await Task.Delay(request.TimeoutMs, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); return "External service response"; } @@ -378,7 +381,7 @@ public sealed class TestValidatedRequestHandler : IRequestHandler($"Value: {request.Value}"); @@ -406,7 +409,7 @@ public sealed class TestPerformanceRequestHandler : IRequestHandler Handle(TestPerformanceRequest request, CancellationToken cancellationToken) { - await Task.Delay(request.ProcessingTimeMs, cancellationToken); + await Task.Delay(request.ProcessingTimeMs, cancellationToken).ConfigureAwait(false); return request.Id; } } @@ -503,3 +506,4 @@ public sealed record TestDatabaseRequest : IRequest } #endregion +#pragma warning restore MA0048 From 9109eecea9c40a9cac13d931af33658a4d37299a Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:19:16 +0800 Subject: [PATCH 03/10] =?UTF-8?q?test(cqrs):=20=E5=87=8F=E5=B0=91=20Mediat?= =?UTF-8?q?or=20=E7=BB=BC=E5=90=88=E6=B5=8B=E8=AF=95=E5=91=8A=E8=AD=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化测试 helper 类型作用域以消除文件名匹配告警 - 补充异步等待 ConfigureAwait(false) 以满足 analyzer 约束 - 调整集合抽象、字符串比较器和异常参数名用法 --- .../Mediator/MediatorComprehensiveTests.cs | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs index 423b1c9b..0871144b 100644 --- a/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs @@ -86,7 +86,7 @@ public class MediatorComprehensiveTests public async Task SendRequestAsync_Should_ReturnResult_When_Request_IsValid() { var testRequest = new TestRequest { Value = 42 }; - var result = await _context!.SendRequestAsync(testRequest); + var result = await _context!.SendRequestAsync(testRequest).ConfigureAwait(false); Assert.That(result, Is.EqualTo(42)); } @@ -98,7 +98,7 @@ public class MediatorComprehensiveTests public void SendRequestAsync_Should_ThrowArgumentNullException_When_Request_IsNull() { Assert.ThrowsAsync(async () => - await _context!.SendRequestAsync(null!)); + await _context!.SendRequestAsync(null!).ConfigureAwait(false)); } /// @@ -122,8 +122,8 @@ public class MediatorComprehensiveTests TestNotificationHandler.LastReceivedMessage = null; var notification = new TestNotification { Message = "test" }; - await _context!.PublishAsync(notification); - await Task.Delay(100); + await _context!.PublishAsync(notification).ConfigureAwait(false); + await Task.Delay(100).ConfigureAwait(false); Assert.That(TestNotificationHandler.LastReceivedMessage, Is.EqualTo("test")); } @@ -138,7 +138,7 @@ public class MediatorComprehensiveTests var stream = _context!.CreateStream(testStreamRequest); var results = new List(); - await foreach (var item in stream) + await foreach (var item in stream.ConfigureAwait(false)) { results.Add(item); } @@ -153,7 +153,7 @@ public class MediatorComprehensiveTests public async Task SendAsync_CommandWithoutResult_Should_Execute_When_Command_IsValid() { var testCommand = new TestCommand { ShouldExecute = true }; - await _context!.SendAsync(testCommand); + await _context!.SendAsync(testCommand).ConfigureAwait(false); Assert.That(testCommand.Executed, Is.True); } @@ -165,7 +165,7 @@ public class MediatorComprehensiveTests public async Task SendAsync_CommandWithResult_Should_ReturnResult_When_Command_IsValid() { var testCommand = new TestCommandWithResult { ResultValue = 42 }; - var result = await _context!.SendAsync(testCommand); + var result = await _context!.SendAsync(testCommand).ConfigureAwait(false); Assert.That(result, Is.EqualTo(42)); } @@ -198,7 +198,7 @@ public class MediatorComprehensiveTests var testRequest = new TestRequest { Value = 42 }; Assert.ThrowsAsync(async () => - await contextWithoutHandlers.SendRequestAsync(testRequest)); + await contextWithoutHandlers.SendRequestAsync(testRequest).ConfigureAwait(false)); } /// @@ -213,8 +213,8 @@ public class MediatorComprehensiveTests TestNotificationHandler3.LastReceivedMessage = null; var notification = new TestNotification { Message = "multi-handler test" }; - await _context!.PublishAsync(notification); - await Task.Delay(100); + await _context!.PublishAsync(notification).ConfigureAwait(false); + await Task.Delay(100).ConfigureAwait(false); // 验证所有处理器都被调用 Assert.That(TestNotificationHandler.LastReceivedMessage, Is.EqualTo("multi-handler test")); @@ -233,7 +233,7 @@ public class MediatorComprehensiveTests // 应该在50ms后被取消 Assert.ThrowsAsync(async () => - await _context!.SendRequestAsync(longRequest, cts.Token)); + await _context!.SendRequestAsync(longRequest, cts.Token).ConfigureAwait(false)); } /// @@ -251,7 +251,7 @@ public class MediatorComprehensiveTests // 流应该在100ms后被取消(TaskCanceledException 继承自 OperationCanceledException) Assert.CatchAsync(async () => { - await foreach (var item in stream) + await foreach (var item in stream.ConfigureAwait(false)) { results.Add(item); } @@ -277,7 +277,7 @@ public class MediatorComprehensiveTests tasks.Add(_context!.SendRequestAsync(request).AsTask()); } - var results = await Task.WhenAll(tasks); + var results = await Task.WhenAll(tasks).ConfigureAwait(false); // 验证所有结果都正确返回 Assert.That(results.Length, Is.EqualTo(requestCount)); @@ -293,7 +293,7 @@ public class MediatorComprehensiveTests var faultyRequest = new TestFaultyRequest(); Assert.ThrowsAsync(async () => - await _context!.SendRequestAsync(faultyRequest)); + await _context!.SendRequestAsync(faultyRequest).ConfigureAwait(false)); } /// @@ -306,8 +306,8 @@ public class MediatorComprehensiveTests var command1 = new TestModifyDataCommand { Data = sharedData, Value = 10 }; var command2 = new TestModifyDataCommand { Data = sharedData, Value = 20 }; - await _context!.SendAsync(command1); - await _context.SendAsync(command2); + await _context!.SendAsync(command1).ConfigureAwait(false); + await _context.SendAsync(command2).ConfigureAwait(false); // 验证数据被正确修改 Assert.That(sharedData.Value, Is.EqualTo(30)); // 10 + 20 @@ -331,10 +331,10 @@ public class MediatorComprehensiveTests foreach (var notification in notifications) { - await _context!.PublishAsync(notification); + await _context!.PublishAsync(notification).ConfigureAwait(false); } - await Task.Delay(200); // 等待所有处理完成 + await Task.Delay(200).ConfigureAwait(false); // 等待所有处理完成 // 验证接收顺序与发送顺序一致 Assert.That(receivedOrder.Count, Is.EqualTo(3)); @@ -358,7 +358,7 @@ public class MediatorComprehensiveTests var stream = _context!.CreateStream(filterRequest); var results = new List(); - await foreach (var item in stream) + await foreach (var item in stream.ConfigureAwait(false)) { results.Add(item); } @@ -377,7 +377,7 @@ public class MediatorComprehensiveTests var invalidCommand = new TestValidatedCommand { Name = "" }; // 无效:空字符串 Assert.ThrowsAsync(async () => - await _context!.SendAsync(invalidCommand)); + await _context!.SendAsync(invalidCommand).ConfigureAwait(false)); } /// @@ -392,7 +392,7 @@ public class MediatorComprehensiveTests for (int i = 0; i < iterations; i++) { var request = new TestRequest { Value = i }; - var result = await _context!.SendRequestAsync(request); + var result = await _context!.SendRequestAsync(request).ConfigureAwait(false); Assert.That(result, Is.EqualTo(i)); } @@ -417,15 +417,13 @@ public class MediatorComprehensiveTests // 使用自有 CQRS 方式 var mediatorCommand = new TestCommandWithResult { ResultValue = 999 }; - var result = await _context.SendAsync(mediatorCommand); + var result = await _context.SendAsync(mediatorCommand).ConfigureAwait(false); Assert.That(result, Is.EqualTo(999)); // 验证两者可以同时工作 Assert.That(legacyCommand.Executed, Is.True); Assert.That(result, Is.EqualTo(999)); } -} - #region Advanced Test Classes for CQRS Features public sealed record TestLongRunningRequest : IRequest @@ -437,7 +435,7 @@ public sealed class TestLongRunningRequestHandler : IRequestHandler Handle(TestLongRunningRequest request, CancellationToken cancellationToken) { - await Task.Delay(request.DelayMs, cancellationToken); + await Task.Delay(request.DelayMs, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); return "Completed"; } @@ -458,7 +456,7 @@ public sealed class TestLongStreamRequestHandler : IStreamRequestHandler { public string Key { get; init; } = string.Empty; - public Dictionary Cache { get; init; } = new(); + public IDictionary Cache { get; init; } = new Dictionary(StringComparer.Ordinal); } public sealed class TestCachingQueryHandler : IRequestHandler @@ -522,7 +520,7 @@ public sealed record TestOrderedNotification : INotification public sealed class TestOrderedNotificationHandler : INotificationHandler { - public static List ReceivedMessages { get; set; } = new(); + public static ICollection ReceivedMessages { get; set; } = new List(); public ValueTask Handle(TestOrderedNotification notification, CancellationToken cancellationToken) { @@ -590,7 +588,7 @@ public sealed class TestValidatedCommandHandler : IRequestHandler Date: Wed, 29 Apr 2026 08:26:19 +0800 Subject: [PATCH 04/10] =?UTF-8?q?fix(game):=20=E6=94=B6=E7=B4=A7=20schema?= =?UTF-8?q?=20=E6=AD=A3=E5=88=99=E6=A0=A1=E9=AA=8C=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 schema 正则校验缺少超时边界导致的 analyzer 风险 - 更新字符串等值比较为 ordinal 语义 - 补充 warning reduction 批处理恢复状态与验证结果 --- .../Config/YamlConfigSchemaValidator.cs | 35 +++++++----- .../analyzer-warning-reduction-tracking.md | 54 +++++++++++-------- .../analyzer-warning-reduction-trace.md | 39 ++++++++++++++ 3 files changed, 95 insertions(+), 33 deletions(-) diff --git a/GFramework.Game/Config/YamlConfigSchemaValidator.cs b/GFramework.Game/Config/YamlConfigSchemaValidator.cs index e33e86de..4350e035 100644 --- a/GFramework.Game/Config/YamlConfigSchemaValidator.cs +++ b/GFramework.Game/Config/YamlConfigSchemaValidator.cs @@ -22,34 +22,42 @@ internal static partial class YamlConfigSchemaValidator // JS tooling so grouping and backreferences behave consistently across environments. private const RegexOptions SupportedPatternRegexOptions = RegexOptions.CultureInvariant; private const string SupportedStringFormatNames = "'date', 'date-time', 'duration', 'email', 'time', 'uri', 'uuid'"; + private static readonly TimeSpan SupportedFormatRegexTimeout = TimeSpan.FromSeconds(1); private static readonly Regex ExactDecimalPattern = new( @"^(?[+-]?)(?:(?\d+)(?:\.(?\d*))?|\.(?\d+))(?:[eE](?[+-]?\d+))?$", - RegexOptions.CultureInvariant | RegexOptions.Compiled); + RegexOptions.CultureInvariant | RegexOptions.Compiled, + SupportedFormatRegexTimeout); private static readonly Regex SupportedEmailFormatRegex = new( @"^[^@\s]+@[^@\s]+\.[^@\s]+$", - RegexOptions.CultureInvariant | RegexOptions.Compiled); + RegexOptions.CultureInvariant | RegexOptions.Compiled, + SupportedFormatRegexTimeout); private static readonly Regex SupportedDateFormatRegex = new( @"^(?\d{4})-(?\d{2})-(?\d{2})$", - RegexOptions.CultureInvariant | RegexOptions.Compiled); + RegexOptions.CultureInvariant | RegexOptions.Compiled, + SupportedFormatRegexTimeout); private static readonly Regex SupportedDateTimeFormatRegex = new( @"^(?\d{4})-(?\d{2})-(?\d{2})T(?\d{2}):(?\d{2}):(?\d{2})(?\.\d+)?(?Z|[+-]\d{2}:\d{2})$", - RegexOptions.CultureInvariant | RegexOptions.Compiled); + RegexOptions.CultureInvariant | RegexOptions.Compiled, + SupportedFormatRegexTimeout); private static readonly Regex SupportedDurationFormatRegex = new( @"^P(?:(?\d+)D)?(?:T(?:(?\d+)H)?(?:(?\d+)M)?(?:(?\d+(?:\.\d+)?)S)?)?$", - RegexOptions.CultureInvariant | RegexOptions.Compiled); + RegexOptions.CultureInvariant | RegexOptions.Compiled, + SupportedFormatRegexTimeout); private static readonly Regex SupportedTimeFormatRegex = new( @"^(?\d{2}):(?\d{2}):(?\d{2})(?\.\d+)?(?Z|[+-]\d{2}:\d{2})$", - RegexOptions.CultureInvariant | RegexOptions.Compiled); + RegexOptions.CultureInvariant | RegexOptions.Compiled, + SupportedFormatRegexTimeout); private static readonly Regex SupportedUriSchemeRegex = new( @"^[A-Za-z][A-Za-z0-9+\.-]*:", - RegexOptions.CultureInvariant | RegexOptions.Compiled); + RegexOptions.CultureInvariant | RegexOptions.Compiled, + SupportedFormatRegexTimeout); /// /// 从磁盘加载并解析一个 JSON Schema 文件。 @@ -1875,7 +1883,7 @@ internal static partial class YamlConfigSchemaValidator var pattern = patternElement.GetString() ?? string.Empty; try { - _ = new Regex(pattern, SupportedPatternRegexOptions); + _ = new Regex(pattern, SupportedPatternRegexOptions, SupportedFormatRegexTimeout); } catch (ArgumentException exception) { @@ -2347,7 +2355,8 @@ internal static partial class YamlConfigSchemaValidator ? null : new Regex( pattern, - SupportedPatternRegexOptions), + SupportedPatternRegexOptions, + SupportedFormatRegexTimeout), formatConstraint); } @@ -2695,7 +2704,7 @@ internal static partial class YamlConfigSchemaValidator } var offset = match.Groups["offset"].Value; - if (offset == "Z") + if (string.Equals(offset, "Z", StringComparison.Ordinal)) { return true; } @@ -3287,7 +3296,7 @@ internal static partial class YamlConfigSchemaValidator } significand = BigInteger.Parse(digits, CultureInfo.InvariantCulture); - if (match.Groups["sign"].Value == "-") + if (string.Equals(match.Groups["sign"].Value, "-", StringComparison.Ordinal)) { significand = BigInteger.Negate(significand); } @@ -3579,7 +3588,9 @@ internal static partial class YamlConfigSchemaValidator /// 组合后的路径。 private static string CombineSchemaPath(string parentPath, string propertyName) { - return parentPath == "" ? propertyName : $"{parentPath}.{propertyName}"; + return string.Equals(parentPath, "", StringComparison.Ordinal) + ? propertyName + : $"{parentPath}.{propertyName}"; } /// diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index 9eef9b7e..920e3240 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -6,33 +6,44 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-092` -- 当前阶段:`Phase 92` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-093` +- 当前阶段:`Phase 93` - 当前焦点: - - `2026-04-28` 复核 `PR #300` 最新 open threads:代码类线程已与当前工作树对齐,仅剩 `ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md` 的文件计数与验证口径漂移仍然成立 - - 已将 tracking 文档修正为与 `6cc87a9...HEAD` 的实际变更规模一致,并与 trace 中记录的 `dotnet build`、定向 `dotnet test`、`git diff --check` 验证口径保持一致 - - `dotnet format --verify-no-changes` 的 `GFramework.Core.Tests` 既有 `FINALNEWLINE`、`CHARSET`、`WHITESPACE` 基线仍保持独立,不与当前 `ai-plan` 同步修复混提 + - `2026-04-29` 使用 `$gframework-batch-boot 50` 从 clean build warning 基线继续分批清理 analyzer warnings + - 已接受三个 worker 的 `GFramework.Cqrs.Tests/Mediator/*` 独立切片,三个 Mediator 测试文件的 warning 已清零 + - 主线程补齐 `YamlConfigSchemaValidator` 运行时正则 timeout 与 ordinal 字符串比较,先收掉低风险 `MA0009` / `MA0006` + - 当前停止条件为相对 `origin/main` 接近 `50` 个变更文件;本轮尚未接近阈值,下一批可继续处理 `GFramework.Game/Config/YamlConfigSchemaValidator*` ## 当前活跃事实 -- 当前 `origin/main` 基线提交为 `6cc87a9`(`2026-04-27T20:28:50+08:00`)。 +- 当前 `origin/main` 基线提交为 `0e32dab`(`2026-04-28T17:15:47+08:00`)。 - 当前直接验证结果: - - `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release` + - `dotnet clean -p:RestoreFallbackFolders= -v:quiet` + - 最新结果:成功;标准 `dotnet clean` 仍会先命中当前 WSL 环境的 Windows NuGet fallback 目录,已按既有环境口径先执行 `dotnet restore GFramework.sln -p:RestoreFallbackFolders= --disable-parallel` 后清理 + - `dotnet build -p:RestoreFallbackFolders= -clp:WarningsOnly -v:minimal -m:1 -nodeReuse:false` + - 最新结果:成功;`75` warnings、`0` errors;warning 从本轮基线 `236` 降到 `75` + - `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false -clp:Summary` - 最新结果:成功;`0 Warning(s)`、`0 Error(s)` - - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~ArchitectureServicesTests|FullyQualifiedName~ContextAwareServiceExtensionsTests|FullyQualifiedName~TestArchitectureContextBehaviorTests|FullyQualifiedName~RegistryInitializationHookBaseTests|FullyQualifiedName~ArchitectureContextTests"` - - 最新结果:成功;`67` 通过、`0` 失败 + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-build -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Mediator"` + - 最新结果:成功;`45` 通过、`0` 失败 + - `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false -clp:Summary` + - 最新结果:成功;`75 Warning(s)`、`0 Error(s)`;剩余均为 `YamlConfigSchemaValidator*` 的 `MA0048` / `MA0051` + - `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~YamlConfigLoaderTests|FullyQualifiedName~YamlConfigSchemaValidatorTests"` + - 最新结果:成功;`80` 通过、`0` 失败 - 当前批次摘要: - - 当前分支相对 `6cc87a9...HEAD` 包含 `18` 个已修改文件与 `38` 个新增文件(合计 `56` 个变更文件),分别位于 `GFramework.Core.Tests`、`GFramework.Cqrs.Tests`、`GFramework.Core`、`.agents/skills/gframework-pr-review/` 与 `ai-plan/public/analyzer-warning-reduction` - - 本轮没有触碰 `Mediator/*`、`YamlConfigSchemaValidator*` 或 `GFramework.Core.Tests` 的整项目格式基线波次 + - 当前已提交分支相对 `origin/main...HEAD` 包含 `3` 个变更文件;本次主线程待提交的 `GFramework.Game/Config/YamlConfigSchemaValidator.cs` 与 `ai-plan` 文档会把累计变更推到约 `6` 个文件,低于 `50` 个文件阈值 + - 已完成 worker 切片: + - `ed269d4`:`MediatorArchitectureIntegrationTests.cs`,清理 `MA0048` / `MA0004` / `MA0016` + - `121df44`:`MediatorAdvancedFeaturesTests.cs`,清理 `MA0048` / `MA0004` / `MA0015` + - `9109eec`:`MediatorComprehensiveTests.cs`,清理 `MA0048` / `MA0004` / `MA0016` / `MA0002` / `MA0015` + - 主线程切片:`YamlConfigSchemaValidator.cs` 正则 timeout 与 ordinal equality,清理 `MA0009` / `MA0006` ## 当前风险 -- GitHub PR 上的 open threads 可能仍显示为未关闭,因为当前只同步了 `ai-plan` 文档,尚未推送新的 head 供审查机器人重新折叠线程。 - - 缓解措施:推送本次 `ai-plan` 同步提交后重新执行 `$gframework-pr-review`,以最新 head 再核对 thread 状态。 -- `dotnet format GFramework.Core.Tests/GFramework.Core.Tests.csproj --verify-no-changes` 当前会命中项目内大量历史格式诊断。 - - 缓解措施:本轮只记录为现存基线,不把 `PR #300` 的 review follow-up 扩展成整项目格式清理。 -- `GFramework.Game/Config/YamlConfigSchemaValidator*` 仍然是仓库根 warning 热点,但与本轮 review 修复无交集。 - - 缓解措施:继续保持为独立高耦合波次。 +- `GFramework.Game/Config/YamlConfigSchemaValidator*` 仍然是仓库根 warning 热点,剩余 `45` 条 `MA0048` 与 `30` 条 `MA0051`。 + - 缓解措施:下一批优先把 `YamlConfigSchemaValidator.cs` 末尾 schema model 类型拆到独立文件,再评估 `MA0051` 方法拆分。 +- 标准 `dotnet clean` 在当前 WSL 环境仍会读取失效的 Windows fallback package folder。 + - 缓解措施:本主题验证继续沿用 `-p:RestoreFallbackFolders=`,必要时先执行 solution restore 刷新 Linux 侧资产。 ## 活跃文档 @@ -53,12 +64,13 @@ ## 验证说明 - 权威验证结果统一维护在“当前活跃事实”。 -- `GFramework.Core.Tests` 的当前受影响项目 Release 构建已清零,并通过对应定向测试回归。 +- `GFramework.Cqrs.Tests` 的当前受影响项目 Release 构建已清零,并通过 Mediator 定向测试回归。 +- `GFramework.Game` 当前低风险正则 / 字符串比较切片通过 Release 构建与 config 定向测试;剩余 warning 属于拆文件与复杂度拆分。 - `git diff --check` 结果为空,说明本轮新增改动没有引入新的尾随空格或冲突标记。 - warning reduction 的仓库级真值以同轮 `dotnet build`、定向 `dotnet test` 与 `git diff --check` 为准,并与 trace 中的验证里程碑保持一致。 ## 下一步建议 -1. 提交本轮 `PR #300` nitpick follow-up、技能规则更新与 `ai-plan` 同步。 -2. 推送后重新执行 `$gframework-pr-review`,确认 `ai-plan` 相关 thread 是否随最新 head 自动收口。 -3. 若要清理 `dotnet format` 基线,另开 `GFramework.Core.Tests` 格式治理切片,不与当前 PR review 修复混提。 +1. 提交主线程 `YamlConfigSchemaValidator` 正则安全补丁与本轮 `ai-plan` 同步。 +2. 继续下一批 `GFramework.Game/Config/YamlConfigSchemaValidator.cs` 末尾 schema model 类型拆文件,目标清理 `MA0048`。 +3. 再评估 `YamlConfigSchemaValidator.ObjectKeywords.cs` 与主 validator 的 `MA0051` 方法拆分,避免单批触碰过多高耦合逻辑。 diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index e41efd96..95d3f165 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -1,5 +1,44 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-29 — RP-093 + +### 阶段:按 `$gframework-batch-boot 50` 从 clean build warning 基线分批清理 + +- 触发背景: + - 用户要求先拿构建 warning,再分批指派 subagent 加快处理;停止条件解析为分支相对 `origin/main` 接近 `50` 个变更文件 +- 基线与环境: + - 当前 `origin/main` 为 `0e32dab`(`2026-04-28T17:15:47+08:00`) + - 标准 `dotnet clean` 在当前 WSL 环境仍被 Windows NuGet fallback package folder 阻塞;按既有环境口径先执行 `dotnet restore GFramework.sln -p:RestoreFallbackFolders= --disable-parallel` 后,使用 `-p:RestoreFallbackFolders=` 完成 clean / build + - clean 后 warning 基线:`236` warnings、`0` errors +- 已接受的 worker 范围: + - `ed269d4`:`GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs`,清理 `MA0048` / `MA0004` / `MA0016` + - `121df44`:`GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs`,清理 `MA0048` / `MA0004` / `MA0015` + - `9109eec`:`GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs`,清理 `MA0048` / `MA0004` / `MA0016` / `MA0002` / `MA0015` +- 主线程实施: + - 在 `GFramework.Game/Config/YamlConfigSchemaValidator.cs` 为固定格式正则与 schema `pattern` 正则补充 timeout,避免运行时正则输入继续触发 `MA0009` + - 将三处字符串等值比较改为 ordinal `string.Equals`,清理 `MA0006` +- 验证里程碑: + - `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false -clp:Summary` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-build -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Mediator"` + - 结果:成功;`45` 通过、`0` 失败 + - `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false -clp:Summary` + - 结果:成功;`75 Warning(s)`、`0 Error(s)` + - `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~YamlConfigLoaderTests|FullyQualifiedName~YamlConfigSchemaValidatorTests"` + - 结果:成功;`80` 通过、`0` 失败 + - `dotnet clean -p:RestoreFallbackFolders= -v:quiet` + - 结果:成功 + - `dotnet build -p:RestoreFallbackFolders= -clp:WarningsOnly -v:minimal -m:1 -nodeReuse:false` + - 结果:成功;`75` warnings、`0` errors + - `git diff --check` + - 结果:成功;无新增 whitespace / conflict-marker 问题 +- 当前指标: + - warning 总数:`236` -> `75` + - 剩余 warning 分布:`GFramework.Game/Config/YamlConfigSchemaValidator.cs` `60` 条,`GFramework.Game/Config/YamlConfigSchemaValidator.ObjectKeywords.cs` `15` 条;规则为 `MA0048` `45` 条、`MA0051` `30` 条 + - 已提交分支 diff:`3` 个文件;主线程待提交后预计约 `6` 个文件,低于 `50` 个文件阈值 +- 下一步: + - 提交主线程 Game / ai-plan 同步后,继续 `YamlConfigSchemaValidator.cs` 末尾 schema model 类型拆文件,优先清理 `MA0048` + ## 2026-04-28 — RP-092 ### 阶段:复核 `PR #300` 的 open threads,并只修正当前分支仍然成立的 `ai-plan` 漂移 From 1395b84439eb34c83c0cbc59ddf293556fd5d16a Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:32:04 +0800 Subject: [PATCH 05/10] =?UTF-8?q?refactor(game):=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=20schema=20=E5=85=B3=E9=94=AE=E5=AD=97?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 dependentRequired 与 dependentSchemas 的单项解析流程 - 重构 allOf 与条件 schema 的分支解析流程 - 优化 object-focused 内联 schema 的 properties 与 required 校验拆分 --- ...amlConfigSchemaValidator.ObjectKeywords.cs | 686 ++++++++++++------ 1 file changed, 470 insertions(+), 216 deletions(-) diff --git a/GFramework.Game/Config/YamlConfigSchemaValidator.ObjectKeywords.cs b/GFramework.Game/Config/YamlConfigSchemaValidator.ObjectKeywords.cs index 4358d850..7572655f 100644 --- a/GFramework.Game/Config/YamlConfigSchemaValidator.ObjectKeywords.cs +++ b/GFramework.Game/Config/YamlConfigSchemaValidator.ObjectKeywords.cs @@ -104,67 +104,12 @@ internal static partial class YamlConfigSchemaValidator var dependentRequired = new Dictionary>(StringComparer.Ordinal); foreach (var dependency in dependentRequiredElement.EnumerateObject()) { - if (!properties.ContainsKey(dependency.Name)) - { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.SchemaUnsupported, - tableName, - $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentRequired' for undeclared property '{dependency.Name}'.", - schemaPath: schemaPath, - displayPath: GetDiagnosticPath(propertyPath)); - } - - if (dependency.Value.ValueKind != JsonValueKind.Array) - { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.SchemaUnsupported, - tableName, - $"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentRequired' as an array of sibling property names.", - schemaPath: schemaPath, - displayPath: GetDiagnosticPath(propertyPath)); - } - - var dependencyTargets = new List(); - var seenDependencyTargets = new HashSet(StringComparer.Ordinal); - foreach (var dependencyTarget in dependency.Value.EnumerateArray()) - { - if (dependencyTarget.ValueKind != JsonValueKind.String) - { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.SchemaUnsupported, - tableName, - $"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentRequired' entries as strings.", - schemaPath: schemaPath, - displayPath: GetDiagnosticPath(propertyPath)); - } - - var dependencyTargetName = dependencyTarget.GetString(); - if (string.IsNullOrWhiteSpace(dependencyTargetName)) - { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.SchemaUnsupported, - tableName, - $"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' cannot declare blank 'dependentRequired' entries.", - schemaPath: schemaPath, - displayPath: GetDiagnosticPath(propertyPath)); - } - - if (!properties.ContainsKey(dependencyTargetName)) - { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.SchemaUnsupported, - tableName, - $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentRequired' target '{dependencyTargetName}' that is not declared in the same object schema.", - schemaPath: schemaPath, - displayPath: GetDiagnosticPath(propertyPath)); - } - - if (seenDependencyTargets.Add(dependencyTargetName)) - { - dependencyTargets.Add(dependencyTargetName); - } - } - + var dependencyTargets = ParseDependentRequiredConstraint( + tableName, + schemaPath, + propertyPath, + dependency, + properties); if (dependencyTargets.Count > 0) { dependentRequired[dependency.Name] = dependencyTargets; @@ -176,6 +121,116 @@ internal static partial class YamlConfigSchemaValidator : dependentRequired; } + /// + /// 解析单个 dependentRequired 触发字段的依赖目标列表。 + /// 触发字段和目标字段必须都来自父对象已声明属性;重复目标会被去重以保持运行时约束稳定。 + /// + /// 所属配置表名称。 + /// Schema 文件路径。 + /// 对象字段路径。 + /// 当前触发字段声明。 + /// 当前对象已声明的属性集合。 + /// 去重后的依赖目标列表。 + private static IReadOnlyList ParseDependentRequiredConstraint( + string tableName, + string schemaPath, + string propertyPath, + JsonProperty dependency, + IReadOnlyDictionary properties) + { + if (!properties.ContainsKey(dependency.Name)) + { + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentRequired' for undeclared property '{dependency.Name}'.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(propertyPath)); + } + + if (dependency.Value.ValueKind != JsonValueKind.Array) + { + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentRequired' as an array of sibling property names.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(propertyPath)); + } + + var dependencyTargets = new List(); + var seenDependencyTargets = new HashSet(StringComparer.Ordinal); + foreach (var dependencyTarget in dependency.Value.EnumerateArray()) + { + var dependencyTargetName = ParseDependentRequiredTargetName( + tableName, + schemaPath, + propertyPath, + dependency.Name, + dependencyTarget, + properties); + if (seenDependencyTargets.Add(dependencyTargetName)) + { + dependencyTargets.Add(dependencyTargetName); + } + } + + return dependencyTargets; + } + + /// + /// 读取并校验 dependentRequired 的单个目标字段名。 + /// 目标必须是非空字符串并已在同一个对象 schema 中声明,避免依赖关系指向不可满足字段。 + /// + /// 所属配置表名称。 + /// Schema 文件路径。 + /// 对象字段路径。 + /// 当前触发字段名称。 + /// 当前依赖目标节点。 + /// 当前对象已声明的属性集合。 + /// 已校验的依赖目标字段名。 + private static string ParseDependentRequiredTargetName( + string tableName, + string schemaPath, + string propertyPath, + string dependencyName, + JsonElement dependencyTarget, + IReadOnlyDictionary properties) + { + if (dependencyTarget.ValueKind != JsonValueKind.String) + { + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"Property '{dependencyName}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentRequired' entries as strings.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(propertyPath)); + } + + var dependencyTargetName = dependencyTarget.GetString(); + if (string.IsNullOrWhiteSpace(dependencyTargetName)) + { + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"Property '{dependencyName}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' cannot declare blank 'dependentRequired' entries.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(propertyPath)); + } + + if (properties.ContainsKey(dependencyTargetName)) + { + return dependencyTargetName; + } + + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentRequired' target '{dependencyTargetName}' that is not declared in the same object schema.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(propertyPath)); + } + /// /// 解析对象节点声明的 dependentSchemas 条件 schema。 /// 当前实现把它作为“当触发字段出现时,当前对象还必须额外满足一段内联 schema”来解释, @@ -212,43 +267,12 @@ internal static partial class YamlConfigSchemaValidator var dependentSchemas = new Dictionary(StringComparer.Ordinal); foreach (var dependency in dependentSchemasElement.EnumerateObject()) { - if (!properties.ContainsKey(dependency.Name)) - { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.SchemaUnsupported, - tableName, - $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentSchemas' for undeclared property '{dependency.Name}'.", - schemaPath: schemaPath, - displayPath: GetDiagnosticPath(propertyPath)); - } - - if (dependency.Value.ValueKind != JsonValueKind.Object) - { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.SchemaUnsupported, - tableName, - $"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentSchemas' as an object-valued schema.", - schemaPath: schemaPath, - displayPath: GetDiagnosticPath(propertyPath)); - } - - var dependencySchemaPath = BuildNestedSchemaPath(propertyPath, $"dependentSchemas:{dependency.Name}"); - var dependencySchemaNode = ParseNode( + dependentSchemas[dependency.Name] = ParseDependentSchemaConstraint( tableName, schemaPath, - dependencySchemaPath, - dependency.Value); - if (dependencySchemaNode.NodeType != YamlConfigSchemaPropertyType.Object) - { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.SchemaUnsupported, - tableName, - $"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare an object-typed 'dependentSchemas' schema.", - schemaPath: schemaPath, - displayPath: GetDiagnosticPath(dependencySchemaPath)); - } - - dependentSchemas[dependency.Name] = dependencySchemaNode; + propertyPath, + dependency, + properties); } return dependentSchemas.Count == 0 @@ -256,6 +280,62 @@ internal static partial class YamlConfigSchemaValidator : dependentSchemas; } + /// + /// 解析单个 dependentSchemas 触发字段关联的 object-typed schema。 + /// 触发字段必须属于当前对象,关联 schema 继续通过通用节点解析流程获得完整约束模型。 + /// + /// 所属配置表名称。 + /// Schema 文件路径。 + /// 对象字段路径。 + /// 当前触发字段声明。 + /// 当前对象已声明的属性集合。 + /// 解析后的 object-typed 条件 schema。 + private static YamlConfigSchemaNode ParseDependentSchemaConstraint( + string tableName, + string schemaPath, + string propertyPath, + JsonProperty dependency, + IReadOnlyDictionary properties) + { + if (!properties.ContainsKey(dependency.Name)) + { + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentSchemas' for undeclared property '{dependency.Name}'.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(propertyPath)); + } + + if (dependency.Value.ValueKind != JsonValueKind.Object) + { + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentSchemas' as an object-valued schema.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(propertyPath)); + } + + var dependencySchemaPath = BuildNestedSchemaPath(propertyPath, $"dependentSchemas:{dependency.Name}"); + var dependencySchemaNode = ParseNode( + tableName, + schemaPath, + dependencySchemaPath, + dependency.Value); + if (dependencySchemaNode.NodeType == YamlConfigSchemaPropertyType.Object) + { + return dependencySchemaNode; + } + + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare an object-typed 'dependentSchemas' schema.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(dependencySchemaPath)); + } + /// /// 解析对象节点声明的 allOf 组合约束。 /// 当前实现仅接受 object-typed 内联 schema,并把每个条目当成 focused constraint block @@ -293,40 +373,13 @@ internal static partial class YamlConfigSchemaValidator var allOfIndex = 0; foreach (var allOfSchemaElement in allOfElement.EnumerateArray()) { - if (allOfSchemaElement.ValueKind != JsonValueKind.Object) - { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.SchemaUnsupported, - tableName, - $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'allOf' entries as object-valued schemas.", - schemaPath: schemaPath, - displayPath: GetDiagnosticPath(propertyPath)); - } - - var allOfSchemaPath = BuildNestedSchemaPath(propertyPath, $"allOf[{allOfIndex.ToString(CultureInfo.InvariantCulture)}]"); - ValidateInlineObjectSchemaTargetsAgainstParentObject( + var allOfSchemaNode = ParseAllOfSchemaConstraint( tableName, schemaPath, propertyPath, - allOfSchemaPath, - $"Entry #{(allOfIndex + 1).ToString(CultureInfo.InvariantCulture)} in 'allOf'", allOfSchemaElement, + allOfIndex, properties); - var allOfSchemaNode = ParseNode( - tableName, - schemaPath, - allOfSchemaPath, - allOfSchemaElement); - if (allOfSchemaNode.NodeType != YamlConfigSchemaPropertyType.Object) - { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.SchemaUnsupported, - tableName, - $"Entry #{(allOfIndex + 1).ToString(CultureInfo.InvariantCulture)} in 'allOf' for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare an object-typed schema.", - schemaPath: schemaPath, - displayPath: GetDiagnosticPath(allOfSchemaPath)); - } - allOfSchemas.Add(allOfSchemaNode); allOfIndex++; } @@ -336,6 +389,64 @@ internal static partial class YamlConfigSchemaValidator : allOfSchemas; } + /// + /// 解析 allOf 中的单个 object-focused schema 条目。 + /// 每个条目只允许约束父对象已声明的字段,并且必须保持 object-typed 语义。 + /// + /// 所属配置表名称。 + /// Schema 文件路径。 + /// 父对象路径。 + /// 当前 allOf 条目。 + /// 当前 allOf 条目的零基索引。 + /// 父对象已声明的属性集合。 + /// 解析后的 object-typed schema。 + private static YamlConfigSchemaNode ParseAllOfSchemaConstraint( + string tableName, + string schemaPath, + string propertyPath, + JsonElement allOfSchemaElement, + int allOfIndex, + IReadOnlyDictionary properties) + { + if (allOfSchemaElement.ValueKind != JsonValueKind.Object) + { + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'allOf' entries as object-valued schemas.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(propertyPath)); + } + + var allOfEntryLabel = $"Entry #{(allOfIndex + 1).ToString(CultureInfo.InvariantCulture)} in 'allOf'"; + var allOfSchemaPath = BuildNestedSchemaPath(propertyPath, $"allOf[{allOfIndex.ToString(CultureInfo.InvariantCulture)}]"); + ValidateInlineObjectSchemaTargetsAgainstParentObject( + tableName, + schemaPath, + propertyPath, + allOfSchemaPath, + allOfEntryLabel, + allOfSchemaElement, + properties); + + var allOfSchemaNode = ParseNode( + tableName, + schemaPath, + allOfSchemaPath, + allOfSchemaElement); + if (allOfSchemaNode.NodeType == YamlConfigSchemaPropertyType.Object) + { + return allOfSchemaNode; + } + + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"{allOfEntryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare an object-typed schema.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(allOfSchemaPath)); + } + /// /// 解析对象节点声明的 object-focused if / then / else 条件约束。 /// 当前共享子集要求三段内联 schema 都保持 object-typed focused block 语义, @@ -362,25 +473,7 @@ internal static partial class YamlConfigSchemaValidator return null; } - if (!hasIf) - { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.SchemaUnsupported, - tableName, - $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'if' when using 'then' or 'else'.", - schemaPath: schemaPath, - displayPath: GetDiagnosticPath(propertyPath)); - } - - if (!hasThen && !hasElse) - { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.SchemaUnsupported, - tableName, - $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare at least one of 'then' or 'else' when using 'if'.", - schemaPath: schemaPath, - displayPath: GetDiagnosticPath(propertyPath)); - } + ValidateConditionalSchemaKeywordPresence(tableName, schemaPath, propertyPath, hasIf, hasThen, hasElse); var ifSchemaPath = BuildNestedSchemaPath(propertyPath, "if"); var ifSchemaNode = ParseConditionalObjectSchema( @@ -391,31 +484,100 @@ internal static partial class YamlConfigSchemaValidator "if", ifElement, properties); - - var thenSchemaNode = hasThen - ? ParseConditionalObjectSchema( - tableName, - schemaPath, - propertyPath, - BuildNestedSchemaPath(propertyPath, "then"), - "then", - thenElement, - properties) - : null; - var elseSchemaNode = hasElse - ? ParseConditionalObjectSchema( - tableName, - schemaPath, - propertyPath, - BuildNestedSchemaPath(propertyPath, "else"), - "else", - elseElement, - properties) - : null; + var thenSchemaNode = ParseOptionalConditionalObjectSchema( + tableName, + schemaPath, + propertyPath, + "then", + hasThen, + thenElement, + properties); + var elseSchemaNode = ParseOptionalConditionalObjectSchema( + tableName, + schemaPath, + propertyPath, + "else", + hasElse, + elseElement, + properties); return new YamlConfigConditionalSchemas(ifSchemaNode, thenSchemaNode, elseSchemaNode); } + /// + /// 校验 object-focused 条件关键字的组合关系。 + /// thenelse 只能跟随 if,而单独的 if 没有可执行分支。 + /// + /// 所属配置表名称。 + /// Schema 文件路径。 + /// 对象字段路径。 + /// 是否声明 if。 + /// 是否声明 then。 + /// 是否声明 else。 + private static void ValidateConditionalSchemaKeywordPresence( + string tableName, + string schemaPath, + string propertyPath, + bool hasIf, + bool hasThen, + bool hasElse) + { + if (!hasIf) + { + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'if' when using 'then' or 'else'.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(propertyPath)); + } + + if (hasThen || hasElse) + { + return; + } + + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare at least one of 'then' or 'else' when using 'if'.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(propertyPath)); + } + + /// + /// 解析可选的 thenelse 条件分支。 + /// 未声明的分支保留为空,声明的分支必须通过 object-focused schema 校验。 + /// + /// 所属配置表名称。 + /// Schema 文件路径。 + /// 对象字段路径。 + /// 条件关键字名称。 + /// 是否声明该条件关键字。 + /// 条件关键字对应的 schema 节点。 + /// 父对象已声明的属性集合。 + /// 解析后的条件分支;未声明时返回空。 + private static YamlConfigSchemaNode? ParseOptionalConditionalObjectSchema( + string tableName, + string schemaPath, + string propertyPath, + string keywordName, + bool hasKeyword, + JsonElement keywordElement, + IReadOnlyDictionary properties) + { + return hasKeyword + ? ParseConditionalObjectSchema( + tableName, + schemaPath, + propertyPath, + BuildNestedSchemaPath(propertyPath, keywordName), + keywordName, + keywordElement, + properties) + : null; + } + /// /// 解析单个条件分支的 object-focused 内联 schema。 /// @@ -494,34 +656,96 @@ internal static partial class YamlConfigSchemaValidator JsonElement inlineSchemaElement, IReadOnlyDictionary properties) { - if (inlineSchemaElement.TryGetProperty("properties", out var inlinePropertiesElement)) + ValidateInlineObjectSchemaPropertiesAgainstParentObject( + tableName, + schemaPath, + propertyPath, + inlineSchemaPath, + entryLabel, + inlineSchemaElement, + properties); + + ValidateInlineRequiredPropertiesAgainstParentObject( + tableName, + schemaPath, + propertyPath, + inlineSchemaPath, + entryLabel, + inlineSchemaElement, + properties); + } + + /// + /// 校验 object-focused 内联 schema 的 properties 只引用父对象字段。 + /// focused block 不负责声明新字段,所以任何父对象未声明字段都会在 schema 加载时被拒绝。 + /// + /// 所属配置表名称。 + /// Schema 文件路径。 + /// 父对象路径。 + /// 当前内联 schema 路径。 + /// 用于诊断文本的条目标签。 + /// 当前内联 schema。 + /// 父对象已声明的属性集合。 + private static void ValidateInlineObjectSchemaPropertiesAgainstParentObject( + string tableName, + string schemaPath, + string propertyPath, + string inlineSchemaPath, + string entryLabel, + JsonElement inlineSchemaElement, + IReadOnlyDictionary properties) + { + if (!inlineSchemaElement.TryGetProperty("properties", out var inlinePropertiesElement)) { - if (inlinePropertiesElement.ValueKind != JsonValueKind.Object) - { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.SchemaUnsupported, - tableName, - $"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'properties' as an object-valued map.", - schemaPath: schemaPath, - displayPath: GetDiagnosticPath(inlineSchemaPath)); - } - - foreach (var property in inlinePropertiesElement.EnumerateObject()) - { - if (properties.ContainsKey(property.Name)) - { - continue; - } - - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.SchemaUnsupported, - tableName, - $"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' declares property '{property.Name}', but that property is not declared in the parent object schema.", - schemaPath: schemaPath, - displayPath: GetDiagnosticPath(inlineSchemaPath)); - } + return; } + if (inlinePropertiesElement.ValueKind != JsonValueKind.Object) + { + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'properties' as an object-valued map.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(inlineSchemaPath)); + } + + foreach (var property in inlinePropertiesElement.EnumerateObject()) + { + if (properties.ContainsKey(property.Name)) + { + continue; + } + + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' declares property '{property.Name}', but that property is not declared in the parent object schema.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(inlineSchemaPath)); + } + } + + /// + /// 校验 object-focused 内联 schema 的 required 只引用父对象字段。 + /// 该校验在加载期暴露不可满足的条件块,而不是等到运行时才发现无效字段名。 + /// + /// 所属配置表名称。 + /// Schema 文件路径。 + /// 父对象路径。 + /// 当前内联 schema 路径。 + /// 用于诊断文本的条目标签。 + /// 当前内联 schema。 + /// 父对象已声明的属性集合。 + private static void ValidateInlineRequiredPropertiesAgainstParentObject( + string tableName, + string schemaPath, + string propertyPath, + string inlineSchemaPath, + string entryLabel, + JsonElement inlineSchemaElement, + IReadOnlyDictionary properties) + { if (!inlineSchemaElement.TryGetProperty("required", out var inlineRequiredElement)) { return; @@ -539,39 +763,69 @@ internal static partial class YamlConfigSchemaValidator foreach (var requiredProperty in inlineRequiredElement.EnumerateArray()) { - if (requiredProperty.ValueKind != JsonValueKind.String) - { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.SchemaUnsupported, - tableName, - $"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'required' entries as property-name strings.", - schemaPath: schemaPath, - displayPath: GetDiagnosticPath(inlineSchemaPath)); - } - - var requiredPropertyName = requiredProperty.GetString(); - if (string.IsNullOrWhiteSpace(requiredPropertyName)) - { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.SchemaUnsupported, - tableName, - $"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' cannot declare blank property names in 'required'.", - schemaPath: schemaPath, - displayPath: GetDiagnosticPath(inlineSchemaPath)); - } - - if (properties.ContainsKey(requiredPropertyName)) - { - continue; - } + ValidateInlineRequiredPropertyAgainstParentObject( + tableName, + schemaPath, + propertyPath, + inlineSchemaPath, + entryLabel, + requiredProperty, + properties); + } + } + /// + /// 校验 object-focused 内联 schema 的单个 required 字段名。 + /// 字段名必须是非空字符串并且属于父对象声明范围,保持条件块与父对象形状一致。 + /// + /// 所属配置表名称。 + /// Schema 文件路径。 + /// 父对象路径。 + /// 当前内联 schema 路径。 + /// 用于诊断文本的条目标签。 + /// 当前 required 条目。 + /// 父对象已声明的属性集合。 + private static void ValidateInlineRequiredPropertyAgainstParentObject( + string tableName, + string schemaPath, + string propertyPath, + string inlineSchemaPath, + string entryLabel, + JsonElement requiredProperty, + IReadOnlyDictionary properties) + { + if (requiredProperty.ValueKind != JsonValueKind.String) + { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, - $"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' requires property '{requiredPropertyName}', but that property is not declared in the parent object schema.", + $"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'required' entries as property-name strings.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(inlineSchemaPath)); } + + var requiredPropertyName = requiredProperty.GetString(); + if (string.IsNullOrWhiteSpace(requiredPropertyName)) + { + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' cannot declare blank property names in 'required'.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(inlineSchemaPath)); + } + + if (properties.ContainsKey(requiredPropertyName)) + { + return; + } + + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' requires property '{requiredPropertyName}', but that property is not declared in the parent object schema.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(inlineSchemaPath)); } /// From 104ac25dc36394080eb37f3e9ab65ed469c8cda4 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:38:23 +0800 Subject: [PATCH 06/10] =?UTF-8?q?refactor(game):=20=E6=8B=86=E5=88=86=20sc?= =?UTF-8?q?hema=20=E6=A0=A1=E9=AA=8C=E6=A8=A1=E5=9E=8B=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 拆分 schema model 类型到独立同名文件 - 清理 schema 校验模型的文件命名 analyzer 告警 - 更新 warning reduction 批处理收口状态 --- .../Config/YamlConfigAllowedValue.cs | 32 + .../Config/YamlConfigArrayConstraints.cs | 47 + .../YamlConfigArrayContainsConstraints.cs | 41 + .../Config/YamlConfigConditionalSchemas.cs | 41 + .../Config/YamlConfigConstantValue.cs | 32 + .../Config/YamlConfigNumericConstraints.cs | 55 + .../Config/YamlConfigObjectConstraints.cs | 67 ++ .../Config/YamlConfigReferenceUsage.cs | 74 ++ .../Config/YamlConfigScalarConstraints.cs | 31 + GFramework.Game/Config/YamlConfigSchema.cs | 44 + .../Config/YamlConfigSchemaNode.cs | 330 ++++++ .../Config/YamlConfigSchemaPropertyType.cs | 37 + .../Config/YamlConfigSchemaValidator.cs | 947 ------------------ .../Config/YamlConfigStringConstraints.cs | 58 ++ .../YamlConfigStringFormatConstraint.cs | 33 + .../Config/YamlConfigStringFormatKind.cs | 42 + .../analyzer-warning-reduction-tracking.md | 24 +- .../analyzer-warning-reduction-trace.md | 18 +- 18 files changed, 990 insertions(+), 963 deletions(-) create mode 100644 GFramework.Game/Config/YamlConfigAllowedValue.cs create mode 100644 GFramework.Game/Config/YamlConfigArrayConstraints.cs create mode 100644 GFramework.Game/Config/YamlConfigArrayContainsConstraints.cs create mode 100644 GFramework.Game/Config/YamlConfigConditionalSchemas.cs create mode 100644 GFramework.Game/Config/YamlConfigConstantValue.cs create mode 100644 GFramework.Game/Config/YamlConfigNumericConstraints.cs create mode 100644 GFramework.Game/Config/YamlConfigObjectConstraints.cs create mode 100644 GFramework.Game/Config/YamlConfigReferenceUsage.cs create mode 100644 GFramework.Game/Config/YamlConfigScalarConstraints.cs create mode 100644 GFramework.Game/Config/YamlConfigSchema.cs create mode 100644 GFramework.Game/Config/YamlConfigSchemaNode.cs create mode 100644 GFramework.Game/Config/YamlConfigSchemaPropertyType.cs create mode 100644 GFramework.Game/Config/YamlConfigStringConstraints.cs create mode 100644 GFramework.Game/Config/YamlConfigStringFormatConstraint.cs create mode 100644 GFramework.Game/Config/YamlConfigStringFormatKind.cs diff --git a/GFramework.Game/Config/YamlConfigAllowedValue.cs b/GFramework.Game/Config/YamlConfigAllowedValue.cs new file mode 100644 index 00000000..60077ef5 --- /dev/null +++ b/GFramework.Game/Config/YamlConfigAllowedValue.cs @@ -0,0 +1,32 @@ +namespace GFramework.Game.Config; + +/// +/// 表示一个节点上声明的单个 enum 候选值。 +/// 该模型同时保留稳定比较键与原始 JSON 文本,分别供运行时匹配和诊断输出复用。 +/// +internal sealed class YamlConfigAllowedValue +{ + /// + /// 初始化一个枚举候选值模型。 + /// + /// 用于与 YAML 节点比较的稳定键。 + /// 用于诊断输出的原始 JSON 文本。 + public YamlConfigAllowedValue(string comparableValue, string displayValue) + { + ArgumentNullException.ThrowIfNull(comparableValue); + ArgumentException.ThrowIfNullOrWhiteSpace(displayValue); + + ComparableValue = comparableValue; + DisplayValue = displayValue; + } + + /// + /// 获取用于运行时比较的稳定键。 + /// + public string ComparableValue { get; } + + /// + /// 获取用于诊断输出的原始 JSON 文本。 + /// + public string DisplayValue { get; } +} diff --git a/GFramework.Game/Config/YamlConfigArrayConstraints.cs b/GFramework.Game/Config/YamlConfigArrayConstraints.cs new file mode 100644 index 00000000..64ddaf3c --- /dev/null +++ b/GFramework.Game/Config/YamlConfigArrayConstraints.cs @@ -0,0 +1,47 @@ +namespace GFramework.Game.Config; + +/// +/// 表示一个数组节点上声明的元素数量、去重与 contains 匹配计数约束。 +/// 该模型与标量约束拆分保存,避免数组节点继续共享不适用的标量字段。 +/// +internal sealed class YamlConfigArrayConstraints +{ + /// + /// 初始化数组约束模型。 + /// + /// 最小元素数量约束。 + /// 最大元素数量约束。 + /// 是否要求数组元素唯一。 + /// 数组 contains 约束;未声明时为空。 + public YamlConfigArrayConstraints( + int? minItems, + int? maxItems, + bool uniqueItems, + YamlConfigArrayContainsConstraints? containsConstraints) + { + MinItems = minItems; + MaxItems = maxItems; + UniqueItems = uniqueItems; + ContainsConstraints = containsConstraints; + } + + /// + /// 获取最小元素数量约束。 + /// + public int? MinItems { get; } + + /// + /// 获取最大元素数量约束。 + /// + public int? MaxItems { get; } + + /// + /// 获取是否要求数组元素唯一。 + /// + public bool UniqueItems { get; } + + /// + /// 获取数组 contains 约束;未声明时返回空。 + /// + public YamlConfigArrayContainsConstraints? ContainsConstraints { get; } +} diff --git a/GFramework.Game/Config/YamlConfigArrayContainsConstraints.cs b/GFramework.Game/Config/YamlConfigArrayContainsConstraints.cs new file mode 100644 index 00000000..a61800f8 --- /dev/null +++ b/GFramework.Game/Config/YamlConfigArrayContainsConstraints.cs @@ -0,0 +1,41 @@ +namespace GFramework.Game.Config; + +/// +/// 表示数组节点声明的 contains 匹配约束。 +/// 该模型把 contains 子 schema 与匹配数量边界聚合在一起,避免数组节点再额外散落多组相关成员。 +/// +internal sealed class YamlConfigArrayContainsConstraints +{ + /// + /// 初始化数组 contains 约束模型。 + /// + /// contains 子 schema。 + /// 最小匹配数量;为 时按 JSON Schema 语义默认 1。 + /// 最大匹配数量。 + public YamlConfigArrayContainsConstraints( + YamlConfigSchemaNode containsNode, + int? minContains, + int? maxContains) + { + ArgumentNullException.ThrowIfNull(containsNode); + + ContainsNode = containsNode; + MinContains = minContains; + MaxContains = maxContains; + } + + /// + /// 获取 contains 子 schema。 + /// + public YamlConfigSchemaNode ContainsNode { get; } + + /// + /// 获取最小匹配数量;未显式声明时返回空,由调用方按默认值 1 解释。 + /// + public int? MinContains { get; } + + /// + /// 获取最大匹配数量。 + /// + public int? MaxContains { get; } +} diff --git a/GFramework.Game/Config/YamlConfigConditionalSchemas.cs b/GFramework.Game/Config/YamlConfigConditionalSchemas.cs new file mode 100644 index 00000000..7d4bbbf2 --- /dev/null +++ b/GFramework.Game/Config/YamlConfigConditionalSchemas.cs @@ -0,0 +1,41 @@ +namespace GFramework.Game.Config; + +/// +/// 表示一个对象节点上声明的 object-focused if / then / else 条件约束。 +/// 三个分支都共享父对象已声明字段集合,不会把分支 schema 扩展成新的生成类型形状。 +/// +internal sealed class YamlConfigConditionalSchemas +{ + /// + /// 初始化条件分支约束模型。 + /// + /// 条件判断 schema。 + /// 条件命中时需要满足的 schema。 + /// 条件未命中时需要满足的 schema。 + public YamlConfigConditionalSchemas( + YamlConfigSchemaNode ifSchema, + YamlConfigSchemaNode? thenSchema, + YamlConfigSchemaNode? elseSchema) + { + ArgumentNullException.ThrowIfNull(ifSchema); + + IfSchema = ifSchema; + ThenSchema = thenSchema; + ElseSchema = elseSchema; + } + + /// + /// 获取条件判断 schema。 + /// + public YamlConfigSchemaNode IfSchema { get; } + + /// + /// 获取条件命中时需要满足的 schema。 + /// + public YamlConfigSchemaNode? ThenSchema { get; } + + /// + /// 获取条件未命中时需要满足的 schema。 + /// + public YamlConfigSchemaNode? ElseSchema { get; } +} diff --git a/GFramework.Game/Config/YamlConfigConstantValue.cs b/GFramework.Game/Config/YamlConfigConstantValue.cs new file mode 100644 index 00000000..48caa4c5 --- /dev/null +++ b/GFramework.Game/Config/YamlConfigConstantValue.cs @@ -0,0 +1,32 @@ +namespace GFramework.Game.Config; + +/// +/// 表示一个节点上声明的 const 约束。 +/// 该模型同时保留稳定比较键与原始 JSON 文本,分别供运行时匹配和诊断输出复用。 +/// +internal sealed class YamlConfigConstantValue +{ + /// + /// 初始化常量约束模型。 + /// + /// 用于与 YAML 节点比较的稳定键。 + /// 用于诊断输出的原始常量文本。 + public YamlConfigConstantValue(string comparableValue, string displayValue) + { + ArgumentNullException.ThrowIfNull(comparableValue); + ArgumentException.ThrowIfNullOrWhiteSpace(displayValue); + + ComparableValue = comparableValue; + DisplayValue = displayValue; + } + + /// + /// 获取用于运行时比较的稳定键。 + /// + public string ComparableValue { get; } + + /// + /// 获取用于诊断输出的原始 JSON 常量文本。 + /// + public string DisplayValue { get; } +} diff --git a/GFramework.Game/Config/YamlConfigNumericConstraints.cs b/GFramework.Game/Config/YamlConfigNumericConstraints.cs new file mode 100644 index 00000000..977eb007 --- /dev/null +++ b/GFramework.Game/Config/YamlConfigNumericConstraints.cs @@ -0,0 +1,55 @@ +namespace GFramework.Game.Config; + +/// +/// 表示标量节点上声明的数值范围与步进约束。 +/// 该类型只覆盖整数 / 浮点共享的关键字,避免字符串字段继续暴露不相关的成员。 +/// +internal sealed class YamlConfigNumericConstraints +{ + /// + /// 初始化数值约束模型。 + /// + /// 最小值约束。 + /// 最大值约束。 + /// 开区间最小值约束。 + /// 开区间最大值约束。 + /// 数值步进约束。 + public YamlConfigNumericConstraints( + double? minimum, + double? maximum, + double? exclusiveMinimum, + double? exclusiveMaximum, + double? multipleOf) + { + Minimum = minimum; + Maximum = maximum; + ExclusiveMinimum = exclusiveMinimum; + ExclusiveMaximum = exclusiveMaximum; + MultipleOf = multipleOf; + } + + /// + /// 获取最小值约束。 + /// + public double? Minimum { get; } + + /// + /// 获取最大值约束。 + /// + public double? Maximum { get; } + + /// + /// 获取开区间最小值约束。 + /// + public double? ExclusiveMinimum { get; } + + /// + /// 获取开区间最大值约束。 + /// + public double? ExclusiveMaximum { get; } + + /// + /// 获取数值步进约束。 + /// + public double? MultipleOf { get; } +} diff --git a/GFramework.Game/Config/YamlConfigObjectConstraints.cs b/GFramework.Game/Config/YamlConfigObjectConstraints.cs new file mode 100644 index 00000000..01b460f9 --- /dev/null +++ b/GFramework.Game/Config/YamlConfigObjectConstraints.cs @@ -0,0 +1,67 @@ +namespace GFramework.Game.Config; + +/// +/// 表示一个对象节点上声明的属性数量约束、字段依赖约束、条件子 schema 与组合约束。 +/// 该模型将对象级约束与数组 / 标量约束拆开保存,避免运行时节点继续暴露无关成员。 +/// +internal sealed class YamlConfigObjectConstraints +{ + /// + /// 初始化对象约束模型。 + /// + /// 最小属性数量约束。 + /// 最大属性数量约束。 + /// 对象内字段依赖约束。 + /// 对象内条件 schema 约束。 + /// 对象内组合 schema 约束。 + /// 对象内条件分支约束。 + public YamlConfigObjectConstraints( + int? minProperties, + int? maxProperties, + IReadOnlyDictionary>? dependentRequired, + IReadOnlyDictionary? dependentSchemas, + IReadOnlyList? allOfSchemas, + YamlConfigConditionalSchemas? conditionalSchemas) + { + MinProperties = minProperties; + MaxProperties = maxProperties; + DependentRequired = dependentRequired; + DependentSchemas = dependentSchemas; + AllOfSchemas = allOfSchemas; + ConditionalSchemas = conditionalSchemas; + } + + /// + /// 获取最小属性数量约束。 + /// + public int? MinProperties { get; } + + /// + /// 获取最大属性数量约束。 + /// + public int? MaxProperties { get; } + + /// + /// 获取对象内字段依赖约束。 + /// 键表示“触发字段”,值表示“触发字段出现后还必须存在的同级字段集合”。 + /// + public IReadOnlyDictionary>? DependentRequired { get; } + + /// + /// 获取对象内条件 schema 约束。 + /// 键表示“触发字段”,值表示“触发字段出现后当前对象还必须满足的额外 schema 子树”。 + /// + public IReadOnlyDictionary? DependentSchemas { get; } + + /// + /// 获取对象内 allOf 组合约束。 + /// 每个条目都表示“当前对象还必须额外满足的 focused constraint block”。 + /// + public IReadOnlyList? AllOfSchemas { get; } + + /// + /// 获取对象内 object-focused if / then / else 条件约束。 + /// 该模型会先用 if 试匹配当前对象,再只对命中的分支叠加 focused constraint block。 + /// + public YamlConfigConditionalSchemas? ConditionalSchemas { get; } +} diff --git a/GFramework.Game/Config/YamlConfigReferenceUsage.cs b/GFramework.Game/Config/YamlConfigReferenceUsage.cs new file mode 100644 index 00000000..ba9ff1ea --- /dev/null +++ b/GFramework.Game/Config/YamlConfigReferenceUsage.cs @@ -0,0 +1,74 @@ +namespace GFramework.Game.Config; + +/// +/// 表示单个 YAML 文件中提取出的跨表引用。 +/// 该模型保留源文件、字段路径和目标表等诊断信息,以便加载器在批量校验失败时给出可定位的错误。 +/// +internal sealed class YamlConfigReferenceUsage +{ + /// + /// 初始化一个跨表引用使用记录。 + /// + /// 源 YAML 文件路径。 + /// 定义该引用的 schema 文件路径。 + /// 声明引用的字段路径。 + /// YAML 中的原始标量值。 + /// 目标配置表名称。 + /// 引用值的 schema 标量类型。 + public YamlConfigReferenceUsage( + string yamlPath, + string schemaPath, + string propertyPath, + string rawValue, + string referencedTableName, + YamlConfigSchemaPropertyType valueType) + { + ArgumentNullException.ThrowIfNull(yamlPath); + ArgumentNullException.ThrowIfNull(schemaPath); + ArgumentNullException.ThrowIfNull(propertyPath); + ArgumentNullException.ThrowIfNull(rawValue); + ArgumentNullException.ThrowIfNull(referencedTableName); + + YamlPath = yamlPath; + SchemaPath = schemaPath; + PropertyPath = propertyPath; + RawValue = rawValue; + ReferencedTableName = referencedTableName; + ValueType = valueType; + } + + /// + /// 获取源 YAML 文件路径。 + /// + public string YamlPath { get; } + + /// + /// 获取定义该引用的 schema 文件路径。 + /// + public string SchemaPath { get; } + + /// + /// 获取声明引用的字段路径。 + /// + public string PropertyPath { get; } + + /// + /// 获取 YAML 中的原始标量值。 + /// + public string RawValue { get; } + + /// + /// 获取目标配置表名称。 + /// + public string ReferencedTableName { get; } + + /// + /// 获取引用值的 schema 标量类型。 + /// + public YamlConfigSchemaPropertyType ValueType { get; } + + /// + /// 获取便于诊断显示的字段路径。 + /// + public string DisplayPath => PropertyPath; +} diff --git a/GFramework.Game/Config/YamlConfigScalarConstraints.cs b/GFramework.Game/Config/YamlConfigScalarConstraints.cs new file mode 100644 index 00000000..df632030 --- /dev/null +++ b/GFramework.Game/Config/YamlConfigScalarConstraints.cs @@ -0,0 +1,31 @@ +namespace GFramework.Game.Config; + +/// +/// 聚合一个标量节点上声明的数值约束与字符串约束。 +/// 该包装层保留“标量字段有约束”的统一入口,同时把不同语义的约束分成更小的专用模型。 +/// +internal sealed class YamlConfigScalarConstraints +{ + /// + /// 初始化标量约束模型。 + /// + /// 数值约束分组。 + /// 字符串约束分组。 + public YamlConfigScalarConstraints( + YamlConfigNumericConstraints? numericConstraints, + YamlConfigStringConstraints? stringConstraints) + { + NumericConstraints = numericConstraints; + StringConstraints = stringConstraints; + } + + /// + /// 获取数值约束分组。 + /// + public YamlConfigNumericConstraints? NumericConstraints { get; } + + /// + /// 获取字符串约束分组。 + /// + public YamlConfigStringConstraints? StringConstraints { get; } +} diff --git a/GFramework.Game/Config/YamlConfigSchema.cs b/GFramework.Game/Config/YamlConfigSchema.cs new file mode 100644 index 00000000..e3ae2f0c --- /dev/null +++ b/GFramework.Game/Config/YamlConfigSchema.cs @@ -0,0 +1,44 @@ +namespace GFramework.Game.Config; + +/// +/// 表示已解析并可用于运行时校验的 JSON Schema。 +/// 该模型保留根节点与引用依赖集合,避免运行时引入完整 schema 引擎。 +/// +internal sealed class YamlConfigSchema +{ + /// + /// 初始化一个可用于运行时校验的 schema 模型。 + /// + /// Schema 文件路径。 + /// 根节点模型。 + /// Schema 声明的目标引用表名称集合。 + public YamlConfigSchema( + string schemaPath, + YamlConfigSchemaNode rootNode, + IReadOnlyCollection referencedTableNames) + { + ArgumentNullException.ThrowIfNull(schemaPath); + ArgumentNullException.ThrowIfNull(rootNode); + ArgumentNullException.ThrowIfNull(referencedTableNames); + + SchemaPath = schemaPath; + RootNode = rootNode; + ReferencedTableNames = referencedTableNames; + } + + /// + /// 获取 schema 文件路径。 + /// + public string SchemaPath { get; } + + /// + /// 获取根节点模型。 + /// + public YamlConfigSchemaNode RootNode { get; } + + /// + /// 获取 schema 声明的目标引用表名称集合。 + /// 该信息用于热重载时推导受影响的依赖表闭包。 + /// + public IReadOnlyCollection ReferencedTableNames { get; } +} diff --git a/GFramework.Game/Config/YamlConfigSchemaNode.cs b/GFramework.Game/Config/YamlConfigSchemaNode.cs new file mode 100644 index 00000000..35509bef --- /dev/null +++ b/GFramework.Game/Config/YamlConfigSchemaNode.cs @@ -0,0 +1,330 @@ +namespace GFramework.Game.Config; + +/// +/// 表示单个 schema 节点的最小运行时描述。 +/// 同一个模型同时覆盖对象、数组和标量,便于递归校验逻辑只依赖一种树结构。 +/// +internal sealed class YamlConfigSchemaNode +{ + private readonly NodeChildren _children; + private readonly NodeValidation _validation; + + private YamlConfigSchemaNode( + YamlConfigSchemaPropertyType nodeType, + NodeChildren children, + NodeValidation validation, + string schemaPathHint) + { + ArgumentNullException.ThrowIfNull(children); + ArgumentNullException.ThrowIfNull(validation); + ArgumentNullException.ThrowIfNull(schemaPathHint); + + _children = children; + _validation = validation; + NodeType = nodeType; + Properties = children.Properties; + RequiredProperties = children.RequiredProperties; + ItemNode = children.ItemNode; + ReferenceTableName = validation.ReferenceTableName; + AllowedValues = validation.AllowedValues; + Constraints = validation.Constraints; + ArrayConstraints = validation.ArrayConstraints; + ObjectConstraints = validation.ObjectConstraints; + ConstantValue = validation.ConstantValue; + NegatedSchemaNode = validation.NegatedSchemaNode; + SchemaPathHint = schemaPathHint; + } + + /// + /// 获取节点类型。 + /// + public YamlConfigSchemaPropertyType NodeType { get; } + + /// + /// 获取对象属性集合;非对象节点时返回空。 + /// + public IReadOnlyDictionary? Properties { get; } + + /// + /// 获取对象必填属性集合;非对象节点时返回空。 + /// + public IReadOnlyCollection? RequiredProperties { get; } + + /// + /// 获取数组元素节点;非数组节点时返回空。 + /// + public YamlConfigSchemaNode? ItemNode { get; } + + /// + /// 获取目标引用表名称;未声明跨表引用时返回空。 + /// + public string? ReferenceTableName { get; } + + /// + /// 获取节点允许值集合;未声明 enum 时返回空。 + /// + public IReadOnlyCollection? AllowedValues { get; } + + /// + /// 获取标量范围与长度约束;未声明时返回空。 + /// + public YamlConfigScalarConstraints? Constraints { get; } + + /// + /// 获取对象属性数量约束;未声明时返回空。 + /// + public YamlConfigObjectConstraints? ObjectConstraints { get; } + + /// + /// 获取数组元素数量约束;未声明时返回空。 + /// + public YamlConfigArrayConstraints? ArrayConstraints { get; } + + /// + /// 获取节点常量约束;未声明 const 时返回空。 + /// + public YamlConfigConstantValue? ConstantValue { get; } + + /// + /// 获取节点声明的 not 子 schema;未声明时返回空。 + /// + public YamlConfigSchemaNode? NegatedSchemaNode { get; } + + /// + /// 获取用于诊断显示的 schema 路径提示。 + /// 当前节点本身不记录独立路径,因此对象校验会回退到所属根 schema 路径。 + /// + public string SchemaPathHint { get; } + + /// + /// 创建对象节点描述。 + /// + /// 对象属性集合。 + /// 对象必填属性集合。 + /// 对象属性数量约束。 + /// 用于错误信息的 schema 文件路径提示。 + /// 对象节点模型。 + public static YamlConfigSchemaNode CreateObject( + IReadOnlyDictionary? properties, + IReadOnlyCollection? requiredProperties, + YamlConfigObjectConstraints? objectConstraints, + string schemaPathHint) + { + return new YamlConfigSchemaNode( + YamlConfigSchemaPropertyType.Object, + new NodeChildren(properties, requiredProperties, itemNode: null), + new NodeValidation( + referenceTableName: null, + allowedValues: null, + constraints: null, + arrayConstraints: null, + objectConstraints, + constantValue: null, + negatedSchemaNode: null), + schemaPathHint); + } + + /// + /// 创建数组节点描述。 + /// + /// 数组元素节点。 + /// 数组节点允许值集合。 + /// 数组元素数量约束。 + /// 用于错误信息的 schema 文件路径提示。 + /// 数组节点模型。 + public static YamlConfigSchemaNode CreateArray( + YamlConfigSchemaNode itemNode, + IReadOnlyCollection? allowedValues, + YamlConfigArrayConstraints? arrayConstraints, + string schemaPathHint) + { + return new YamlConfigSchemaNode( + YamlConfigSchemaPropertyType.Array, + new NodeChildren(properties: null, requiredProperties: null, itemNode), + new NodeValidation( + referenceTableName: null, + allowedValues, + constraints: null, + arrayConstraints, + objectConstraints: null, + constantValue: null, + negatedSchemaNode: null), + schemaPathHint); + } + + /// + /// 创建标量节点描述。 + /// + /// 标量节点类型。 + /// 目标引用表名称。 + /// 标量允许值集合。 + /// 标量范围与长度约束。 + /// 用于错误信息的 schema 文件路径提示。 + /// 标量节点模型。 + public static YamlConfigSchemaNode CreateScalar( + YamlConfigSchemaPropertyType nodeType, + string? referenceTableName, + IReadOnlyCollection? allowedValues, + YamlConfigScalarConstraints? constraints, + string schemaPathHint) + { + return new YamlConfigSchemaNode( + nodeType, + NodeChildren.None, + new NodeValidation( + referenceTableName, + allowedValues, + constraints, + arrayConstraints: null, + objectConstraints: null, + constantValue: null, + negatedSchemaNode: null), + schemaPathHint); + } + + /// + /// 基于当前节点复制一个只替换引用表名称的新节点。 + /// 该方法用于把数组级别的 ref-table 语义挂接到元素节点上。 + /// + /// 新的目标引用表名称。 + /// 复制后的节点。 + public YamlConfigSchemaNode WithReferenceTable(string referenceTableName) + { + return new YamlConfigSchemaNode( + NodeType, + _children, + _validation.WithReferenceTable(referenceTableName), + SchemaPathHint); + } + + /// + /// 基于当前节点复制一个只替换 enum 允许值集合的新节点。 + /// + /// 新的允许值集合。 + /// 复制后的节点。 + public YamlConfigSchemaNode WithAllowedValues(IReadOnlyCollection? allowedValues) + { + return new YamlConfigSchemaNode( + NodeType, + _children, + _validation.WithAllowedValues(allowedValues), + SchemaPathHint); + } + + /// + /// 基于当前节点复制一个只替换常量约束的新节点。 + /// + /// 新的常量约束。 + /// 复制后的节点。 + public YamlConfigSchemaNode WithConstantValue(YamlConfigConstantValue? constantValue) + { + return new YamlConfigSchemaNode( + NodeType, + _children, + _validation.WithConstantValue(constantValue), + SchemaPathHint); + } + + /// + /// 基于当前节点复制一个只替换 not 子 schema 的新节点。 + /// + /// 新的 negated schema。 + /// 复制后的节点。 + public YamlConfigSchemaNode WithNegatedSchemaNode(YamlConfigSchemaNode? negatedSchemaNode) + { + return new YamlConfigSchemaNode( + NodeType, + _children, + _validation.WithNegatedSchemaNode(negatedSchemaNode), + SchemaPathHint); + } + + private sealed class NodeChildren + { + public NodeChildren( + IReadOnlyDictionary? properties, + IReadOnlyCollection? requiredProperties, + YamlConfigSchemaNode? itemNode) + { + Properties = properties; + RequiredProperties = requiredProperties; + ItemNode = itemNode; + } + + public static NodeChildren None { get; } = new(properties: null, requiredProperties: null, itemNode: null); + + public IReadOnlyDictionary? Properties { get; } + + public IReadOnlyCollection? RequiredProperties { get; } + + public YamlConfigSchemaNode? ItemNode { get; } + } + + private sealed class NodeValidation + { + public NodeValidation( + string? referenceTableName, + IReadOnlyCollection? allowedValues, + YamlConfigScalarConstraints? constraints, + YamlConfigArrayConstraints? arrayConstraints, + YamlConfigObjectConstraints? objectConstraints, + YamlConfigConstantValue? constantValue, + YamlConfigSchemaNode? negatedSchemaNode) + { + ReferenceTableName = referenceTableName; + AllowedValues = allowedValues; + Constraints = constraints; + ArrayConstraints = arrayConstraints; + ObjectConstraints = objectConstraints; + ConstantValue = constantValue; + NegatedSchemaNode = negatedSchemaNode; + } + + public static NodeValidation None { get; } = new( + referenceTableName: null, + allowedValues: null, + constraints: null, + arrayConstraints: null, + objectConstraints: null, + constantValue: null, + negatedSchemaNode: null); + + public string? ReferenceTableName { get; } + + public IReadOnlyCollection? AllowedValues { get; } + + public YamlConfigScalarConstraints? Constraints { get; } + + public YamlConfigArrayConstraints? ArrayConstraints { get; } + + public YamlConfigObjectConstraints? ObjectConstraints { get; } + + public YamlConfigConstantValue? ConstantValue { get; } + + public YamlConfigSchemaNode? NegatedSchemaNode { get; } + + public NodeValidation WithReferenceTable(string referenceTableName) + { + return new NodeValidation(referenceTableName, AllowedValues, Constraints, ArrayConstraints, + ObjectConstraints, ConstantValue, NegatedSchemaNode); + } + + public NodeValidation WithAllowedValues(IReadOnlyCollection? allowedValues) + { + return new NodeValidation(ReferenceTableName, allowedValues, Constraints, ArrayConstraints, + ObjectConstraints, ConstantValue, NegatedSchemaNode); + } + + public NodeValidation WithConstantValue(YamlConfigConstantValue? constantValue) + { + return new NodeValidation(ReferenceTableName, AllowedValues, Constraints, ArrayConstraints, + ObjectConstraints, constantValue, NegatedSchemaNode); + } + + public NodeValidation WithNegatedSchemaNode(YamlConfigSchemaNode? negatedSchemaNode) + { + return new NodeValidation(ReferenceTableName, AllowedValues, Constraints, ArrayConstraints, + ObjectConstraints, ConstantValue, negatedSchemaNode); + } + } +} diff --git a/GFramework.Game/Config/YamlConfigSchemaPropertyType.cs b/GFramework.Game/Config/YamlConfigSchemaPropertyType.cs new file mode 100644 index 00000000..725819dc --- /dev/null +++ b/GFramework.Game/Config/YamlConfigSchemaPropertyType.cs @@ -0,0 +1,37 @@ +namespace GFramework.Game.Config; + +/// +/// 表示当前运行时 schema 校验器支持的属性类型。 +/// +internal enum YamlConfigSchemaPropertyType +{ + /// + /// 对象类型。 + /// + Object, + + /// + /// 整数类型。 + /// + Integer, + + /// + /// 数值类型。 + /// + Number, + + /// + /// 布尔类型。 + /// + Boolean, + + /// + /// 字符串类型。 + /// + String, + + /// + /// 数组类型。 + /// + Array +} diff --git a/GFramework.Game/Config/YamlConfigSchemaValidator.cs b/GFramework.Game/Config/YamlConfigSchemaValidator.cs index 4350e035..42d187ce 100644 --- a/GFramework.Game/Config/YamlConfigSchemaValidator.cs +++ b/GFramework.Game/Config/YamlConfigSchemaValidator.cs @@ -3650,950 +3650,3 @@ internal static partial class YamlConfigSchemaValidator !string.Equals(tag, "tag:yaml.org,2002:null", StringComparison.Ordinal); } } - -/// -/// 表示已解析并可用于运行时校验的 JSON Schema。 -/// 该模型保留根节点与引用依赖集合,避免运行时引入完整 schema 引擎。 -/// -internal sealed class YamlConfigSchema -{ - /// - /// 初始化一个可用于运行时校验的 schema 模型。 - /// - /// Schema 文件路径。 - /// 根节点模型。 - /// Schema 声明的目标引用表名称集合。 - public YamlConfigSchema( - string schemaPath, - YamlConfigSchemaNode rootNode, - IReadOnlyCollection referencedTableNames) - { - ArgumentNullException.ThrowIfNull(schemaPath); - ArgumentNullException.ThrowIfNull(rootNode); - ArgumentNullException.ThrowIfNull(referencedTableNames); - - SchemaPath = schemaPath; - RootNode = rootNode; - ReferencedTableNames = referencedTableNames; - } - - /// - /// 获取 schema 文件路径。 - /// - public string SchemaPath { get; } - - /// - /// 获取根节点模型。 - /// - public YamlConfigSchemaNode RootNode { get; } - - /// - /// 获取 schema 声明的目标引用表名称集合。 - /// 该信息用于热重载时推导受影响的依赖表闭包。 - /// - public IReadOnlyCollection ReferencedTableNames { get; } -} - -/// -/// 表示单个 schema 节点的最小运行时描述。 -/// 同一个模型同时覆盖对象、数组和标量,便于递归校验逻辑只依赖一种树结构。 -/// -internal sealed class YamlConfigSchemaNode -{ - private readonly NodeChildren _children; - private readonly NodeValidation _validation; - - private YamlConfigSchemaNode( - YamlConfigSchemaPropertyType nodeType, - NodeChildren children, - NodeValidation validation, - string schemaPathHint) - { - ArgumentNullException.ThrowIfNull(children); - ArgumentNullException.ThrowIfNull(validation); - ArgumentNullException.ThrowIfNull(schemaPathHint); - - _children = children; - _validation = validation; - NodeType = nodeType; - Properties = children.Properties; - RequiredProperties = children.RequiredProperties; - ItemNode = children.ItemNode; - ReferenceTableName = validation.ReferenceTableName; - AllowedValues = validation.AllowedValues; - Constraints = validation.Constraints; - ArrayConstraints = validation.ArrayConstraints; - ObjectConstraints = validation.ObjectConstraints; - ConstantValue = validation.ConstantValue; - NegatedSchemaNode = validation.NegatedSchemaNode; - SchemaPathHint = schemaPathHint; - } - - /// - /// 获取节点类型。 - /// - public YamlConfigSchemaPropertyType NodeType { get; } - - /// - /// 获取对象属性集合;非对象节点时返回空。 - /// - public IReadOnlyDictionary? Properties { get; } - - /// - /// 获取对象必填属性集合;非对象节点时返回空。 - /// - public IReadOnlyCollection? RequiredProperties { get; } - - /// - /// 获取数组元素节点;非数组节点时返回空。 - /// - public YamlConfigSchemaNode? ItemNode { get; } - - /// - /// 获取目标引用表名称;未声明跨表引用时返回空。 - /// - public string? ReferenceTableName { get; } - - /// - /// 获取节点允许值集合;未声明 enum 时返回空。 - /// - public IReadOnlyCollection? AllowedValues { get; } - - /// - /// 获取标量范围与长度约束;未声明时返回空。 - /// - public YamlConfigScalarConstraints? Constraints { get; } - - /// - /// 获取对象属性数量约束;未声明时返回空。 - /// - public YamlConfigObjectConstraints? ObjectConstraints { get; } - - /// - /// 获取数组元素数量约束;未声明时返回空。 - /// - public YamlConfigArrayConstraints? ArrayConstraints { get; } - - /// - /// 获取节点常量约束;未声明 const 时返回空。 - /// - public YamlConfigConstantValue? ConstantValue { get; } - - /// - /// 获取节点声明的 not 子 schema;未声明时返回空。 - /// - public YamlConfigSchemaNode? NegatedSchemaNode { get; } - - /// - /// 获取用于诊断显示的 schema 路径提示。 - /// 当前节点本身不记录独立路径,因此对象校验会回退到所属根 schema 路径。 - /// - public string SchemaPathHint { get; } - - /// - /// 创建对象节点描述。 - /// - /// 对象属性集合。 - /// 对象必填属性集合。 - /// 对象属性数量约束。 - /// 用于错误信息的 schema 文件路径提示。 - /// 对象节点模型。 - public static YamlConfigSchemaNode CreateObject( - IReadOnlyDictionary? properties, - IReadOnlyCollection? requiredProperties, - YamlConfigObjectConstraints? objectConstraints, - string schemaPathHint) - { - return new YamlConfigSchemaNode( - YamlConfigSchemaPropertyType.Object, - new NodeChildren(properties, requiredProperties, itemNode: null), - new NodeValidation( - referenceTableName: null, - allowedValues: null, - constraints: null, - arrayConstraints: null, - objectConstraints, - constantValue: null, - negatedSchemaNode: null), - schemaPathHint); - } - - /// - /// 创建数组节点描述。 - /// - /// 数组元素节点。 - /// 数组节点允许值集合。 - /// 数组元素数量约束。 - /// 用于错误信息的 schema 文件路径提示。 - /// 数组节点模型。 - public static YamlConfigSchemaNode CreateArray( - YamlConfigSchemaNode itemNode, - IReadOnlyCollection? allowedValues, - YamlConfigArrayConstraints? arrayConstraints, - string schemaPathHint) - { - return new YamlConfigSchemaNode( - YamlConfigSchemaPropertyType.Array, - new NodeChildren(properties: null, requiredProperties: null, itemNode), - new NodeValidation( - referenceTableName: null, - allowedValues, - constraints: null, - arrayConstraints, - objectConstraints: null, - constantValue: null, - negatedSchemaNode: null), - schemaPathHint); - } - - /// - /// 创建标量节点描述。 - /// - /// 标量节点类型。 - /// 目标引用表名称。 - /// 标量允许值集合。 - /// 标量范围与长度约束。 - /// 用于错误信息的 schema 文件路径提示。 - /// 标量节点模型。 - public static YamlConfigSchemaNode CreateScalar( - YamlConfigSchemaPropertyType nodeType, - string? referenceTableName, - IReadOnlyCollection? allowedValues, - YamlConfigScalarConstraints? constraints, - string schemaPathHint) - { - return new YamlConfigSchemaNode( - nodeType, - NodeChildren.None, - new NodeValidation( - referenceTableName, - allowedValues, - constraints, - arrayConstraints: null, - objectConstraints: null, - constantValue: null, - negatedSchemaNode: null), - schemaPathHint); - } - - /// - /// 基于当前节点复制一个只替换引用表名称的新节点。 - /// 该方法用于把数组级别的 ref-table 语义挂接到元素节点上。 - /// - /// 新的目标引用表名称。 - /// 复制后的节点。 - public YamlConfigSchemaNode WithReferenceTable(string referenceTableName) - { - return new YamlConfigSchemaNode( - NodeType, - _children, - _validation.WithReferenceTable(referenceTableName), - SchemaPathHint); - } - - /// - /// 基于当前节点复制一个只替换 enum 允许值集合的新节点。 - /// - /// 新的允许值集合。 - /// 复制后的节点。 - public YamlConfigSchemaNode WithAllowedValues(IReadOnlyCollection? allowedValues) - { - return new YamlConfigSchemaNode( - NodeType, - _children, - _validation.WithAllowedValues(allowedValues), - SchemaPathHint); - } - - /// - /// 基于当前节点复制一个只替换常量约束的新节点。 - /// - /// 新的常量约束。 - /// 复制后的节点。 - public YamlConfigSchemaNode WithConstantValue(YamlConfigConstantValue? constantValue) - { - return new YamlConfigSchemaNode( - NodeType, - _children, - _validation.WithConstantValue(constantValue), - SchemaPathHint); - } - - /// - /// 基于当前节点复制一个只替换 not 子 schema 的新节点。 - /// - /// 新的 negated schema。 - /// 复制后的节点。 - public YamlConfigSchemaNode WithNegatedSchemaNode(YamlConfigSchemaNode? negatedSchemaNode) - { - return new YamlConfigSchemaNode( - NodeType, - _children, - _validation.WithNegatedSchemaNode(negatedSchemaNode), - SchemaPathHint); - } - - private sealed class NodeChildren - { - public NodeChildren( - IReadOnlyDictionary? properties, - IReadOnlyCollection? requiredProperties, - YamlConfigSchemaNode? itemNode) - { - Properties = properties; - RequiredProperties = requiredProperties; - ItemNode = itemNode; - } - - public static NodeChildren None { get; } = new(properties: null, requiredProperties: null, itemNode: null); - - public IReadOnlyDictionary? Properties { get; } - - public IReadOnlyCollection? RequiredProperties { get; } - - public YamlConfigSchemaNode? ItemNode { get; } - } - - private sealed class NodeValidation - { - public NodeValidation( - string? referenceTableName, - IReadOnlyCollection? allowedValues, - YamlConfigScalarConstraints? constraints, - YamlConfigArrayConstraints? arrayConstraints, - YamlConfigObjectConstraints? objectConstraints, - YamlConfigConstantValue? constantValue, - YamlConfigSchemaNode? negatedSchemaNode) - { - ReferenceTableName = referenceTableName; - AllowedValues = allowedValues; - Constraints = constraints; - ArrayConstraints = arrayConstraints; - ObjectConstraints = objectConstraints; - ConstantValue = constantValue; - NegatedSchemaNode = negatedSchemaNode; - } - - public static NodeValidation None { get; } = new( - referenceTableName: null, - allowedValues: null, - constraints: null, - arrayConstraints: null, - objectConstraints: null, - constantValue: null, - negatedSchemaNode: null); - - public string? ReferenceTableName { get; } - - public IReadOnlyCollection? AllowedValues { get; } - - public YamlConfigScalarConstraints? Constraints { get; } - - public YamlConfigArrayConstraints? ArrayConstraints { get; } - - public YamlConfigObjectConstraints? ObjectConstraints { get; } - - public YamlConfigConstantValue? ConstantValue { get; } - - public YamlConfigSchemaNode? NegatedSchemaNode { get; } - - public NodeValidation WithReferenceTable(string referenceTableName) - { - return new NodeValidation(referenceTableName, AllowedValues, Constraints, ArrayConstraints, - ObjectConstraints, ConstantValue, NegatedSchemaNode); - } - - public NodeValidation WithAllowedValues(IReadOnlyCollection? allowedValues) - { - return new NodeValidation(ReferenceTableName, allowedValues, Constraints, ArrayConstraints, - ObjectConstraints, ConstantValue, NegatedSchemaNode); - } - - public NodeValidation WithConstantValue(YamlConfigConstantValue? constantValue) - { - return new NodeValidation(ReferenceTableName, AllowedValues, Constraints, ArrayConstraints, - ObjectConstraints, constantValue, NegatedSchemaNode); - } - - public NodeValidation WithNegatedSchemaNode(YamlConfigSchemaNode? negatedSchemaNode) - { - return new NodeValidation(ReferenceTableName, AllowedValues, Constraints, ArrayConstraints, - ObjectConstraints, ConstantValue, negatedSchemaNode); - } - } -} - -/// -/// 表示一个节点上声明的 const 约束。 -/// 该模型同时保留稳定比较键与原始 JSON 文本,分别供运行时匹配和诊断输出复用。 -/// -internal sealed class YamlConfigConstantValue -{ - /// - /// 初始化常量约束模型。 - /// - /// 用于与 YAML 节点比较的稳定键。 - /// 用于诊断输出的原始常量文本。 - public YamlConfigConstantValue(string comparableValue, string displayValue) - { - ArgumentNullException.ThrowIfNull(comparableValue); - ArgumentException.ThrowIfNullOrWhiteSpace(displayValue); - - ComparableValue = comparableValue; - DisplayValue = displayValue; - } - - /// - /// 获取用于运行时比较的稳定键。 - /// - public string ComparableValue { get; } - - /// - /// 获取用于诊断输出的原始 JSON 常量文本。 - /// - public string DisplayValue { get; } -} - -/// -/// 表示一个节点上声明的单个 enum 候选值。 -/// 该模型同时保留稳定比较键与原始 JSON 文本,分别供运行时匹配和诊断输出复用。 -/// -internal sealed class YamlConfigAllowedValue -{ - /// - /// 初始化一个枚举候选值模型。 - /// - /// 用于与 YAML 节点比较的稳定键。 - /// 用于诊断输出的原始 JSON 文本。 - public YamlConfigAllowedValue(string comparableValue, string displayValue) - { - ArgumentNullException.ThrowIfNull(comparableValue); - ArgumentException.ThrowIfNullOrWhiteSpace(displayValue); - - ComparableValue = comparableValue; - DisplayValue = displayValue; - } - - /// - /// 获取用于运行时比较的稳定键。 - /// - public string ComparableValue { get; } - - /// - /// 获取用于诊断输出的原始 JSON 文本。 - /// - public string DisplayValue { get; } -} - -/// -/// 表示一个对象节点上声明的属性数量约束、字段依赖约束、条件子 schema 与组合约束。 -/// 该模型将对象级约束与数组 / 标量约束拆开保存,避免运行时节点继续暴露无关成员。 -/// -internal sealed class YamlConfigObjectConstraints -{ - /// - /// 初始化对象约束模型。 - /// - /// 最小属性数量约束。 - /// 最大属性数量约束。 - /// 对象内字段依赖约束。 - /// 对象内条件 schema 约束。 - /// 对象内组合 schema 约束。 - /// 对象内条件分支约束。 - public YamlConfigObjectConstraints( - int? minProperties, - int? maxProperties, - IReadOnlyDictionary>? dependentRequired, - IReadOnlyDictionary? dependentSchemas, - IReadOnlyList? allOfSchemas, - YamlConfigConditionalSchemas? conditionalSchemas) - { - MinProperties = minProperties; - MaxProperties = maxProperties; - DependentRequired = dependentRequired; - DependentSchemas = dependentSchemas; - AllOfSchemas = allOfSchemas; - ConditionalSchemas = conditionalSchemas; - } - - /// - /// 获取最小属性数量约束。 - /// - public int? MinProperties { get; } - - /// - /// 获取最大属性数量约束。 - /// - public int? MaxProperties { get; } - - /// - /// 获取对象内字段依赖约束。 - /// 键表示“触发字段”,值表示“触发字段出现后还必须存在的同级字段集合”。 - /// - public IReadOnlyDictionary>? DependentRequired { get; } - - /// - /// 获取对象内条件 schema 约束。 - /// 键表示“触发字段”,值表示“触发字段出现后当前对象还必须满足的额外 schema 子树”。 - /// - public IReadOnlyDictionary? DependentSchemas { get; } - - /// - /// 获取对象内 allOf 组合约束。 - /// 每个条目都表示“当前对象还必须额外满足的 focused constraint block”。 - /// - public IReadOnlyList? AllOfSchemas { get; } - - /// - /// 获取对象内 object-focused if / then / else 条件约束。 - /// 该模型会先用 if 试匹配当前对象,再只对命中的分支叠加 focused constraint block。 - /// - public YamlConfigConditionalSchemas? ConditionalSchemas { get; } -} - -/// -/// 表示一个对象节点上声明的 object-focused if / then / else 条件约束。 -/// 三个分支都共享父对象已声明字段集合,不会把分支 schema 扩展成新的生成类型形状。 -/// -internal sealed class YamlConfigConditionalSchemas -{ - /// - /// 初始化条件分支约束模型。 - /// - /// 条件判断 schema。 - /// 条件命中时需要满足的 schema。 - /// 条件未命中时需要满足的 schema。 - public YamlConfigConditionalSchemas( - YamlConfigSchemaNode ifSchema, - YamlConfigSchemaNode? thenSchema, - YamlConfigSchemaNode? elseSchema) - { - ArgumentNullException.ThrowIfNull(ifSchema); - - IfSchema = ifSchema; - ThenSchema = thenSchema; - ElseSchema = elseSchema; - } - - /// - /// 获取条件判断 schema。 - /// - public YamlConfigSchemaNode IfSchema { get; } - - /// - /// 获取条件命中时需要满足的 schema。 - /// - public YamlConfigSchemaNode? ThenSchema { get; } - - /// - /// 获取条件未命中时需要满足的 schema。 - /// - public YamlConfigSchemaNode? ElseSchema { get; } -} - -/// -/// 聚合一个标量节点上声明的数值约束与字符串约束。 -/// 该包装层保留“标量字段有约束”的统一入口,同时把不同语义的约束分成更小的专用模型。 -/// -internal sealed class YamlConfigScalarConstraints -{ - /// - /// 初始化标量约束模型。 - /// - /// 数值约束分组。 - /// 字符串约束分组。 - public YamlConfigScalarConstraints( - YamlConfigNumericConstraints? numericConstraints, - YamlConfigStringConstraints? stringConstraints) - { - NumericConstraints = numericConstraints; - StringConstraints = stringConstraints; - } - - /// - /// 获取数值约束分组。 - /// - public YamlConfigNumericConstraints? NumericConstraints { get; } - - /// - /// 获取字符串约束分组。 - /// - public YamlConfigStringConstraints? StringConstraints { get; } -} - -/// -/// 表示标量节点上声明的数值范围与步进约束。 -/// 该类型只覆盖整数 / 浮点共享的关键字,避免字符串字段继续暴露不相关的成员。 -/// -internal sealed class YamlConfigNumericConstraints -{ - /// - /// 初始化数值约束模型。 - /// - /// 最小值约束。 - /// 最大值约束。 - /// 开区间最小值约束。 - /// 开区间最大值约束。 - /// 数值步进约束。 - public YamlConfigNumericConstraints( - double? minimum, - double? maximum, - double? exclusiveMinimum, - double? exclusiveMaximum, - double? multipleOf) - { - Minimum = minimum; - Maximum = maximum; - ExclusiveMinimum = exclusiveMinimum; - ExclusiveMaximum = exclusiveMaximum; - MultipleOf = multipleOf; - } - - /// - /// 获取最小值约束。 - /// - public double? Minimum { get; } - - /// - /// 获取最大值约束。 - /// - public double? Maximum { get; } - - /// - /// 获取开区间最小值约束。 - /// - public double? ExclusiveMinimum { get; } - - /// - /// 获取开区间最大值约束。 - /// - public double? ExclusiveMaximum { get; } - - /// - /// 获取数值步进约束。 - /// - public double? MultipleOf { get; } -} - -/// -/// 表示标量节点上声明的字符串长度、模式与 format 约束。 -/// 该模型将正则原文、预编译正则和共享 format 枚举绑定保存, -/// 保证诊断内容与运行时匹配逻辑保持一致。 -/// -internal sealed class YamlConfigStringConstraints -{ - /// - /// 初始化字符串约束模型。 - /// - /// 最小长度约束。 - /// 最大长度约束。 - /// 正则模式约束原文。 - /// 已编译的正则表达式。 - /// 字符串 format 约束。 - public YamlConfigStringConstraints( - int? minLength, - int? maxLength, - string? pattern, - Regex? patternRegex, - YamlConfigStringFormatConstraint? formatConstraint) - { - MinLength = minLength; - MaxLength = maxLength; - Pattern = pattern; - PatternRegex = patternRegex; - FormatConstraint = formatConstraint; - } - - /// - /// 获取最小长度约束。 - /// - public int? MinLength { get; } - - /// - /// 获取最大长度约束。 - /// - public int? MaxLength { get; } - - /// - /// 获取正则模式约束原文。 - /// - public string? Pattern { get; } - - /// - /// 获取已编译的正则表达式。 - /// - public Regex? PatternRegex { get; } - - /// - /// 获取字符串 format 约束。 - /// - public YamlConfigStringFormatConstraint? FormatConstraint { get; } -} - -/// -/// 表示一个已归一化的字符串 format 约束。 -/// 该模型同时保留 schema 原文与共享枚举,方便诊断信息稳定展示,又避免运行时校验反复解析字符串。 -/// -internal sealed class YamlConfigStringFormatConstraint -{ - /// - /// 初始化字符串 format 约束模型。 - /// - /// schema 中声明的 format 名称。 - /// 归一化后的共享 format 枚举。 - public YamlConfigStringFormatConstraint( - string schemaName, - YamlConfigStringFormatKind kind) - { - ArgumentException.ThrowIfNullOrWhiteSpace(schemaName); - - SchemaName = schemaName; - Kind = kind; - } - - /// - /// 获取 schema 中声明的 format 名称。 - /// - public string SchemaName { get; } - - /// - /// 获取归一化后的共享 format 枚举。 - /// - public YamlConfigStringFormatKind Kind { get; } -} - -/// -/// 表示当前 Runtime / Generator / Tooling 共享支持的字符串 format 子集。 -/// -internal enum YamlConfigStringFormatKind -{ - /// - /// 表示 yyyy-MM-dd 形式的日期。 - /// - Date, - - /// - /// 表示带显式时区偏移的 RFC 3339 日期时间。 - /// - DateTime, - - /// - /// 表示 day-time duration 形式的持续时间。 - /// - Duration, - - /// - /// 表示基础电子邮件地址格式。 - /// - Email, - - /// - /// 表示带显式时区偏移的 RFC 3339 时间。 - /// - Time, - - /// - /// 表示绝对 URI。 - /// - Uri, - - /// - /// 表示连字符分隔的 UUID 文本。 - /// - Uuid -} - -/// -/// 表示一个数组节点上声明的元素数量、去重与 contains 匹配计数约束。 -/// 该模型与标量约束拆分保存,避免数组节点继续共享不适用的标量字段。 -/// -internal sealed class YamlConfigArrayConstraints -{ - /// - /// 初始化数组约束模型。 - /// - /// 最小元素数量约束。 - /// 最大元素数量约束。 - /// 是否要求数组元素唯一。 - /// 数组 contains 约束;未声明时为空。 - public YamlConfigArrayConstraints( - int? minItems, - int? maxItems, - bool uniqueItems, - YamlConfigArrayContainsConstraints? containsConstraints) - { - MinItems = minItems; - MaxItems = maxItems; - UniqueItems = uniqueItems; - ContainsConstraints = containsConstraints; - } - - /// - /// 获取最小元素数量约束。 - /// - public int? MinItems { get; } - - /// - /// 获取最大元素数量约束。 - /// - public int? MaxItems { get; } - - /// - /// 获取是否要求数组元素唯一。 - /// - public bool UniqueItems { get; } - - /// - /// 获取数组 contains 约束;未声明时返回空。 - /// - public YamlConfigArrayContainsConstraints? ContainsConstraints { get; } -} - -/// -/// 表示数组节点声明的 contains 匹配约束。 -/// 该模型把 contains 子 schema 与匹配数量边界聚合在一起,避免数组节点再额外散落多组相关成员。 -/// -internal sealed class YamlConfigArrayContainsConstraints -{ - /// - /// 初始化数组 contains 约束模型。 - /// - /// contains 子 schema。 - /// 最小匹配数量;为 时按 JSON Schema 语义默认 1。 - /// 最大匹配数量。 - public YamlConfigArrayContainsConstraints( - YamlConfigSchemaNode containsNode, - int? minContains, - int? maxContains) - { - ArgumentNullException.ThrowIfNull(containsNode); - - ContainsNode = containsNode; - MinContains = minContains; - MaxContains = maxContains; - } - - /// - /// 获取 contains 子 schema。 - /// - public YamlConfigSchemaNode ContainsNode { get; } - - /// - /// 获取最小匹配数量;未显式声明时返回空,由调用方按默认值 1 解释。 - /// - public int? MinContains { get; } - - /// - /// 获取最大匹配数量。 - /// - public int? MaxContains { get; } -} - -/// -/// 表示单个 YAML 文件中提取出的跨表引用。 -/// 该模型保留源文件、字段路径和目标表等诊断信息,以便加载器在批量校验失败时给出可定位的错误。 -/// -internal sealed class YamlConfigReferenceUsage -{ - /// - /// 初始化一个跨表引用使用记录。 - /// - /// 源 YAML 文件路径。 - /// 定义该引用的 schema 文件路径。 - /// 声明引用的字段路径。 - /// YAML 中的原始标量值。 - /// 目标配置表名称。 - /// 引用值的 schema 标量类型。 - public YamlConfigReferenceUsage( - string yamlPath, - string schemaPath, - string propertyPath, - string rawValue, - string referencedTableName, - YamlConfigSchemaPropertyType valueType) - { - ArgumentNullException.ThrowIfNull(yamlPath); - ArgumentNullException.ThrowIfNull(schemaPath); - ArgumentNullException.ThrowIfNull(propertyPath); - ArgumentNullException.ThrowIfNull(rawValue); - ArgumentNullException.ThrowIfNull(referencedTableName); - - YamlPath = yamlPath; - SchemaPath = schemaPath; - PropertyPath = propertyPath; - RawValue = rawValue; - ReferencedTableName = referencedTableName; - ValueType = valueType; - } - - /// - /// 获取源 YAML 文件路径。 - /// - public string YamlPath { get; } - - /// - /// 获取定义该引用的 schema 文件路径。 - /// - public string SchemaPath { get; } - - /// - /// 获取声明引用的字段路径。 - /// - public string PropertyPath { get; } - - /// - /// 获取 YAML 中的原始标量值。 - /// - public string RawValue { get; } - - /// - /// 获取目标配置表名称。 - /// - public string ReferencedTableName { get; } - - /// - /// 获取引用值的 schema 标量类型。 - /// - public YamlConfigSchemaPropertyType ValueType { get; } - - /// - /// 获取便于诊断显示的字段路径。 - /// - public string DisplayPath => PropertyPath; -} - -/// -/// 表示当前运行时 schema 校验器支持的属性类型。 -/// -internal enum YamlConfigSchemaPropertyType -{ - /// - /// 对象类型。 - /// - Object, - - /// - /// 整数类型。 - /// - Integer, - - /// - /// 数值类型。 - /// - Number, - - /// - /// 布尔类型。 - /// - Boolean, - - /// - /// 字符串类型。 - /// - String, - - /// - /// 数组类型。 - /// - Array -} diff --git a/GFramework.Game/Config/YamlConfigStringConstraints.cs b/GFramework.Game/Config/YamlConfigStringConstraints.cs new file mode 100644 index 00000000..efb75840 --- /dev/null +++ b/GFramework.Game/Config/YamlConfigStringConstraints.cs @@ -0,0 +1,58 @@ +using System.Text.RegularExpressions; + +namespace GFramework.Game.Config; + +/// +/// 表示标量节点上声明的字符串长度、模式与 format 约束。 +/// 该模型将正则原文、预编译正则和共享 format 枚举绑定保存, +/// 保证诊断内容与运行时匹配逻辑保持一致。 +/// +internal sealed class YamlConfigStringConstraints +{ + /// + /// 初始化字符串约束模型。 + /// + /// 最小长度约束。 + /// 最大长度约束。 + /// 正则模式约束原文。 + /// 已编译的正则表达式。 + /// 字符串 format 约束。 + public YamlConfigStringConstraints( + int? minLength, + int? maxLength, + string? pattern, + Regex? patternRegex, + YamlConfigStringFormatConstraint? formatConstraint) + { + MinLength = minLength; + MaxLength = maxLength; + Pattern = pattern; + PatternRegex = patternRegex; + FormatConstraint = formatConstraint; + } + + /// + /// 获取最小长度约束。 + /// + public int? MinLength { get; } + + /// + /// 获取最大长度约束。 + /// + public int? MaxLength { get; } + + /// + /// 获取正则模式约束原文。 + /// + public string? Pattern { get; } + + /// + /// 获取已编译的正则表达式。 + /// + public Regex? PatternRegex { get; } + + /// + /// 获取字符串 format 约束。 + /// + public YamlConfigStringFormatConstraint? FormatConstraint { get; } +} diff --git a/GFramework.Game/Config/YamlConfigStringFormatConstraint.cs b/GFramework.Game/Config/YamlConfigStringFormatConstraint.cs new file mode 100644 index 00000000..97c3a262 --- /dev/null +++ b/GFramework.Game/Config/YamlConfigStringFormatConstraint.cs @@ -0,0 +1,33 @@ +namespace GFramework.Game.Config; + +/// +/// 表示一个已归一化的字符串 format 约束。 +/// 该模型同时保留 schema 原文与共享枚举,方便诊断信息稳定展示,又避免运行时校验反复解析字符串。 +/// +internal sealed class YamlConfigStringFormatConstraint +{ + /// + /// 初始化字符串 format 约束模型。 + /// + /// schema 中声明的 format 名称。 + /// 归一化后的共享 format 枚举。 + public YamlConfigStringFormatConstraint( + string schemaName, + YamlConfigStringFormatKind kind) + { + ArgumentException.ThrowIfNullOrWhiteSpace(schemaName); + + SchemaName = schemaName; + Kind = kind; + } + + /// + /// 获取 schema 中声明的 format 名称。 + /// + public string SchemaName { get; } + + /// + /// 获取归一化后的共享 format 枚举。 + /// + public YamlConfigStringFormatKind Kind { get; } +} diff --git a/GFramework.Game/Config/YamlConfigStringFormatKind.cs b/GFramework.Game/Config/YamlConfigStringFormatKind.cs new file mode 100644 index 00000000..bb13a63c --- /dev/null +++ b/GFramework.Game/Config/YamlConfigStringFormatKind.cs @@ -0,0 +1,42 @@ +namespace GFramework.Game.Config; + +/// +/// 表示当前 Runtime / Generator / Tooling 共享支持的字符串 format 子集。 +/// +internal enum YamlConfigStringFormatKind +{ + /// + /// 表示 yyyy-MM-dd 形式的日期。 + /// + Date, + + /// + /// 表示带显式时区偏移的 RFC 3339 日期时间。 + /// + DateTime, + + /// + /// 表示 day-time duration 形式的持续时间。 + /// + Duration, + + /// + /// 表示基础电子邮件地址格式。 + /// + Email, + + /// + /// 表示带显式时区偏移的 RFC 3339 时间。 + /// + Time, + + /// + /// 表示绝对 URI。 + /// + Uri, + + /// + /// 表示连字符分隔的 UUID 文本。 + /// + Uuid +} diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index 920e3240..cc61a5bb 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -12,7 +12,8 @@ - `2026-04-29` 使用 `$gframework-batch-boot 50` 从 clean build warning 基线继续分批清理 analyzer warnings - 已接受三个 worker 的 `GFramework.Cqrs.Tests/Mediator/*` 独立切片,三个 Mediator 测试文件的 warning 已清零 - 主线程补齐 `YamlConfigSchemaValidator` 运行时正则 timeout 与 ordinal 字符串比较,先收掉低风险 `MA0009` / `MA0006` - - 当前停止条件为相对 `origin/main` 接近 `50` 个变更文件;本轮尚未接近阈值,下一批可继续处理 `GFramework.Game/Config/YamlConfigSchemaValidator*` + - 已收口两个 Game 追加切片:`YamlConfigSchemaValidator.ObjectKeywords.cs` 方法拆分与 schema model 类型拆文件 + - 当前停止条件为相对 `origin/main` 接近 `50` 个变更文件;本轮按用户要求到此结束,不再继续开新切片 ## 当前活跃事实 @@ -21,27 +22,30 @@ - `dotnet clean -p:RestoreFallbackFolders= -v:quiet` - 最新结果:成功;标准 `dotnet clean` 仍会先命中当前 WSL 环境的 Windows NuGet fallback 目录,已按既有环境口径先执行 `dotnet restore GFramework.sln -p:RestoreFallbackFolders= --disable-parallel` 后清理 - `dotnet build -p:RestoreFallbackFolders= -clp:WarningsOnly -v:minimal -m:1 -nodeReuse:false` - - 最新结果:成功;`75` warnings、`0` errors;warning 从本轮基线 `236` 降到 `75` + - 最新结果:成功;`15` warnings、`0` errors;warning 从本轮基线 `236` 降到 `15` - `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false -clp:Summary` - 最新结果:成功;`0 Warning(s)`、`0 Error(s)` - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-build -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Mediator"` - 最新结果:成功;`45` 通过、`0` 失败 - `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false -clp:Summary` - - 最新结果:成功;`75 Warning(s)`、`0 Error(s)`;剩余均为 `YamlConfigSchemaValidator*` 的 `MA0048` / `MA0051` + - 最新结果:成功;`0 Warning(s)`、`0 Error(s)` - `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~YamlConfigLoaderTests|FullyQualifiedName~YamlConfigSchemaValidatorTests"` - 最新结果:成功;`80` 通过、`0` 失败 - 当前批次摘要: - - 当前已提交分支相对 `origin/main...HEAD` 包含 `3` 个变更文件;本次主线程待提交的 `GFramework.Game/Config/YamlConfigSchemaValidator.cs` 与 `ai-plan` 文档会把累计变更推到约 `6` 个文件,低于 `50` 个文件阈值 + - 当前分支提交后预计相对 `origin/main...HEAD` 包含 `22` 个变更文件,低于 `50` 个文件阈值 - 已完成 worker 切片: - `ed269d4`:`MediatorArchitectureIntegrationTests.cs`,清理 `MA0048` / `MA0004` / `MA0016` - `121df44`:`MediatorAdvancedFeaturesTests.cs`,清理 `MA0048` / `MA0004` / `MA0015` - `9109eec`:`MediatorComprehensiveTests.cs`,清理 `MA0048` / `MA0004` / `MA0016` / `MA0002` / `MA0015` - 主线程切片:`YamlConfigSchemaValidator.cs` 正则 timeout 与 ordinal equality,清理 `MA0009` / `MA0006` + - Game 追加切片: + - `1395b84`:`YamlConfigSchemaValidator.ObjectKeywords.cs`,清理该文件 `MA0051` + - 待提交:将 `YamlConfigSchemaValidator.cs` 末尾 schema model 类型拆到独立同名文件,清理 `MA0048` ## 当前风险 -- `GFramework.Game/Config/YamlConfigSchemaValidator*` 仍然是仓库根 warning 热点,剩余 `45` 条 `MA0048` 与 `30` 条 `MA0051`。 - - 缓解措施:下一批优先把 `YamlConfigSchemaValidator.cs` 末尾 schema model 类型拆到独立文件,再评估 `MA0051` 方法拆分。 +- `GFramework.Game/Config/YamlConfigSchemaValidator.cs` 仍有 `5` 个 `MA0051` 方法长度 warning,跨 `net8.0` / `net9.0` / `net10.0` 重复为 `15` 条。 + - 缓解措施:下一轮只做主 validator 方法拆分,不再混入拆文件或正则安全修复。 - 标准 `dotnet clean` 在当前 WSL 环境仍会读取失效的 Windows fallback package folder。 - 缓解措施:本主题验证继续沿用 `-p:RestoreFallbackFolders=`,必要时先执行 solution restore 刷新 Linux 侧资产。 @@ -65,12 +69,12 @@ - 权威验证结果统一维护在“当前活跃事实”。 - `GFramework.Cqrs.Tests` 的当前受影响项目 Release 构建已清零,并通过 Mediator 定向测试回归。 -- `GFramework.Game` 当前低风险正则 / 字符串比较切片通过 Release 构建与 config 定向测试;剩余 warning 属于拆文件与复杂度拆分。 +- `GFramework.Game` 当前 Release 构建已清零,并通过 config 定向测试;仓库 Debug 构建剩余 warning 属于主 validator 方法复杂度拆分。 - `git diff --check` 结果为空,说明本轮新增改动没有引入新的尾随空格或冲突标记。 - warning reduction 的仓库级真值以同轮 `dotnet build`、定向 `dotnet test` 与 `git diff --check` 为准,并与 trace 中的验证里程碑保持一致。 ## 下一步建议 -1. 提交主线程 `YamlConfigSchemaValidator` 正则安全补丁与本轮 `ai-plan` 同步。 -2. 继续下一批 `GFramework.Game/Config/YamlConfigSchemaValidator.cs` 末尾 schema model 类型拆文件,目标清理 `MA0048`。 -3. 再评估 `YamlConfigSchemaValidator.ObjectKeywords.cs` 与主 validator 的 `MA0051` 方法拆分,避免单批触碰过多高耦合逻辑。 +1. 提交 schema model 拆文件与本轮 `ai-plan` 收口。 +2. 下一轮只处理 `GFramework.Game/Config/YamlConfigSchemaValidator.cs` 剩余 `MA0051` 方法拆分。 +3. 保持 `RestoreFallbackFolders=` 验证口径,避免当前 WSL fallback package folder 干扰。 diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index 95d3f165..f6b4be3f 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -17,27 +17,33 @@ - 主线程实施: - 在 `GFramework.Game/Config/YamlConfigSchemaValidator.cs` 为固定格式正则与 schema `pattern` 正则补充 timeout,避免运行时正则输入继续触发 `MA0009` - 将三处字符串等值比较改为 ordinal `string.Equals`,清理 `MA0006` + - 接受 `1395b84` 的 `YamlConfigSchemaValidator.ObjectKeywords.cs` 方法拆分,清理该文件 `MA0051` + - 收口被中止 worker 留下的 schema model 拆文件变更,将 `YamlConfigSchemaValidator.cs` 末尾类型移动到同名文件,清理 `MA0048` - 验证里程碑: - `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false -clp:Summary` - 结果:成功;`0 Warning(s)`、`0 Error(s)` - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-build -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Mediator"` - 结果:成功;`45` 通过、`0` 失败 - `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false -clp:Summary` - - 结果:成功;`75 Warning(s)`、`0 Error(s)` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` - `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~YamlConfigLoaderTests|FullyQualifiedName~YamlConfigSchemaValidatorTests"` - 结果:成功;`80` 通过、`0` 失败 - `dotnet clean -p:RestoreFallbackFolders= -v:quiet` - 结果:成功 - `dotnet build -p:RestoreFallbackFolders= -clp:WarningsOnly -v:minimal -m:1 -nodeReuse:false` - - 结果:成功;`75` warnings、`0` errors + - 中间结果:成功;`75` warnings、`0` errors + - `dotnet clean -p:RestoreFallbackFolders= -v:quiet` + - 结果:成功 + - `dotnet build -p:RestoreFallbackFolders= -clp:Summary -v:minimal -m:1 -nodeReuse:false` + - 结果:成功;`15 Warning(s)`、`0 Error(s)` - `git diff --check` - 结果:成功;无新增 whitespace / conflict-marker 问题 - 当前指标: - - warning 总数:`236` -> `75` - - 剩余 warning 分布:`GFramework.Game/Config/YamlConfigSchemaValidator.cs` `60` 条,`GFramework.Game/Config/YamlConfigSchemaValidator.ObjectKeywords.cs` `15` 条;规则为 `MA0048` `45` 条、`MA0051` `30` 条 - - 已提交分支 diff:`3` 个文件;主线程待提交后预计约 `6` 个文件,低于 `50` 个文件阈值 + - warning 总数:`236` -> `15` + - 剩余 warning 分布:`GFramework.Game/Config/YamlConfigSchemaValidator.cs` 的 `MA0051` `15` 条(5 个方法跨 3 个 TFM) + - 本轮提交后预计分支 diff:`22` 个文件,低于 `50` 个文件阈值 - 下一步: - - 提交主线程 Game / ai-plan 同步后,继续 `YamlConfigSchemaValidator.cs` 末尾 schema model 类型拆文件,优先清理 `MA0048` + - 按用户要求本轮到此结束;下一轮只处理 `YamlConfigSchemaValidator.cs` 剩余 `MA0051` 方法拆分 ## 2026-04-28 — RP-092 From 7da985947cbeec40b1812435f4a2ff6932936451 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:55:03 +0800 Subject: [PATCH 07/10] =?UTF-8?q?fix(game):=20=E6=B8=85=E7=90=86=E5=89=A9?= =?UTF-8?q?=E4=BD=99=E9=85=8D=E7=BD=AE=20schema=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 YamlConfigSchemaValidator 的长方法为语义化 helper,清理剩余 MA0051 warning - 修复 条件分支 helper 的字符串比较方式,避免新增 MA0006 warning - 更新 analyzer warning reduction 跟踪与 trace,记录仓库根 clean build 已归零 --- .../Config/YamlConfigSchemaValidator.cs | 805 +++++++++++++----- .../analyzer-warning-reduction-tracking.md | 51 +- .../analyzer-warning-reduction-trace.md | 35 + 3 files changed, 646 insertions(+), 245 deletions(-) diff --git a/GFramework.Game/Config/YamlConfigSchemaValidator.cs b/GFramework.Game/Config/YamlConfigSchemaValidator.cs index 42d187ce..3aaf1d0d 100644 --- a/GFramework.Game/Config/YamlConfigSchemaValidator.cs +++ b/GFramework.Game/Config/YamlConfigSchemaValidator.cs @@ -320,6 +320,30 @@ internal static partial class YamlConfigSchemaValidator string propertyPath, JsonElement element, bool isRoot = false) + { + var typeName = ResolveNodeTypeName(tableName, schemaPath, propertyPath, element); + var referenceTableName = TryGetReferenceTableName(tableName, schemaPath, propertyPath, element); + ValidateObjectOnlyKeywords(tableName, schemaPath, propertyPath, element, typeName); + + var parsedNode = CreateParsedNodeForType( + tableName, + schemaPath, + propertyPath, + element, + typeName, + referenceTableName, + isRoot); + return parsedNode.WithNegatedSchemaNode(ParseNegatedSchemaNode(tableName, schemaPath, propertyPath, element)); + } + + /// + /// 解析 schema 节点声明的类型名称,并在缺失或类型错误时立刻给出定位清晰的诊断。 + /// + private static string ResolveNodeTypeName( + string tableName, + string schemaPath, + string propertyPath, + JsonElement element) { if (!element.TryGetProperty("type", out var typeElement) || typeElement.ValueKind != JsonValueKind.String) @@ -332,20 +356,46 @@ internal static partial class YamlConfigSchemaValidator displayPath: GetDiagnosticPath(propertyPath)); } - var typeName = typeElement.GetString() ?? string.Empty; - var referenceTableName = TryGetReferenceTableName(tableName, schemaPath, propertyPath, element); - if (!string.Equals(typeName, "object", StringComparison.Ordinal) && - TryGetObjectOnlyKeywordName(element) is { } objectOnlyKeywordName) + return typeElement.GetString() ?? string.Empty; + } + + /// + /// 限制只允许对象 schema 使用对象专属关键字,避免后续分支在运行时才发现语义不兼容。 + /// + private static void ValidateObjectOnlyKeywords( + string tableName, + string schemaPath, + string propertyPath, + JsonElement element, + string typeName) + { + if (string.Equals(typeName, "object", StringComparison.Ordinal) || + TryGetObjectOnlyKeywordName(element) is not { } objectOnlyKeywordName) { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.SchemaUnsupported, - tableName, - $"Property '{propertyPath}' in schema file '{schemaPath}' can only declare '{objectOnlyKeywordName}' on object schemas.", - schemaPath: schemaPath, - displayPath: GetDiagnosticPath(propertyPath)); + return; } - var parsedNode = typeName switch + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"Property '{propertyPath}' in schema file '{schemaPath}' can only declare '{objectOnlyKeywordName}' on object schemas.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(propertyPath)); + } + + /// + /// 根据声明的 schema 类型分派到对应的节点解析器。 + /// + private static YamlConfigSchemaNode CreateParsedNodeForType( + string tableName, + string schemaPath, + string propertyPath, + JsonElement element, + string typeName, + string? referenceTableName, + bool isRoot) + { + return typeName switch { "object" => ParseObjectSchemaNode( tableName, @@ -391,7 +441,6 @@ internal static partial class YamlConfigSchemaValidator displayPath: GetDiagnosticPath(propertyPath), rawValue: typeName) }; - return parsedNode.WithNegatedSchemaNode(ParseNegatedSchemaNode(tableName, schemaPath, propertyPath, element)); } /// @@ -727,18 +776,68 @@ internal static partial class YamlConfigSchemaValidator ICollection? references, bool allowUnknownObjectProperties) { - if (node is not YamlMappingNode mappingNode) + var mappingNode = GetObjectMappingNode(tableName, yamlPath, displayPath, node, schemaNode); + var seenProperties = ValidateObjectPropertyEntries( + tableName, + yamlPath, + displayPath, + mappingNode, + schemaNode, + references, + allowUnknownObjectProperties); + ValidateRequiredObjectProperties(tableName, yamlPath, displayPath, schemaNode, seenProperties); + + ValidateObjectConstraints( + tableName, + yamlPath, + displayPath, + mappingNode, + seenProperties, + schemaNode, + references); + + ValidateAllowedValues(tableName, yamlPath, displayPath, mappingNode, schemaNode); + ValidateConstantValue(tableName, yamlPath, displayPath, mappingNode, schemaNode); + ValidateNegatedSchemaConstraint(tableName, yamlPath, displayPath, mappingNode, schemaNode); + } + + /// + /// 确认当前 YAML 节点确实是对象节点,避免后续属性枚举阶段再做重复判空与类型判断。 + /// + private static YamlMappingNode GetObjectMappingNode( + string tableName, + string yamlPath, + string displayPath, + YamlNode node, + YamlConfigSchemaNode schemaNode) + { + if (node is YamlMappingNode mappingNode) { - var subject = displayPath.Length == 0 ? "Root object" : $"Property '{displayPath}'"; - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.PropertyTypeMismatch, - tableName, - $"{subject} in config file '{yamlPath}' must be an object.", - yamlPath: yamlPath, - schemaPath: schemaNode.SchemaPathHint, - displayPath: GetDiagnosticPath(displayPath)); + return mappingNode; } + var subject = displayPath.Length == 0 ? "Root object" : $"Property '{displayPath}'"; + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.PropertyTypeMismatch, + tableName, + $"{subject} in config file '{yamlPath}' must be an object.", + yamlPath: yamlPath, + schemaPath: schemaNode.SchemaPathHint, + displayPath: GetDiagnosticPath(displayPath)); + } + + /// + /// 遍历对象属性并递归校验每个已声明字段,同时记录用于后续 required 与 dependency 判断的 sibling 集合。 + /// + private static HashSet ValidateObjectPropertyEntries( + string tableName, + string yamlPath, + string displayPath, + YamlMappingNode mappingNode, + YamlConfigSchemaNode schemaNode, + ICollection? references, + bool allowUnknownObjectProperties) + { var seenProperties = new HashSet(StringComparer.Ordinal); foreach (var entry in mappingNode.Children) { @@ -795,6 +894,19 @@ internal static partial class YamlConfigSchemaValidator allowUnknownObjectProperties); } + return seenProperties; + } + + /// + /// 在对象主体字段遍历结束后统一检查缺失的 required 字段,保证错误消息使用稳定的完整路径。 + /// + private static void ValidateRequiredObjectProperties( + string tableName, + string yamlPath, + string displayPath, + YamlConfigSchemaNode schemaNode, + HashSet seenProperties) + { if (schemaNode.RequiredProperties is null) { return; @@ -816,19 +928,6 @@ internal static partial class YamlConfigSchemaValidator schemaPath: schemaNode.SchemaPathHint, displayPath: requiredPath); } - - ValidateObjectConstraints( - tableName, - yamlPath, - displayPath, - mappingNode, - seenProperties, - schemaNode, - references); - - ValidateAllowedValues(tableName, yamlPath, displayPath, mappingNode, schemaNode); - ValidateConstantValue(tableName, yamlPath, displayPath, mappingNode, schemaNode); - ValidateNegatedSchemaConstraint(tableName, yamlPath, displayPath, mappingNode, schemaNode); } /// @@ -864,11 +963,76 @@ internal static partial class YamlConfigSchemaValidator } var propertyCount = seenProperties.Count; - var subject = string.IsNullOrWhiteSpace(displayPath) + var subject = GetObjectConstraintSubject(displayPath); + + ValidateObjectPropertyCountConstraints( + tableName, + yamlPath, + displayPath, + schemaNode, + constraints, + subject, + propertyCount); + ValidateDependentRequiredConstraints( + tableName, + yamlPath, + displayPath, + schemaNode, + constraints, + seenProperties); + ValidateDependentSchemas( + tableName, + yamlPath, + displayPath, + mappingNode, + schemaNode, + constraints, + references, + seenProperties, + subject); + ValidateAllOfSchemas( + tableName, + yamlPath, + displayPath, + mappingNode, + schemaNode, + constraints, + references, + subject); + ValidateConditionalObjectSchemas( + tableName, + yamlPath, + displayPath, + mappingNode, + schemaNode, + constraints, + references, + subject); + } + + /// + /// 为对象约束构造统一的诊断主语,保证根对象与嵌套对象的错误消息格式一致。 + /// + private static string GetObjectConstraintSubject(string displayPath) + { + return string.IsNullOrWhiteSpace(displayPath) ? "Root object" : $"Property '{displayPath}'"; - var rawValue = propertyCount.ToString(CultureInfo.InvariantCulture); + } + /// + /// 校验对象属性数量上下限。 + /// + private static void ValidateObjectPropertyCountConstraints( + string tableName, + string yamlPath, + string displayPath, + YamlConfigSchemaNode schemaNode, + YamlConfigObjectConstraints constraints, + string subject, + int propertyCount) + { + var rawValue = propertyCount.ToString(CultureInfo.InvariantCulture); if (constraints.MinProperties.HasValue && propertyCount < constraints.MinProperties.Value) { @@ -898,116 +1062,177 @@ internal static partial class YamlConfigSchemaValidator detail: $"Maximum property count: {constraints.MaxProperties.Value.ToString(CultureInfo.InvariantCulture)}."); } + } - if (constraints.DependentRequired is not null && - constraints.DependentRequired.Count > 0) + /// + /// 使用已见 sibling 集合校验 dependentRequired,确保对象主路径与试匹配路径共用同一判定语义。 + /// + private static void ValidateDependentRequiredConstraints( + string tableName, + string yamlPath, + string displayPath, + YamlConfigSchemaNode schemaNode, + YamlConfigObjectConstraints constraints, + HashSet seenProperties) + { + if (constraints.DependentRequired is null || + constraints.DependentRequired.Count == 0) { - // Reuse the collected sibling-name set so the main validation path and - // the contains/not matcher both interpret object dependencies identically. - foreach (var dependency in constraints.DependentRequired) - { - if (!seenProperties.Contains(dependency.Key)) - { - continue; - } - - var triggerPath = CombineDisplayPath(displayPath, dependency.Key); - foreach (var dependentProperty in dependency.Value) - { - if (seenProperties.Contains(dependentProperty)) - { - continue; - } - - var requiredPath = CombineDisplayPath(displayPath, dependentProperty); - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.MissingRequiredProperty, - tableName, - $"Property '{requiredPath}' in config file '{yamlPath}' is required when sibling property '{triggerPath}' is present.", - yamlPath: yamlPath, - schemaPath: schemaNode.SchemaPathHint, - displayPath: requiredPath, - detail: - $"Dependent requirement: when '{triggerPath}' exists, '{requiredPath}' must also be declared."); - } - } + return; } - if (constraints.DependentSchemas is not null && - constraints.DependentSchemas.Count > 0) + // Reuse the collected sibling-name set so the main validation path and + // the contains/not matcher both interpret object dependencies identically. + foreach (var dependency in constraints.DependentRequired) { - foreach (var dependency in constraints.DependentSchemas) + if (!seenProperties.Contains(dependency.Key)) { - if (!seenProperties.Contains(dependency.Key)) - { - continue; - } - - var triggerPath = CombineDisplayPath(displayPath, dependency.Key); - // dependentSchemas acts as an additional conditional constraint block on the - // current object. Keep undeclared sibling fields outside the dependent sub-schema - // from blocking the match so schema authors can express focused follow-up rules. - // The trial matcher merges only new reference usages back into the outer collector, - // so re-checking the same scalar via a conditional sub-schema does not duplicate - // cross-table validation work later in the loader pipeline. - if (TryMatchSchemaNode( - tableName, - yamlPath, - displayPath, - mappingNode, - dependency.Value, - references, - allowUnknownObjectProperties: true)) + continue; + } + + var triggerPath = CombineDisplayPath(displayPath, dependency.Key); + foreach (var dependentProperty in dependency.Value) + { + if (seenProperties.Contains(dependentProperty)) { continue; } + var requiredPath = CombineDisplayPath(displayPath, dependentProperty); throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.ConstraintViolation, + ConfigLoadFailureKind.MissingRequiredProperty, tableName, - $"{subject} in config file '{yamlPath}' must satisfy the 'dependentSchemas' schema triggered by sibling property '{triggerPath}'.", + $"Property '{requiredPath}' in config file '{yamlPath}' is required when sibling property '{triggerPath}' is present.", yamlPath: yamlPath, schemaPath: schemaNode.SchemaPathHint, - displayPath: GetDiagnosticPath(displayPath), + displayPath: requiredPath, detail: - $"Dependent schema: when '{triggerPath}' exists, the current object must satisfy the corresponding inline schema."); + $"Dependent requirement: when '{triggerPath}' exists, '{requiredPath}' must also be declared."); } } + } - if (constraints.AllOfSchemas is not null && - constraints.AllOfSchemas.Count > 0) + /// + /// 在触发字段出现时,以 focused matcher 语义试跑 dependentSchemas。 + /// + private static void ValidateDependentSchemas( + string tableName, + string yamlPath, + string displayPath, + YamlMappingNode mappingNode, + YamlConfigSchemaNode schemaNode, + YamlConfigObjectConstraints constraints, + ICollection? references, + HashSet seenProperties, + string subject) + { + if (constraints.DependentSchemas is null || + constraints.DependentSchemas.Count == 0) { - for (var index = 0; index < constraints.AllOfSchemas.Count; index++) - { - var allOfSchema = constraints.AllOfSchemas[index]; - // allOf follows the same focused constraint block semantics as dependentSchemas: - // the inline schema may validate a subset of the current object without forcing - // unrelated sibling fields to be restated. - if (TryMatchSchemaNode( - tableName, - yamlPath, - displayPath, - mappingNode, - allOfSchema, - references, - allowUnknownObjectProperties: true)) - { - continue; - } - - var allOfEntryNumber = index + 1; - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.ConstraintViolation, - tableName, - $"{subject} in config file '{yamlPath}' must satisfy all 'allOf' schemas, but entry #{allOfEntryNumber.ToString(CultureInfo.InvariantCulture)} did not match.", - yamlPath: yamlPath, - schemaPath: schemaNode.SchemaPathHint, - displayPath: GetDiagnosticPath(displayPath), - detail: - $"allOf entry #{allOfEntryNumber.ToString(CultureInfo.InvariantCulture)} must match the current object."); - } + return; } + foreach (var dependency in constraints.DependentSchemas) + { + if (!seenProperties.Contains(dependency.Key)) + { + continue; + } + + var triggerPath = CombineDisplayPath(displayPath, dependency.Key); + // dependentSchemas acts as an additional conditional constraint block on the + // current object. Keep undeclared sibling fields outside the dependent sub-schema + // from blocking the match so schema authors can express focused follow-up rules. + // The trial matcher merges only new reference usages back into the outer collector, + // so re-checking the same scalar via a conditional sub-schema does not duplicate + // cross-table validation work later in the loader pipeline. + if (TryMatchSchemaNode( + tableName, + yamlPath, + displayPath, + mappingNode, + dependency.Value, + references, + allowUnknownObjectProperties: true)) + { + continue; + } + + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.ConstraintViolation, + tableName, + $"{subject} in config file '{yamlPath}' must satisfy the 'dependentSchemas' schema triggered by sibling property '{triggerPath}'.", + yamlPath: yamlPath, + schemaPath: schemaNode.SchemaPathHint, + displayPath: GetDiagnosticPath(displayPath), + detail: + $"Dependent schema: when '{triggerPath}' exists, the current object must satisfy the corresponding inline schema."); + } + } + + /// + /// 逐条校验 allOf 约束,保持与 dependentSchemas 相同的 focused object 匹配语义。 + /// + private static void ValidateAllOfSchemas( + string tableName, + string yamlPath, + string displayPath, + YamlMappingNode mappingNode, + YamlConfigSchemaNode schemaNode, + YamlConfigObjectConstraints constraints, + ICollection? references, + string subject) + { + if (constraints.AllOfSchemas is null || + constraints.AllOfSchemas.Count == 0) + { + return; + } + + for (var index = 0; index < constraints.AllOfSchemas.Count; index++) + { + var allOfSchema = constraints.AllOfSchemas[index]; + // allOf follows the same focused constraint block semantics as dependentSchemas: + // the inline schema may validate a subset of the current object without forcing + // unrelated sibling fields to be restated. + if (TryMatchSchemaNode( + tableName, + yamlPath, + displayPath, + mappingNode, + allOfSchema, + references, + allowUnknownObjectProperties: true)) + { + continue; + } + + var allOfEntryNumber = index + 1; + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.ConstraintViolation, + tableName, + $"{subject} in config file '{yamlPath}' must satisfy all 'allOf' schemas, but entry #{allOfEntryNumber.ToString(CultureInfo.InvariantCulture)} did not match.", + yamlPath: yamlPath, + schemaPath: schemaNode.SchemaPathHint, + displayPath: GetDiagnosticPath(displayPath), + detail: + $"allOf entry #{allOfEntryNumber.ToString(CultureInfo.InvariantCulture)} must match the current object."); + } + } + + /// + /// 执行对象级 if/then/else 约束,并沿用 focused matcher 允许条件 schema 只声明关注字段。 + /// + private static void ValidateConditionalObjectSchemas( + string tableName, + string yamlPath, + string displayPath, + YamlMappingNode mappingNode, + YamlConfigSchemaNode schemaNode, + YamlConfigObjectConstraints constraints, + ICollection? references, + string subject) + { var conditionalSchemas = constraints.ConditionalSchemas; if (conditionalSchemas is null) { @@ -1025,49 +1250,72 @@ internal static partial class YamlConfigSchemaValidator conditionalSchemas.IfSchema, references, allowUnknownObjectProperties: true); - if (ifMatched && - conditionalSchemas.ThenSchema is not null && - !TryMatchSchemaNode( + ValidateConditionalSchemaBranch( + tableName, + yamlPath, + displayPath, + mappingNode, + schemaNode, + references, + subject, + ifMatched, + conditionalSchemas.ThenSchema, + branchName: "then", + failureDetail: + "Conditional schema: the current object matched the inline 'if' schema, so it must also satisfy the corresponding 'then' schema."); + ValidateConditionalSchemaBranch( + tableName, + yamlPath, + displayPath, + mappingNode, + schemaNode, + references, + subject, + !ifMatched, + conditionalSchemas.ElseSchema, + branchName: "else", + failureDetail: + "Conditional schema: the current object did not match the inline 'if' schema, so it must satisfy the corresponding 'else' schema."); + } + + /// + /// 校验 if/then/else 的单个分支,并在条件命中但分支未通过时提供统一诊断。 + /// + private static void ValidateConditionalSchemaBranch( + string tableName, + string yamlPath, + string displayPath, + YamlMappingNode mappingNode, + YamlConfigSchemaNode schemaNode, + ICollection? references, + string subject, + bool shouldValidate, + YamlConfigSchemaNode? branchSchema, + string branchName, + string failureDetail) + { + if (!shouldValidate || + branchSchema is null || + TryMatchSchemaNode( tableName, yamlPath, displayPath, mappingNode, - conditionalSchemas.ThenSchema, + branchSchema, references, allowUnknownObjectProperties: true)) { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.ConstraintViolation, - tableName, - $"{subject} in config file '{yamlPath}' must satisfy the 'then' schema because the inline 'if' condition matched.", - yamlPath: yamlPath, - schemaPath: schemaNode.SchemaPathHint, - displayPath: GetDiagnosticPath(displayPath), - detail: - "Conditional schema: the current object matched the inline 'if' schema, so it must also satisfy the corresponding 'then' schema."); + return; } - if (!ifMatched && - conditionalSchemas.ElseSchema is not null && - !TryMatchSchemaNode( - tableName, - yamlPath, - displayPath, - mappingNode, - conditionalSchemas.ElseSchema, - references, - allowUnknownObjectProperties: true)) - { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.ConstraintViolation, - tableName, - $"{subject} in config file '{yamlPath}' must satisfy the 'else' schema because the inline 'if' condition did not match.", - yamlPath: yamlPath, - schemaPath: schemaNode.SchemaPathHint, - displayPath: GetDiagnosticPath(displayPath), - detail: - "Conditional schema: the current object did not match the inline 'if' schema, so it must satisfy the corresponding 'else' schema."); - } + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.ConstraintViolation, + tableName, + $"{subject} in config file '{yamlPath}' must satisfy the '{branchName}' schema because the inline 'if' condition {(string.Equals(branchName, "then", StringComparison.Ordinal) ? "matched" : "did not match")}.", + yamlPath: yamlPath, + schemaPath: schemaNode.SchemaPathHint, + displayPath: GetDiagnosticPath(displayPath), + detail: failureDetail); } /// @@ -1154,29 +1402,81 @@ internal static partial class YamlConfigSchemaValidator YamlConfigSchemaNode schemaNode, ICollection? references) { - if (node is not YamlScalarNode scalarNode) + var scalarNode = GetScalarNodeOrThrow(tableName, yamlPath, displayPath, node, schemaNode); + var value = GetScalarValueOrThrow(tableName, yamlPath, displayPath, scalarNode, schemaNode); + ValidateScalarTypeOrThrow(tableName, yamlPath, displayPath, scalarNode, schemaNode, value); + var normalizedValue = NormalizeScalarValue(schemaNode.NodeType, value); + ValidateAllowedValues(tableName, yamlPath, displayPath, scalarNode, schemaNode); + + if (schemaNode.Constraints is not null) { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.PropertyTypeMismatch, - tableName, - $"Property '{displayPath}' in config file '{yamlPath}' must be a scalar value of type '{GetTypeName(schemaNode.NodeType)}'.", - yamlPath: yamlPath, - schemaPath: schemaNode.SchemaPathHint, - displayPath: GetDiagnosticPath(displayPath)); + ValidateScalarConstraints(tableName, yamlPath, displayPath, value, normalizedValue, schemaNode); } - var value = scalarNode.Value; - if (value is null) + ValidateConstantValue(tableName, yamlPath, displayPath, scalarNode, schemaNode); + ValidateNegatedSchemaConstraint(tableName, yamlPath, displayPath, scalarNode, schemaNode); + CollectScalarReferenceUsage(yamlPath, displayPath, schemaNode, references, normalizedValue); + } + + /// + /// 确认 schema 期望的节点在 YAML 中仍然表现为标量。 + /// + private static YamlScalarNode GetScalarNodeOrThrow( + string tableName, + string yamlPath, + string displayPath, + YamlNode node, + YamlConfigSchemaNode schemaNode) + { + if (node is YamlScalarNode scalarNode) { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.NullScalarValue, - tableName, - $"Property '{displayPath}' in config file '{yamlPath}' cannot be null when schema type is '{GetTypeName(schemaNode.NodeType)}'.", - yamlPath: yamlPath, - schemaPath: schemaNode.SchemaPathHint, - displayPath: GetDiagnosticPath(displayPath)); + return scalarNode; } + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.PropertyTypeMismatch, + tableName, + $"Property '{displayPath}' in config file '{yamlPath}' must be a scalar value of type '{GetTypeName(schemaNode.NodeType)}'.", + yamlPath: yamlPath, + schemaPath: schemaNode.SchemaPathHint, + displayPath: GetDiagnosticPath(displayPath)); + } + + /// + /// 读取标量文本值,并把 YAML 显式 null 与普通空字符串区分开。 + /// + private static string GetScalarValueOrThrow( + string tableName, + string yamlPath, + string displayPath, + YamlScalarNode scalarNode, + YamlConfigSchemaNode schemaNode) + { + if (scalarNode.Value is { } value) + { + return value; + } + + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.NullScalarValue, + tableName, + $"Property '{displayPath}' in config file '{yamlPath}' cannot be null when schema type is '{GetTypeName(schemaNode.NodeType)}'.", + yamlPath: yamlPath, + schemaPath: schemaNode.SchemaPathHint, + displayPath: GetDiagnosticPath(displayPath)); + } + + /// + /// 按 schema 声明的标量类型验证 YAML 文本值。 + /// + private static void ValidateScalarTypeOrThrow( + string tableName, + string yamlPath, + string displayPath, + YamlScalarNode scalarNode, + YamlConfigSchemaNode schemaNode, + string value) + { var tag = scalarNode.Tag.ToString(); var isValid = schemaNode.NodeType switch { @@ -1194,42 +1494,45 @@ internal static partial class YamlConfigSchemaValidator YamlConfigSchemaPropertyType.Boolean => bool.TryParse(value, out _), _ => false }; - - if (!isValid) + if (isValid) { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.PropertyTypeMismatch, - tableName, - $"Property '{displayPath}' in config file '{yamlPath}' must be of type '{GetTypeName(schemaNode.NodeType)}', but the current YAML scalar value is '{value}'.", - yamlPath: yamlPath, - schemaPath: schemaNode.SchemaPathHint, - displayPath: GetDiagnosticPath(displayPath), - rawValue: value); + return; } - var normalizedValue = NormalizeScalarValue(schemaNode.NodeType, value); - ValidateAllowedValues(tableName, yamlPath, displayPath, scalarNode, schemaNode); + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.PropertyTypeMismatch, + tableName, + $"Property '{displayPath}' in config file '{yamlPath}' must be of type '{GetTypeName(schemaNode.NodeType)}', but the current YAML scalar value is '{value}'.", + yamlPath: yamlPath, + schemaPath: schemaNode.SchemaPathHint, + displayPath: GetDiagnosticPath(displayPath), + rawValue: value); + } - if (schemaNode.Constraints is not null) + /// + /// 在标量值成功通过本地校验后,再把引用信息回写给外层收集器。 + /// + private static void CollectScalarReferenceUsage( + string yamlPath, + string displayPath, + YamlConfigSchemaNode schemaNode, + ICollection? references, + string normalizedValue) + { + if (schemaNode.ReferenceTableName is null || + references is null) { - ValidateScalarConstraints(tableName, yamlPath, displayPath, value, normalizedValue, schemaNode); + return; } - ValidateConstantValue(tableName, yamlPath, displayPath, scalarNode, schemaNode); - ValidateNegatedSchemaConstraint(tableName, yamlPath, displayPath, scalarNode, schemaNode); - - if (schemaNode.ReferenceTableName != null && - references is not null) - { - references.Add( - new YamlConfigReferenceUsage( - yamlPath, - schemaNode.SchemaPathHint, - displayPath, - normalizedValue, - schemaNode.ReferenceTableName, - schemaNode.NodeType)); - } + references.Add( + new YamlConfigReferenceUsage( + yamlPath, + schemaNode.SchemaPathHint, + displayPath, + normalizedValue, + schemaNode.ReferenceTableName, + schemaNode.NodeType)); } /// @@ -2392,6 +2695,45 @@ internal static partial class YamlConfigSchemaValidator rawValue, normalizedValue, schemaNode); + ValidateNumericLowerBounds( + tableName, + yamlPath, + displayPath, + rawValue, + schemaNode, + constraints, + numericValue); + ValidateNumericUpperBounds( + tableName, + yamlPath, + displayPath, + rawValue, + schemaNode, + constraints, + numericValue); + ValidateNumericMultipleOfConstraint( + tableName, + yamlPath, + displayPath, + rawValue, + normalizedValue, + schemaNode, + constraints, + numericValue); + } + + /// + /// 校验数值的最小值与开区间下界。 + /// + private static void ValidateNumericLowerBounds( + string tableName, + string yamlPath, + string displayPath, + string rawValue, + YamlConfigSchemaNode schemaNode, + YamlConfigNumericConstraints constraints, + double numericValue) + { if (constraints.Minimum.HasValue && numericValue < constraints.Minimum.Value) { throw ConfigLoadExceptionFactory.Create( @@ -2418,7 +2760,20 @@ internal static partial class YamlConfigSchemaValidator detail: $"Exclusive minimum allowed value: {constraints.ExclusiveMinimum.Value.ToString(CultureInfo.InvariantCulture)}."); } + } + /// + /// 校验数值的最大值与开区间上界。 + /// + private static void ValidateNumericUpperBounds( + string tableName, + string yamlPath, + string displayPath, + string rawValue, + YamlConfigSchemaNode schemaNode, + YamlConfigNumericConstraints constraints, + double numericValue) + { if (constraints.Maximum.HasValue && numericValue > constraints.Maximum.Value) { throw ConfigLoadExceptionFactory.Create( @@ -2445,21 +2800,37 @@ internal static partial class YamlConfigSchemaValidator detail: $"Exclusive maximum allowed value: {constraints.ExclusiveMaximum.Value.ToString(CultureInfo.InvariantCulture)}."); } + } - if (constraints.MultipleOf.HasValue && - !IsMultipleOf(normalizedValue, numericValue, constraints.MultipleOf.Value)) + /// + /// 校验数值是否满足 multipleOf 步进约束。 + /// + private static void ValidateNumericMultipleOfConstraint( + string tableName, + string yamlPath, + string displayPath, + string rawValue, + string normalizedValue, + YamlConfigSchemaNode schemaNode, + YamlConfigNumericConstraints constraints, + double numericValue) + { + if (!constraints.MultipleOf.HasValue || + IsMultipleOf(normalizedValue, numericValue, constraints.MultipleOf.Value)) { - throw ConfigLoadExceptionFactory.Create( - ConfigLoadFailureKind.ConstraintViolation, - tableName, - $"Property '{displayPath}' in config file '{yamlPath}' must be a multiple of {constraints.MultipleOf.Value.ToString(CultureInfo.InvariantCulture)}, but the current YAML scalar value is '{rawValue}'.", - yamlPath: yamlPath, - schemaPath: schemaNode.SchemaPathHint, - displayPath: GetDiagnosticPath(displayPath), - rawValue: rawValue, - detail: - $"Required numeric step: {constraints.MultipleOf.Value.ToString(CultureInfo.InvariantCulture)}."); + return; } + + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.ConstraintViolation, + tableName, + $"Property '{displayPath}' in config file '{yamlPath}' must be a multiple of {constraints.MultipleOf.Value.ToString(CultureInfo.InvariantCulture)}, but the current YAML scalar value is '{rawValue}'.", + yamlPath: yamlPath, + schemaPath: schemaNode.SchemaPathHint, + displayPath: GetDiagnosticPath(displayPath), + rawValue: rawValue, + detail: + $"Required numeric step: {constraints.MultipleOf.Value.ToString(CultureInfo.InvariantCulture)}."); } /// diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index cc61a5bb..315d0e0c 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -6,48 +6,43 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-093` -- 当前阶段:`Phase 93` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-094` +- 当前阶段:`Phase 94` - 当前焦点: - - `2026-04-29` 使用 `$gframework-batch-boot 50` 从 clean build warning 基线继续分批清理 analyzer warnings - - 已接受三个 worker 的 `GFramework.Cqrs.Tests/Mediator/*` 独立切片,三个 Mediator 测试文件的 warning 已清零 - - 主线程补齐 `YamlConfigSchemaValidator` 运行时正则 timeout 与 ordinal 字符串比较,先收掉低风险 `MA0009` / `MA0006` - - 已收口两个 Game 追加切片:`YamlConfigSchemaValidator.ObjectKeywords.cs` 方法拆分与 schema model 类型拆文件 - - 当前停止条件为相对 `origin/main` 接近 `50` 个变更文件;本轮按用户要求到此结束,不再继续开新切片 + - `2026-04-29` 继续按 `$gframework-batch-boot 50` 从仓库根 `dotnet clean` + `dotnet build` 的权威 warning 基线收尾 `YamlConfigSchemaValidator` + - 本轮 clean build 只剩 `15` 条 warning,但实际只对应 `YamlConfigSchemaValidator.cs` 同一文件中的 `5` 个独立 `MA0051` 热点,因此不再并发派发 worker,避免同文件冲突 + - 已将 `ParseNode`、`ValidateObjectNode`、`ValidateObjectConstraints`、`ValidateScalarNode`、`ValidateNumericScalarConstraints` 按语义拆成 helper,并补齐对象条件分支 helper + - 当前仓库根 clean build 已收敛到 `0` warnings、`0` errors;本轮停止原因从“接近文件阈值”切换为“当前 warning hotspot 已耗尽” ## 当前活跃事实 - 当前 `origin/main` 基线提交为 `0e32dab`(`2026-04-28T17:15:47+08:00`)。 - 当前直接验证结果: - - `dotnet clean -p:RestoreFallbackFolders= -v:quiet` - - 最新结果:成功;标准 `dotnet clean` 仍会先命中当前 WSL 环境的 Windows NuGet fallback 目录,已按既有环境口径先执行 `dotnet restore GFramework.sln -p:RestoreFallbackFolders= --disable-parallel` 后清理 - - `dotnet build -p:RestoreFallbackFolders= -clp:WarningsOnly -v:minimal -m:1 -nodeReuse:false` - - 最新结果:成功;`15` warnings、`0` errors;warning 从本轮基线 `236` 降到 `15` - - `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false -clp:Summary` + - `dotnet clean` + - 最新结果:成功;标准仓库根 clean 本轮可直接运行,未再命中需要额外绕开的环境噪音 + - `dotnet build` + - 最新结果:成功;`0 Warning(s)`、`0 Error(s)`;本轮开始时同一口径 clean build 的 `15` 条 warning 已全部清零 + - `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -clp:Summary` - 最新结果:成功;`0 Warning(s)`、`0 Error(s)` - - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-build -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Mediator"` - - 最新结果:成功;`45` 通过、`0` 失败 - - `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false -clp:Summary` - - 最新结果:成功;`0 Warning(s)`、`0 Error(s)` - - `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~YamlConfigLoaderTests|FullyQualifiedName~YamlConfigSchemaValidatorTests"` + - `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderTests|FullyQualifiedName~YamlConfigSchemaValidatorTests"` - 最新结果:成功;`80` 通过、`0` 失败 + - `git diff --check` + - 最新结果:成功;无新增 whitespace / conflict-marker 问题 - 当前批次摘要: - 当前分支提交后预计相对 `origin/main...HEAD` 包含 `22` 个变更文件,低于 `50` 个文件阈值 - 已完成 worker 切片: - `ed269d4`:`MediatorArchitectureIntegrationTests.cs`,清理 `MA0048` / `MA0004` / `MA0016` - `121df44`:`MediatorAdvancedFeaturesTests.cs`,清理 `MA0048` / `MA0004` / `MA0015` - `9109eec`:`MediatorComprehensiveTests.cs`,清理 `MA0048` / `MA0004` / `MA0016` / `MA0002` / `MA0015` - - 主线程切片:`YamlConfigSchemaValidator.cs` 正则 timeout 与 ordinal equality,清理 `MA0009` / `MA0006` + - 主线程切片:`YamlConfigSchemaValidator.cs` 方法拆分,清理剩余 `MA0051`,并修正新增 helper 里的 `MA0006` - Game 追加切片: - `1395b84`:`YamlConfigSchemaValidator.ObjectKeywords.cs`,清理该文件 `MA0051` - - 待提交:将 `YamlConfigSchemaValidator.cs` 末尾 schema model 类型拆到独立同名文件,清理 `MA0048` + - 已完成:将 `YamlConfigSchemaValidator.cs` 末尾 schema model 类型拆到独立同名文件,清理 `MA0048` ## 当前风险 -- `GFramework.Game/Config/YamlConfigSchemaValidator.cs` 仍有 `5` 个 `MA0051` 方法长度 warning,跨 `net8.0` / `net9.0` / `net10.0` 重复为 `15` 条。 - - 缓解措施:下一轮只做主 validator 方法拆分,不再混入拆文件或正则安全修复。 -- 标准 `dotnet clean` 在当前 WSL 环境仍会读取失效的 Windows fallback package folder。 - - 缓解措施:本主题验证继续沿用 `-p:RestoreFallbackFolders=`,必要时先执行 solution restore 刷新 Linux 侧资产。 +- 当前仓库根 clean build warning 已清零,本主题暂时没有剩余源码 warning 风险。 + - 缓解措施:若后续继续 batch warning 清理,先重新执行同轮 `dotnet clean` + `dotnet build` 采样,再决定是否需要分派 subagent。 ## 活跃文档 @@ -68,13 +63,13 @@ ## 验证说明 - 权威验证结果统一维护在“当前活跃事实”。 -- `GFramework.Cqrs.Tests` 的当前受影响项目 Release 构建已清零,并通过 Mediator 定向测试回归。 -- `GFramework.Game` 当前 Release 构建已清零,并通过 config 定向测试;仓库 Debug 构建剩余 warning 属于主 validator 方法复杂度拆分。 +- `GFramework.Game` 当前 Release 构建已清零,并通过 config 定向测试;本轮标准仓库根 Debug clean build 也已清零。 +- 本轮标准仓库根 `dotnet clean` + `dotnet build` 已直接回到 `0 Warning(s)`、`0 Error(s)`,因此 warning reduction 真值已从模块级验证收口到仓库级 clean build。 - `git diff --check` 结果为空,说明本轮新增改动没有引入新的尾随空格或冲突标记。 - warning reduction 的仓库级真值以同轮 `dotnet build`、定向 `dotnet test` 与 `git diff --check` 为准,并与 trace 中的验证里程碑保持一致。 ## 下一步建议 -1. 提交 schema model 拆文件与本轮 `ai-plan` 收口。 -2. 下一轮只处理 `GFramework.Game/Config/YamlConfigSchemaValidator.cs` 剩余 `MA0051` 方法拆分。 -3. 保持 `RestoreFallbackFolders=` 验证口径,避免当前 WSL fallback package folder 干扰。 +1. 提交 `YamlConfigSchemaValidator` 收尾重构与本轮 `ai-plan` 同步。 +2. 如需继续 warning reduction,先从新的仓库根 clean build 重新采样是否还有新增 warning hotspot。 +3. 若未来 warning 再次分散到多个文件,再按 `$gframework-batch-boot 50` 规则切换回多 worker 并行模式。 diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index f6b4be3f..8df8306c 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -1,5 +1,40 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-29 — RP-094 + +### 阶段:收尾 `YamlConfigSchemaValidator` 剩余 `MA0051` 并将仓库根 clean build 归零 + +- 触发背景: + - 用户要求先拿构建 warning,再在 warning 很多时分批指派 subagent;本轮按 `$gframework-batch-boot 50` 继续执行 +- 基线与停机判断: + - 当前 `origin/main` 仍为 `0e32dab`(`2026-04-28T17:15:47+08:00`) + - 本轮标准仓库根 `dotnet clean` + `dotnet build` 直接成功;warning 总数为 `15`,但全部集中在 `GFramework.Game/Config/YamlConfigSchemaValidator.cs` + - 由于 `15` 条 warning 实际只对应同一文件内 `5` 个独立 `MA0051` 方法,不满足“warning 非常多且可安全分派多个独立写边界”的条件,因此不再新增 worker +- 主线程实施: + - 将 `ParseNode` 拆成 `ResolveNodeTypeName`、`ValidateObjectOnlyKeywords`、`CreateParsedNodeForType` + - 将 `ValidateObjectNode` 拆成对象类型确认、属性遍历与 required 校验 helper + - 将 `ValidateObjectConstraints` 拆成 property count、`dependentRequired`、`dependentSchemas`、`allOf`、条件分支五个 helper + - 将 `ValidateScalarNode` 与 `ValidateNumericScalarConstraints` 分别拆成标量类型确认、引用回写、数值上下界和 `multipleOf` helper + - 追加 `ValidateConditionalSchemaBranch` 收口 if/then/else 分支;随后修正该 helper 引入的 `MA0006` +- 验证里程碑: + - `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -clp:Summary` + - 第一次结果:成功;`3` warnings、`0` errors(均为新 helper 中 `branchName == "then"` 引入的 `MA0006`) + - 第二次结果:成功;`0 Warning(s)`、`0 Error(s)` + - `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderTests|FullyQualifiedName~YamlConfigSchemaValidatorTests"` + - 结果:成功;`80` 通过、`0` 失败 + - `dotnet clean` + - 结果:成功 + - `dotnet build` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` + - `git diff --check` + - 结果:成功;无新增 whitespace / conflict-marker 问题 +- 当前指标: + - 仓库根 clean build warning:`15` -> `0` + - 当前分支相对 `origin/main...HEAD` 仍为 `22` 个变更文件,低于 `$gframework-batch-boot 50` 的文件阈值 + - 当前停止原因:warning hotspot 已耗尽,不再有可重复切片 +- 下一步: + - 提交 `YamlConfigSchemaValidator` 收尾重构与本轮 `ai-plan` 真值更新 + ## 2026-04-29 — RP-093 ### 阶段:按 `$gframework-batch-boot 50` 从 clean build warning 基线分批清理 From f5f2c251e51d3baf02bda18e9e430fc1db02ca62 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:19:24 +0800 Subject: [PATCH 08/10] =?UTF-8?q?fix(pr-review):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=BD=93=E5=89=8D=E8=AF=84=E5=AE=A1=E4=B8=AD=E4=BB=8D=E7=84=B6?= =?UTF-8?q?=E6=88=90=E7=AB=8B=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 Mediator 集成测试中的阻塞等待、缓存竞态与共享状态原子性问题 - 补充 YamlConfig 运行时模型的构造期约束与 exception XML 文档 - 新增 模型契约回归测试并更新 analyzer warning reduction 恢复文档 --- .../Mediator/MediatorAdvancedFeaturesTests.cs | 1 + .../MediatorArchitectureIntegrationTests.cs | 38 +++--- .../Config/YamlConfigModelContractTests.cs | 125 ++++++++++++++++++ .../Config/YamlConfigAllowedValue.cs | 4 +- .../Config/YamlConfigArrayConstraints.cs | 19 +++ .../YamlConfigArrayContainsConstraints.cs | 19 +++ .../Config/YamlConfigConditionalSchemas.cs | 1 + .../Config/YamlConfigConstantValue.cs | 4 +- .../Config/YamlConfigObjectConstraints.cs | 19 +++ GFramework.Game/Config/YamlConfigSchema.cs | 3 +- .../Config/YamlConfigSchemaNode.cs | 9 -- .../Config/YamlConfigStringConstraints.cs | 6 + .../YamlConfigStringFormatConstraint.cs | 2 + .../analyzer-warning-reduction-tracking.md | 55 ++++---- .../analyzer-warning-reduction-trace.md | 33 +++++ 15 files changed, 278 insertions(+), 60 deletions(-) create mode 100644 GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs diff --git a/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs index 344a0203..d8e9de59 100644 --- a/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs @@ -236,6 +236,7 @@ public class MediatorAdvancedFeaturesTests } } +// 这些高级特性测试需要把一组仅供当前文件使用的辅助类型共置,避免拆成多个噪声文件。 #pragma warning disable MA0048 #region Advanced Test Classes diff --git a/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs b/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs index b1ac1d24..c0d1f260 100644 --- a/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs +++ b/GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; @@ -331,13 +332,7 @@ public class MediatorArchitectureIntegrationTests public ValueTask Handle(TestNestedRequest request, CancellationToken cancellationToken) { TestNestedRequestHandler2.ExecutionCount++; - - if (request.Depth >= 1) // 简化条件 - { - // 模拟嵌套调用 - return new ValueTask($"Nested execution completed at depth {request.Depth}"); - } - + // 模拟嵌套调用 return new ValueTask($"Nested execution completed at depth {request.Depth}"); } } @@ -391,30 +386,28 @@ public class MediatorArchitectureIntegrationTests public sealed class TestUncachedRequestHandler : IRequestHandler { - public ValueTask Handle(TestUncachedRequest request, CancellationToken cancellationToken) + public async ValueTask Handle(TestUncachedRequest request, CancellationToken cancellationToken) { // 模拟一些处理时间 - Task.Delay(5, cancellationToken).Wait(cancellationToken); - return new ValueTask(request.Id); + await Task.Delay(5, cancellationToken).ConfigureAwait(false); + return request.Id; } } public sealed class TestCachedRequestHandler : IRequestHandler { - private static readonly Dictionary _cache = new(); + private static readonly ConcurrentDictionary _cache = new(); - public ValueTask Handle(TestCachedRequest request, CancellationToken cancellationToken) + public async ValueTask Handle(TestCachedRequest request, CancellationToken cancellationToken) { if (_cache.TryGetValue(request.Id, out var cachedValue)) { - return new ValueTask(cachedValue); + return cachedValue; } // 模拟处理时间 - Task.Delay(10, cancellationToken).Wait(cancellationToken); - var newValue = request.Id; - _cache[request.Id] = newValue; - return new ValueTask(newValue); + await Task.Delay(10, cancellationToken).ConfigureAwait(false); + return _cache.GetOrAdd(request.Id, static id => id); } } @@ -435,7 +428,7 @@ public class MediatorArchitectureIntegrationTests { public ValueTask Handle(TestStateModificationRequest request, CancellationToken cancellationToken) { - request.SharedState.Counter += request.Increment; + request.SharedState.IncrementBy(request.Increment); return new ValueTask("State modified"); } } @@ -567,7 +560,14 @@ public class MediatorArchitectureIntegrationTests // 并发测试相关类 public class SharedState { - public int Counter { get; set; } + private int _counter; + + public int Counter => _counter; + + public void IncrementBy(int increment) + { + Interlocked.Add(ref _counter, increment); + } } public sealed record TestConcurrentRequest : IRequest diff --git a/GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs b/GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs new file mode 100644 index 00000000..9839b853 --- /dev/null +++ b/GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs @@ -0,0 +1,125 @@ +using System.Text.RegularExpressions; +using GFramework.Game.Config; + +namespace GFramework.Game.Tests.Config; + +/// +/// 验证内部 schema 运行时模型会在构造阶段拒绝无效状态, +/// 避免调用方把不一致的约束对象继续传入加载器和校验器。 +/// +[TestFixture] +public sealed class YamlConfigModelContractTests +{ + /// + /// 验证枚举允许值模型会拒绝空白比较键。 + /// + [Test] + public void AllowedValue_Should_Reject_Whitespace_Comparable_Value() + { + Assert.Throws(() => new YamlConfigAllowedValue(" ", "visible")); + } + + /// + /// 验证常量约束模型会拒绝空白比较键。 + /// + [Test] + public void ConstantValue_Should_Reject_Whitespace_Comparable_Value() + { + Assert.Throws(() => new YamlConfigConstantValue(" ", "\"visible\"")); + } + + /// + /// 验证 contains 约束模型会在构造阶段拦截负值和反向区间。 + /// + [Test] + public void ArrayContainsConstraints_Should_Reject_Invalid_Bounds() + { + var itemNode = CreateStringNode(); + + Assert.Multiple(() => + { + Assert.Throws(() => new YamlConfigArrayContainsConstraints(itemNode, -1, null)); + Assert.Throws(() => new YamlConfigArrayContainsConstraints(itemNode, null, -1)); + Assert.Throws(() => new YamlConfigArrayContainsConstraints(itemNode, 3, 2)); + }); + } + + /// + /// 验证数组约束模型会在构造阶段拦截负值和反向区间。 + /// + [Test] + public void ArrayConstraints_Should_Reject_Invalid_Bounds() + { + Assert.Multiple(() => + { + Assert.Throws(() => new YamlConfigArrayConstraints(-1, null, false, null)); + Assert.Throws(() => new YamlConfigArrayConstraints(null, -1, false, null)); + Assert.Throws(() => new YamlConfigArrayConstraints(4, 3, false, null)); + }); + } + + /// + /// 验证对象约束模型会在构造阶段拦截负值和反向区间。 + /// + [Test] + public void ObjectConstraints_Should_Reject_Invalid_Bounds() + { + Assert.Multiple(() => + { + Assert.Throws(() => + new YamlConfigObjectConstraints(-1, null, null, null, null, null)); + Assert.Throws(() => + new YamlConfigObjectConstraints(null, -1, null, null, null, null)); + Assert.Throws(() => + new YamlConfigObjectConstraints(5, 4, null, null, null, null)); + }); + } + + /// + /// 验证字符串约束模型要求正则原文与预编译正则成对出现。 + /// + [Test] + public void StringConstraints_Should_Require_Pattern_And_Regex_To_Be_Paired() + { + Assert.Multiple(() => + { + Assert.Throws(() => + new YamlConfigStringConstraints(null, null, "value", null, null)); + Assert.Throws(() => + new YamlConfigStringConstraints( + null, + null, + null, + new Regex("value", RegexOptions.None, TimeSpan.FromSeconds(1)), + null)); + }); + } + + /// + /// 验证 schema 模型会复制引用表集合,避免外部可变集合继续污染内部状态。 + /// + [Test] + public void Schema_Should_Copy_Referenced_Table_Names() + { + var referencedTableNames = new List { "item" }; + var schema = new YamlConfigSchema("monster.schema.json", CreateStringNode(), referencedTableNames); + + referencedTableNames.Add("weapon"); + + Assert.Multiple(() => + { + Assert.That(schema.ReferencedTableNames, Is.EqualTo(new[] { "item" })); + Assert.That(schema.ReferencedTableNames, Is.Not.SameAs(referencedTableNames)); + }); + } + + private static YamlConfigSchemaNode CreateStringNode() + { + return YamlConfigSchemaNode.CreateScalar( + YamlConfigSchemaPropertyType.String, + referenceTableName: null, + allowedValues: null, + constraints: null, + schemaPathHint: "tests.schema.json"); + } +} diff --git a/GFramework.Game/Config/YamlConfigAllowedValue.cs b/GFramework.Game/Config/YamlConfigAllowedValue.cs index 60077ef5..bbbfd99b 100644 --- a/GFramework.Game/Config/YamlConfigAllowedValue.cs +++ b/GFramework.Game/Config/YamlConfigAllowedValue.cs @@ -11,9 +11,11 @@ internal sealed class YamlConfigAllowedValue /// /// 用于与 YAML 节点比较的稳定键。 /// 用于诊断输出的原始 JSON 文本。 + /// 时抛出。 + /// 为空或仅包含空白字符时抛出。 public YamlConfigAllowedValue(string comparableValue, string displayValue) { - ArgumentNullException.ThrowIfNull(comparableValue); + ArgumentException.ThrowIfNullOrWhiteSpace(comparableValue); ArgumentException.ThrowIfNullOrWhiteSpace(displayValue); ComparableValue = comparableValue; diff --git a/GFramework.Game/Config/YamlConfigArrayConstraints.cs b/GFramework.Game/Config/YamlConfigArrayConstraints.cs index 64ddaf3c..b7a91128 100644 --- a/GFramework.Game/Config/YamlConfigArrayConstraints.cs +++ b/GFramework.Game/Config/YamlConfigArrayConstraints.cs @@ -13,12 +13,31 @@ internal sealed class YamlConfigArrayConstraints /// 最大元素数量约束。 /// 是否要求数组元素唯一。 /// 数组 contains 约束;未声明时为空。 + /// 为负数时抛出。 + /// 大于 时抛出。 public YamlConfigArrayConstraints( int? minItems, int? maxItems, bool uniqueItems, YamlConfigArrayContainsConstraints? containsConstraints) { + if (minItems is < 0) + { + throw new ArgumentOutOfRangeException(nameof(minItems), minItems, "minItems 不能为负数。"); + } + + if (maxItems is < 0) + { + throw new ArgumentOutOfRangeException(nameof(maxItems), maxItems, "maxItems 不能为负数。"); + } + + if (minItems.HasValue && + maxItems.HasValue && + minItems.Value > maxItems.Value) + { + throw new ArgumentException("minItems 不能大于 maxItems。", nameof(minItems)); + } + MinItems = minItems; MaxItems = maxItems; UniqueItems = uniqueItems; diff --git a/GFramework.Game/Config/YamlConfigArrayContainsConstraints.cs b/GFramework.Game/Config/YamlConfigArrayContainsConstraints.cs index a61800f8..e954fcd8 100644 --- a/GFramework.Game/Config/YamlConfigArrayContainsConstraints.cs +++ b/GFramework.Game/Config/YamlConfigArrayContainsConstraints.cs @@ -12,12 +12,31 @@ internal sealed class YamlConfigArrayContainsConstraints /// contains 子 schema。 /// 最小匹配数量;为 时按 JSON Schema 语义默认 1。 /// 最大匹配数量。 + /// 时抛出。 + /// 为负数时抛出。 + /// 大于 时抛出。 public YamlConfigArrayContainsConstraints( YamlConfigSchemaNode containsNode, int? minContains, int? maxContains) { ArgumentNullException.ThrowIfNull(containsNode); + if (minContains is < 0) + { + throw new ArgumentOutOfRangeException(nameof(minContains), minContains, "minContains 不能为负数。"); + } + + if (maxContains is < 0) + { + throw new ArgumentOutOfRangeException(nameof(maxContains), maxContains, "maxContains 不能为负数。"); + } + + if (minContains.HasValue && + maxContains.HasValue && + minContains.Value > maxContains.Value) + { + throw new ArgumentException("minContains 不能大于 maxContains。", nameof(minContains)); + } ContainsNode = containsNode; MinContains = minContains; diff --git a/GFramework.Game/Config/YamlConfigConditionalSchemas.cs b/GFramework.Game/Config/YamlConfigConditionalSchemas.cs index 7d4bbbf2..b71cdfc3 100644 --- a/GFramework.Game/Config/YamlConfigConditionalSchemas.cs +++ b/GFramework.Game/Config/YamlConfigConditionalSchemas.cs @@ -12,6 +12,7 @@ internal sealed class YamlConfigConditionalSchemas /// 条件判断 schema。 /// 条件命中时需要满足的 schema。 /// 条件未命中时需要满足的 schema。 + /// 时抛出。 public YamlConfigConditionalSchemas( YamlConfigSchemaNode ifSchema, YamlConfigSchemaNode? thenSchema, diff --git a/GFramework.Game/Config/YamlConfigConstantValue.cs b/GFramework.Game/Config/YamlConfigConstantValue.cs index 48caa4c5..b40a1e7a 100644 --- a/GFramework.Game/Config/YamlConfigConstantValue.cs +++ b/GFramework.Game/Config/YamlConfigConstantValue.cs @@ -11,9 +11,11 @@ internal sealed class YamlConfigConstantValue /// /// 用于与 YAML 节点比较的稳定键。 /// 用于诊断输出的原始常量文本。 + /// 时抛出。 + /// 为空或仅包含空白字符时抛出。 public YamlConfigConstantValue(string comparableValue, string displayValue) { - ArgumentNullException.ThrowIfNull(comparableValue); + ArgumentException.ThrowIfNullOrWhiteSpace(comparableValue); ArgumentException.ThrowIfNullOrWhiteSpace(displayValue); ComparableValue = comparableValue; diff --git a/GFramework.Game/Config/YamlConfigObjectConstraints.cs b/GFramework.Game/Config/YamlConfigObjectConstraints.cs index 01b460f9..ae148d6c 100644 --- a/GFramework.Game/Config/YamlConfigObjectConstraints.cs +++ b/GFramework.Game/Config/YamlConfigObjectConstraints.cs @@ -15,6 +15,8 @@ internal sealed class YamlConfigObjectConstraints /// 对象内条件 schema 约束。 /// 对象内组合 schema 约束。 /// 对象内条件分支约束。 + /// 为负数时抛出。 + /// 大于 时抛出。 public YamlConfigObjectConstraints( int? minProperties, int? maxProperties, @@ -23,6 +25,23 @@ internal sealed class YamlConfigObjectConstraints IReadOnlyList? allOfSchemas, YamlConfigConditionalSchemas? conditionalSchemas) { + if (minProperties is < 0) + { + throw new ArgumentOutOfRangeException(nameof(minProperties), minProperties, "minProperties 不能为负数。"); + } + + if (maxProperties is < 0) + { + throw new ArgumentOutOfRangeException(nameof(maxProperties), maxProperties, "maxProperties 不能为负数。"); + } + + if (minProperties.HasValue && + maxProperties.HasValue && + minProperties.Value > maxProperties.Value) + { + throw new ArgumentException("minProperties 不能大于 maxProperties。", nameof(minProperties)); + } + MinProperties = minProperties; MaxProperties = maxProperties; DependentRequired = dependentRequired; diff --git a/GFramework.Game/Config/YamlConfigSchema.cs b/GFramework.Game/Config/YamlConfigSchema.cs index e3ae2f0c..a0ebc969 100644 --- a/GFramework.Game/Config/YamlConfigSchema.cs +++ b/GFramework.Game/Config/YamlConfigSchema.cs @@ -12,6 +12,7 @@ internal sealed class YamlConfigSchema /// Schema 文件路径。 /// 根节点模型。 /// Schema 声明的目标引用表名称集合。 + /// 时抛出。 public YamlConfigSchema( string schemaPath, YamlConfigSchemaNode rootNode, @@ -23,7 +24,7 @@ internal sealed class YamlConfigSchema SchemaPath = schemaPath; RootNode = rootNode; - ReferencedTableNames = referencedTableNames; + ReferencedTableNames = [.. referencedTableNames]; } /// diff --git a/GFramework.Game/Config/YamlConfigSchemaNode.cs b/GFramework.Game/Config/YamlConfigSchemaNode.cs index 35509bef..aeeb3ab2 100644 --- a/GFramework.Game/Config/YamlConfigSchemaNode.cs +++ b/GFramework.Game/Config/YamlConfigSchemaNode.cs @@ -280,15 +280,6 @@ internal sealed class YamlConfigSchemaNode NegatedSchemaNode = negatedSchemaNode; } - public static NodeValidation None { get; } = new( - referenceTableName: null, - allowedValues: null, - constraints: null, - arrayConstraints: null, - objectConstraints: null, - constantValue: null, - negatedSchemaNode: null); - public string? ReferenceTableName { get; } public IReadOnlyCollection? AllowedValues { get; } diff --git a/GFramework.Game/Config/YamlConfigStringConstraints.cs b/GFramework.Game/Config/YamlConfigStringConstraints.cs index efb75840..013e52e1 100644 --- a/GFramework.Game/Config/YamlConfigStringConstraints.cs +++ b/GFramework.Game/Config/YamlConfigStringConstraints.cs @@ -17,6 +17,7 @@ internal sealed class YamlConfigStringConstraints /// 正则模式约束原文。 /// 已编译的正则表达式。 /// 字符串 format 约束。 + /// 未成对出现时抛出。 public YamlConfigStringConstraints( int? minLength, int? maxLength, @@ -24,6 +25,11 @@ internal sealed class YamlConfigStringConstraints Regex? patternRegex, YamlConfigStringFormatConstraint? formatConstraint) { + if ((pattern is null) != (patternRegex is null)) + { + throw new ArgumentException("pattern 与 patternRegex 必须同时为空或同时提供。", nameof(pattern)); + } + MinLength = minLength; MaxLength = maxLength; Pattern = pattern; diff --git a/GFramework.Game/Config/YamlConfigStringFormatConstraint.cs b/GFramework.Game/Config/YamlConfigStringFormatConstraint.cs index 97c3a262..6127a784 100644 --- a/GFramework.Game/Config/YamlConfigStringFormatConstraint.cs +++ b/GFramework.Game/Config/YamlConfigStringFormatConstraint.cs @@ -11,6 +11,8 @@ internal sealed class YamlConfigStringFormatConstraint /// /// schema 中声明的 format 名称。 /// 归一化后的共享 format 枚举。 + /// 时抛出。 + /// 为空或仅包含空白字符时抛出。 public YamlConfigStringFormatConstraint( string schemaName, YamlConfigStringFormatKind kind) diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index 315d0e0c..66d65d6e 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -6,43 +6,40 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-094` -- 当前阶段:`Phase 94` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-095` +- 当前阶段:`Phase 95` - 当前焦点: - - `2026-04-29` 继续按 `$gframework-batch-boot 50` 从仓库根 `dotnet clean` + `dotnet build` 的权威 warning 基线收尾 `YamlConfigSchemaValidator` - - 本轮 clean build 只剩 `15` 条 warning,但实际只对应 `YamlConfigSchemaValidator.cs` 同一文件中的 `5` 个独立 `MA0051` 热点,因此不再并发派发 worker,避免同文件冲突 - - 已将 `ParseNode`、`ValidateObjectNode`、`ValidateObjectConstraints`、`ValidateScalarNode`、`ValidateNumericScalarConstraints` 按语义拆成 helper,并补齐对象条件分支 helper - - 当前仓库根 clean build 已收敛到 `0` warnings、`0` errors;本轮停止原因从“接近文件阈值”切换为“当前 warning hotspot 已耗尽” + - `2026-04-29` 继续处理 `PR #301` 的 latest-head review threads,只修复当前工作树上仍然成立的问题 + - 已修复 `MediatorArchitectureIntegrationTests` 中仍然成立的并发与阻塞问题:移除冗余分支、把 `Task.Delay().Wait()` 改为 `await`、把静态缓存换成 `ConcurrentDictionary`、并把共享计数更新改成原子操作 + - 已补 `GFramework.Game/Config` 运行时 schema 模型的构造期契约校验与 `` XML 文档,并新增 `YamlConfigModelContractTests` 锁定这些无效状态保护 + - 本轮明确暂不接受两个误报方向:`YamlConfigReferenceUsage.DisplayPath` 别名删除建议,以及两个本地枚举补 `[GenerateEnumExtensions]` 的泛化建议 ## 当前活跃事实 - 当前 `origin/main` 基线提交为 `0e32dab`(`2026-04-28T17:15:47+08:00`)。 - 当前直接验证结果: - - `dotnet clean` - - 最新结果:成功;标准仓库根 clean 本轮可直接运行,未再命中需要额外绕开的环境噪音 - - `dotnet build` - - 最新结果:成功;`0 Warning(s)`、`0 Error(s)`;本轮开始时同一口径 clean build 的 `15` 条 warning 已全部清零 - `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -clp:Summary` - 最新结果:成功;`0 Warning(s)`、`0 Error(s)` - - `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderTests|FullyQualifiedName~YamlConfigSchemaValidatorTests"` - - 最新结果:成功;`80` 通过、`0` 失败 + - `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigSchemaValidatorTests|FullyQualifiedName~YamlConfigModelContractTests"` + - 最新结果:成功;`10` 通过、`0` 失败 + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~MediatorArchitectureIntegrationTests|FullyQualifiedName~MediatorAdvancedFeaturesTests"` + - 最新结果:成功;`25` 通过、`0` 失败 - `git diff --check` - 最新结果:成功;无新增 whitespace / conflict-marker 问题 - 当前批次摘要: - - 当前分支提交后预计相对 `origin/main...HEAD` 包含 `22` 个变更文件,低于 `50` 个文件阈值 - - 已完成 worker 切片: - - `ed269d4`:`MediatorArchitectureIntegrationTests.cs`,清理 `MA0048` / `MA0004` / `MA0016` - - `121df44`:`MediatorAdvancedFeaturesTests.cs`,清理 `MA0048` / `MA0004` / `MA0015` - - `9109eec`:`MediatorComprehensiveTests.cs`,清理 `MA0048` / `MA0004` / `MA0016` / `MA0002` / `MA0015` - - 主线程切片:`YamlConfigSchemaValidator.cs` 方法拆分,清理剩余 `MA0051`,并修正新增 helper 里的 `MA0006` - - Game 追加切片: - - `1395b84`:`YamlConfigSchemaValidator.ObjectKeywords.cs`,清理该文件 `MA0051` - - 已完成:将 `YamlConfigSchemaValidator.cs` 末尾 schema model 类型拆到独立同名文件,清理 `MA0048` + - 当前切片直接修改 `12` 个已有文件,并新增 `YamlConfigModelContractTests.cs` 作为模型契约回归覆盖 + - 本轮修复集中在 `GFramework.Cqrs.Tests` 与 `GFramework.Game` 两个最新 review thread 热区,没有再扩写回 warning-batch 的多文件并发清理范围 + - PR review triage 结论: + - 接受:并发共享状态、阻塞等待、无效约束状态、缺失 `` 文档 + - 延后:`DisplayPath` 诊断别名删除建议 + - 驳回:两个枚举补 `[GenerateEnumExtensions]` 的泛化建议 ## 当前风险 -- 当前仓库根 clean build warning 已清零,本主题暂时没有剩余源码 warning 风险。 - - 缓解措施:若后续继续 batch warning 清理,先重新执行同轮 `dotnet clean` + `dotnet build` 采样,再决定是否需要分派 subagent。 +- 当前 GitHub PR 仍会保留尚未推送折叠的 open threads,以及被明确延后 / 驳回的机器人建议。 + - 缓解措施:提交并推送后重新执行 `$gframework-pr-review`,只保留仍有真实依据的剩余线程。 +- 本轮未重跑仓库根 `dotnet clean` + `dotnet build`,因此 RP-094 的仓库级 warning 真值不能直接外推到这次 PR-review follow-up 之后。 + - 缓解措施:若下一轮重新回到 analyzer warning reduction 主线,先按仓库规则重新采样仓库根 clean build。 ## 活跃文档 @@ -63,13 +60,13 @@ ## 验证说明 - 权威验证结果统一维护在“当前活跃事实”。 -- `GFramework.Game` 当前 Release 构建已清零,并通过 config 定向测试;本轮标准仓库根 Debug clean build 也已清零。 -- 本轮标准仓库根 `dotnet clean` + `dotnet build` 已直接回到 `0 Warning(s)`、`0 Error(s)`,因此 warning reduction 真值已从模块级验证收口到仓库级 clean build。 +- `GFramework.Game` 当前 Release 构建已清零,并通过 config 定向测试。 +- `GFramework.Cqrs.Tests` 当前 PR-review follow-up 定向测试通过,说明并发/缓存测试辅助实现的行为修正没有破坏现有集成断言。 - `git diff --check` 结果为空,说明本轮新增改动没有引入新的尾随空格或冲突标记。 -- warning reduction 的仓库级真值以同轮 `dotnet build`、定向 `dotnet test` 与 `git diff --check` 为准,并与 trace 中的验证里程碑保持一致。 +- 本轮以受影响项目的 Release build / tests 为完成条件;若下轮恢复 warning reduction 仓库级真值,需要重新执行仓库根 `dotnet clean` + `dotnet build`。 ## 下一步建议 -1. 提交 `YamlConfigSchemaValidator` 收尾重构与本轮 `ai-plan` 同步。 -2. 如需继续 warning reduction,先从新的仓库根 clean build 重新采样是否还有新增 warning hotspot。 -3. 若未来 warning 再次分散到多个文件,再按 `$gframework-batch-boot 50` 规则切换回多 worker 并行模式。 +1. 提交当前 PR-review follow-up 与本轮 `ai-plan` 同步。 +2. 推送分支后重新执行 `$gframework-pr-review`,确认剩余 open threads 是否只剩延后 / 误报项。 +3. 若下一轮恢复 warning reduction 主线,先重新执行仓库根 `dotnet clean` + `dotnet build` 建立新的权威基线。 diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index 8df8306c..26dab494 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -1,5 +1,38 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-29 — RP-095 + +### 阶段:复核 `PR #301` latest-head review threads,并只修复当前工作树上仍然成立的问题 + +- 触发背景: + - 用户显式要求执行 `$gframework-pr-review`,需要把 GitHub PR review 信号与本地代码现状重新核对,而不是沿用旧的 warning-batch 假设 +- 本轮 triage 结论: + - 接受并修复: + - `MediatorArchitectureIntegrationTests` 中 `Task.Delay().Wait()` 阻塞、静态 `Dictionary` 竞态、`SharedState.Counter +=` 非原子更新、以及 `TestNestedRequestHandler` 冗余分支 + - `GFramework.Game/Config` 中仍然成立的模型契约缺口:空白比较键、数组 / 对象边界非法状态、`Pattern` / `PatternRegex` 不一致、`ReferencedTableNames` 未做 defensive copy、以及缺失的 `` XML 文档 + - `MediatorAdvancedFeaturesTests` 中 `MA0048` 抑制缺少原因注释 + - `YamlConfigSchemaNode.NodeValidation.None` 未被引用,按 review 建议删除死代码 + - 明确不接受或延后: + - `YamlConfigReferenceUsage.DisplayPath`:当前在 loader 诊断与测试断言中承担独立语义标签,不作为“纯冗余 alias”删除 + - `YamlConfigSchemaPropertyType` / `YamlConfigStringFormatKind` 补 `[GenerateEnumExtensions]`:仓库产品代码没有现成约定或使用面,判断为泛化误报 +- 主线程实施: + - 将 CQRS 集成测试辅助处理器改为真正异步,并用 `ConcurrentDictionary` / `Interlocked` 收口并发共享状态 + - 为 `YamlConfigAllowedValue`、`YamlConfigConstantValue`、`YamlConfigArrayContainsConstraints`、`YamlConfigArrayConstraints`、`YamlConfigObjectConstraints`、`YamlConfigStringConstraints`、`YamlConfigSchema`、`YamlConfigConditionalSchemas`、`YamlConfigStringFormatConstraint` 补运行时契约或 `` 注释 + - 新增 `YamlConfigModelContractTests`,锁定上述模型拒绝无效状态的行为 +- 验证里程碑: + - `dotnet build GFramework.Game/GFramework.Game.csproj -c Release` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` + - `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigSchemaValidatorTests|FullyQualifiedName~YamlConfigModelContractTests"` + - 第一次结果:成功;`10` 通过、`0` 失败,但新增测试触发 `MA0009` + - 第二次结果:成功;`10` 通过、`0` 失败;为测试中的 `Regex` 补 timeout 后 warning 清零 + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~MediatorArchitectureIntegrationTests|FullyQualifiedName~MediatorAdvancedFeaturesTests"` + - 结果:成功;`25` 通过、`0` 失败 + - `git diff --check` + - 结果:成功;无新增 whitespace / conflict-marker 问题 +- 下一步: + - 提交当前 PR-review follow-up 与 `ai-plan` 同步 + - 推送后重新执行 `$gframework-pr-review`,确认 remaining open threads 是否已缩减到延后 / 误报项 + ## 2026-04-29 — RP-094 ### 阶段:收尾 `YamlConfigSchemaValidator` 剩余 `MA0051` 并将仓库根 clean build 归零 From 590f2cb5162adbae787f5e246931376d03ee34d6 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:17:02 +0800 Subject: [PATCH 09/10] =?UTF-8?q?fix(enum)=EF=BC=9A=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E6=9E=9A=E4=B8=BE=E6=B3=A8=E8=A7=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GFramework.Game/Config/YamlConfigSchemaPropertyType.cs | 3 +++ GFramework.Game/Config/YamlConfigStringFormatKind.cs | 3 +++ GFramework.Game/GFramework.Game.csproj | 1 + 3 files changed, 7 insertions(+) diff --git a/GFramework.Game/Config/YamlConfigSchemaPropertyType.cs b/GFramework.Game/Config/YamlConfigSchemaPropertyType.cs index 725819dc..cd1d464d 100644 --- a/GFramework.Game/Config/YamlConfigSchemaPropertyType.cs +++ b/GFramework.Game/Config/YamlConfigSchemaPropertyType.cs @@ -1,8 +1,11 @@ +using GFramework.Core.SourceGenerators.Abstractions.Enums; + namespace GFramework.Game.Config; /// /// 表示当前运行时 schema 校验器支持的属性类型。 /// +[GenerateEnumExtensions] internal enum YamlConfigSchemaPropertyType { /// diff --git a/GFramework.Game/Config/YamlConfigStringFormatKind.cs b/GFramework.Game/Config/YamlConfigStringFormatKind.cs index bb13a63c..4be2a273 100644 --- a/GFramework.Game/Config/YamlConfigStringFormatKind.cs +++ b/GFramework.Game/Config/YamlConfigStringFormatKind.cs @@ -1,8 +1,11 @@ +using GFramework.Core.SourceGenerators.Abstractions.Enums; + namespace GFramework.Game.Config; /// /// 表示当前 Runtime / Generator / Tooling 共享支持的字符串 format 子集。 /// +[GenerateEnumExtensions] internal enum YamlConfigStringFormatKind { /// diff --git a/GFramework.Game/GFramework.Game.csproj b/GFramework.Game/GFramework.Game.csproj index 6a81f01f..f697feee 100644 --- a/GFramework.Game/GFramework.Game.csproj +++ b/GFramework.Game/GFramework.Game.csproj @@ -9,6 +9,7 @@ true + From 0ad2ed17617b9ee923859db6c7300ae71b3bfbe0 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:27:01 +0800 Subject: [PATCH 10/10] =?UTF-8?q?fix(game):=20=E4=BF=AE=E5=A4=8D=E7=A9=BA?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E9=85=8D=E7=BD=AE=E6=AF=94=E8=BE=83=E9=94=AE?= =?UTF-8?q?=E5=B9=B6=E5=BD=92=E6=A1=A3=20warning=20reduction=20=E4=B8=BB?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 YamlConfigAllowedValue 与 YamlConfigConstantValue 对空对象 const 或 enum 比较键的误判,同时继续拒绝非空纯空白输入 - 补充 YamlConfigModelContractTests 对空比较键与纯空白比较键的回归覆盖,并验证空对象 const 场景 - 更新 ai-plan 公共索引并归档 analyzer-warning-reduction 主题,保留最终 PR review 结论与验证记录 --- .../Config/YamlConfigModelContractTests.cs | 22 ++++++++++ .../Config/YamlConfigAllowedValue.cs | 9 +++- .../Config/YamlConfigConstantValue.cs | 9 +++- ai-plan/public/README.md | 11 ++--- ...nalyzer-warning-reduction-history-rp001.md | 0 ...r-warning-reduction-history-rp002-rp041.md | 0 ...r-warning-reduction-history-rp042-rp048.md | 0 ...r-warning-reduction-history-rp074-rp078.md | 0 ...nalyzer-warning-reduction-history-rp001.md | 0 ...r-warning-reduction-history-rp002-rp041.md | 0 ...r-warning-reduction-history-rp042-rp048.md | 0 ...r-warning-reduction-history-rp062-rp071.md | 0 ...r-warning-reduction-history-rp073-rp078.md | 0 ...r-warning-reduction-history-rp083-rp088.md | 0 .../analyzer-warning-reduction-tracking.md | 42 +++++++++---------- .../analyzer-warning-reduction-trace.md | 29 +++++++++++++ 16 files changed, 89 insertions(+), 33 deletions(-) rename ai-plan/public/{ => archive}/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp001.md (100%) rename ai-plan/public/{ => archive}/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp002-rp041.md (100%) rename ai-plan/public/{ => archive}/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp042-rp048.md (100%) rename ai-plan/public/{ => archive}/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp074-rp078.md (100%) rename ai-plan/public/{ => archive}/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp001.md (100%) rename ai-plan/public/{ => archive}/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp002-rp041.md (100%) rename ai-plan/public/{ => archive}/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp042-rp048.md (100%) rename ai-plan/public/{ => archive}/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp062-rp071.md (100%) rename ai-plan/public/{ => archive}/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp073-rp078.md (100%) rename ai-plan/public/{ => archive}/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp083-rp088.md (100%) rename ai-plan/public/{ => archive}/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md (50%) rename ai-plan/public/{ => archive}/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md (85%) diff --git a/GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs b/GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs index 9839b853..8abea9d6 100644 --- a/GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs +++ b/GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs @@ -19,6 +19,17 @@ public sealed class YamlConfigModelContractTests Assert.Throws(() => new YamlConfigAllowedValue(" ", "visible")); } + /// + /// 验证枚举允许值模型会保留空对象等合法结构产生的空比较键。 + /// + [Test] + public void AllowedValue_Should_Accept_Empty_Comparable_Value() + { + var allowedValue = new YamlConfigAllowedValue(string.Empty, "{}"); + + Assert.That(allowedValue.ComparableValue, Is.Empty); + } + /// /// 验证常量约束模型会拒绝空白比较键。 /// @@ -28,6 +39,17 @@ public sealed class YamlConfigModelContractTests Assert.Throws(() => new YamlConfigConstantValue(" ", "\"visible\"")); } + /// + /// 验证常量约束模型会保留空对象等合法结构产生的空比较键。 + /// + [Test] + public void ConstantValue_Should_Accept_Empty_Comparable_Value() + { + var constantValue = new YamlConfigConstantValue(string.Empty, "{}"); + + Assert.That(constantValue.ComparableValue, Is.Empty); + } + /// /// 验证 contains 约束模型会在构造阶段拦截负值和反向区间。 /// diff --git a/GFramework.Game/Config/YamlConfigAllowedValue.cs b/GFramework.Game/Config/YamlConfigAllowedValue.cs index bbbfd99b..504d285b 100644 --- a/GFramework.Game/Config/YamlConfigAllowedValue.cs +++ b/GFramework.Game/Config/YamlConfigAllowedValue.cs @@ -12,11 +12,16 @@ internal sealed class YamlConfigAllowedValue /// 用于与 YAML 节点比较的稳定键。 /// 用于诊断输出的原始 JSON 文本。 /// 时抛出。 - /// 为空或仅包含空白字符时抛出。 + /// 虽然非空但仅包含空白字符,或 为空或仅包含空白字符时抛出。 public YamlConfigAllowedValue(string comparableValue, string displayValue) { - ArgumentException.ThrowIfNullOrWhiteSpace(comparableValue); + ArgumentNullException.ThrowIfNull(comparableValue); ArgumentException.ThrowIfNullOrWhiteSpace(displayValue); + if (comparableValue.Length > 0 && + string.IsNullOrWhiteSpace(comparableValue)) + { + throw new ArgumentException("The value cannot be composed entirely of whitespace.", nameof(comparableValue)); + } ComparableValue = comparableValue; DisplayValue = displayValue; diff --git a/GFramework.Game/Config/YamlConfigConstantValue.cs b/GFramework.Game/Config/YamlConfigConstantValue.cs index b40a1e7a..0156a710 100644 --- a/GFramework.Game/Config/YamlConfigConstantValue.cs +++ b/GFramework.Game/Config/YamlConfigConstantValue.cs @@ -12,11 +12,16 @@ internal sealed class YamlConfigConstantValue /// 用于与 YAML 节点比较的稳定键。 /// 用于诊断输出的原始常量文本。 /// 时抛出。 - /// 为空或仅包含空白字符时抛出。 + /// 虽然非空但仅包含空白字符,或 为空或仅包含空白字符时抛出。 public YamlConfigConstantValue(string comparableValue, string displayValue) { - ArgumentException.ThrowIfNullOrWhiteSpace(comparableValue); + ArgumentNullException.ThrowIfNull(comparableValue); ArgumentException.ThrowIfNullOrWhiteSpace(displayValue); + if (comparableValue.Length > 0 && + string.IsNullOrWhiteSpace(comparableValue)) + { + throw new ArgumentException("The value cannot be composed entirely of whitespace.", nameof(comparableValue)); + } ComparableValue = comparableValue; DisplayValue = displayValue; diff --git a/ai-plan/public/README.md b/ai-plan/public/README.md index 9ead4d21..c77c8a14 100644 --- a/ai-plan/public/README.md +++ b/ai-plan/public/README.md @@ -12,11 +12,6 @@ help the current worktree land on the right recovery documents without scanning ## Active Topics -- `analyzer-warning-reduction` - - Purpose: track the analyzer warning reduction branch, including the current recovery point, remaining warning - hotspots, and the next safe warning-reduction slice. - - Tracking: `ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md` - - Trace: `ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md` - `ai-plan-governance` - Purpose: govern the `ai-plan/` directory model, startup index, and archive policy. - Tracking: `ai-plan/public/ai-plan-governance/todos/ai-plan-governance-tracking.md` @@ -50,9 +45,6 @@ help the current worktree land on the right recovery documents without scanning ## Worktree To Active Topic Map -- Branch: `fix/analyzer-warning-reduction-batch` - - Worktree hint: `GFramework-analyzer` - - Priority 1: `analyzer-warning-reduction` - Branch: `feat/ai-first-config` - Worktree hint: `GFramework-Ai-First-Config` - Priority 1: `ai-first-config-system` @@ -75,6 +67,9 @@ help the current worktree land on the right recovery documents without scanning - Priority 1: `documentation-full-coverage-governance` ## Archived Topics +- `analyzer-warning-reduction` + - Archive root: `ai-plan/public/archive/analyzer-warning-reduction/` + - Note: 长期 warning-reduction 分支已收尾;PR #301 的最终 review follow-up 已本地闭环,后续仅作为历史恢复材料保留。 - `cqrs-cache-docs-hardening` - Archive root: `ai-plan/public/archive/cqrs-cache-docs-hardening/` - Note: archived topics stay outside the default `boot` context until a user explicitly requests historical review. diff --git a/ai-plan/public/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp001.md b/ai-plan/public/archive/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp001.md similarity index 100% rename from ai-plan/public/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp001.md rename to ai-plan/public/archive/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp001.md diff --git a/ai-plan/public/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp002-rp041.md b/ai-plan/public/archive/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp002-rp041.md similarity index 100% rename from ai-plan/public/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp002-rp041.md rename to ai-plan/public/archive/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp002-rp041.md diff --git a/ai-plan/public/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp042-rp048.md b/ai-plan/public/archive/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp042-rp048.md similarity index 100% rename from ai-plan/public/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp042-rp048.md rename to ai-plan/public/archive/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp042-rp048.md diff --git a/ai-plan/public/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp074-rp078.md b/ai-plan/public/archive/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp074-rp078.md similarity index 100% rename from ai-plan/public/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp074-rp078.md rename to ai-plan/public/archive/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp074-rp078.md diff --git a/ai-plan/public/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp001.md b/ai-plan/public/archive/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp001.md similarity index 100% rename from ai-plan/public/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp001.md rename to ai-plan/public/archive/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp001.md diff --git a/ai-plan/public/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp002-rp041.md b/ai-plan/public/archive/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp002-rp041.md similarity index 100% rename from ai-plan/public/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp002-rp041.md rename to ai-plan/public/archive/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp002-rp041.md diff --git a/ai-plan/public/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp042-rp048.md b/ai-plan/public/archive/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp042-rp048.md similarity index 100% rename from ai-plan/public/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp042-rp048.md rename to ai-plan/public/archive/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp042-rp048.md diff --git a/ai-plan/public/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp062-rp071.md b/ai-plan/public/archive/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp062-rp071.md similarity index 100% rename from ai-plan/public/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp062-rp071.md rename to ai-plan/public/archive/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp062-rp071.md diff --git a/ai-plan/public/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp073-rp078.md b/ai-plan/public/archive/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp073-rp078.md similarity index 100% rename from ai-plan/public/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp073-rp078.md rename to ai-plan/public/archive/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp073-rp078.md diff --git a/ai-plan/public/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp083-rp088.md b/ai-plan/public/archive/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp083-rp088.md similarity index 100% rename from ai-plan/public/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp083-rp088.md rename to ai-plan/public/archive/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp083-rp088.md diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/archive/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md similarity index 50% rename from ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md rename to ai-plan/public/archive/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index 66d65d6e..e073676d 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/archive/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -6,13 +6,12 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-095` -- 当前阶段:`Phase 95` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-096` +- 当前阶段:`Completed` - 当前焦点: - - `2026-04-29` 继续处理 `PR #301` 的 latest-head review threads,只修复当前工作树上仍然成立的问题 - - 已修复 `MediatorArchitectureIntegrationTests` 中仍然成立的并发与阻塞问题:移除冗余分支、把 `Task.Delay().Wait()` 改为 `await`、把静态缓存换成 `ConcurrentDictionary`、并把共享计数更新改成原子操作 - - 已补 `GFramework.Game/Config` 运行时 schema 模型的构造期契约校验与 `` XML 文档,并新增 `YamlConfigModelContractTests` 锁定这些无效状态保护 - - 本轮明确暂不接受两个误报方向:`YamlConfigReferenceUsage.DisplayPath` 别名删除建议,以及两个本地枚举补 `[GenerateEnumExtensions]` 的泛化建议 + - `2026-04-29` 已完成 `PR #301` latest-head review threads 的最终本地复核,并修复仍然成立的空对象 `const` 比较键回归 + - 当前 topic 已达到归档条件:长期 warning-reduction 分支的实现、PR review follow-up 与最小验证均已完成 + - 当前目录已迁入 `ai-plan/public/archive/analyzer-warning-reduction/`,后续仅保留历史恢复价值 ## 当前活跃事实 @@ -20,26 +19,27 @@ - 当前直接验证结果: - `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -clp:Summary` - 最新结果:成功;`0 Warning(s)`、`0 Error(s)` - - `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigSchemaValidatorTests|FullyQualifiedName~YamlConfigModelContractTests"` + - `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~LoadAsync_Should_Accept_Empty_Object_Schema_Const|FullyQualifiedName~YamlConfigModelContractTests"` - 最新结果:成功;`10` 通过、`0` 失败 - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~MediatorArchitectureIntegrationTests|FullyQualifiedName~MediatorAdvancedFeaturesTests"` - 最新结果:成功;`25` 通过、`0` 失败 + - `dotnet format GFramework.sln --verify-no-changes --include GFramework.Game/Config/YamlConfigAllowedValue.cs GFramework.Game/Config/YamlConfigConstantValue.cs GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs` + - 最新结果:成功;当前修复范围内无格式漂移 - `git diff --check` - 最新结果:成功;无新增 whitespace / conflict-marker 问题 - 当前批次摘要: - - 当前切片直接修改 `12` 个已有文件,并新增 `YamlConfigModelContractTests.cs` 作为模型契约回归覆盖 - - 本轮修复集中在 `GFramework.Cqrs.Tests` 与 `GFramework.Game` 两个最新 review thread 热区,没有再扩写回 warning-batch 的多文件并发清理范围 + - 当前最终收尾切片直接修改 `3` 个已有文件,不再扩写 warning-batch 的多文件清理范围 + - 这次收尾把 `YamlConfigAllowedValue` / `YamlConfigConstantValue` 的 `comparableValue` 契约收窄为“允许空字符串,但拒绝非空纯空白”,恢复空对象 `const` / `enum` 的合法比较键语义 - PR review triage 结论: - - 接受:并发共享状态、阻塞等待、无效约束状态、缺失 `` 文档 - - 延后:`DisplayPath` 诊断别名删除建议 - - 驳回:两个枚举补 `[GenerateEnumExtensions]` 的泛化建议 + - 接受并完成:并发共享状态、阻塞等待、无效约束状态、缺失 `` 文档、空对象比较键回归 + - 归档前剩余 open threads 只包含两类:尚未推送折叠的 stale 线程,以及已明确延后 / 驳回的建议(`DisplayPath` 与枚举特性泛化) ## 当前风险 -- 当前 GitHub PR 仍会保留尚未推送折叠的 open threads,以及被明确延后 / 驳回的机器人建议。 - - 缓解措施:提交并推送后重新执行 `$gframework-pr-review`,只保留仍有真实依据的剩余线程。 -- 本轮未重跑仓库根 `dotnet clean` + `dotnet build`,因此 RP-094 的仓库级 warning 真值不能直接外推到这次 PR-review follow-up 之后。 - - 缓解措施:若下一轮重新回到 analyzer warning reduction 主线,先按仓库规则重新采样仓库根 clean build。 +- 当前 GitHub PR 在本地提交并推送前仍可能显示旧的 open threads。 + - 缓解措施:以本文件中的本地验证结果为 archive 真值;若未来需要复查 PR 页面,应从 archive 恢复而不是重新激活 topic。 +- 本轮仅对 `GFramework.Game` 收尾回归做了受影响模块验证,没有重新建立新的仓库根 clean build 基线。 + - 缓解措施:后续若有新的 warning-reduction 任务,应创建新 topic,并重新执行仓库根 `dotnet clean` + `dotnet build` 采样。 ## 活跃文档 @@ -60,13 +60,13 @@ ## 验证说明 - 权威验证结果统一维护在“当前活跃事实”。 -- `GFramework.Game` 当前 Release 构建已清零,并通过 config 定向测试。 +- `GFramework.Game` 当前 Release 构建已清零,并通过空对象 `const` 回归与模型契约定向测试。 - `GFramework.Cqrs.Tests` 当前 PR-review follow-up 定向测试通过,说明并发/缓存测试辅助实现的行为修正没有破坏现有集成断言。 +- `dotnet format --verify-no-changes` 已确认当前收尾改动未引入新的格式化偏差。 - `git diff --check` 结果为空,说明本轮新增改动没有引入新的尾随空格或冲突标记。 -- 本轮以受影响项目的 Release build / tests 为完成条件;若下轮恢复 warning reduction 仓库级真值,需要重新执行仓库根 `dotnet clean` + `dotnet build`。 +- 本 topic 已进入 archive;若未来重启 warning reduction,应以新 topic 和新的仓库级 clean build 基线继续。 ## 下一步建议 -1. 提交当前 PR-review follow-up 与本轮 `ai-plan` 同步。 -2. 推送分支后重新执行 `$gframework-pr-review`,确认剩余 open threads 是否只剩延后 / 误报项。 -3. 若下一轮恢复 warning reduction 主线,先重新执行仓库根 `dotnet clean` + `dotnet build` 建立新的权威基线。 +1. 保持当前 archive 状态,不要再把该 topic 作为默认 boot 入口。 +2. 若未来需要继续 warning reduction,创建新的 active topic,并重新建立仓库根 clean build 真值。 diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/archive/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md similarity index 85% rename from ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md rename to ai-plan/public/archive/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index 26dab494..233ab639 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/archive/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -1,5 +1,34 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-29 — RP-096 + +### 阶段:完成 `PR #301` 最终收尾并归档长期 warning-reduction 主题 + +- 触发背景: + - 用户要求先用 `$gframework-pr-review` 解决当前 PR review 的剩余问题,然后把整个长期分支主题归档 +- 本轮 triage 结论: + - `MediatorArchitectureIntegrationTests` 并发更新、`YamlConfigConditionalSchemas` / `YamlConfigStringFormatConstraint` 的 `` 文档,以及两个枚举的 `[GenerateEnumExtensions]` 在当前工作树上均已存在,对应 open threads 判定为 stale + - `YamlConfigReferenceUsage.DisplayPath` 删除建议继续判定为不成立,因为 loader 诊断、引用索引和测试断言仍把它作为稳定语义标签使用 + - `LoadAsync_Should_Accept_Empty_Object_Schema_Const` 失败仍然成立:上轮把 `YamlConfigAllowedValue` / `YamlConfigConstantValue` 的 `comparableValue` 收紧成 `ThrowIfNullOrWhiteSpace(...)` 后,误伤了空对象常量的合法空比较键 +- 主线程实施: + - 将 `YamlConfigAllowedValue` 与 `YamlConfigConstantValue` 的比较键契约调整为: + - 允许 `string.Empty` + - 继续拒绝非空纯空白字符串 + - 保留 `displayValue` 的非空白要求 + - 扩充 `YamlConfigModelContractTests`,新增空比较键的正向覆盖,同时保留纯空白比较键的回归保护 +- 验证里程碑: + - `dotnet build GFramework.Game/GFramework.Game.csproj -c Release` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` + - `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~LoadAsync_Should_Accept_Empty_Object_Schema_Const|FullyQualifiedName~YamlConfigModelContractTests"` + - 结果:成功;`10` 通过、`0` 失败 + - `dotnet format GFramework.sln --verify-no-changes --include GFramework.Game/Config/YamlConfigAllowedValue.cs GFramework.Game/Config/YamlConfigConstantValue.cs GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs` + - 结果:成功 + - `git diff --check` + - 结果:成功;无新增 whitespace / conflict-marker 问题 +- 归档结论: + - `analyzer-warning-reduction` 当前 topic 已满足归档条件:长期 warning-reduction 主线已收尾,PR #301 的本地 follow-up 闭环完成 + - 整个 topic 目录已迁入 `ai-plan/public/archive/analyzer-warning-reduction/`,不再作为 active 默认入口 + ## 2026-04-29 — RP-095 ### 阶段:复核 `PR #301` latest-head review threads,并只修复当前工作树上仍然成立的问题