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
145 changes: 139 additions & 6 deletions src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using HotChocolate.Types.Analyzers.Models;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.SymbolDisplayFormat;
using static Microsoft.CodeAnalysis.SymbolDisplayMiscellaneousOptions;

Expand Down Expand Up @@ -225,7 +226,7 @@ public static MethodDescription GetDescription(this IMethodSymbol method, Compil
return null;
}

var xml = symbol.GetDocumentationCommentXml();
var xml = GetXmlDocumentationFromSyntax(symbol);
if (string.IsNullOrEmpty(xml))
{
return null;
Expand Down Expand Up @@ -365,6 +366,46 @@ static string GetReturnsElementText(XDocument doc)
}
}

private static string? GetXmlDocumentationFromSyntax(ISymbol symbol)
{
// Note: One currently can't use GetDocumentationCommentXml in source generators.
// See https://github.com/dotnet/roslyn/issues/23673
var syntax = symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax();
while (syntax is VariableDeclaratorSyntax vds)
{
syntax = vds.Parent?.Parent;
}

if (syntax == null || syntax.SyntaxTree.Options.DocumentationMode == DocumentationMode.None)
{
// See https://github.com/dotnet/roslyn/issues/58210,
// for DocumentationMode.None we can't reliably extract the XML doc header.
return null;
}

var trivia = syntax.GetLeadingTrivia();
StringBuilder? builder = null;
foreach (var comment in trivia)
{
if (comment.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia)
|| comment.IsKind(SyntaxKind.MultiLineDocumentationCommentTrivia))
{
var stringComment = comment.ToString();
foreach (var s in stringComment.Split('\n'))
{
builder ??= new StringBuilder();
builder.Append(s.TrimStart().Replace("///", string.Empty));
builder.Append('\n');
}
}
}

return builder?
.Insert(0, "<member>")
.Append("</member>")
.ToString();
}

