Skip to content

Commit 68644ff

Browse files
committed
Initial implementation
1 parent e507a91 commit 68644ff

File tree

32 files changed

+564
-7
lines changed

32 files changed

+564
-7
lines changed

src/Validation/gen/Parsers/ValidationsGenerator.AttributeParser.cs

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
45
using System.Collections.Generic;
56
using System.Collections.Immutable;
7+
using System.Linq;
68
using System.Threading;
79
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
810
using Microsoft.CodeAnalysis;
@@ -12,12 +14,12 @@ namespace Microsoft.Extensions.Validation;
1214

1315
public sealed partial class ValidationsGenerator : IIncrementalGenerator
1416
{
15-
internal static bool ShouldTransformSymbolWithAttribute(SyntaxNode syntaxNode, CancellationToken cancellationToken)
17+
internal static bool ShouldTransformSymbolWithAttribute(SyntaxNode syntaxNode, CancellationToken _)
1618
{
1719
return syntaxNode is ClassDeclarationSyntax or RecordDeclarationSyntax;
1820
}
1921

20-
internal ImmutableArray<ValidatableType> TransformValidatableTypeWithAttribute(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
22+
internal ImmutableArray<ValidatableType> TransformValidatableTypeWithAttribute(GeneratorAttributeSyntaxContext context, CancellationToken _)
2123
{
2224
var validatableTypes = new HashSet<ValidatableType>(ValidatableTypeComparer.Instance);
2325
List<ITypeSymbol> visitedTypes = [];
@@ -28,4 +30,88 @@ internal ImmutableArray<ValidatableType> TransformValidatableTypeWithAttribute(G
2830
}
2931
return [];
3032
}
33+
34+
internal static bool ShouldTransformSymbolWithValidatableTypeAttribute(SyntaxNode syntaxNode, CancellationToken _)
35+
{
36+
// Only process class and record declarations
37+
if (syntaxNode is not (ClassDeclarationSyntax or RecordDeclarationSyntax))
38+
{
39+
return false;
40+
}
41+
42+
// Check if the type has any attribute that could be ValidatableTypeAttribute
43+
var typeDeclaration = (TypeDeclarationSyntax)syntaxNode;
44+
return typeDeclaration.AttributeLists
45+
.SelectMany(al => al.Attributes)
46+
.Any(IsValidatableTypeAttribute);
47+
}
48+
49+
internal ImmutableArray<ValidatableType> TransformValidatableTypeWithValidatableTypeAttribute(GeneratorSyntaxContext context, CancellationToken cancellationToken)
50+
{
51+
if (context.Node is not (ClassDeclarationSyntax or RecordDeclarationSyntax))
52+
{
53+
return [];
54+
}
55+
56+
var typeSymbol = context.SemanticModel.GetDeclaredSymbol(context.Node, cancellationToken) as ITypeSymbol;
57+
if (typeSymbol == null)
58+
{
59+
return [];
60+
}
61+
62+
// Check if the type has a ValidatableTypeAttribute (framework or auto-generated)
63+
if (!HasValidatableTypeAttribute(typeSymbol))
64+
{
65+
return [];
66+
}
67+
68+
var validatableTypes = new HashSet<ValidatableType>(ValidatableTypeComparer.Instance);
69+
List<ITypeSymbol> visitedTypes = [];
70+
var wellKnownTypes = WellKnownTypes.GetOrCreate(context.SemanticModel.Compilation);
71+
72+
if (TryExtractValidatableType(typeSymbol, wellKnownTypes, ref validatableTypes, ref visitedTypes))
73+
{
74+
return [..validatableTypes];
75+
}
76+
return [];
77+
}
78+
79+
private static bool IsValidatableTypeAttribute(AttributeSyntax attribute)
80+
{
81+
var name = attribute.Name.ToString();
82+
return name is "ValidatableType" or "ValidatableTypeAttribute" ||
83+
name.EndsWith(".ValidatableType", StringComparison.Ordinal) ||
84+
name.EndsWith(".ValidatableTypeAttribute", StringComparison.Ordinal);
85+
}
86+
87+
private static bool HasValidatableTypeAttribute(ITypeSymbol typeSymbol)
88+
{
89+
return typeSymbol.GetAttributes().Any(attr =>
90+
{
91+
var attributeClass = attr.AttributeClass;
92+
if (attributeClass == null)
93+
{
94+
return false;
95+
}
96+
97+
var name = attributeClass.Name;
98+
var fullName = attributeClass.ToDisplayString();
99+
100+
// Check for framework attribute
101+
if (fullName == "Microsoft.Extensions.Validation.ValidatableTypeAttribute")
102+
{
103+
return true;
104+
}
105+
106+
// Check for auto-generated attribute (any namespace)
107+
if (name == "ValidatableTypeAttribute")
108+
{
109+
// Additional check: ensure it's marked with [Embedded] to confirm it's auto-generated
110+
return attributeClass.GetAttributes().Any(embeddedAttr =>
111+
embeddedAttr.AttributeClass?.ToDisplayString() == "Microsoft.CodeAnalysis.EmbeddedAttribute");
112+
}
113+
114+
return false;
115+
});
116+
}
31117
}

src/Validation/gen/ValidationsGenerator.cs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,20 @@ public sealed partial class ValidationsGenerator : IIncrementalGenerator
1111
{
1212
public void Initialize(IncrementalGeneratorInitializationContext context)
1313
{
14+
// Emit the ValidatableTypeAttribute with embedded support
15+
context.RegisterPostInitializationOutput(EmitValidatableTypeAttribute);
16+
1417
// Find the builder.Services.AddValidation() call in the application.
1518
var addValidation = context.SyntaxProvider.CreateSyntaxProvider(
1619
predicate: FindAddValidation,
1720
transform: TransformAddValidation
1821
);
1922
// Extract types that have been marked with [ValidatableType].
20-
var validatableTypesWithAttribute = context.SyntaxProvider.ForAttributeWithMetadataName(
21-
"Microsoft.Extensions.Validation.ValidatableTypeAttribute",
22-
predicate: ShouldTransformSymbolWithAttribute,
23-
transform: TransformValidatableTypeWithAttribute
24-
);
23+
// This handles both the framework attribute and the auto-generated attribute.
24+
var validatableTypesWithAttribute = context.SyntaxProvider.CreateSyntaxProvider(
25+
predicate: ShouldTransformSymbolWithValidatableTypeAttribute,
26+
transform: TransformValidatableTypeWithValidatableTypeAttribute
27+
).Where(types => types.Length > 0);
2528
// Extract all minimal API endpoints in the application.
2629
var endpoints = context.SyntaxProvider
2730
.CreateSyntaxProvider(
@@ -44,4 +47,34 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
4447
// ValidatableTypeInfo for all validatable types.
4548
context.RegisterSourceOutput(emitInputs, Emit);
4649
}
50+
51+
private static void EmitValidatableTypeAttribute(IncrementalGeneratorPostInitializationContext context)
52+
{
53+
// First, emit the EmbeddedAttribute if it doesn't exist
54+
var embeddedAttributeSource = """
55+
// <auto-generated/>
56+
namespace Microsoft.CodeAnalysis
57+
{
58+
[global::System.AttributeUsage(global::System.AttributeTargets.All, AllowMultiple = true, Inherited = false)]
59+
internal sealed class EmbeddedAttribute : global::System.Attribute
60+
{
61+
}
62+
}
63+
""";
64+
65+
context.AddSource("EmbeddedAttribute.g.cs", embeddedAttributeSource);
66+
67+
// Then emit the ValidatableTypeAttribute in the global namespace
68+
// We use global namespace since we don't have access to the project's root namespace here
69+
var validatableTypeAttributeSource = """
70+
// <auto-generated/>
71+
[global::Microsoft.CodeAnalysis.EmbeddedAttribute]
72+
[global::System.AttributeUsage(global::System.AttributeTargets.Class)]
73+
internal sealed class ValidatableTypeAttribute : global::System.Attribute
74+
{
75+
}
76+
""";
77+
78+
context.AddSource("ValidatableTypeAttribute.g.cs", validatableTypeAttributeSource);
79+
}
4780
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Extensions.Validation.GeneratorTests;
5+
using VerifyXunit;
6+
using Xunit;
7+
8+
namespace Microsoft.Extensions.Validation.GeneratorTests;
9+
10+
[UsesVerify]
11+
public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase
12+
{
13+
[Fact]
14+
public async Task CanUseAutoGeneratedValidatableTypeAttribute()
15+
{
16+
var source = """
17+
using Microsoft.AspNetCore.Builder;
18+
using Microsoft.Extensions.DependencyInjection;
19+
using System.ComponentModel.DataAnnotations;
20+
21+
var builder = WebApplication.CreateBuilder();
22+
builder.Services.AddValidation();
23+
var app = builder.Build();
24+
25+
// Using the auto-generated ValidatableTypeAttribute
26+
// No manual attribute definition needed!
27+
[ValidatableType]
28+
public class Customer
29+
{
30+
[Required]
31+
public string Name { get; set; } = "";
32+
33+
[EmailAddress]
34+
public string Email { get; set; } = "";
35+
}
36+
37+
app.MapPost("/customers", (Customer customer) => "OK");
38+
39+
app.Run();
40+
""";
41+
42+
await Verify(source, out var compilation);
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//HintName: EmbeddedAttribute.g.cs
2+
// <auto-generated/>
3+
namespace Microsoft.CodeAnalysis
4+
{
5+
[global::System.AttributeUsage(global::System.AttributeTargets.All, AllowMultiple = true, Inherited = false)]
6+
internal sealed class EmbeddedAttribute : global::System.Attribute
7+
{
8+
}
9+
}

0 commit comments

Comments
 (0)