From 8948872f73b734f921781c31d1cf0b5f6bc3efd0 Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Fri, 9 Oct 2026 15:42:31 -0300 Subject: [PATCH] Treat ValidationAttribute on generic methods as [Validated] so closed types are registered. --- AGENTS.md | 2 +- readme.md | 2 +- .../NativeValidationCatalog.cs | 3 +- src/NativeValidation/ValidateAttribute.cs | 5 ++ src/Tests/NativeValidationClosureTests.cs | 59 +++++++++++++++++++ 5 files changed, 68 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0da11a6..1b0b5ba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ Namespace `Devlooped.DataAnnotations`. - The per-check methods (`Required`, `StringLength`, and the rest) are the `IsValid` layer. A bad value returns `ValidationResult`, including a value of the wrong type. Malformed bounds still throw `InvalidOperationException`. - `TryValidate` / `Validate(instance, method, arguments)` run the rules registered for a method or constructor. `Validator` has no equivalent. - `NativeValidation.Register`, `RegisterProperties`, and `RegisterType` are the generator entries. Lookups key a member by declaring type, metadata name (`set_Email`, `Save`, `.ctor`), and `NativeValidationSignature`. Property getters are emitted so object validation does not reflect. -- `ValidatedAttribute` on a generic factory, and `[assembly: Validate]`, close open generic targets the syntax walk cannot see. +- `ValidatedAttribute` on a generic factory, a generic method that carries a `ValidationAttribute` subclass, and `[assembly: Validate]`, close open generic targets the syntax walk cannot see. ## Generator diff --git a/readme.md b/readme.md index 479729d..7ff16bd 100644 --- a/readme.md +++ b/readme.md @@ -102,7 +102,7 @@ Static members are skipped. Instance properties are registered through the sette The options generator reports `SYSLIB1201` for open generic member types. ASP.NET's generator includes a type it cannot see from a signature only when that type carries `[ValidatableType]`. This generator registers a type when an annotated member's declaring type does not depend on a type parameter. Members declared on open `Box` are skipped, because that declaration has no closed type to emit. Naming `Box` in ordinary source, including `new Box()`, does not register it either. A closed type that appears only at run time (`MakeGenericType`, or a factory the walk cannot see) is the same gap. -[`Validated`](https://github.com/devlooped/NativeValidation/blob/main/src/NativeValidation/ValidateAttribute.cs) on a generic factory registers every closed type argument and the closed return type at each call site in the compilation. `Create()` on `[Validated] static Box Create()` registers `Box`. A wrapper that only writes `Create()` does not close `T`. Mark the wrapper `[Validated]` as well. The generator substitutes call-site arguments through `[Validated]` methods, up to eight levels, and registers the constructed return type plus its base types and interfaces. +[`Validated`](https://github.com/devlooped/NativeValidation/blob/main/src/NativeValidation/ValidateAttribute.cs) on a generic factory registers every closed type argument and the closed return type at each call site in the compilation. `Create()` on `[Validated] static Box Create()` registers `Box`. A generic method annotated with any attribute derived from `ValidationAttribute` is recognized the same way, as if it also carried `[Validated]`. A wrapper that only writes `Create()` does not close `T`. Mark the wrapper `[Validated]` as well, or annotate that generic method with a `ValidationAttribute` subclass. The generator substitutes call-site arguments through these methods, up to eight levels, and registers the constructed return type plus its base types and interfaces. ```csharp public class Box diff --git a/src/NativeValidation.Generator/NativeValidationCatalog.cs b/src/NativeValidation.Generator/NativeValidationCatalog.cs index 0e33309..9ebf6bb 100644 --- a/src/NativeValidation.Generator/NativeValidationCatalog.cs +++ b/src/NativeValidation.Generator/NativeValidationCatalog.cs @@ -1200,7 +1200,8 @@ static ImmutableDictionary Map(IMethodSymbol } static bool IsValidated(IMethodSymbol method) => - HasAttribute(method, "ValidatedAttribute"); + HasAttribute(method, "ValidatedAttribute") || + (method.IsGenericMethod && method.OriginalDefinition.GetAttributes().Any(attribute => IsValidation(attribute.AttributeClass))); static bool HasAttribute(IMethodSymbol method, string name) => method.OriginalDefinition.GetAttributes().Any(attribute => diff --git a/src/NativeValidation/ValidateAttribute.cs b/src/NativeValidation/ValidateAttribute.cs index 58bca0f..9cf357a 100644 --- a/src/NativeValidation/ValidateAttribute.cs +++ b/src/NativeValidation/ValidateAttribute.cs @@ -15,6 +15,11 @@ namespace Devlooped.DataAnnotations /// Make tells the generator to substitute Make<int>() through factories it calls, /// including other methods, up to eight levels. /// + /// + /// A generic method that carries any attribute derived from + /// is treated as + /// as well. + /// /// [AttributeUsage(AttributeTargets.Method, Inherited = false)] public sealed class ValidatedAttribute : Attribute diff --git a/src/Tests/NativeValidationClosureTests.cs b/src/Tests/NativeValidationClosureTests.cs index ec6e685..feed790 100644 --- a/src/Tests/NativeValidationClosureTests.cs +++ b/src/Tests/NativeValidationClosureTests.cs @@ -73,6 +73,65 @@ class Program Assert.DoesNotContain("ILabeled", source); } + [Fact] + public void ValidationAttributeOnAGenericMethodClosesTheReturnType() + { + var source = Generate(@" +public class ChecksAttribute : System.ComponentModel.DataAnnotations.ValidationAttribute { } +public interface ILabeled +{ + [System.ComponentModel.DataAnnotations.Required] + T Label { get; set; } +} +public static class Factory +{ + [Checks] + public static ILabeled Labeled() => default; + + [Checks] + public static object Make() => Labeled(); +} +class Program +{ + void Use() => Factory.Make(); +} +"); + + Assert.Contains("ILabeled", source); + Assert.DoesNotContain("ILabeled", source); + } + + [Fact] + public void ValidationAttributeOnANonGenericMethodDoesNotCloseTypes() + { + var source = Generate(@" +public class ChecksAttribute : System.ComponentModel.DataAnnotations.ValidationAttribute { } +public interface ILabeled +{ + [System.ComponentModel.DataAnnotations.Required] + T Label { get; set; } +} +public static class Factory +{ + [Checks] + public static ILabeled Fixed() => default; + + [System.Obsolete] + public static ILabeled Labeled() => default; +} +class Program +{ + void Use() + { + Factory.Fixed(); + Factory.Labeled(); + } +} +"); + + Assert.DoesNotContain("ILabeled<", source); + } + static string Generate(string source) { var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!)