/// <summary>
/// Resolves an inheritdoc element by finding the referenced member.
/// </summary>
Expand All @@ -378,7 +419,7 @@ static string GetReturnsElementText(XDocument doc)
var crefAttr = inheritdocElement.Attribute("cref");
if (crefAttr != null)
{
var referencedSymbol = ResolveDocumentationId(crefAttr.Value, compilation);
var referencedSymbol = ResolveDocumentationId(crefAttr.Value, compilation, symbol);
if (referencedSymbol != null)
{
return GetSummaryDocumentationWithInheritanceCore(referencedSymbol, compilation, visited);
Expand Down Expand Up @@ -494,16 +535,108 @@ static string GetReturnsElementText(XDocument doc)
/// Resolves a documentation ID (cref value) to a symbol.
/// Handles format like "T:Namespace.Type", "M:Namespace.Type.Method", "T:Namespace.Type`1", etc.
/// </summary>
private static ISymbol? ResolveDocumentationId(string documentationId, Compilation compilation)
private static ISymbol? ResolveDocumentationId(string documentationId, Compilation compilation, ISymbol contextSymbol)
{
if (string.IsNullOrEmpty(documentationId))
{
return null;
}

if (documentationId.Length > 1 && documentationId[1] == ':')
{
documentationId = documentationId.Substring(2);
}

var result = compilation.GetTypeByMetadataName(documentationId) ??
ResolveMemberSymbol(documentationId, compilation) ??
ResolveMethodSymbol(documentationId, compilation);

var @namespace = contextSymbol.ContainingNamespace?.ToString();
if (result == null && !string.IsNullOrEmpty(@namespace) && !documentationId.StartsWith(@namespace))
{
documentationId = @namespace + "." + documentationId;
result = compilation.GetTypeByMetadataName(documentationId) ??
ResolveMemberSymbol(documentationId, compilation) ??
ResolveMethodSymbol(documentationId, compilation);
}

return result;
}

private static ISymbol? ResolveMethodSymbol(string documentationId, Compilation compilation)
{
if (string.IsNullOrEmpty(documentationId))
{
return null;
}

// Documentation ID format: Prefix:FullyQualifiedName
// Prefixes: T: (type), M: (method), P: (property), F: (field), E: (event)#
return DocumentationCommentId.GetSymbolsForDeclarationId(documentationId, compilation).FirstOrDefault();
var openParenthesisIndex = documentationId.LastIndexOf('(');
var qualifiedName = openParenthesisIndex >= 0
? documentationId.Substring(0, openParenthesisIndex)
: documentationId;

var lastDotIndex = qualifiedName.LastIndexOf('.');
if (lastDotIndex < 0)
{
return null;
}

var typeName = qualifiedName.Substring(0, lastDotIndex);
var methodName = qualifiedName.Substring(lastDotIndex + 1);

var typeSymbol = ResolveTypeSymbol(typeName, compilation);
if (typeSymbol == null)
{
return null;
}

return typeSymbol
.GetMembers(methodName)
.OfType<IMethodSymbol>()
.FirstOrDefault(m => m.ToString() == documentationId);
}

private static ISymbol? ResolveMemberSymbol(string documentationId, Compilation compilation)
{
var lastDotIndex = documentationId.LastIndexOf('.');
if (lastDotIndex < 0)
{
return null;
}

var typeName = documentationId.Substring(0, lastDotIndex);
var memberName = documentationId.Substring(lastDotIndex + 1);

var typeSymbol = ResolveTypeSymbol(typeName, compilation);
return typeSymbol?.GetMembers(memberName).FirstOrDefault();
}

private static INamedTypeSymbol? ResolveTypeSymbol(string typeName, Compilation compilation)
{
// Non-nested type
var symbol = compilation.GetTypeByMetadataName(typeName);
if (symbol != null)
{
return symbol;
}

// Nested type
var nestedName = typeName;
while (true)
{
var lastDot = nestedName.LastIndexOf('.');
if (lastDot < 0)
{
return null;
}

nestedName = nestedName.Remove(lastDot, 1).Insert(lastDot, "+");
symbol = compilation.GetTypeByMetadataName(nestedName);
if (symbol != null)
{
return symbol;
}
}
}

public static bool IsNullableType(this ITypeSymbol typeSymbol)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
<NoWarn>$(NoWarn);GD0001</NoWarn>
</PropertyGroup>

<PropertyGroup>
<!--Required since otherwise the XML docs won't be inferred by the source generator.-->
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\GreenDonut\src\GreenDonut.Data.Abstractions\GreenDonut.Data.Abstractions.csproj" />
<ProjectReference Include="..\..\..\..\GreenDonut\src\GreenDonut.Data.EntityFramework\GreenDonut.Data.EntityFramework.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public static string NullableArgumentWithExplicitType(
[QueryType]
public static partial class Query
{
/// <summary>
/// Gets the product.
/// </summary>
/// <returns>The only product.</returns>
public static Product GetProduct()
=> new Book { Id = "1", Title = "GraphQL in Action" };

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ type ProductsEdge {
}

type Query {
"""
Gets the product.


**Returns:**
The only product.
"""
product: Product!
products("Returns the elements in the list that come after the specified cursor." after: Version2 "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): ProductsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10")
argumentWithExplicitType(arg: Version2): String!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ internal static partial class Query
var content = snapshot.Match();
var emitted = s_description.Matches(content).Single().Groups;
Assert.Equal(
"Query and manages users.\\n \\nPlease note:\\n* Users ...\\n* Users ...\\n * Users ...\\n"
+ " * Users ...\\n \\nYou need one of the following role: Owner,\\n"
"Query and manages users.\\n\\nPlease note:\\n* Users ...\\n* Users ...\\n * Users ...\\n"
+ " * Users ...\\n\\nYou need one of the following role: Owner,\\n"
+ "Editor, use XYZ to manage permissions.",
emitted[1].Value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ namespace TestNamespace
var bindingResolver = field.Context.ParameterBindingResolver;
var naming = field.Context.Naming;

configuration.Description = "Query and manages users.\n \nPlease note:\n* Users ...\n* Users ...\n * Users ...\n * Users ...\n \nYou need one of the following role: Owner,\nEditor, use XYZ to manage permissions.";
configuration.Description = "Query and manages users.\n\nPlease note:\n* Users ...\n* Users ...\n * Users ...\n * Users ...\n\nYou need one of the following role: Owner,\nEditor, use XYZ to manage permissions.";
configuration.Type = typeInspector.GetTypeRef(typeof(string), HotChocolate.Types.TypeContext.Output);
configuration.ResultType = typeof(string);

Expand Down
Loading