Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 2 additions & 2 deletions src/OpenApi/gen/XmlCommentGenerator.Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public sealed partial class XmlCommentGenerator
if (DocumentationCommentId.GetFirstSymbolForDeclarationId(name, compilation) is ISymbol symbol &&
// Only include symbols that are declared in the application assembly or are
// accessible from the application assembly.
(SymbolEqualityComparer.Default.Equals(symbol.ContainingAssembly, input.Compilation.Assembly) || symbol.IsAccessibleType()) &&
(SymbolEqualityComparer.Default.Equals(symbol.ContainingAssembly, compilation.Assembly) || symbol.IsAccessibleType()) &&
// Skip static classes that are just containers for members with annotations
// since they cannot be instantiated.
symbol is not INamedTypeSymbol { TypeKind: TypeKind.Class, IsStatic: true })
Expand All @@ -104,7 +104,7 @@ public sealed partial class XmlCommentGenerator
{
var memberKey = symbol switch
{
IMethodSymbol methodSymbol => MemberKey.FromMethodSymbol(methodSymbol, input.Compilation),
IMethodSymbol methodSymbol => MemberKey.FromMethodSymbol(methodSymbol),
IPropertySymbol propertySymbol => MemberKey.FromPropertySymbol(propertySymbol),
INamedTypeSymbol typeSymbol => MemberKey.FromTypeSymbol(typeSymbol),
_ => null
Expand Down
241 changes: 166 additions & 75 deletions src/OpenApi/gen/XmlComments/MemberKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;

namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml;
Expand All @@ -14,14 +14,19 @@ internal sealed record MemberKey(
MemberType MemberKind,
string? Name,
string? ReturnType,
string[]? Parameters) : IEquatable<MemberKey>
List<string>? Parameters) : IEquatable<MemberKey>
{
private static readonly SymbolDisplayFormat _typeKeyFormat = new(
private static readonly SymbolDisplayFormat _withTypeParametersFormat = new(
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included,
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters);

public static MemberKey FromMethodSymbol(IMethodSymbol method, Compilation compilation)
private static readonly SymbolDisplayFormat _withoutTypeParametersFormat = new(
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included,
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
genericsOptions: SymbolDisplayGenericsOptions.None);

public static MemberKey? FromMethodSymbol(IMethodSymbol method)
{
string returnType;
if (method.ReturnsVoid)
Expand All @@ -30,108 +35,194 @@ public static MemberKey FromMethodSymbol(IMethodSymbol method, Compilation compi
}
else
{
// Handle Task/ValueTask for async methods
var actualReturnType = method.ReturnType;
if (method.IsAsync && actualReturnType is INamedTypeSymbol namedType)
if (actualReturnType.TypeKind == TypeKind.TypeParameter)
{
if (namedType.TypeArguments.Length > 0)
{
actualReturnType = namedType.TypeArguments[0];
}
else
{
actualReturnType = compilation.GetSpecialType(SpecialType.System_Void);
}
returnType = "typeof(object)";
}
else if (TryGetFormattedTypeName(actualReturnType, out var formattedReturnType))
{
returnType = $"typeof({formattedReturnType})";
}
else
{
return null;
}

returnType = actualReturnType.TypeKind == TypeKind.TypeParameter
? "typeof(object)"
: $"typeof({ReplaceGenericArguments(actualReturnType.ToDisplayString(_typeKeyFormat))})";
}

// Handle extension methods by skipping the 'this' parameter
var parameters = method.Parameters
.Where(p => !p.IsThis)
.Select(p =>
List<string> parameters = [];
foreach (var parameter in method.Parameters)
{
if (parameter.IsThis)
{
continue;
}

if (parameter.Type.TypeKind == TypeKind.TypeParameter)
{
parameters.Add("typeof(object)");
}
else if (parameter.IsParams && parameter.Type is IArrayTypeSymbol arrayType)
{
if (p.Type.TypeKind == TypeKind.TypeParameter)
if (TryGetFormattedTypeName(arrayType.ElementType, out var formattedArrayType))
{
return "typeof(object)";
parameters.Add($"typeof({formattedArrayType}[])");
}

// For params arrays, use the array type
if (p.IsParams && p.Type is IArrayTypeSymbol arrayType)
else
{
return $"typeof({ReplaceGenericArguments(arrayType.ToDisplayString(_typeKeyFormat))})";
return null;
}
}
else if (TryGetFormattedTypeName(parameter.Type, out var formattedParameterType))
{
parameters.Add($"typeof({formattedParameterType})");
}
else
{
return null;
}
}

return $"typeof({ReplaceGenericArguments(p.Type.ToDisplayString(_typeKeyFormat))})";
})
.ToArray();

// For generic methods, use the containing type with generic parameters
var declaringType = method.ContainingType;
var typeDisplay = declaringType.ToDisplayString(_typeKeyFormat);

// If the method is in a generic type, we need to handle the type parameters
if (declaringType.IsGenericType)
if (TryGetFormattedTypeName(method.ContainingType, out var formattedDeclaringType))
{
typeDisplay = ReplaceGenericArguments(typeDisplay);
return new MemberKey(
$"typeof({formattedDeclaringType})",
MemberType.Method,
method.MetadataName, // Use MetadataName to match runtime MethodInfo.Name
returnType,
parameters);
}

return new MemberKey(
$"typeof({typeDisplay})",
MemberType.Method,
method.MetadataName, // Use MetadataName to match runtime MethodInfo.Name
returnType,
parameters);
return null;
}

public static MemberKey FromPropertySymbol(IPropertySymbol property)
public static MemberKey? FromPropertySymbol(IPropertySymbol property)
{
return new MemberKey(
$"typeof({ReplaceGenericArguments(property.ContainingType.ToDisplayString(_typeKeyFormat))})",
MemberType.Property,
property.Name,
null,
null);
if (TryGetFormattedTypeName(property.ContainingType, out var typeName))
{
return new MemberKey(
$"typeof({typeName})",
MemberType.Property,
property.Name,
null,
null);
}
return null;
}

public static MemberKey FromTypeSymbol(INamedTypeSymbol type)
public static MemberKey? FromTypeSymbol(INamedTypeSymbol type)
{
return new MemberKey(
$"typeof({ReplaceGenericArguments(type.ToDisplayString(_typeKeyFormat))})",
MemberType.Type,
null,
null,
null);
if (TryGetFormattedTypeName(type, out var typeName))
{
return new MemberKey(
$"typeof({typeName})",
MemberType.Type,
null,
null,
null);
}
return null;
}

/// Supports replacing generic type arguments to support use of open
/// generics in `typeof` expressions for the declaring type.
private static string ReplaceGenericArguments(string typeName)
private static bool TryGetFormattedTypeName(ITypeSymbol typeSymbol, [NotNullWhen(true)] out string? typeName, bool isNestedCall = false)
{
if (typeSymbol is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } nullableType)
{
typeName = typeSymbol.ToDisplayString(_withTypeParametersFormat);
return true;
}

// Handle tuples specially since they are represented as generic
// ValueTuple types and trigger the logic for handling generics in
// nested values.
if (typeSymbol is INamedTypeSymbol { IsTupleType: true } namedType)
{
return TryHandleTupleType(namedType, out typeName);
}

if (typeSymbol is INamedTypeSymbol { IsGenericType: true } genericType)
{
// If any of the type arguments are type parameters, then they have not
// been substituted for a concrete type and we need to model them as open
// generics if possible to avoid emitting a type with type parameters that
// cannot be used in a typeof expression.
var hasTypeParameters = genericType.TypeArguments.Any(t => t.TypeKind == TypeKind.TypeParameter);
var typeNameWithoutGenerics = genericType.ToDisplayString(_withoutTypeParametersFormat);

if (!hasTypeParameters)
{
var typeArgStrings = new List<string>();
var allArgumentsResolved = true;

// Loop through each type argument to handle nested generics.
foreach (var typeArg in genericType.TypeArguments)
{
if (TryGetFormattedTypeName(typeArg, out var argTypeName, isNestedCall: true))
{
typeArgStrings.Add(argTypeName);
}
else
{
typeName = null;
return false;
}
}

if (allArgumentsResolved)
{
typeName = $"{typeNameWithoutGenerics}<{string.Join(", ", typeArgStrings)}>";
return true;
}
}
else
{
if (isNestedCall)
{
// If this is a nested call, we can't use open generics so there's no way
// for us to emit a member key. Return false and skip over this type in the code
// generation.
typeName = null;
return false;
}

// If we got here, we can successfully emit a member key for the open generic type.
var genericArgumentsCount = genericType.TypeArguments.Length;
var openGenericsPlaceholder = "<" + new string(',', genericArgumentsCount - 1) + ">";

typeName = typeNameWithoutGenerics + openGenericsPlaceholder;
return true;
}
}

typeName = typeSymbol.ToDisplayString(_withTypeParametersFormat);
return true;
}

private static bool TryHandleTupleType(INamedTypeSymbol tupleType, [NotNullWhen(true)] out string? typeName)
{
var stack = new Stack<int>();
var result = new StringBuilder(typeName);
for (var i = 0; i < result.Length; i++)
List<string> elementTypes = [];
foreach (var element in tupleType.TupleElements)
{
if (result[i] == '<')
if (element.Type.TypeKind == TypeKind.TypeParameter)
{
stack.Push(i);
elementTypes.Add("object");
}
else if (result[i] == '>' && stack.Count > 0)
else
{
var start = stack.Pop();
// Replace everything between < and > with empty strings separated by commas
var segment = result.ToString(start + 1, i - start - 1);
var commaCount = segment.Count(c => c == ',');
var replacement = new string(',', commaCount);
result.Remove(start + 1, i - start - 1);
result.Insert(start + 1, replacement);
i = start + replacement.Length + 1;
// Process each tuple element and handle nested generics
if (!TryGetFormattedTypeName(element.Type, out var elementTypeName, isNestedCall: true))
{
typeName = null;
return false;
}
elementTypes.Add(elementTypeName);
}
}
return result.ToString();

typeName = $"global::System.ValueTuple<{string.Join(", ", elementTypes)}>";
return true;
}
}

Expand Down
Loading