Skip to content

Commit 238c76c

Browse files
authored
Use symbol metadata for attribute detection (#26)
1 parent c666e4e commit 238c76c

File tree

1 file changed

+113
-78
lines changed

1 file changed

+113
-78
lines changed

src/GeneratedEndpoints/MinimalApiGenerator.cs

Lines changed: 113 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator
1414
{
1515
private const string BaseNamespace = "Microsoft.AspNetCore.Generated";
1616
private const string AttributesNamespace = $"{BaseNamespace}.Attributes";
17+
private static readonly string[] AttributesNamespaceParts = AttributesNamespace.Split('.');
18+
private static readonly string[] AspNetCoreHttpNamespaceParts = new[] { "Microsoft", "AspNetCore", "Http" };
19+
private static readonly string[] AspNetCoreAuthorizationNamespaceParts = new[] { "Microsoft", "AspNetCore", "Authorization" };
20+
private static readonly string[] AspNetCoreRoutingNamespaceParts = new[] { "Microsoft", "AspNetCore", "Routing" };
1721

1822
private static readonly ImmutableArray<HttpAttributeDefinition> HttpAttributeDefinitions =
1923
[
@@ -47,7 +51,7 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator
4751
private const string DisableAntiforgeryAttributeFullyQualifiedName = $"{AttributesNamespace}.{DisableAntiforgeryAttributeName}";
4852
private const string DisableAntiforgeryAttributeHint = $"{DisableAntiforgeryAttributeFullyQualifiedName}.gs.cs";
4953

50-
private const string AllowAnonymousAttributeFullyQualifiedName = "Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute";
54+
private const string AllowAnonymousAttributeName = "AllowAnonymousAttribute";
5155

5256
private const string AcceptsAttributeName = "AcceptsAttribute";
5357
private const string AcceptsAttributeFullyQualifiedName = $"{AttributesNamespace}.{AcceptsAttributeName}";
@@ -716,96 +720,108 @@ ref bool hasRequireAuthorizationAttribute
716720
if (attributeClass is null)
717721
continue;
718722

719-
var fullyQualifiedName = attributeClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
720-
721-
if (IsGeneratedAttribute(fullyQualifiedName, AcceptsAttributeName))
723+
if (IsGeneratedAttribute(attributeClass, AcceptsAttributeName))
722724
{
723725
TryAddAcceptsMetadata(attribute, attributeClass, ref accepts);
724726
continue;
725727
}
726728

727-
if (IsGeneratedAttribute(fullyQualifiedName, ProducesResponseAttributeName))
729+
if (IsGeneratedAttribute(attributeClass, ProducesResponseAttributeName))
728730
{
729731
TryAddProducesMetadata(attribute, attributeClass, ref produces);
730732
continue;
731733
}
732734

733-
switch (fullyQualifiedName)
735+
if (IsAttribute(attributeClass, "TagsAttribute", AspNetCoreHttpNamespaceParts))
734736
{
735-
case "global::Microsoft.AspNetCore.Http.TagsAttribute":
736-
if (attribute.ConstructorArguments.Length > 0)
737-
{
738-
var arg = attribute.ConstructorArguments[0];
739-
if (arg.Values.Length > 0)
740-
{
741-
var values = arg.Values
742-
.Select(v => v.Value as string)
743-
.Where(s => !string.IsNullOrWhiteSpace(s))
744-
.Select(s => s!.Trim());
745-
746-
MergeInto(ref tags, values);
747-
}
748-
}
749-
break;
750-
case $"global::{RequireAuthorizationAttributeFullyQualifiedName}":
751-
requireAuthorization = true;
752-
hasRequireAuthorizationAttribute = true;
753-
if (attribute.ConstructorArguments.Length == 1)
737+
if (attribute.ConstructorArguments.Length > 0)
738+
{
739+
var arg = attribute.ConstructorArguments[0];
740+
if (arg.Values.Length > 0)
754741
{
755-
var arg = attribute.ConstructorArguments[0];
756-
if (arg.Values.Length > 0)
757-
{
758-
var values = arg.Values
759-
.Select(v => v.Value as string)
760-
.Where(s => !string.IsNullOrWhiteSpace(s))
761-
.Select(s => s!.Trim());
762-
763-
MergeInto(ref authorizationPolicies, values);
764-
}
742+
var values = arg.Values
743+
.Select(v => v.Value as string)
744+
.Where(s => !string.IsNullOrWhiteSpace(s))
745+
.Select(s => s!.Trim());
746+
747+
MergeInto(ref tags, values);
765748
}
766-
break;
767-
case $"global::{DisableAntiforgeryAttributeFullyQualifiedName}":
768-
disableAntiforgery = true;
769-
break;
770-
case $"global::{AllowAnonymousAttributeFullyQualifiedName}":
771-
allowAnonymous = true;
772-
hasAllowAnonymousAttribute = true;
773-
break;
774-
case "global::Microsoft.AspNetCore.Routing.ExcludeFromDescriptionAttribute":
775-
excludeFromDescription = true;
776-
break;
777-
case $"global::{ProducesProblemAttributeFullyQualifiedName}":
778-
{
779-
var statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesProblemStatusCode
780-
? producesProblemStatusCode
781-
: 500;
782-
var contentType = attribute.ConstructorArguments.Length > 1
783-
? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string)
784-
: null;
785-
var additionalContentTypes = attribute.ConstructorArguments.Length > 2
786-
? GetStringArrayValues(attribute.ConstructorArguments[2])
787-
: null;
788-
789-
var producesProblemList = producesProblem ??= [];
790-
producesProblemList.Add(new ProducesProblemMetadata(statusCode, contentType, additionalContentTypes));
791-
break;
792749
}
793-
case $"global::{ProducesValidationProblemAttributeFullyQualifiedName}":
750+
751+
continue;
752+
}
753+
754+
if (IsGeneratedAttribute(attributeClass, RequireAuthorizationAttributeName))
755+
{
756+
requireAuthorization = true;
757+
hasRequireAuthorizationAttribute = true;
758+
if (attribute.ConstructorArguments.Length == 1)
794759
{
795-
var statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesValidationProblemStatusCode
796-
? producesValidationProblemStatusCode
797-
: 400;
798-
var contentType = attribute.ConstructorArguments.Length > 1
799-
? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string)
800-
: null;
801-
var additionalContentTypes = attribute.ConstructorArguments.Length > 2
802-
? GetStringArrayValues(attribute.ConstructorArguments[2])
803-
: null;
804-
805-
var producesValidationProblemList = producesValidationProblem ??= [];
806-
producesValidationProblemList.Add(new ProducesValidationProblemMetadata(statusCode, contentType, additionalContentTypes));
807-
break;
760+
var arg = attribute.ConstructorArguments[0];
761+
if (arg.Values.Length > 0)
762+
{
763+
var values = arg.Values
764+
.Select(v => v.Value as string)
765+
.Where(s => !string.IsNullOrWhiteSpace(s))
766+
.Select(s => s!.Trim());
767+
768+
MergeInto(ref authorizationPolicies, values);
769+
}
808770
}
771+
772+
continue;
773+
}
774+
775+
if (IsGeneratedAttribute(attributeClass, DisableAntiforgeryAttributeName))
776+
{
777+
disableAntiforgery = true;
778+
continue;
779+
}
780+
781+
if (IsAttribute(attributeClass, AllowAnonymousAttributeName, AspNetCoreAuthorizationNamespaceParts))
782+
{
783+
allowAnonymous = true;
784+
hasAllowAnonymousAttribute = true;
785+
continue;
786+
}
787+
788+
if (IsAttribute(attributeClass, "ExcludeFromDescriptionAttribute", AspNetCoreRoutingNamespaceParts))
789+
{
790+
excludeFromDescription = true;
791+
continue;
792+
}
793+
794+
if (IsGeneratedAttribute(attributeClass, ProducesProblemAttributeName))
795+
{
796+
var statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesProblemStatusCode
797+
? producesProblemStatusCode
798+
: 500;
799+
var contentType = attribute.ConstructorArguments.Length > 1
800+
? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string)
801+
: null;
802+
var additionalContentTypes = attribute.ConstructorArguments.Length > 2
803+
? GetStringArrayValues(attribute.ConstructorArguments[2])
804+
: null;
805+
806+
var producesProblemList = producesProblem ??= [];
807+
producesProblemList.Add(new ProducesProblemMetadata(statusCode, contentType, additionalContentTypes));
808+
continue;
809+
}
810+
811+
if (IsGeneratedAttribute(attributeClass, ProducesValidationProblemAttributeName))
812+
{
813+
var statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesValidationProblemStatusCode
814+
? producesValidationProblemStatusCode
815+
: 400;
816+
var contentType = attribute.ConstructorArguments.Length > 1
817+
? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string)
818+
: null;
819+
var additionalContentTypes = attribute.ConstructorArguments.Length > 2
820+
? GetStringArrayValues(attribute.ConstructorArguments[2])
821+
: null;
822+
823+
var producesValidationProblemList = producesValidationProblem ??= [];
824+
producesValidationProblemList.Add(new ProducesValidationProblemMetadata(statusCode, contentType, additionalContentTypes));
809825
}
810826
}
811827
}
@@ -846,10 +862,29 @@ private static string NormalizeRequiredContentType(string? contentType, string d
846862
return builder.Count > 0 ? builder.ToEquatableImmutable() : null;
847863
}
848864

