diff --git a/.agents/agents/sdk-consumer-setup.md b/.agents/agents/sdk-consumer-setup.md new file mode 100644 index 0000000..b356a41 --- /dev/null +++ b/.agents/agents/sdk-consumer-setup.md @@ -0,0 +1,34 @@ +# sdk-consumer-setup (generic agent spec) + +## Goal + +Help a consuming repository adopt or troubleshoot `Purview.DotNetProjectSdk` correctly, without breaking existing build behaviour. + +## Workflow + +1. Confirm the SDK is imported in `Directory.Build.props`/`Directory.Build.targets` via + `` and the matching `Sdk.targets` import. +2. Check pre-import bootstrap properties are set **before** the `Sdk.props` import when they must affect + evaluation: `NamespacePrefix`, `UsePackageJsonVersion`, `RootPackageJson`. +3. If version resolution looks wrong, verify `package.json` discovery: explicit `RootPackageJson`, then CI + variables, `.git` root, or a nearby `package.json`. `UsePackageJsonVersion=Strict` fails fast instead of + silently skipping resolution. +4. If the bundled `.agents/**` content isn't appearing in the repo root, check `EnableAgentFolderInPackage` + (default `true`) and `AgentPackDestinationFolder` (default `.agents`) — the copy runs before build via + `EnsureAgentFolderInPackageTarget`. +5. For test-framework or project-shape questions, confirm the project follows repo naming and placement + conventions the SDK expects, rather than introducing bespoke structure. +6. Re-run `dotnet build` (or the repo's canonical build command) after each configuration change to confirm + the fix. + +## Constraints + +- Prefer minimal, targeted property changes over broad `Directory.Build.props` rewrites. +- Do not disable `PurviewAutoSdkPack` or `EnableAgentFolderInPackage` unless the consumer explicitly asks to + opt out. +- Do not duplicate SDK-managed properties in individual project files unless the scenario is intentionally + project-specific. + +## Related skill + +See `../skills/sdk-configuration-reference/SKILL.md` for the full property reference. diff --git a/.agents/agents/source-generator-framework-writer.agent.md b/.agents/agents/source-generator-framework-writer.agent.md new file mode 100644 index 0000000..80fec8c --- /dev/null +++ b/.agents/agents/source-generator-framework-writer.agent.md @@ -0,0 +1,55 @@ +--- +name: Source Generator Framework Writer +description: "Specialist for Purview.SourceGeneratorFramework generation code using CodeWriter and XmlCodeWriter-style XML doc extensions; ideal for creating or refactoring generator emitters." +tools: + [ + "search/codebase", + "edit/editFiles", + "search", + "execute/getTerminalOutput", + "execute/runInTerminal", + "read/terminalLastCommand", + "read/terminalSelection", + "execute/createAndRunTask", + "execute/runTask", + "read/getTaskOutput", + "vscodeTasks/createAndRunTask", + "vscodeTasks/getTaskOutput", + "vscodeTasks/runTask", + ] +--- + +You are a specialist for `Purview.SourceGeneratorFramework` emitter authoring. + +## Primary objective + +Produce clear, deterministic, maintainable source-generator emission code using `CodeWriter` and XML extension helpers from `XmlCommentWriter`. + +## Must-follow rules + +1. Prefer structured declaration APIs over handwritten declaration strings. +2. Prefer XML helper extensions (`XmlSummary`, `XmlParam`, etc.) over raw `///` output. +3. Keep `CodeWriter` instances output-scoped; never cache in incremental provider state. +4. Preserve semantic behavior while modernizing implementation style. +5. Keep edits minimal and localized to emitter concerns. + +## Refactoring posture + +When modernizing legacy code: + +- Replace manual indentation/braces with scope APIs. +- Replace signature text with declaration option records. +- Replace ad-hoc XML tags with helper APIs. +- Preserve diagnostics and emitted symbol names. + +## Quality gates + +- Build/tests pass for impacted projects. +- No scope leaks when materializing generated source. +- Generated artifacts remain deterministic and reviewable. + +## Skill routing + +When relevant, first load and apply: + +- `source-generator-codewriter-modernization` diff --git a/.agents/prompts/refactor-source-generator-to-codewriter.prompt.md b/.agents/prompts/refactor-source-generator-to-codewriter.prompt.md new file mode 100644 index 0000000..9cf531b --- /dev/null +++ b/.agents/prompts/refactor-source-generator-to-codewriter.prompt.md @@ -0,0 +1,52 @@ +--- +agent: ask +description: "Refactor a legacy source generator emitter from string/StringBuilder to CodeWriter + XmlCodeWriter-style XML extensions with behavior parity." +--- + +You are modernizing a source generator implementation in this repository. + +## Inputs + +- Target file(s): `${input:targetFiles:Path(s) to emitter file(s)}` +- Generator type name: `${input:generatorName:Generator class name}` +- Generator version: `${input:generatorVersion:Version string (for generated attributes/header)}` +- Keep output byte-identical where possible: `${input:preserveFormatting:true|false}` + +## Task + +Refactor the selected legacy emitter implementation from manual `string` / `StringBuilder` output construction to `CodeWriter` and XML documentation extension helpers from `XmlCommentWriter` (XmlCodeWriter-style API usage). + +### Requirements + +1. Use structured declaration APIs where applicable: + - `WriteClass/WriteStruct/WriteRecordClass/WriteInterface/WriteEnum` + - `WriteMethod`, `WriteProperty`, `WriteField`, `WriteConstructor` +2. Use XML helper extensions instead of raw `///` composition: + - `XmlSummary`, `XmlParam`, `XmlReturn`, `XmlRemarks`, `XmlCode` or `XmlCodeBlock` +3. Use `TypeReferenceOptions` when type text becomes complex (nullability, generics, arrays). +4. Ensure writer lifetime is output-scoped (`generationContext.CreateCodeWriter()` inside callback). +5. Preserve behavior, diagnostics, and generated names. +6. Keep changes minimal and focused; do not reformat unrelated logic. + +### Migration strategy + +- Identify emitter phases: header, namespace, type declarations, member declarations. +- Replace indentation/braces with scoped APIs. +- Replace signature strings with declaration options. +- Replace XML comments with XmlCommentWriter extension methods. +- Keep semantic equivalence; call out any intentional deltas. + +### Verification + +- Run relevant tests. +- Confirm generated files still compile. +- Confirm no `CodeWriter` scope leaks (`OpenScopeCount == 0` when materialized). + +### Output format + +Return: + +1. Files changed +2. Why each change was necessary +3. Risks/behavior differences (if any) +4. Verification performed diff --git a/.agents/prompts/sdk-diagnose-agent-folder-copy.md b/.agents/prompts/sdk-diagnose-agent-folder-copy.md new file mode 100644 index 0000000..49ad1c2 --- /dev/null +++ b/.agents/prompts/sdk-diagnose-agent-folder-copy.md @@ -0,0 +1,26 @@ +# sdk-diagnose-agent-folder-copy (generic prompt spec) + +Diagnose why the bundled `.agents/**` folder from `Purview.DotNetProjectSdk` did not appear at the expected +destination in a consuming repository. + +## Required behaviour + +1. Confirm the NuGet package actually contains `.agents/**` content (inspect the `.nupkg` if available). +2. Confirm the consuming project is packable/buildable and imports the SDK via + `Sdk.props`/`Sdk.targets`, since the copy runs in `EnsureAgentFolderInPackageTarget` before build. +3. Check `EnableAgentFolderInPackage` is not set to `false` anywhere in the build (project file, + `Directory.Build.props`, or command-line `-p:` overrides). +4. Confirm the destination folder: default is `.agents` at the repo root, overridable per-build with + `-p:AgentPackDestinationFolder=`. +5. Verify repo-root discovery succeeded: explicit `RepoRoot`, then a nearby `AGENTS.md`, then source-control + root metadata. +6. Re-run the build and confirm the destination folder now contains the copied files (including the + generated `.gitignore` for skill/prompt/agent subfolders). + +## Suggested output + +- A short root-cause explanation (missing import, disabled flag, wrong destination override, or repo-root + discovery miss). +- The exact command used to reproduce/verify the fix (for example + `dotnet build -p:AgentPackDestinationFolder=`). +- Confirmation that the expected files exist at the resolved destination path. diff --git a/.agents/skills/source-generator-codewriter-modernization/.gitignore b/.agents/skills/source-generator-codewriter-modernization/.gitignore new file mode 100644 index 0000000..2799754 --- /dev/null +++ b/.agents/skills/source-generator-codewriter-modernization/.gitignore @@ -0,0 +1,8 @@ +# Ignore all files +* + +# Don't ignore directories, so Git can traverse them +!*/ + +# Keep this file +!.gitignore \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 3700f14..22cf3a2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,8 +8,8 @@ --> 4.13.0 [5.6.0,) - [1.65.0,) - [1.0.0-prerelease.23,) + [1.65.68,) + [1.0.0-prerelease.25,) diff --git a/global.json b/global.json index f6570c9..7c80d95 100644 --- a/global.json +++ b/global.json @@ -5,7 +5,7 @@ "allowPrerelease": false }, "msbuild-sdks": { - "Purview.DotNetProjectSdk": "1.0.0-prerelease.39" + "Purview.DotNetProjectSdk": "1.0.0-prerelease.40" }, "test": { "runner": "Microsoft.Testing.Platform" diff --git a/src/src/SourceGenerators/Extensions/Purview/SourceGeneratorFramework/Helpers/TypeHelpersExtensions.cs b/src/src/SourceGenerators/Extensions/Purview/SourceGeneratorFramework/Helpers/TypeHelpersExtensions.cs index e6a7283..8767969 100644 --- a/src/src/SourceGenerators/Extensions/Purview/SourceGeneratorFramework/Helpers/TypeHelpersExtensions.cs +++ b/src/src/SourceGenerators/Extensions/Purview/SourceGeneratorFramework/Helpers/TypeHelpersExtensions.cs @@ -129,10 +129,10 @@ public static bool IsNamedType(ITypeSymbol type, string fullyQualifiedMetadataNa && string.Equals(namedType.ToDisplayString(), fullyQualifiedMetadataName, StringComparison.Ordinal); } - public static bool HasAttribute(IEnumerable attributes, TypeValueObject attribute) => + public static bool HasAttribute(IEnumerable attributes, TypeIdentity attribute) => attributes.Any(attribute.Equals); - public static bool IsOrImplements(ITypeSymbol type, TypeValueObject interfaceType) + public static bool IsOrImplements(ITypeSymbol type, TypeIdentity interfaceType) { var unwrapped = StripNullableAnnotations(type); return TypeHelpers.IsNamedType(unwrapped, interfaceType.MetadataFullName) diff --git a/src/src/SourceGenerators/Helpers/AttributeGenHelper.cs b/src/src/SourceGenerators/Helpers/AttributeGenHelper.cs index d73f939..10348c4 100644 --- a/src/src/SourceGenerators/Helpers/AttributeGenHelper.cs +++ b/src/src/SourceGenerators/Helpers/AttributeGenHelper.cs @@ -1,5 +1,4 @@ using Microsoft.CodeAnalysis.Text; - using ZodSharp.SourceGenerators.Models; namespace ZodSharp.SourceGenerators.Helpers; @@ -13,7 +12,7 @@ static class AttributeGenHelper static SourceText ZodSchemaAttribute() { - CodeWriter writer = new(typeof(ZodSchemaGenerator).FullName, AssemblyInfo.Version); + CodeWriter writer = new(GenerationSettings.Create()); writer.WriteAutoGeneratedHeader().WriteFileScopedNamespace(TypeLibrary.ZodSchemaAttribute); @@ -32,7 +31,11 @@ static SourceText ZodSchemaAttribute() "If not specified, uses \"{ClassName}Schema\"." ) .WriteProperty( - new(nameof(ZodSchemaAttributeData.SchemaName), PurviewTypeLibrary.System.String.AsTypeReference().Nullable()) + new( + nameof(ZodSchemaAttributeData.SchemaName), + PurviewTypeLibrary.System.String.AsTypeReference().Nullable(), + TypeDeclarationAccessibility.Public + ) { IsInitOnly = true, } @@ -40,9 +43,12 @@ static SourceText ZodSchemaAttribute() body.XmlSummary("Whether to generate a static Validate method.", "Default is true.") .WriteProperty( - new(nameof(ZodSchemaAttributeData.GenerateValidateMethod), PurviewTypeLibrary.System.Boolean) + new( + nameof(ZodSchemaAttributeData.GenerateValidateMethod), + PurviewTypeLibrary.System.Boolean, + TypeDeclarationAccessibility.Public + ) { - Accessibility = TypeDeclarationAccessibility.Public, IsInitOnly = true, Initializer = "true", } @@ -53,9 +59,12 @@ static SourceText ZodSchemaAttribute() $"Default is true." ) .WriteProperty( - new(nameof(ZodSchemaAttributeData.GenerateParseMethod), PurviewTypeLibrary.System.Boolean) + new( + nameof(ZodSchemaAttributeData.GenerateParseMethod), + PurviewTypeLibrary.System.Boolean, + TypeDeclarationAccessibility.Public + ) { - Accessibility = TypeDeclarationAccessibility.Public, IsInitOnly = true, Initializer = "true", } @@ -66,9 +75,12 @@ static SourceText ZodSchemaAttribute() $"Default is true." ) .WriteProperty( - new(nameof(ZodSchemaAttributeData.EnableComposition), PurviewTypeLibrary.System.Boolean) + new( + nameof(ZodSchemaAttributeData.EnableComposition), + PurviewTypeLibrary.System.Boolean, + TypeDeclarationAccessibility.Public + ) { - Accessibility = TypeDeclarationAccessibility.Public, IsInitOnly = true, Initializer = "true", } @@ -83,9 +95,12 @@ static SourceText ZodSchemaAttribute() "No diagnostic is reported when the default name has no matching method." ) .WriteProperty( - new(nameof(ZodSchemaAttributeData.CustomValidationMethodName), PurviewTypeLibrary.System.String.AsTypeReference().Nullable()) + new( + nameof(ZodSchemaAttributeData.CustomValidationMethodName), + PurviewTypeLibrary.System.String.AsTypeReference().Nullable(), + TypeDeclarationAccessibility.Public + ) { - Accessibility = TypeDeclarationAccessibility.Public, IsInitOnly = true, } ); diff --git a/src/src/SourceGenerators/Helpers/CodeGenHelpers.cs b/src/src/SourceGenerators/Helpers/CodeGenHelpers.cs index ef11cdd..04a40b7 100644 --- a/src/src/SourceGenerators/Helpers/CodeGenHelpers.cs +++ b/src/src/SourceGenerators/Helpers/CodeGenHelpers.cs @@ -21,8 +21,8 @@ string errorMessage "));", bodyWriter => { - bodyWriter.Write(errorCode.Surround()).WriteLine(","); - bodyWriter.Write(errorMessage.Surround()).WriteLine(","); + bodyWriter.Write(Quote(errorCode)).WriteLine(","); + bodyWriter.Write(Quote(errorMessage)).WriteLine(","); bodyWriter.WriteLine($"new[] {{ \"{propertyName}\" }}"); } ); diff --git a/src/src/SourceGenerators/Helpers/SourceGenLibrary.cs b/src/src/SourceGenerators/Helpers/SourceGenLibrary.cs index 3f26c7c..6a274b8 100644 --- a/src/src/SourceGenerators/Helpers/SourceGenLibrary.cs +++ b/src/src/SourceGenerators/Helpers/SourceGenLibrary.cs @@ -1,6 +1,5 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using ZodSharp.SourceGenerators.Models; @@ -12,50 +11,121 @@ public static IncrementalValueProvider GetGeneratorValueP IncrementalGeneratorInitializationContext context ) { - var outputContext = IncrementalPipeline.GenerationContextValueProvider( + var generationContext = IncrementalPipeline.GenerationContextValueProvider< + SchemaGenerationCapabilities, + ZodSchemaGenerator + >( context, - typeof(ZodSchemaGenerator).FullName, - AssemblyInfo.Version, - (compilation, generatorSettings, logger, _) => new(compilation, generatorSettings, logger), + static (compilation, _, _, _) => + new(compilation) + { + HasRequiredAttribute = TypeHelpers.HasType( + compilation, + TypeLibrary.DataAnnotations.RequiredAttribute + ), + }, PropertyLibrary.DisableZodSharpSourceGeneratorProperty ); - var zodSchemas = IncrementalPipeline.ForAttributeWithMetadataName( + var schemaSets = IncrementalPipeline.ForAttributeWithMetadataName( context, TypeLibrary.ZodSchemaAttribute, - predicate: static (s, _) => - s is ClassDeclarationSyntax or StructDeclarationSyntax or RecordDeclarationSyntax, - transform: static (ctx, ct) => GetZodSchemaTargetForGeneration(ctx, ct) + predicate: static (node, _) => node is TypeDeclarationSyntax, + transform: static (attributeContext, cancellationToken) => + GetSchemasForGeneration(attributeContext, cancellationToken) ); - return outputContext.CollectWith( - zodSchemas, - static (generationContext, zodSchemas, _) => new SchemaGenerationModel(generationContext, zodSchemas), + return generationContext.CollectWith( + schemaSets, + static (outputContext, sets, _) => + new SchemaGenerationModel(outputContext) { ZodSchemas = Deduplicate(sets) }, "CollectZodSchemas" ); } - static GeneratorResult GetZodSchemaTargetForGeneration( + static GeneratorResult GetSchemasForGeneration( GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken ) { - var declaration = (TypeDeclarationSyntax)context.TargetNode; - if (context.SemanticModel.GetDeclaredSymbol(declaration, cancellationToken) is not INamedTypeSymbol symbol) - return GeneratorResult.Empty; + if (context.SemanticModel.GetDeclaredSymbol(context.TargetNode, cancellationToken) is not INamedTypeSymbol root) + return GeneratorResult.Empty; - ZodSchemaDescriptor result = new(symbol, new(symbol), GetZodProperties(symbol)); + var schemas = ImmutableArray.CreateBuilder(); + var seen = new HashSet(); + var queue = new Queue<(INamedTypeSymbol Symbol, bool IsPrimary)>(); + queue.Enqueue((root, true)); - return GeneratorResult.Ok(result); + while (queue.Count > 0) + { + var (symbol, isPrimary) = queue.Dequeue(); + var identity = new TypeIdentity(symbol); + if (!seen.Add(identity)) + continue; + + schemas.Add(new(identity, GetDeclarationFingerprint(symbol, cancellationToken), isPrimary)); + foreach (var property in GetZodProperties(symbol)) + { + if (TryGetNestedSchemaType(property, out var nested)) + queue.Enqueue((nested, false)); + } + } + + return GeneratorResult.Ok(new(schemas.ToImmutable())); } - public static ImmutableArray GetZodProperties(INamedTypeSymbol symbol) + static EquatableArray> Deduplicate( + ImmutableArray> sets + ) + { + var results = ImmutableArray.CreateBuilder>(); + var seen = new HashSet(); + foreach (var set in sets) + { + if (!set.ShouldProcess) + continue; + + foreach (var schema in set.Value.Schemas) + { + if (seen.Add(schema.SchemaType)) + results.Add(GeneratorResult.Ok(schema)); + } + } + + return new(results.ToImmutable()); + } + + static string GetDeclarationFingerprint(INamedTypeSymbol symbol, CancellationToken cancellationToken) => + string.Join( + "\n", + symbol + .DeclaringSyntaxReferences.Select(reference => reference.GetSyntax(cancellationToken).ToFullString()) + .OrderBy(static text => text, StringComparer.Ordinal) + ); + + static ImmutableArray GetZodProperties(INamedTypeSymbol symbol) => + [ + .. symbol + .GetMembers() + .OfType() + .Where(static property => property.DeclaredAccessibility == Accessibility.Public && !property.IsStatic), + ]; + + static bool TryGetNestedSchemaType(IPropertySymbol property, out INamedTypeSymbol nested) { - var properties = symbol - .GetMembers() - .OfType() - .Where(p => p.DeclaredAccessibility == Accessibility.Public) - .Select(p => new ZodPropertyDescriptor(p)); + var propertyType = TypeHelpers.UnwrapNullableType(property.Type); + if (propertyType is IArrayTypeSymbol array) + propertyType = array.ElementType; + else if (propertyType is INamedTypeSymbol named) + { + var enumerable = named.AllInterfaces.FirstOrDefault(TypeLibrary.Collections.IEnumerableT.Equals); + if (enumerable is not null) + propertyType = enumerable.TypeArguments[0]; + } - return [.. properties]; + propertyType = TypeHelpers.UnwrapNullableType(propertyType); + nested = propertyType as INamedTypeSymbol ?? null!; + return nested is not null + && nested.Locations.Any(static location => location.IsInSource) + && !ZodSchemaGenerator.IsScalarType(nested); } } diff --git a/src/src/SourceGenerators/Helpers/TypeLibrary.DataAnnotations.cs b/src/src/SourceGenerators/Helpers/TypeLibrary.DataAnnotations.cs index 6b57617..38f9dac 100644 --- a/src/src/SourceGenerators/Helpers/TypeLibrary.DataAnnotations.cs +++ b/src/src/SourceGenerators/Helpers/TypeLibrary.DataAnnotations.cs @@ -6,29 +6,29 @@ public static class DataAnnotations { public const string Namespace = "System.ComponentModel.DataAnnotations"; - public static readonly TypeValueObject DisplayAttribute = new(nameof(DisplayAttribute), Namespace); - public static readonly TypeValueObject RequiredAttribute = new(nameof(RequiredAttribute), Namespace); + public static readonly TypeIdentity DisplayAttribute = new(nameof(DisplayAttribute), Namespace); + public static readonly TypeIdentity RequiredAttribute = new(nameof(RequiredAttribute), Namespace); - public static readonly TypeValueObject EmailAddressAttribute = new(nameof(EmailAddressAttribute), Namespace); - public static readonly TypeValueObject StringLengthAttribute = new(nameof(StringLengthAttribute), Namespace); - public static readonly TypeValueObject MinLengthAttribute = new(nameof(MinLengthAttribute), Namespace); - public static readonly TypeValueObject MaxLengthAttribute = new(nameof(MaxLengthAttribute), Namespace); - public static readonly TypeValueObject RangeAttribute = new(nameof(RangeAttribute), Namespace); - public static readonly TypeValueObject LengthAttribute = new(nameof(LengthAttribute), Namespace); - public static readonly TypeValueObject RegularExpressionAttribute = new( + public static readonly TypeIdentity EmailAddressAttribute = new(nameof(EmailAddressAttribute), Namespace); + public static readonly TypeIdentity StringLengthAttribute = new(nameof(StringLengthAttribute), Namespace); + public static readonly TypeIdentity MinLengthAttribute = new(nameof(MinLengthAttribute), Namespace); + public static readonly TypeIdentity MaxLengthAttribute = new(nameof(MaxLengthAttribute), Namespace); + public static readonly TypeIdentity RangeAttribute = new(nameof(RangeAttribute), Namespace); + public static readonly TypeIdentity LengthAttribute = new(nameof(LengthAttribute), Namespace); + public static readonly TypeIdentity RegularExpressionAttribute = new( nameof(RegularExpressionAttribute), Namespace ); - public static readonly TypeValueObject AllowedValuesAttribute = new(nameof(AllowedValuesAttribute), Namespace); - public static readonly TypeValueObject DeniedValuesAttribute = new(nameof(DeniedValuesAttribute), Namespace); - public static readonly TypeValueObject UrlAttribute = new(nameof(UrlAttribute), Namespace); - public static readonly TypeValueObject PhoneAttribute = new(nameof(PhoneAttribute), Namespace); - public static readonly TypeValueObject CreditCardAttribute = new(nameof(CreditCardAttribute), Namespace); - public static readonly TypeValueObject CompareAttribute = new(nameof(CompareAttribute), Namespace); - public static readonly TypeValueObject Base64StringAttribute = new(nameof(Base64StringAttribute), Namespace); + public static readonly TypeIdentity AllowedValuesAttribute = new(nameof(AllowedValuesAttribute), Namespace); + public static readonly TypeIdentity DeniedValuesAttribute = new(nameof(DeniedValuesAttribute), Namespace); + public static readonly TypeIdentity UrlAttribute = new(nameof(UrlAttribute), Namespace); + public static readonly TypeIdentity PhoneAttribute = new(nameof(PhoneAttribute), Namespace); + public static readonly TypeIdentity CreditCardAttribute = new(nameof(CreditCardAttribute), Namespace); + public static readonly TypeIdentity CompareAttribute = new(nameof(CompareAttribute), Namespace); + public static readonly TypeIdentity Base64StringAttribute = new(nameof(Base64StringAttribute), Namespace); // This is abstract and the base class to all the other validation attributes, // so we can use it to get the base properties - public static readonly TypeValueObject ValidationAttribute = new(nameof(ValidationAttribute), Namespace); + public static readonly TypeIdentity ValidationAttribute = new(nameof(ValidationAttribute), Namespace); } } diff --git a/src/src/SourceGenerators/Helpers/TypeLibrary.Others.cs b/src/src/SourceGenerators/Helpers/TypeLibrary.Others.cs index d249f3c..d4bf0e9 100644 --- a/src/src/SourceGenerators/Helpers/TypeLibrary.Others.cs +++ b/src/src/SourceGenerators/Helpers/TypeLibrary.Others.cs @@ -5,24 +5,24 @@ namespace ZodSharp.SourceGenerators.Helpers; partial class TypeLibrary { - public static readonly TypeValueObject CancellationToken = new(typeof(CancellationToken)); + public static readonly TypeIdentity CancellationToken = new(typeof(CancellationToken)); - public static readonly TypeValueObject ValueTask = new(typeof(ValueTask)); + public static readonly TypeIdentity ValueTask = new(typeof(ValueTask)); public static class Collections { - public static readonly TypeValueObject ImmutableArray = new(typeof(ImmutableArray)); + public static readonly TypeIdentity ImmutableArray = new(typeof(ImmutableArray)); - public static readonly TypeValueObject List = new(typeof(List<>)); + public static readonly TypeIdentity List = new(typeof(List<>)); - public static readonly TypeValueObject ICollection = new(typeof(ICollection)); + public static readonly TypeIdentity ICollection = new(typeof(ICollection)); - public static readonly TypeValueObject IEnumerable = new(typeof(IEnumerable)); + public static readonly TypeIdentity IEnumerable = new(typeof(IEnumerable)); - public static readonly TypeValueObject ICollectionT = new(typeof(ICollection<>)); + public static readonly TypeIdentity ICollectionT = new(typeof(ICollection<>)); - public static readonly TypeValueObject IEnumerableT = new(typeof(IEnumerable<>)); + public static readonly TypeIdentity IEnumerableT = new(typeof(IEnumerable<>)); - public static readonly TypeValueObject IReadOnlyCollectionT = new(typeof(IReadOnlyCollection<>)); + public static readonly TypeIdentity IReadOnlyCollectionT = new(typeof(IReadOnlyCollection<>)); } } diff --git a/src/src/SourceGenerators/Helpers/TypeLibrary.cs b/src/src/SourceGenerators/Helpers/TypeLibrary.cs index 359d735..605ef70 100644 --- a/src/src/SourceGenerators/Helpers/TypeLibrary.cs +++ b/src/src/SourceGenerators/Helpers/TypeLibrary.cs @@ -10,20 +10,20 @@ static partial class TypeLibrary public const string DefaultCustomValidationMethodName = "CustomValidationAsync"; // This matches the name of the class, just so we can use the `nameof` for later... - public static readonly TypeValueObject ZodSchemaAttribute = new(nameof(ZodSchemaAttribute), ZodSharpNamespace); + public static readonly TypeIdentity ZodSchemaAttribute = new(nameof(ZodSchemaAttribute), ZodSharpNamespace); - public static readonly TypeValueObject ZodSchemaGeneratedAttribute = new( + public static readonly TypeIdentity ZodSchemaGeneratedAttribute = new( nameof(ZodSchemaGeneratedAttribute), ZodSharpCoreNamespace ); // Other ZodSharp types... - public static readonly TypeValueObject ValidationResult = new(nameof(ValidationResult), ZodSharpCoreNamespace); + public static readonly TypeIdentity ValidationResult = new(nameof(ValidationResult), ZodSharpCoreNamespace); - public static readonly TypeValueObject ValidationResultMetadataName = new( + public static readonly TypeIdentity ValidationResultMetadataName = new( nameof(ValidationResultMetadataName), ZodSharpCoreNamespace ); - public static readonly TypeValueObject ValidationError = new(nameof(ValidationError), ZodSharpCoreNamespace); + public static readonly TypeIdentity ValidationError = new(nameof(ValidationError), ZodSharpCoreNamespace); } diff --git a/src/src/SourceGenerators/Models/DataAttributes/RangeAttributeData.cs b/src/src/SourceGenerators/Models/DataAttributes/RangeAttributeData.cs index 66d514e..5a778df 100644 --- a/src/src/SourceGenerators/Models/DataAttributes/RangeAttributeData.cs +++ b/src/src/SourceGenerators/Models/DataAttributes/RangeAttributeData.cs @@ -1,6 +1,5 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis; -using Purview.SourceGeneratorFramework.Extensions; using ZodSharp.SourceGenerators.Helpers; namespace ZodSharp.SourceGenerators.Models.DataAttributes; diff --git a/src/src/SourceGenerators/Models/SchemaGenerationModel.cs b/src/src/SourceGenerators/Models/SchemaGenerationModel.cs index 23a5457..7d12eef 100644 --- a/src/src/SourceGenerators/Models/SchemaGenerationModel.cs +++ b/src/src/SourceGenerators/Models/SchemaGenerationModel.cs @@ -1,43 +1,31 @@ -using System.Collections.Immutable; using Microsoft.CodeAnalysis; -using ZodSharp.SourceGenerators.Helpers; namespace ZodSharp.SourceGenerators.Models; -sealed record SchemaGenerationModel(SchemaGenerationContext GenerationContext, ImmutableArray> ZodSchemas) +sealed record SchemaGenerationModel(GenerationContext Context) { - public ImmutableArray Diagnostics { get; set; } = []; + public EquatableArray> ZodSchemas { get; init; } = []; + + public EquatableArray Diagnostics { get; init; } = []; } -sealed class SchemaGenerationContext : GenerationContext +sealed record SchemaGenerationCapabilities(Compilation Compilation) : IGenerationCapabilities { - public SchemaGenerationContext(Compilation compilation, GenerationSettings settings, ISourceGenLogger? logger) - : base(compilation, settings, logger) - { - RequiredAttribute = GetTypeByMetadataName(TypeLibrary.DataAnnotations.RequiredAttribute); - } - - public INamedTypeSymbol? RequiredAttribute { get; } + public bool HasRequiredAttribute { get; init; } } // This is recreated outside of the pipeline to avoid the state // of the CodeWriter being shared across multiple source outputs. -sealed class SchemaGenerationOutputContext(SchemaGenerationContext generationContext) : ISourceGenLogger +sealed record SchemaGenerationOutputContext(GenerationContext Context) : ISourceGenLogger { - public SchemaGenerationContext Generation { get; } = generationContext; - - public CodeWriter Writer { get; private set; } = generationContext.CreateCodeWriter(); + public CodeWriter Writer { get; private set; } = Context.CreateCodeWriter(); - public CodeWriter CreateCodeWriter() => Writer = Generation.CreateCodeWriter(); + public CodeWriter CreateCodeWriter() => Writer = Context.CreateCodeWriter(); public void Log(SourceGenLogLevel level, int indentation, string message, params object[] args) => - Generation.Log(level, indentation, message, args); + Context.Log(level, indentation, message, args); } -readonly record struct ZodSchemaDescriptor( - INamedTypeSymbol Symbol, - TypeValueObject SchemaType, - ImmutableArray Properties -); +readonly record struct ZodSchemaDescriptor(TypeIdentity SchemaType, string DeclarationFingerprint, bool IsPrimary); -readonly record struct ZodPropertyDescriptor(IPropertySymbol Symbol); +readonly record struct SchemaSet(EquatableArray Schemas); diff --git a/src/src/SourceGenerators/ZodSchemaGenerator.BuildSchema.cs b/src/src/SourceGenerators/ZodSchemaGenerator.BuildSchema.cs index 26a6d7a..d1ad503 100644 --- a/src/src/SourceGenerators/ZodSchemaGenerator.BuildSchema.cs +++ b/src/src/SourceGenerators/ZodSchemaGenerator.BuildSchema.cs @@ -12,22 +12,23 @@ namespace ZodSharp.SourceGenerators; partial class ZodSchemaGenerator { static void BuildSchema( - ZodSchemaDescriptor schemaDescriptor, + INamedTypeSymbol schemaSymbol, + TypeIdentity schemaType, SchemaGenerationOutputContext outputContext, SourceProductionContext context, bool isPrimary ) { - outputContext.Info($"Building schema for {schemaDescriptor.Symbol.Name}"); + outputContext.Info($"Building schema for {schemaSymbol.Name}"); try { List diagnostics = []; - var source = GenerateSchemaClass(schemaDescriptor.Symbol, outputContext, diagnostics, isPrimary); + var source = GenerateSchemaClass(schemaSymbol, outputContext, diagnostics, isPrimary); if (diagnostics.Count > 0) ReportDiagnostics(context, diagnostics, outputContext); - var fileName = $"{schemaDescriptor.Symbol.Name}Schema.g.cs"; + var fileName = $"{schemaSymbol.Name}Schema.g.cs"; context.AddSource(fileName, SourceText.From(source, Encoding.UTF8)); // Emit module-level registration attribute for DI discovery. @@ -35,21 +36,21 @@ bool isPrimary // Skip registration for nested types because private nested types are inaccessible at // assembly scope and public/internal nested types are best discovered through their // containing type. - if (isPrimary && schemaDescriptor.Symbol.ContainingType is null) + if (isPrimary && schemaSymbol.ContainingType is null) { // Create a new writer for the assembly-level attribute to avoid polluting the schema class file. - var writer = outputContext.Generation.CreateCodeWriter(); + var writer = outputContext.Context.CreateCodeWriter(); writer.WriteAutoGeneratedHeader(); writer .Write("[assembly: global::") .Write(TypeLibrary.ZodSchemaGeneratedAttribute.MetadataFullName) .Write("(typeof(") - .Write(schemaDescriptor.SchemaType) + .Write(schemaType) .WriteLine("))]"); context.AddSource( - $"{schemaDescriptor.Symbol.Name}SchemaRegistration.g.cs", + $"{schemaSymbol.Name}SchemaRegistration.g.cs", SourceText.From(writer.ToString(), Encoding.UTF8) ); } @@ -65,8 +66,8 @@ bool isPrimary // Report diagnostic if generation fails var diagnostic = DiagnosticInfo.Create( DiagnosticLibrary.UnhandledException, - schemaDescriptor.Symbol.Locations.FirstOrDefault(), - schemaDescriptor.Symbol.Name, + schemaSymbol, + schemaSymbol.Name, ex.Message ); @@ -74,81 +75,7 @@ bool isPrimary } } - static ImmutableArray<(ZodSchemaDescriptor Descriptor, bool IsPrimary)> DiscoverAllSchemas( - ImmutableArray> primarySchemas - ) - { - var builder = ImmutableArray.CreateBuilder<(ZodSchemaDescriptor Descriptor, bool IsPrimary)>(); - - builder.AddRange(primarySchemas.Select(p => (p.Value, true))); - var seen = new HashSet( - primarySchemas.Select(p => p.Value.Symbol), - SymbolEqualityComparer.Default - ); - Queue queue = new(primarySchemas.Select(p => p.Value.Symbol)); - - while (queue.Count > 0) - { - var current = queue.Dequeue(); - foreach ( - var property in current - .GetMembers() - .OfType() - .Where(static p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) - ) - { - var propertyType = TypeHelpers.UnwrapNullableType(property.Type); - - // Discover element types of arrays and generic collections. - if (propertyType is IArrayTypeSymbol arrayType) - { - propertyType = arrayType.ElementType; - } - else if (propertyType is INamedTypeSymbol namedType) - { - var enumerableInterface = namedType.AllInterfaces.FirstOrDefault( - TypeLibrary.Collections.IEnumerableT.Equals - ); - if (enumerableInterface is not null) - propertyType = enumerableInterface.TypeArguments[0]; - } - - if (propertyType is not INamedTypeSymbol namedPropertyType) - continue; - - propertyType = TypeHelpers.UnwrapNullableType(namedPropertyType); - if (propertyType is not INamedTypeSymbol unwrappedPropertyType) - continue; - - // Only generate schemas for source-defined types in the same compilation. - if (!unwrappedPropertyType.Locations.Any(static l => l.IsInSource)) - continue; - - // Skip scalar types that have their own validators. - if (IsScalarType(unwrappedPropertyType)) - continue; - - if (seen.Add(unwrappedPropertyType)) - { - builder.Add( - ( - new( - unwrappedPropertyType, - new(unwrappedPropertyType), - SourceGenLibrary.GetZodProperties(unwrappedPropertyType) - ), - false - ) - ); - queue.Enqueue(unwrappedPropertyType); - } - } - } - - return builder.ToImmutableArray(); - } - - static bool IsScalarType(INamedTypeSymbol type) => + internal static bool IsScalarType(INamedTypeSymbol type) => type.SpecialType is SpecialType.System_Boolean or SpecialType.System_Char @@ -234,7 +161,10 @@ bool isPrimary ) { writer.WriteField( - new("EmptyPath", TypeLibrary.Collections.ImmutableArray.MakeGeneric(PurviewTypeLibrary.System.String)) + new( + "EmptyPath", + TypeLibrary.Collections.ImmutableArray.MakeGeneric(PurviewTypeLibrary.System.String) + ) { Accessibility = TypeDeclarationAccessibility.Private, IsStatic = true, @@ -434,8 +364,8 @@ static void GenerateValidateMethod( List diagnostics ) { - outputContext.Writer - .XmlSummary("Validates an instance of the target type.") + outputContext + .Writer.XmlSummary("Validates an instance of the target type.") .WriteLine( $"public static {TypeLibrary.ValidationResult.MakeGeneric(fullTypeName)} Validate({fullTypeName} value)" ); @@ -467,8 +397,8 @@ List diagnostics ); } - outputContext.Writer - .NewLine() + outputContext + .Writer.NewLine() .WriteLine($"{TypeLibrary.Collections.List.MakeGeneric(TypeLibrary.ValidationError)}? errors = null;"); var properties = classSymbol @@ -482,10 +412,14 @@ List diagnostics using (outputContext.Writer.OpenBlockScope("if (errors is not null)")) { - outputContext.Writer.WriteLine($"return {TypeLibrary.ValidationResult}<{fullTypeName}>.Failure(errors);"); + outputContext.Writer.WriteLine( + $"return {TypeLibrary.ValidationResult}<{fullTypeName}>.Failure(errors);" + ); } - outputContext.Writer.WriteLine().WriteLine($"return {TypeLibrary.ValidationResult}<{fullTypeName}>.Success(value);"); + outputContext + .Writer.WriteLine() + .WriteLine($"return {TypeLibrary.ValidationResult}<{fullTypeName}>.Success(value);"); } outputContext.Writer.NewLine(); @@ -531,25 +465,11 @@ List diagnostics outputContext.Writer.WriteRule(propertyName, comparision, "missing_field", errorMessage); block = outputContext.Writer.OpenBlockScope("else"); - GenerateValueSetValidations( - outputContext, - property, - property.Type, - propertyName, - attributes, - diagnostics - ); + GenerateValueSetValidations(outputContext, property, property.Type, propertyName, attributes, diagnostics); } else { - GenerateValueSetValidations( - outputContext, - property, - property.Type, - propertyName, - attributes, - diagnostics - ); + GenerateValueSetValidations(outputContext, property, property.Type, propertyName, attributes, diagnostics); if (canBeNull) block = outputContext.Writer.OpenBlockScope($"if (value.{propertyName} != null)"); @@ -661,14 +581,7 @@ rangeAttributeData is not null } else if (TypeHelpers.IsNumericType(propertyType)) { - GenerateNumericValidations( - outputContext, - property, - propertyType, - propertyName, - attributes, - diagnostics - ); + GenerateNumericValidations(outputContext, property, propertyType, propertyName, attributes, diagnostics); } else if ( propertyType is IArrayTypeSymbol @@ -676,14 +589,7 @@ propertyType is IArrayTypeSymbol || TypeHelpers.Implements(propertyType, TypeLibrary.Collections.IEnumerableT) ) { - GenerateCollectionValidations( - outputContext, - property, - propertyType, - propertyName, - attributes, - diagnostics - ); + GenerateCollectionValidations(outputContext, property, propertyType, propertyName, attributes, diagnostics); var elementType = GetCollectionElementType(propertyType); if ( @@ -708,7 +614,7 @@ static void CheckUnsupportedStringOnlyAttribute( ImmutableArray attributes, string propertyName, ITypeSymbol propertyType, - TypeValueObject attributeType, + TypeIdentity attributeType, string attributeName ) { @@ -908,9 +814,7 @@ bool canBeNull } else { - outputContext.Writer.WriteLine( - $"var {nestedResultName} = {schemaTypeName}.Validate({propertyValueName});" - ); + outputContext.Writer.WriteLine($"var {nestedResultName} = {schemaTypeName}.Validate({propertyValueName});"); using (outputContext.Writer.OpenBlockScope($"if (!{nestedResultName}.IsSuccess)")) { outputContext.Writer.WriteLine( @@ -995,23 +899,11 @@ out var maximumExpression ) { var fqTypeName = GetFullyQualifiedTypeName(propertyType); - outputContext.Writer.WriteField( - new(GetRangeMinimumFieldName(property.Name), fqTypeName) - { - IsReadOnly = true, - IsStatic = true, - Accessibility = TypeDeclarationAccessibility.Private, - Initializer = minimumExpression, - } + outputContext.Writer.WriteLine( + $"private static readonly {fqTypeName} {GetRangeMinimumFieldName(property.Name)} = {minimumExpression};" ); - outputContext.Writer.WriteField( - new(GetRangeMaximumFieldName(property.Name), fqTypeName) - { - IsReadOnly = true, - IsStatic = true, - Accessibility = TypeDeclarationAccessibility.Private, - Initializer = maximumExpression, - } + outputContext.Writer.WriteLine( + $"private static readonly {fqTypeName} {GetRangeMaximumFieldName(property.Name)} = {maximumExpression};" ); } } diff --git a/src/src/SourceGenerators/ZodSchemaGenerator.CollectionValidators.cs b/src/src/SourceGenerators/ZodSchemaGenerator.CollectionValidators.cs index eb18b44..dcdb63d 100644 --- a/src/src/SourceGenerators/ZodSchemaGenerator.CollectionValidators.cs +++ b/src/src/SourceGenerators/ZodSchemaGenerator.CollectionValidators.cs @@ -90,11 +90,7 @@ List diagnostics lengthAttr.MinimumLength.ToString(CultureInfo.InvariantCulture) ); - using ( - outputContext.Writer.OpenBlockScope( - $"if ({propertyLengthName} < {lengthAttr.MinimumLength})" - ) - ) + using (outputContext.Writer.OpenBlockScope($"if ({propertyLengthName} < {lengthAttr.MinimumLength})")) { WriteValidationError( outputContext, @@ -107,9 +103,7 @@ List diagnostics } using ( - outputContext.Writer.OpenBlockScope( - $"else if ({propertyLengthName} > {lengthAttr.MaximumLength})" - ) + outputContext.Writer.OpenBlockScope($"else if ({propertyLengthName} > {lengthAttr.MaximumLength})") ) { WriteValidationError( @@ -136,9 +130,7 @@ List diagnostics minLengthAttr.Length.ToString(CultureInfo.InvariantCulture) ); - using ( - outputContext.Writer.OpenBlockScope($"if ({propertyLengthName} < {minLengthAttr.Length})") - ) + using (outputContext.Writer.OpenBlockScope($"if ({propertyLengthName} < {minLengthAttr.Length})")) { WriteValidationError( outputContext, @@ -164,9 +156,7 @@ List diagnostics maxLengthAttr.Length.ToString(CultureInfo.InvariantCulture) ); - using ( - outputContext.Writer.OpenBlockScope($"if ({propertyLengthName} > {maxLengthAttr.Length})") - ) + using (outputContext.Writer.OpenBlockScope($"if ({propertyLengthName} > {maxLengthAttr.Length})")) { WriteValidationError( outputContext, diff --git a/src/src/SourceGenerators/ZodSchemaGenerator.CustomValidation.cs b/src/src/SourceGenerators/ZodSchemaGenerator.CustomValidation.cs index d098a05..90f6842 100644 --- a/src/src/SourceGenerators/ZodSchemaGenerator.CustomValidation.cs +++ b/src/src/SourceGenerators/ZodSchemaGenerator.CustomValidation.cs @@ -369,7 +369,7 @@ param.RefKind is RefKind.Ref or RefKind.In or RefKind.Out /// Constructs the expected ValueTask<ValidationResult<T>> symbol /// from the compilation's framework symbols. /// - static TypeValueObject GetExpectedReturnType(INamedTypeSymbol classSymbol) => + static TypeIdentity GetExpectedReturnType(INamedTypeSymbol classSymbol) => TypeLibrary.ValueTask.MakeGeneric( TypeLibrary.ValidationResult.MakeGeneric( classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) diff --git a/src/src/SourceGenerators/ZodSchemaGenerator.StringValidators.cs b/src/src/SourceGenerators/ZodSchemaGenerator.StringValidators.cs index 48d68c0..61abb98 100644 --- a/src/src/SourceGenerators/ZodSchemaGenerator.StringValidators.cs +++ b/src/src/SourceGenerators/ZodSchemaGenerator.StringValidators.cs @@ -325,9 +325,7 @@ List diagnostics outputContext.Writer.WriteLine($"var {propertyValueName} = value.{propertyName};"); using (outputContext.Writer.OpenBlockScope($"if ({propertyValueName} is not null)")) { - outputContext.Writer.WriteLine( - $"var {propertyLengthName} = {propertyValueName}.Length;" - ); + outputContext.Writer.WriteLine($"var {propertyLengthName} = {propertyValueName}.Length;"); using ( outputContext.Writer.OpenBlockScope( $"if ({propertyLengthName} < {lengthAttr.MinimumLength})" @@ -416,9 +414,7 @@ List diagnostics } using ( - outputContext.Writer.OpenBlockScope( - $"if ({propertyLengthName} > {stringLengthAttr.MaximumLength})" - ) + outputContext.Writer.OpenBlockScope($"if ({propertyLengthName} > {stringLengthAttr.MaximumLength})") ) { WriteValidationError( @@ -453,9 +449,7 @@ List diagnostics outputContext.Writer.WriteLine($"var {propertyValueName} = value.{propertyName};"); outputContext.Writer.WriteLine($"var {propertyLengthName} = {propertyValueName}.Length;"); - using ( - outputContext.Writer.OpenBlockScope($"if ({propertyLengthName} < {minLengthAttr.Length})") - ) + using (outputContext.Writer.OpenBlockScope($"if ({propertyLengthName} < {minLengthAttr.Length})")) { WriteValidationError( outputContext, @@ -489,9 +483,7 @@ List diagnostics outputContext.Writer.WriteLine($"var {propertyValueName} = value.{propertyName};"); outputContext.Writer.WriteLine($"var {propertyLengthName} = {propertyValueName}.Length;"); - using ( - outputContext.Writer.OpenBlockScope($"if ({propertyLengthName} > {maxLengthAttr.Length})") - ) + using (outputContext.Writer.OpenBlockScope($"if ({propertyLengthName} > {maxLengthAttr.Length})")) { WriteValidationError( outputContext, diff --git a/src/src/SourceGenerators/ZodSchemaGenerator.ValueValidators.cs b/src/src/SourceGenerators/ZodSchemaGenerator.ValueValidators.cs index 2d12d2f..de28a5f 100644 --- a/src/src/SourceGenerators/ZodSchemaGenerator.ValueValidators.cs +++ b/src/src/SourceGenerators/ZodSchemaGenerator.ValueValidators.cs @@ -18,22 +18,8 @@ static void GenerateValueSetValidations( List diagnostics ) { - GenerateAllowedValuesValidation( - outputContext, - property, - propertyType, - propertyName, - attributes, - diagnostics - ); - GenerateDeniedValuesValidation( - outputContext, - property, - propertyType, - propertyName, - attributes, - diagnostics - ); + GenerateAllowedValuesValidation(outputContext, property, propertyType, propertyName, attributes, diagnostics); + GenerateDeniedValuesValidation(outputContext, property, propertyType, propertyName, attributes, diagnostics); } static void GenerateAllowedValuesValidation( diff --git a/src/src/SourceGenerators/ZodSchemaGenerator.cs b/src/src/SourceGenerators/ZodSchemaGenerator.cs index a539a24..9862bb4 100644 --- a/src/src/SourceGenerators/ZodSchemaGenerator.cs +++ b/src/src/SourceGenerators/ZodSchemaGenerator.cs @@ -13,13 +13,13 @@ public sealed partial class ZodSchemaGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterEmbeddedAttribute(AssemblyInfo.AssemblyName, AssemblyInfo.Version); - - context.RegisterPostInitializationOutput(static ctx => - { - foreach (var (HintName, SourceText) in AttributeGenHelper.GenerateMarkers()) - ctx.AddSource($"{HintName}.g.cs", SourceText); - }); + context + .RegisterEmbeddedAttribute() + .RegisterPostInitializationOutput(static ctx => + { + foreach (var (HintName, SourceText) in AttributeGenHelper.GenerateMarkers()) + ctx.AddSource($"{HintName}.g.cs", SourceText); + }); var generationValueProviders = SourceGenLibrary.GetGeneratorValueProviders(context); @@ -28,31 +28,26 @@ public void Initialize(IncrementalGeneratorInitializationContext context) generationValueProviders, (spc, model) => { - if (model.GenerationContext.Settings.IsSourceGeneratorDisabled) + if (model.Context.Settings.IsSourceGeneratorDisabled) return; - var isFatal = false; foreach (var schema in model.ZodSchemas) { if (schema.HasDiagnostics) - { - ReportDiagnostics(spc, schema.Diagnostics, model.GenerationContext); - } + spc.ReportDiagnostics(schema.Diagnostics); - if (schema.IsFatal) - isFatal = true; - } + if (!schema.ShouldProcess) + continue; - if (isFatal) - return; + var symbol = SymbolResolver.Resolve( + model.Context.Capabilities.Compilation, + schema.Value.SchemaType + ); + if (symbol is null) + continue; - SchemaGenerationOutputContext outputContext = new(model.GenerationContext); - var allSchemas = DiscoverAllSchemas(model.ZodSchemas); - foreach (var (descriptor, isPrimary) in allSchemas) - { - // We'll create a new new code writer for each schema. - outputContext.CreateCodeWriter(); - BuildSchema(descriptor, outputContext, spc, isPrimary); + SchemaGenerationOutputContext outputContext = new(model.Context); + BuildSchema(symbol, schema.Value.SchemaType, outputContext, spc, schema.Value.IsPrimary); } } ); diff --git a/src/src/ZodSharp/ZodSharp.csproj b/src/src/ZodSharp/ZodSharp.csproj index db60d42..c821f61 100644 --- a/src/src/ZodSharp/ZodSharp.csproj +++ b/src/src/ZodSharp/ZodSharp.csproj @@ -1,8 +1,4 @@  - - true - - true ZodSharp @@ -11,6 +7,7 @@ Features zero-allocation validation, struct-based rules, fluent API, and source generator support for maximum performance. + true zod;validation;schema $(NoWarn);NU5118 diff --git a/src/tests/SourceGenerators.UnitTests/Infra/IncrementalSourceGeneratorTestBase.cs b/src/tests/SourceGenerators.UnitTests/Infra/IncrementalSourceGeneratorTestBase.cs deleted file mode 100644 index 072a784..0000000 --- a/src/tests/SourceGenerators.UnitTests/Infra/IncrementalSourceGeneratorTestBase.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.CodeAnalysis; - -namespace ZodSharp.SourceGenerators.Infra; - -public abstract class IncrementalSourceGeneratorTestBase - : TUnitSourceGeneratorTestBase - where TGenerator : class, IIncrementalGenerator, new() -{ - protected static readonly int ExpectedFileCount = ZodSharpGeneratorTestOptions.GeneratedAttributes.Length; - - protected static readonly int ExpectedFileCountPlusGen = ExpectedFileCount + 1; - - protected const int HintNameHashHexLength = 16; - - protected const string GeneratedSourceFileSuffix = ".g.cs"; -} diff --git a/src/tests/SourceGenerators.UnitTests/Infra/ZodSharpGeneratorTestOptions.cs b/src/tests/SourceGenerators.UnitTests/Infra/ZodSharpGeneratorTestOptions.cs deleted file mode 100644 index 1f5f521..0000000 --- a/src/tests/SourceGenerators.UnitTests/Infra/ZodSharpGeneratorTestOptions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Immutable; -using System.ComponentModel.DataAnnotations; -using ZodSharp.SourceGenerators.Helpers; - -namespace ZodSharp.SourceGenerators.Infra; - -public record class ZodSharpGeneratorTestOptions : SourceGeneratorTestOptions -{ - static readonly Type[] DefaultAssemblyTypes = -[ - typeof(Z), - typeof(ImmutableArray), - typeof(RequiredAttribute), - typeof(System.Text.Json.JsonSerializer), - typeof(System.Text.RegularExpressions.Regex), - ]; - - public static readonly string[] GeneratedAttributes = ["EmbeddedAttribute.cs", "ZodSchemaAttribute.g.cs"]; - - public ZodSharpGeneratorTestOptions() - { - IncludeDefaultNamespaces = true; - ThrowOnGenerationException = true; - DisableSourceGeneratorPropertyName = PropertyLibrary.DisableZodSharpSourceGeneratorProperty; - CompileToAssembly = true; - - AdditionalNamespaces = [TypeLibrary.ZodSharpNamespace]; - AdditionalAssemblyTypes = ImmutableArray.Create(DefaultAssemblyTypes); - ExcludeGeneratedSourceHintNames = [.. GeneratedAttributes]; - } -} diff --git a/src/tests/SourceGenerators.UnitTests/Infra/ZodSharpSourceGeneratorTestBase.cs b/src/tests/SourceGenerators.UnitTests/Infra/ZodSharpSourceGeneratorTestBase.cs new file mode 100644 index 0000000..302fb68 --- /dev/null +++ b/src/tests/SourceGenerators.UnitTests/Infra/ZodSharpSourceGeneratorTestBase.cs @@ -0,0 +1,18 @@ +using Microsoft.CodeAnalysis; + +namespace ZodSharp.SourceGenerators.Infra; + +public abstract class ZodSharpSourceGeneratorTestBase + : TUnitSourceGeneratorTestBase + where TGenerator : class, IIncrementalGenerator, new() +{ + public static readonly string[] GeneratedAttributes = ["EmbeddedAttribute.cs", "ZodSchemaAttribute.g.cs"]; + + public static readonly int ExpectedFileCount = GeneratedAttributes.Length; + + public static readonly int ExpectedFileCountPlusGen = ExpectedFileCount + 1; + + public const int HintNameHashHexLength = 16; + + public const string GeneratedSourceFileSuffix = ".g.cs"; +} diff --git a/src/tests/SourceGenerators.UnitTests/Infra/ZodSourceGeneratorTestOptions.cs b/src/tests/SourceGenerators.UnitTests/Infra/ZodSourceGeneratorTestOptions.cs new file mode 100644 index 0000000..1ad1922 --- /dev/null +++ b/src/tests/SourceGenerators.UnitTests/Infra/ZodSourceGeneratorTestOptions.cs @@ -0,0 +1,25 @@ +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using ZodSharp.SourceGenerators.Helpers; + +namespace ZodSharp.SourceGenerators.Infra; + +public sealed record ZodSourceGeneratorTestOptions : SourceGeneratorTestOptions +{ + public ZodSourceGeneratorTestOptions() + { + AdditionalAssemblyTypes = + [ + typeof(Z), + typeof(ImmutableArray), + typeof(RequiredAttribute), + typeof(System.Text.Json.JsonSerializer), + typeof(System.Text.RegularExpressions.Regex), + ]; + AdditionalNamespaces = [TypeLibrary.ZodSharpNamespace]; + ExcludeGeneratedSourceHintNames = ["EmbeddedAttribute", "ZodSchemaAttribute"]; + DisableSourceGeneratorPropertyName = PropertyLibrary.DisableZodSharpSourceGeneratorProperty; + } + + public static readonly ZodSourceGeneratorTestOptions NoValidation = new() { ThrowOnGenerationException = false }; +} diff --git a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.AttributeGen.cs b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.AttributeGen.cs index 7045ce6..3af46f4 100644 --- a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.AttributeGen.cs +++ b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.AttributeGen.cs @@ -25,7 +25,7 @@ public class Empty { } var driverResult = await GenerateAsync(source, cancellationToken); // Assert - await Assert.That(driverResult.AllSyntaxTrees).Count().IsEqualTo(ExpectedFileCount); + await Assert.That(driverResult.AllSyntaxTrees.Length).IsEqualTo(ExpectedFileCount); } [Test] diff --git a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.BasicGen.cs b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.BasicGen.cs index 3ef6ebf..4d908ba 100644 --- a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.BasicGen.cs +++ b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.BasicGen.cs @@ -47,9 +47,9 @@ namespace Testing // Act var driverResult = await GenerateAsync(source, cancellationToken); - var generatedSource = GetSchemaGeneratedSource(driverResult); + var generatedSource = driverResult.GetSource(); // Assert — generated file starts with auto-generated header - await Assert.That(generatedSource).Contains(expectation); + await Assert.That(generatedSource).ContainsGeneratedCode(expectation); } } diff --git a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.CompositionMethods.cs b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.CompositionMethods.cs index 3fc1cec..476858d 100644 --- a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.CompositionMethods.cs +++ b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.CompositionMethods.cs @@ -20,13 +20,11 @@ public class ComposeModel // Act var driverResult = await GenerateAsync(source, cancellationToken); - var generatedSource = GetSchemaGeneratedSource(driverResult, "ComposeModelSchema"); + var generatedSource = driverResult.GetSource("ComposeModelSchema"); - await Assert.That(generatedSource).Contains("ApplyAnd"); - await Assert.That(generatedSource).Contains("ApplyOr"); - await Assert.That(generatedSource).Contains("ApplyRefine"); - // Compilation succeeding proves the previously-buggy Or emission (a stray - // dollar sign before the return type) is fixed. + await Assert.That(generatedSource).ContainsGeneratedCode("ApplyAnd"); + await Assert.That(generatedSource).ContainsGeneratedCode("ApplyOr"); + await Assert.That(generatedSource).ContainsGeneratedCode("ApplyRefine"); } [Test] diff --git a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.CustomValidation.cs b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.CustomValidation.cs index c1bf7e3..3fac531 100644 --- a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.CustomValidation.cs +++ b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.CustomValidation.cs @@ -1,4 +1,5 @@ using ZodSharp.SourceGenerators.Helpers; +using ZodSharp.SourceGenerators.Infra; namespace ZodSharp.SourceGenerators; @@ -18,13 +19,16 @@ public class NoCustom { public string? Name { get; set; } } }"; var driverResult = await GenerateAsync(source, cancellationToken); - var generated = GetSchemaGeneratedSource(driverResult, "NoCustomSchema"); + var generated = driverResult.GetSource("NoCustomSchema"); // No async state machine - await Assert.That(generated).Contains("ValueTask.FromResult").Because("Should use FromResult, not async"); await Assert - .That(generated.Contains("async ", StringComparison.Ordinal)) - .IsFalse() + .That(generated) + .ContainsGeneratedCode("ValueTask.FromResult") + .Because("Should use FromResult, not async"); + await Assert + .That(generated) + .DoesNotContain("async ", StringComparison.Ordinal) .Because("Should not generate async when no custom method"); } @@ -51,15 +55,15 @@ internal static ValueTask> CustomValidationAsync( }"; var driverResult = await GenerateAsync(source, cancellationToken); - var generated = GetSchemaGeneratedSource(driverResult, "WithDefaultSchema"); + var generated = driverResult.GetSource("WithDefaultSchema"); await Assert - .That(generated.Contains("async ", StringComparison.Ordinal)) - .IsTrue() + .That(generated) + .Contains("async ", StringComparison.Ordinal) .Because("Should generate async when custom method exists"); - await Assert.That(generated).Contains("await global::Testing.WithDefault.CustomValidationAsync"); - await Assert.That(generated).Contains(".ConfigureAwait(false)"); - await Assert.That(generated).Contains(".Merge(syncResult, customResult)"); + await Assert.That(generated).ContainsGeneratedCode("await global::Testing.WithDefault.CustomValidationAsync"); + await Assert.That(generated).ContainsGeneratedCode(".ConfigureAwait(false)"); + await Assert.That(generated).ContainsGeneratedCode(".Merge(syncResult, customResult)"); } [Test] @@ -85,9 +89,9 @@ internal static ValueTask> ValidateRulesAsync( }"; var driverResult = await GenerateAsync(source, cancellationToken); - var generated = GetSchemaGeneratedSource(driverResult, "WithOverrideSchema"); + var generated = driverResult.GetSource("WithOverrideSchema"); - await Assert.That(generated).Contains("await global::Testing.WithOverride.ValidateRulesAsync"); + await Assert.That(generated).ContainsGeneratedCode("await global::Testing.WithOverride.ValidateRulesAsync"); } [Test] @@ -101,7 +105,7 @@ namespace Testing public class MissingMethod { public string? Name { get; set; } } }"; - var driverResult = await GenerateAsync(source, cancellationToken); + var driverResult = await GenerateAsync(source, ZodSourceGeneratorTestOptions.NoValidation, cancellationToken); await Assert.That(driverResult).HasDiagnostic(DiagnosticLibrary.CustomValidationMethodNotFound); } @@ -128,7 +132,7 @@ internal static Task> CustomValidationAsync( } }"; - var driverResult = await GenerateAsync(source, cancellationToken); + var driverResult = await GenerateAsync(source, ZodSourceGeneratorTestOptions.NoValidation, cancellationToken); await Assert.That(driverResult).HasDiagnostic(DiagnosticLibrary.CustomValidationInvalidReturnType); } @@ -154,7 +158,7 @@ internal static ValueTask> CustomValidationAsync( } }"; - var driverResult = await GenerateAsync(source, cancellationToken); + var driverResult = await GenerateAsync(source, ZodSourceGeneratorTestOptions.NoValidation, cancellationToken); await Assert.That(driverResult).HasDiagnostic(DiagnosticLibrary.CustomValidationInvalidParameterCount); } @@ -182,7 +186,7 @@ internal static ValueTask> CustomValidationAsync( } }"; - var driverResult = await GenerateAsync(source, cancellationToken); + var driverResult = await GenerateAsync(source, ZodSourceGeneratorTestOptions.NoValidation, cancellationToken); await Assert.That(driverResult).HasDiagnostic(DiagnosticLibrary.CustomValidationInvalidModelParameter); } @@ -210,7 +214,7 @@ internal static ValueTask> CustomValidationAsync( } }"; - var driverResult = await GenerateAsync(source, cancellationToken); + var driverResult = await GenerateAsync(source, ZodSourceGeneratorTestOptions.NoValidation, cancellationToken); await Assert.That(driverResult).HasDiagnostic(DiagnosticLibrary.CustomValidationInvalidCancellationToken); } @@ -236,7 +240,7 @@ internal static ValueTask> CustomValidationAsync( } }"; - var driverResult = await GenerateAsync(source, cancellationToken); + var driverResult = await GenerateAsync(source, ZodSourceGeneratorTestOptions.NoValidation, cancellationToken); await Assert.That(driverResult).HasDiagnostic(DiagnosticLibrary.CustomValidationGenericMethod); } @@ -262,7 +266,7 @@ internal ValueTask> CustomValidationAsync( } }"; - var driverResult = await GenerateAsync(source, cancellationToken); + var driverResult = await GenerateAsync(source, ZodSourceGeneratorTestOptions.NoValidation, cancellationToken); await Assert.That(driverResult).HasDiagnostic(DiagnosticLibrary.CustomValidationInvalidStaticInstance); } @@ -288,7 +292,7 @@ private static ValueTask> CustomValidationAsync( } }"; - var driverResult = await GenerateAsync(source, cancellationToken); + var driverResult = await GenerateAsync(source, ZodSourceGeneratorTestOptions.NoValidation, cancellationToken); await Assert.That(driverResult).HasDiagnostic(DiagnosticLibrary.CustomValidationInaccessible); } @@ -338,7 +342,7 @@ namespace Testing public class BadName { public string? Name { get; set; } } }"; - var driverResult = await GenerateAsync(source, cancellationToken); + var driverResult = await GenerateAsync(source, ZodSourceGeneratorTestOptions.NoValidation, cancellationToken); await Assert.That(driverResult).HasDiagnostic(DiagnosticLibrary.CustomValidationInvalidMethodName); } @@ -364,7 +368,7 @@ internal static ValueTask> CustomValidationAsync( } }"; - var driverResult = await GenerateAsync(source, cancellationToken); + var driverResult = await GenerateAsync(source, ZodSourceGeneratorTestOptions.NoValidation, cancellationToken); await Assert.That(driverResult).HasDiagnostic(DiagnosticLibrary.CustomValidationInvalidParameterModifier); } @@ -398,7 +402,7 @@ internal static ValueTask> CustomValidationA // The first has wrong return type, the second has 3 params (wrong count). // Neither is valid — should get diagnostics but no ambiguity. - var driverResult = await GenerateAsync(source, cancellationToken); + var driverResult = await GenerateAsync(source, ZodSourceGeneratorTestOptions.NoValidation, cancellationToken); await Assert.That(driverResult).HasDiagnostic(DiagnosticLibrary.CustomValidationInvalidReturnType); await Assert.That(driverResult).HasDiagnostic(DiagnosticLibrary.CustomValidationInvalidParameterCount); diff --git a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.DataAnnotations.cs b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.DataAnnotations.cs index 20f1d08..efced9a 100644 --- a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.DataAnnotations.cs +++ b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.DataAnnotations.cs @@ -1,5 +1,6 @@ using System.Reflection; using ZodSharp.SourceGenerators.Helpers; +using ZodSharp.SourceGenerators.Infra; namespace ZodSharp.SourceGenerators; @@ -76,17 +77,20 @@ CancellationToken cancellationToken ) { var driverResult = await GenerateAsync(UserSource, cancellationToken); + var generatedSource = driverResult.GetSource("UserSchema"); - await Assert.That(generatedSource).Contains("value.Name == null"); - await Assert.That(generatedSource).Contains("var nameValue = value.Name;"); - await Assert.That(generatedSource).Contains("var nameLength = nameValue.Length;"); - await Assert.That(generatedSource).Contains("nameLength < 3"); - await Assert.That(generatedSource).Contains("nameLength > 50"); - await Assert.That(generatedSource).Contains("static readonly int RangeMinimum_Age = 0;"); - await Assert.That(generatedSource).Contains("static readonly int RangeMaximum_Age = 120;"); - await Assert.That(generatedSource).Contains("ageValue < RangeMinimum_Age || ageValue > RangeMaximum_Age"); - await Assert.That(generatedSource).Contains("EmailRegex.IsMatch(value.Email)"); + await Assert.That(generatedSource).ContainsGeneratedCode("value.Name == null"); + await Assert.That(generatedSource).ContainsGeneratedCode("var nameValue = value.Name;"); + await Assert.That(generatedSource).ContainsGeneratedCode("var nameLength = nameValue.Length;"); + await Assert.That(generatedSource).ContainsGeneratedCode("nameLength < 3"); + await Assert.That(generatedSource).ContainsGeneratedCode("nameLength > 50"); + await Assert.That(generatedSource).ContainsGeneratedCode("static readonly int RangeMinimum_Age = 0;"); + await Assert.That(generatedSource).ContainsGeneratedCode("static readonly int RangeMaximum_Age = 120;"); + await Assert + .That(generatedSource) + .ContainsGeneratedCode("ageValue < RangeMinimum_Age || ageValue > RangeMaximum_Age"); + await Assert.That(generatedSource).ContainsGeneratedCode("EmailRegex.IsMatch(value.Email)"); } [Test] @@ -124,12 +128,13 @@ public sealed class LengthExamples "; var driverResult = await GenerateAsync(source, cancellationToken); + var generatedSource = driverResult.GetSource("LengthExamplesSchema"); - await Assert.That(generatedSource).Contains("propertyValue.Length"); - await Assert.That(generatedSource).Contains("propertyValue.Count"); - await Assert.That(generatedSource).Contains("CollectionCountHelper.GetCount(propertyValue)"); - await Assert.That(generatedSource).Contains("else if"); + await Assert.That(generatedSource).ContainsGeneratedCode("propertyValue.Length"); + await Assert.That(generatedSource).ContainsGeneratedCode("propertyValue.Count"); + await Assert.That(generatedSource).ContainsGeneratedCode("CollectionCountHelper.GetCount(propertyValue)"); + await Assert.That(generatedSource).ContainsGeneratedCode("else if"); } [Test] @@ -163,15 +168,17 @@ public sealed class AttributeExamples "; var driverResult = await GenerateAsync(source, cancellationToken); - var generatedSource = GetSchemaGeneratedSource(driverResult, "AttributeExamplesSchema"); + var generatedSource = driverResult.GetSource("AttributeExamplesSchema"); await Assert .That(generatedSource) - .Contains("static readonly global::System.Text.RegularExpressions.Regex Regex_CountryCode"); - await Assert.That(generatedSource).Contains("EqualityComparer.Default.Equals(statusValue, \"open\")"); - await Assert.That(generatedSource).Contains("EqualityComparer.Default.Equals(codeValue, 13)"); - await Assert.That(generatedSource).Contains("static readonly decimal RangeMinimum_Price"); - await Assert.That(generatedSource).Contains("Decimal.Parse(\"1.5\""); + .ContainsGeneratedCode("static readonly global::System.Text.RegularExpressions.Regex Regex_CountryCode"); + await Assert + .That(generatedSource) + .ContainsGeneratedCode("EqualityComparer.Default.Equals(statusValue, \"open\")"); + await Assert.That(generatedSource).ContainsGeneratedCode("EqualityComparer.Default.Equals(codeValue, 13)"); + await Assert.That(generatedSource).ContainsGeneratedCode("static readonly decimal RangeMinimum_Price"); + await Assert.That(generatedSource).ContainsGeneratedCode("Decimal.Parse(\"1.5\""); } [Test] @@ -423,7 +430,7 @@ public sealed class InvalidLengthModel } "; - var driverResult = await GenerateAsync(source, cancellationToken); + var driverResult = await GenerateAsync(source, ZodSourceGeneratorTestOptions.NoValidation, cancellationToken); await Assert.That(driverResult).HasDiagnostic(DiagnosticLibrary.InvalidLengthAttribute); } @@ -446,7 +453,7 @@ public sealed class UnsupportedLengthModel } "; - var driverResult = await GenerateAsync(source, cancellationToken); + var driverResult = await GenerateAsync(source, ZodSourceGeneratorTestOptions.NoValidation, cancellationToken); await Assert.That(driverResult).HasDiagnostic(DiagnosticLibrary.UnsupportedLengthAttributeTarget); } @@ -474,11 +481,11 @@ public sealed class UnsupportedAnnotationsModel } "; - var driverResult = await GenerateAsync(source, cancellationToken); + var driverResult = await GenerateAsync(source, ZodSourceGeneratorTestOptions.NoValidation, cancellationToken); - await Assert - .That(driverResult.DriverResult.Diagnostics.Count(static d => d.Id == DiagnosticLibrary.UnsupportedDataAnnotationsUsage.Id)) - .IsGreaterThanOrEqualTo(2); + await Assert.That(driverResult).HasDiagnostics("ZODSGEN006", 2); + //.And + //.IsGreaterThanOrEqualTo(2); } [Test] @@ -499,7 +506,7 @@ public sealed class InvalidResourceModel } "; - var driverResult = await GenerateAsync(source, cancellationToken); + var driverResult = await GenerateAsync(source, ZodSourceGeneratorTestOptions.NoValidation, cancellationToken); await Assert.That(driverResult).HasDiagnostic(DiagnosticLibrary.InvalidDataAnnotationsErrorMessage); } @@ -539,19 +546,23 @@ public sealed class StringFormatModel "; var driverResult = await GenerateAsync(source, cancellationToken); - var generatedSource = GetSchemaGeneratedSource(driverResult, "StringFormatModelSchema"); + var generatedSource = driverResult.GetSource("StringFormatModelSchema"); - await Assert.That(generatedSource).Contains("new global::ZodSharp.Rules.UrlRule().IsValid(websiteValue)"); - await Assert.That(generatedSource).Contains("new global::ZodSharp.Rules.PhoneRule().IsValid(phoneNumberValue)"); await Assert .That(generatedSource) - .Contains("new global::ZodSharp.Rules.CreditCardRule().IsValid(cardNumberValue)"); + .ContainsGeneratedCode("new global::ZodSharp.Rules.UrlRule().IsValid(websiteValue)"); + await Assert + .That(generatedSource) + .ContainsGeneratedCode("new global::ZodSharp.Rules.PhoneRule().IsValid(phoneNumberValue)"); await Assert .That(generatedSource) - .Contains("new global::ZodSharp.Rules.Base64StringRule().IsValid(encodedValue)"); + .ContainsGeneratedCode("new global::ZodSharp.Rules.CreditCardRule().IsValid(cardNumberValue)"); await Assert .That(generatedSource) - .Contains("EqualityComparer.Default.Equals(value.ConfirmPassword, value.Password)"); + .ContainsGeneratedCode("new global::ZodSharp.Rules.Base64StringRule().IsValid(encodedValue)"); + await Assert + .That(generatedSource) + .ContainsGeneratedCode("EqualityComparer.Default.Equals(value.ConfirmPassword, value.Password)"); } [Test] @@ -691,9 +702,9 @@ public sealed class UnsupportedStringFormatModel } "; - var driverResult = await GenerateAsync(source, cancellationToken); + var driverResult = await GenerateAsync(source, ZodSourceGeneratorTestOptions.NoValidation, cancellationToken); - await Assert.That(driverResult.DriverResult.Diagnostics.Count(static d => d.Id == DiagnosticLibrary.UnsupportedDataAnnotationsUsage.Id)).IsEqualTo(4); + await Assert.That(driverResult).HasDiagnostics("ZODSGEN006", 4); } [Test] @@ -716,7 +727,7 @@ public sealed class CompareMissingModel } "; - var driverResult = await GenerateAsync(source, cancellationToken); + var driverResult = await GenerateAsync(source, ZodSourceGeneratorTestOptions.NoValidation, cancellationToken); await Assert.That(driverResult).HasDiagnostic(DiagnosticLibrary.ComparePropertyNotFound); } diff --git a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.GeneratedStructure.cs b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.GeneratedStructure.cs index bca70f6..9075225 100644 --- a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.GeneratedStructure.cs +++ b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.GeneratedStructure.cs @@ -20,12 +20,12 @@ public class Customer // Act var driverResult = await GenerateAsync(source, cancellationToken); - var generatedSource = GetSchemaGeneratedSource(driverResult, "CustomerSchema"); + var generatedSource = driverResult.GetSource("CustomerSchema"); // Assert - await Assert.That(generatedSource).Contains("namespace Testing"); - await Assert.That(generatedSource).Contains("#nullable enable"); - await Assert.That(generatedSource).Contains("public static partial class CustomerSchema"); + await Assert.That(generatedSource).ContainsGeneratedCode("namespace Testing"); + await Assert.That(generatedSource).ContainsGeneratedCode("#nullable enable"); + await Assert.That(generatedSource).ContainsGeneratedCode("public static partial class CustomerSchema"); } [Test] @@ -42,10 +42,10 @@ public class Customer { } // Act var driverResult = await GenerateAsync(source, cancellationToken); - var generatedSource = GetSchemaGeneratedSource(driverResult, "CustomerSchema"); + var generatedSource = driverResult.GetSource("CustomerSchema"); // Assert - await Assert.That(generatedSource).IsEmpty(); + await Assert.That(generatedSource).IsNull(); } [Test] @@ -71,8 +71,8 @@ public record Order { } var driverResult = await GenerateAsync(source, cancellationToken); // Assert - await Assert.That(GetSchemaGeneratedSource(driverResult, "CustomerSchema")).IsNotEmpty(); - await Assert.That(GetSchemaGeneratedSource(driverResult, "AddressSchema")).IsNotEmpty(); - await Assert.That(GetSchemaGeneratedSource(driverResult, "OrderSchema")).IsNotEmpty(); + await Assert.That(driverResult.GetSource("CustomerSchema")).IsNotEmpty(); + await Assert.That(driverResult.GetSource("AddressSchema")).IsNotEmpty(); + await Assert.That(driverResult.GetSource("OrderSchema")).IsNotEmpty(); } } diff --git a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.NestedClasses.cs b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.NestedClasses.cs index 07ec546..9f927c5 100644 --- a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.NestedClasses.cs +++ b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.NestedClasses.cs @@ -23,10 +23,10 @@ private class Inner "; var driverResult = await GenerateAsync(source, cancellationToken); - var generatedSource = GetSchemaGeneratedSource(driverResult, "InnerSchema"); + var generatedSource = driverResult.GetSource("InnerSchema"); - await Assert.That(generatedSource).Contains("private static partial class InnerSchema"); - await Assert.That(generatedSource).Contains("public partial class Outer"); + await Assert.That(generatedSource).ContainsGeneratedCode("private static partial class InnerSchema"); + await Assert.That(generatedSource).ContainsGeneratedCode("public partial class Outer"); } [Test] @@ -52,7 +52,7 @@ private class Inner var driverResult = await GenerateAsync(source, cancellationToken); - await Assert.That(GetSchemaGeneratedSource(driverResult, "InnerSchema")).IsNotEmpty(); + await Assert.That(driverResult.GetSource("InnerSchema")).IsNotEmpty(); } [Test] diff --git a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.ValidatorAdapter.cs b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.ValidatorAdapter.cs index bd66723..dcd66c0 100644 --- a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.ValidatorAdapter.cs +++ b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.ValidatorAdapter.cs @@ -18,7 +18,7 @@ public class Widget } }"; var driverResult = await GenerateAsync(source, cancellationToken); - var generatedSource = GetSchemaGeneratedSource(driverResult, "WidgetSchema"); + var generatedSource = driverResult.GetSource("WidgetSchema"); await Assert.That(generatedSource).Contains("class WidgetSchemaValidator"); await Assert.That(generatedSource).Contains("IZodSchemaValidator"); @@ -40,8 +40,8 @@ public class Gadget { } var driverResult = await GenerateAsync(source, cancellationToken); var allGenerated = string.Join("\n", driverResult.AllSyntaxTrees.Select(static t => t.GetText().ToString())); - await Assert.That(allGenerated).Contains("ZodSchemaGenerated"); - await Assert.That(allGenerated).Contains("Gadget"); + await Assert.That(allGenerated).ContainsGeneratedCode("ZodSchemaGenerated"); + await Assert.That(allGenerated).ContainsGeneratedCode("Gadget"); } [Test] @@ -60,11 +60,11 @@ public class Gizmo } }"; var driverResult = await GenerateAsync(source, cancellationToken); - var generatedSource = GetSchemaGeneratedSource(driverResult, "GizmoSchema"); + var generatedSource = driverResult.GetSource("GizmoSchema"); - await Assert.That(generatedSource).Contains("ValidateAsync"); - await Assert.That(generatedSource).Contains("ValueTask"); - await Assert.That(generatedSource).Contains("GizmoSchemaValidator"); + await Assert.That(generatedSource).ContainsGeneratedCode("ValidateAsync"); + await Assert.That(generatedSource).ContainsGeneratedCode("ValueTask"); + await Assert.That(generatedSource).ContainsGeneratedCode("GizmoSchemaValidator"); } [Test] diff --git a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.cs b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.cs index 8663b6c..9b2b86b 100644 --- a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.cs +++ b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.cs @@ -1,37 +1,6 @@ -using Microsoft.CodeAnalysis; using ZodSharp.SourceGenerators.Infra; namespace ZodSharp.SourceGenerators; [Retry(3)] -public partial class ZodSchemaGeneratorTests : IncrementalSourceGeneratorTestBase -{ - static string GetSchemaGeneratedSource(DriverRunResult driverRunResults) => - GetSchemaGeneratedSource(driverRunResults.DriverResult); - - static string GetSchemaGeneratedSource(GeneratorDriverRunResult result) - { - var syntaxTree = result.GeneratedTrees.FirstOrDefault(static tree => - { - var source = tree.GetText().ToString(); - return source.Contains(" static partial class ", StringComparison.Ordinal) - && source.Contains("Schema", StringComparison.Ordinal); - }); - - return syntaxTree?.GetText().ToString() ?? string.Empty; - } - - static string GetSchemaGeneratedSource(DriverRunResult driverRunResults, string schemaName) => - GetSchemaGeneratedSource(driverRunResults.DriverResult, schemaName); - - static string GetSchemaGeneratedSource(GeneratorDriverRunResult result, string schemaName) - { - var syntaxTree = result.GeneratedTrees.FirstOrDefault(tree => - { - var source = tree.GetText().ToString(); - return source.Contains($" static partial class {schemaName}", StringComparison.Ordinal); - }); - - return syntaxTree?.GetText().ToString() ?? string.Empty; - } -} +public partial class ZodSchemaGeneratorTests : ZodSharpSourceGeneratorTestBase { }