Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>]`, 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<T>]`, close open generic targets the syntax walk cannot see.

## Generator

Expand Down
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` are skipped, because that declaration has no closed type to emit. Naming `Box<int>` in ordinary source, including `new Box<int>()`, 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<int>()` on `[Validated] static Box<T> Create<T>()` registers `Box<int>`. A wrapper that only writes `Create<T>()` 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<int>()` on `[Validated] static Box<T> Create<T>()` registers `Box<int>`. 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<T>()` 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<T>
Expand Down
3 changes: 2 additions & 1 deletion src/NativeValidation.Generator/NativeValidationCatalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1200,7 +1200,8 @@ static ImmutableDictionary<ITypeParameterSymbol, ITypeSymbol> 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 =>
Expand Down
5 changes: 5 additions & 0 deletions src/NativeValidation/ValidateAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ namespace Devlooped.DataAnnotations
/// <c>Make</c> tells the generator to substitute <c>Make&lt;int&gt;()</c> through factories it calls,
/// including other <see cref="ValidatedAttribute"/> methods, up to eight levels.
/// </para>
/// <para>
/// A generic method that carries any attribute derived from
/// <see cref="System.ComponentModel.DataAnnotations.ValidationAttribute"/> is treated as
/// <see cref="ValidatedAttribute"/> as well.
/// </para>
/// </remarks>
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public sealed class ValidatedAttribute : Attribute
Expand Down
59 changes: 59 additions & 0 deletions src/Tests/NativeValidationClosureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,65 @@ class Program
Assert.DoesNotContain("ILabeled<int>", source);
}

[Fact]
public void ValidationAttributeOnAGenericMethodClosesTheReturnType()
{
var source = Generate(@"
public class ChecksAttribute : System.ComponentModel.DataAnnotations.ValidationAttribute { }
public interface ILabeled<T>
{
[System.ComponentModel.DataAnnotations.Required]
T Label { get; set; }
}
public static class Factory
{
[Checks]
public static ILabeled<T> Labeled<T>() => default;

[Checks]
public static object Make<T>() => Labeled<T>();
}
class Program
{
void Use() => Factory.Make<int>();
}
");

Assert.Contains("ILabeled<int>", source);
Assert.DoesNotContain("ILabeled<string>", source);
}

[Fact]
public void ValidationAttributeOnANonGenericMethodDoesNotCloseTypes()
{
var source = Generate(@"
public class ChecksAttribute : System.ComponentModel.DataAnnotations.ValidationAttribute { }
public interface ILabeled<T>
{
[System.ComponentModel.DataAnnotations.Required]
T Label { get; set; }
}
public static class Factory
{
[Checks]
public static ILabeled<int> Fixed() => default;

[System.Obsolete]
public static ILabeled<T> Labeled<T>() => default;
}
class Program
{
void Use()
{
Factory.Fixed();
Factory.Labeled<string>();
}
}
");

Assert.DoesNotContain("ILabeled<", source);
}

static string Generate(string source)
{
var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!)
Expand Down
Loading