849-
private static bool IsGeneratedAttribute(string fullyQualifiedName, string attributeName)
865+
private static bool IsGeneratedAttribute(INamedTypeSymbol attributeClass, string attributeName)
850866
{
851-
var prefix = $"global::{AttributesNamespace}.{attributeName}";
852-
return fullyQualifiedName.StartsWith(prefix, StringComparison.Ordinal);
867+
var definition = attributeClass.OriginalDefinition;
868+
return definition.Name == attributeName && IsInNamespace(definition.ContainingNamespace, AttributesNamespaceParts);
869+
}
870+
871+
private static bool IsAttribute(INamedTypeSymbol attributeClass, string attributeName, string[] namespaceParts)
872+
{
873+
var definition = attributeClass.OriginalDefinition;
874+
return definition.Name == attributeName && IsInNamespace(definition.ContainingNamespace, namespaceParts);
875+
}
876+
877+
private static bool IsInNamespace(INamespaceSymbol? namespaceSymbol, string[] namespaceParts)
878+
{
879+
for (var i = namespaceParts.Length - 1; i >= 0; i--)
880+
{
881+
if (namespaceSymbol is null || namespaceSymbol.Name != namespaceParts[i])
882+
return false;
883+
884+
namespaceSymbol = namespaceSymbol.ContainingNamespace;
885+
}
886+
887+
return namespaceSymbol is null || namespaceSymbol.IsGlobalNamespace;
853888
}
854889

855890
private static void TryAddAcceptsMetadata(

0 commit comments

Comments
 (0)