From fb1c8225904ae53d3e875603fc3d31f71b96b6c8 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Mon, 28 Sep 2026 21:03:45 +0100 Subject: [PATCH 1/2] refactor: source gen update --- Directory.Packages.props | 2 +- .../Helpers/CustomRuleResolver.cs | 2 +- .../Helpers/ExternalSchemaResolver.cs | 18 ++++++++---------- .../SourceGenerators/Helpers/TypeLibrary.cs | 7 ++++++- .../Helpers/ZodRefinementHookResolver.cs | 3 ++- src/src/SourceGenerators/ZodSchemaAnalyzer.cs | 2 +- .../ZodSchemaGenerator.RuleAttributes.cs | 2 ++ src/src/ZodSharp/Core/ZodType.cs | 1 + src/src/ZodSharp/Rules/CreditCardRule.cs | 1 + .../ZodSharp/Schemas/ZodDiscriminatedUnion.cs | 4 ++-- src/src/ZodSharp/Schemas/ZodString.cs | 2 ++ .../ZodSchemaGeneratorTests.CustomRules.cs | 5 +++-- .../ZodSchemaGeneratorTests.GenericRules.cs | 2 +- .../ZodSchemaGeneratorTests.TypeRules.cs | 2 +- .../Core/ValidationErrorTests.cs | 11 ++--------- .../Schemas/ZodStringSpanTests.cs | 2 +- 16 files changed, 35 insertions(+), 31 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index fc481f3..d3b5995 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,7 +11,7 @@ 5.9.0 5.9.0 1.68.17 - 1.0.0-prerelease.53 + 1.0.0-prerelease.54 10.0.12 diff --git a/src/src/SourceGenerators/Helpers/CustomRuleResolver.cs b/src/src/SourceGenerators/Helpers/CustomRuleResolver.cs index e88c1c8..aff2f78 100644 --- a/src/src/SourceGenerators/Helpers/CustomRuleResolver.cs +++ b/src/src/SourceGenerators/Helpers/CustomRuleResolver.cs @@ -274,7 +274,7 @@ static bool TryBuildArguments( out string? unmappedParameterName ) { - arguments = new(ImmutableArray.Empty); + arguments = new([]); unmappedParameterName = null; var constructor = ruleType diff --git a/src/src/SourceGenerators/Helpers/ExternalSchemaResolver.cs b/src/src/SourceGenerators/Helpers/ExternalSchemaResolver.cs index 55029ce..b0aeb9e 100644 --- a/src/src/SourceGenerators/Helpers/ExternalSchemaResolver.cs +++ b/src/src/SourceGenerators/Helpers/ExternalSchemaResolver.cs @@ -24,19 +24,16 @@ namespace ZodSharp.SourceGenerators.Helpers; /// the cache keys and values are value types only, so nothing from a compilation outlives the pass. /// /// -sealed class ExternalSchemaResolver +/// +/// Initializes a new instance of the class. +/// +/// The compilation currently being generated. +/// Thrown when is null. +sealed class ExternalSchemaResolver(Compilation compilation) { - readonly Compilation _compilation; + readonly Compilation _compilation = compilation ?? throw new ArgumentNullException(nameof(compilation)); readonly ConcurrentDictionary<(TypeIdentity Type, AssemblyIdentity Assembly), Resolution> _resolved = new(); - /// - /// Initializes a new instance of the class. - /// - /// The compilation currently being generated. - /// Thrown when is null. - public ExternalSchemaResolver(Compilation compilation) => - _compilation = compilation ?? throw new ArgumentNullException(nameof(compilation)); - /// /// Determines whether the compilation currently being generated declares . /// @@ -107,6 +104,7 @@ public bool TryGetExistingSchema(INamedTypeSymbol type, out TypeIdentity schemaT return null; } + // The schema is accessible and was generated by the assembly declaring the type. return schema; } diff --git a/src/src/SourceGenerators/Helpers/TypeLibrary.cs b/src/src/SourceGenerators/Helpers/TypeLibrary.cs index 201cd98..7a43ceb 100644 --- a/src/src/SourceGenerators/Helpers/TypeLibrary.cs +++ b/src/src/SourceGenerators/Helpers/TypeLibrary.cs @@ -1,7 +1,12 @@ namespace ZodSharp.SourceGenerators.Helpers; +// Deliberately not public (the default accessibility for a top-level type): the generated type +// library exposes Purview.SourceGeneratorFramework type identities, which the IL merge internalizes, +// so a public spec would leave the merged analyzer with a public member whose signature references +// an internal type. The BuildSdk generates InternalsVisibleTo for the matching unit-test assembly, so +// tests still use it. [GenerateTypeLibrary] -public static partial class TypeLibraryGenerator +static partial class TypeLibraryGenerator { public const string ZodSharpNamespace = "ZodSharp"; diff --git a/src/src/SourceGenerators/Helpers/ZodRefinementHookResolver.cs b/src/src/SourceGenerators/Helpers/ZodRefinementHookResolver.cs index c08f86a..622aff4 100644 --- a/src/src/SourceGenerators/Helpers/ZodRefinementHookResolver.cs +++ b/src/src/SourceGenerators/Helpers/ZodRefinementHookResolver.cs @@ -75,7 +75,7 @@ internal static GeneratorResult Resolve(INamedTypeSymbol diagnostics.Add( ReportableDiagnostic.Create( DiagnosticLibrary.SyncRefinementMethodRetired, - true, + false, retiredMethod.Locations.FirstOrDefault(static location => location.IsInSource) ?? GetTypeLocation(classSymbol), retiredMethod.Name, @@ -249,6 +249,7 @@ type is not INamedTypeSymbol named return false; } + // The RefineCtx type argument must be the same as the containing type, but the containing type may be return SymbolEqualityComparer.Default.Equals(named.TypeArguments[0], classSymbol); } diff --git a/src/src/SourceGenerators/ZodSchemaAnalyzer.cs b/src/src/SourceGenerators/ZodSchemaAnalyzer.cs index 66ba2f8..7edde34 100644 --- a/src/src/SourceGenerators/ZodSchemaAnalyzer.cs +++ b/src/src/SourceGenerators/ZodSchemaAnalyzer.cs @@ -241,7 +241,7 @@ ExternalSchemaResolver externalSchemas } } - return reachable.ToImmutableHashSet(); + return [.. reachable]; } static IEnumerable EnumerateNamedTypes(INamespaceSymbol root) diff --git a/src/src/SourceGenerators/ZodSchemaGenerator.RuleAttributes.cs b/src/src/SourceGenerators/ZodSchemaGenerator.RuleAttributes.cs index b663567..bd9ec96 100644 --- a/src/src/SourceGenerators/ZodSchemaGenerator.RuleAttributes.cs +++ b/src/src/SourceGenerators/ZodSchemaGenerator.RuleAttributes.cs @@ -153,6 +153,7 @@ static string BuildInitializer(IParameterSymbol parameter) return literal; } + // If the parameter has no default value, we still need to provide an initializer for the attribute property. return parameter.Type.IsValueType ? "default!" : "null!"; } @@ -195,6 +196,7 @@ static bool IsSupportedAttributePropertyType(ITypeSymbol type) if (unwrapped.ToDisplayString() == "System.Type") return true; + // Only primitive types and string are supported as attribute properties. return unwrapped.SpecialType is SpecialType.System_Boolean or SpecialType.System_Byte diff --git a/src/src/ZodSharp/Core/ZodType.cs b/src/src/ZodSharp/Core/ZodType.cs index 1f3f743..6f349fc 100644 --- a/src/src/ZodSharp/Core/ZodType.cs +++ b/src/src/ZodSharp/Core/ZodType.cs @@ -115,6 +115,7 @@ public ZodType Rule(TRule rule) if (rule is null) throw new ArgumentNullException(nameof(rule)); + // We can safely cast to IValidationRule because TRule is constrained to that interface. return AddRule(rule); } diff --git a/src/src/ZodSharp/Rules/CreditCardRule.cs b/src/src/ZodSharp/Rules/CreditCardRule.cs index 73881d1..16f54da 100644 --- a/src/src/ZodSharp/Rules/CreditCardRule.cs +++ b/src/src/ZodSharp/Rules/CreditCardRule.cs @@ -28,6 +28,7 @@ public bool IsValid(in string value) if (string.IsNullOrWhiteSpace(value)) return false; + // Use AsSpan to avoid allocations return IsValid(value.AsSpan()); } diff --git a/src/src/ZodSharp/Schemas/ZodDiscriminatedUnion.cs b/src/src/ZodSharp/Schemas/ZodDiscriminatedUnion.cs index cdcfbf4..51a5689 100644 --- a/src/src/ZodSharp/Schemas/ZodDiscriminatedUnion.cs +++ b/src/src/ZodSharp/Schemas/ZodDiscriminatedUnion.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Collections.Immutable; using System.Linq.Expressions; using System.Reflection; @@ -28,7 +28,7 @@ ImmutableDictionary> options static readonly ConditionalWeakTable< Type, ConcurrentDictionary> - > DiscriminatorAccessors = new(); + > DiscriminatorAccessors = []; static readonly Func MissingDiscriminatorAccessor = static _ => null; diff --git a/src/src/ZodSharp/Schemas/ZodString.cs b/src/src/ZodSharp/Schemas/ZodString.cs index 362e260..51bc6a0 100644 --- a/src/src/ZodSharp/Schemas/ZodString.cs +++ b/src/src/ZodSharp/Schemas/ZodString.cs @@ -70,6 +70,7 @@ public ValidationResult ValidateSpan(ReadOnlySpan value) if (!SupportsSpanRules) return Validate(value.ToString()); + // Every rule supports the span contract, so validate the span directly and return a string result. return IsValidSpan(value, out var errors) ? ValidationResult.Success(value.ToString()) : ValidationResult.Failure(errors); @@ -85,6 +86,7 @@ public ValidationResult ValidateSpan(ReadOnlySpan value) /// This is the allocation-free span entry point: it validates the span directly when every rule /// implements , and otherwise falls back to the string path once. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1021:Avoid out parameters")] public bool IsValidSpan(ReadOnlySpan value, out ImmutableArray errors) { errors = []; diff --git a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.CustomRules.cs b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.CustomRules.cs index 051a054..a8a19f6 100644 --- a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.CustomRules.cs +++ b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.CustomRules.cs @@ -1,3 +1,5 @@ +using System.Collections.Immutable; +using ZodSharp.Core; using ZodSharp.SourceGenerators.Infra; namespace ZodSharp.SourceGenerators; @@ -75,8 +77,7 @@ CancellationToken cancellationToken var isSuccess = (bool)result.GetType().GetProperty("IsSuccess")!.GetValue(result)!; await Assert.That(isSuccess).IsFalse(); - var errors = (System.Collections.Immutable.ImmutableArray) - result.GetType().GetProperty("Errors")!.GetValue(result)!; + var errors = (ImmutableArray)result.GetType().GetProperty("Errors")!.GetValue(result)!; await Assert.That(errors).HasSingleItem(); await Assert.That(errors[0].Code).IsEqualTo("invalid_string"); await Assert.That(errors[0].Origin).IsEqualTo("string"); diff --git a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.GenericRules.cs b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.GenericRules.cs index f3937f8..7346e4a 100644 --- a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.GenericRules.cs +++ b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.GenericRules.cs @@ -102,7 +102,7 @@ public async Task GenericRule_GivenScalarLikeStruct_FailsAtRuntimeForEmptyGuid(C // Assert await Assert.That((bool)emptyResult.GetType().GetProperty("IsSuccess")!.GetValue(emptyResult)!).IsFalse(); - var errors = (System.Collections.Immutable.ImmutableArray) + var errors = (System.Collections.Immutable.ImmutableArray) emptyResult.GetType().GetProperty("Errors")!.GetValue(emptyResult)!; await Assert.That(errors).HasSingleItem(); await Assert.That(errors[0].Code).IsEqualTo("invalid_asset_id"); diff --git a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.TypeRules.cs b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.TypeRules.cs index 269023b..a3458b8 100644 --- a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.TypeRules.cs +++ b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.TypeRules.cs @@ -95,7 +95,7 @@ CancellationToken cancellationToken // Assert await Assert.That((bool)emptyResult.GetType().GetProperty("IsSuccess")!.GetValue(emptyResult)!).IsFalse(); - var errors = (System.Collections.Immutable.ImmutableArray) + var errors = (System.Collections.Immutable.ImmutableArray) emptyResult.GetType().GetProperty("Errors")!.GetValue(emptyResult)!; await Assert.That(errors).HasSingleItem(); await Assert.That(errors[0].Code).IsEqualTo("invalid_asset_id"); diff --git a/src/tests/ZodSharp.UnitTests/Core/ValidationErrorTests.cs b/src/tests/ZodSharp.UnitTests/Core/ValidationErrorTests.cs index 45b7ac9..d7f0859 100644 --- a/src/tests/ZodSharp.UnitTests/Core/ValidationErrorTests.cs +++ b/src/tests/ZodSharp.UnitTests/Core/ValidationErrorTests.cs @@ -1,5 +1,3 @@ -using System.Collections.Immutable; - namespace ZodSharp.Core; public class ValidationErrorTests @@ -25,12 +23,7 @@ public async Task Create_GivenCategory_PopulatesCategory() const string category = "invalid_value"; // Act - var error = ValidationError.Create( - "invalid_tenant_id", - "The tenant id is invalid.", - ImmutableArray.Empty, - category: category - ); + var error = ValidationError.Create("invalid_tenant_id", "The tenant id is invalid.", [], category: category); // Assert await Assert.That(error.Category).IsEqualTo(category); @@ -40,7 +33,7 @@ public async Task Create_GivenCategory_PopulatesCategory() public async Task Create_GivenNoCategory_LeavesCategoryNull() { // Act - var error = ValidationError.Create("too_small", "Too small.", ImmutableArray.Empty); + var error = ValidationError.Create("too_small", "Too small.", []); // Assert await Assert.That(error.Category).IsNull(); diff --git a/src/tests/ZodSharp.UnitTests/Schemas/ZodStringSpanTests.cs b/src/tests/ZodSharp.UnitTests/Schemas/ZodStringSpanTests.cs index 3e4548d..3aca0c1 100644 --- a/src/tests/ZodSharp.UnitTests/Schemas/ZodStringSpanTests.cs +++ b/src/tests/ZodSharp.UnitTests/Schemas/ZodStringSpanTests.cs @@ -31,7 +31,7 @@ public async Task ValidateSpan_GivenEmptySpan_AppliesRules() // Regression: an empty span previously bypassed the rules and always succeeded. var schema = Z.String().Min(3); - var result = schema.ValidateSpan(ReadOnlySpan.Empty); + var result = schema.ValidateSpan([]); await Assert.That(result.IsSuccess).IsFalse(); } From 42aed618b9c62302feda444f83c4cef1f86cc7d8 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Mon, 28 Sep 2026 21:10:27 +0100 Subject: [PATCH 2/2] chore: bumped version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7cc1249..29fd92c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zodsharp", - "version": "2.0.0-prerelease.27", + "version": "2.0.0-prerelease.28", "private": true, "license": "MIT", "author": {