Skip to content
Open
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
8 changes: 8 additions & 0 deletions src/CookieCrumble/src/CookieCrumble/Snapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,14 @@ public void MatchInline(string expected)
}
}

public string Render()
{
var writer = new ArrayBufferWriter<byte>();
WriteSegments(writer);
EnsureEndOfBufferNewline(writer);
return Encoding.UTF8.GetString(writer.WrittenSpan);
}

private void WriteSegments(IBufferWriter<byte> writer)
{
if (_segments.Count == 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public static string ConvertDefaultValueToString(object? defaultValue, ITypeSymb

if (type.IsNullableValueType())
{
return ConvertDefaultValueToString(defaultValue, namedTypeSymbol.TypeArguments[0]!);
return ConvertDefaultValueToString(defaultValue, namedTypeSymbol.TypeArguments[0]);
}
}

Expand Down Expand Up @@ -159,7 +159,11 @@ public static string SanitizeIdentifier(string input)
}

// Normalize line endings and trim outer newlines
var normalized = "\n" + documentation!.Replace("\r", string.Empty).Trim('\n');
var normalized = documentation!.Replace("\r", string.Empty);
if (normalized[0] == ' ')
{
normalized = "\n" + normalized;
}

// Find common leading whitespace pattern
var whitespace = s_xmlWhitespaceRegex.Match(normalized).Value;
Expand Down
247 changes: 131 additions & 116 deletions src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Xml.Linq;
using HotChocolate.Types.Analyzers.Models;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -37,7 +38,7 @@ public static MethodDescription GetDescription(this IMethodSymbol method, Compil
if (methodDescription == null && compilation != null)
{
// Try inheritance-aware resolution with Compilation
methodDescription = GetDocumentationWithInheritance(method, compilation);
methodDescription = GetSummaryDocumentationWithInheritance(method, compilation);
}
else if (methodDescription == null)
{
Expand Down Expand Up @@ -109,7 +110,7 @@ public static MethodDescription GetDescription(this IMethodSymbol method, Compil
if (compilation != null)
{
// Try inheritance-aware resolution with Compilation
return new PropertyDescription(GetDocumentationWithInheritance(property, compilation));
return new PropertyDescription(GetSummaryDocumentationWithInheritance(property, compilation));
}

// Fallback to simple XML extraction without inheritdoc support
Expand Down Expand Up @@ -148,7 +149,7 @@ public static MethodDescription GetDescription(this IMethodSymbol method, Compil
if (compilation != null)
{
// Try inheritance-aware resolution with Compilation
return GetDocumentationWithInheritance(type, compilation);
return GetSummaryDocumentationWithInheritance(type, compilation);
}

// Fallback to simple XML extraction without inheritdoc support
Expand Down Expand Up @@ -202,18 +203,18 @@ public static MethodDescription GetDescription(this IMethodSymbol method, Compil
}

/// <summary>
/// Extracts summary text from XML documentation, resolving inheritdoc tags.
/// Extracts summary text from XML documentation, resolving tags with semantic relevance (f. e. inheritdoc or see).
/// </summary>
private static string? GetDocumentationWithInheritance(ISymbol symbol, Compilation compilation)
private static string? GetSummaryDocumentationWithInheritance(ISymbol symbol, Compilation compilation)
{
var visited = new HashSet<ISymbol>(SymbolEqualityComparer.Default);
return GetDocumentationWithInheritanceCore(symbol, compilation, visited);
return GetSummaryDocumentationWithInheritanceCore(symbol, compilation, visited);
}

/// <summary>
/// Core implementation with cycle detection.
/// </summary>
private static string? GetDocumentationWithInheritanceCore(
private static string? GetSummaryDocumentationWithInheritanceCore(
ISymbol symbol,
Compilation compilation,
HashSet<ISymbol> visited)
Expand All @@ -232,31 +233,135 @@ public static MethodDescription GetDescription(this IMethodSymbol method, Compil

try
{
var doc = XDocument.Parse(xml);
var doc = XDocument.Parse(xml, LoadOptions.PreserveWhitespace);

// Materialize relevant XML elements (-> replace their element with the actual textual representation)
MaterializeInheritdocElements(doc);
MaterializeSeeElements(doc);
MaterializeParamRefElements(doc);

// Check for inheritdoc element
var inheritdocElement = doc.Descendants("inheritdoc").FirstOrDefault();
var summaryText =
doc.Descendants("summary").FirstOrDefault()?.Value ??
doc.Descendants("member").FirstOrDefault()?.Value;

if (inheritdocElement != null)
summaryText += GetReturnsElementText(doc);

var exceptionDoc = GetExceptionDocumentation(doc);
if (!string.IsNullOrEmpty(exceptionDoc))
{
summaryText += "\n\n**Errors:**\n" + exceptionDoc;
}

return GeneratorUtils.NormalizeXmlDocumentation(summaryText);
}
catch
{
// XML documentation parsing is best-effort only.
return null;
}

void MaterializeInheritdocElements(XDocument doc1)
{
foreach (var inheritdocElement in doc1.Descendants("inheritdoc").ToArray())
{
if (inheritdocElement == null)
{
continue;
}

// Try to resolve the inherited documentation
var inheritedDoc = ResolveInheritdoc(symbol, inheritdocElement, compilation, visited);
if (inheritedDoc != null)
{
return inheritedDoc;
inheritdocElement.ReplaceWith(inheritedDoc);
}
// If resolution fails, return null (no description)
return null;
}
}

// No inheritdoc - extract summary normally
var summaryText = doc.Descendants("summary").FirstOrDefault()?.Value;
return GeneratorUtils.NormalizeXmlDocumentation(summaryText);
static void MaterializeSeeElements(XDocument xDocument)
{
foreach (var seeElement in xDocument.Descendants("see").ToArray())
{
if (seeElement == null)
{
continue;
}

if (!string.IsNullOrEmpty(seeElement.Value))
{
seeElement.ReplaceWith(seeElement.Value);
continue;
}

var attribute = seeElement.Attribute("langword") ?? seeElement.Attribute("href");
if (attribute != null)
{
seeElement.ReplaceWith(attribute.Value);
continue;
}

attribute = seeElement.Attribute("cref");
if (attribute?.Value != null)
{
var index = attribute.Value.LastIndexOf('.');
seeElement.ReplaceWith(attribute.Value.Substring(index + 1));
}
}
}
catch

static void MaterializeParamRefElements(XDocument xDocument)
{
// XML documentation parsing is best-effort only.
return null;
foreach (var paramref in xDocument.Descendants("paramref").ToArray())
{
var attribute = paramref?.Attribute("name");
if (attribute != null)
{
paramref!.ReplaceWith(attribute.Value);
}
}
}

static string GetExceptionDocumentation(XDocument xDocument)
{
StringBuilder? builder = null;
var errorCount = 0;
var exceptionElements = xDocument.Descendants("exception");
foreach (var exceptionElement in exceptionElements)
{
if (string.IsNullOrEmpty(exceptionElement.Value))
{
continue;
}

var code = exceptionElement.Attribute("code");
if (string.IsNullOrEmpty(code?.Value))
{
continue;
}

builder ??= new StringBuilder();
builder.Append(builder.Length > 0 ? "\n" : string.Empty)
.Append(++errorCount)
.Append('.')
.Append(' ')
.Append(code!.Value)
.Append(':')
.Append(' ')
.Append(exceptionElement.Value);
}

return builder?.ToString() ?? string.Empty;
}

static string GetReturnsElementText(XDocument doc)
{
var xElement = doc.Descendants("returns").FirstOrDefault();
if (xElement?.Value != null)
{
return "\n\n**Returns:**\n" + xElement.Value;
}

return string.Empty;
}
}

Expand All @@ -273,10 +378,10 @@ public static MethodDescription GetDescription(this IMethodSymbol method, Compil
var crefAttr = inheritdocElement.Attribute("cref");
if (crefAttr != null)
{
var referencedSymbol = ResolveDocumentationId(crefAttr.Value, compilation, symbol);
var referencedSymbol = ResolveDocumentationId(crefAttr.Value, compilation);
if (referencedSymbol != null)
{
return GetDocumentationWithInheritanceCore(referencedSymbol, compilation, visited);
return GetSummaryDocumentationWithInheritanceCore(referencedSymbol, compilation, visited);
}
return null;
}
Expand All @@ -285,7 +390,7 @@ public static MethodDescription GetDescription(this IMethodSymbol method, Compil
var baseMember = FindBaseMember(symbol);
if (baseMember != null)
{
return GetDocumentationWithInheritanceCore(baseMember, compilation, visited);
return GetSummaryDocumentationWithInheritanceCore(baseMember, compilation, visited);
}

return null;
Expand Down Expand Up @@ -389,106 +494,16 @@ public static MethodDescription GetDescription(this IMethodSymbol method, Compil
/// 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, ISymbol contextSymbol)
private static ISymbol? ResolveDocumentationId(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)

// Remove prefix if present
var colonIndex = documentationId.IndexOf(':');
if (colonIndex > 0)
{
documentationId = documentationId.Substring(colonIndex + 1);
}

// Handle generic types - convert `2 to <T1, T2> format for lookup
// For now, try exact match first
var symbol = compilation.GetTypeByMetadataName(documentationId);
if (symbol != null)
{
return symbol;
}

// Try without generic arity marker
var backtickIndex = documentationId.IndexOf('`');
if (backtickIndex > 0)
{
var typeNameWithoutArity = documentationId.Substring(0, backtickIndex);
symbol = compilation.GetTypeByMetadataName(typeNameWithoutArity);
if (symbol != null)
{
return symbol;
}
}

// Try resolving through context symbol's containing namespace
if (contextSymbol.ContainingNamespace != null)
{
var namespaceName = contextSymbol.ContainingNamespace.ToDisplayString();
var fullName = $"{namespaceName}.{documentationId}";
symbol = compilation.GetTypeByMetadataName(fullName);
if (symbol != null)
{
return symbol;
}
}

// Best effort - search for type by name in compilation
var typeName = documentationId.Split('.').LastOrDefault();
if (!string.IsNullOrEmpty(typeName))
{
// Remove generic arity from type name
var genericIndex = typeName.IndexOf('`');
if (genericIndex > 0)
{
typeName = typeName.Substring(0, genericIndex);
}

// Search in all referenced assemblies
foreach (var assembly in compilation.References)
{
if (compilation.GetAssemblyOrModuleSymbol(assembly) is IAssemblySymbol assemblySymbol)
{
var foundSymbol = FindTypeByName(assemblySymbol.GlobalNamespace, typeName);
if (foundSymbol != null)
{
return foundSymbol;
}
}
}
}

return null;
}

/// <summary>
/// Recursively searches for a type by name in a namespace.
/// </summary>
private static INamedTypeSymbol? FindTypeByName(INamespaceSymbol namespaceSymbol, string typeName)
{
foreach (var member in namespaceSymbol.GetMembers())
{
if (member is INamedTypeSymbol type && type.Name == typeName)
{
return type;
}

if (member is INamespaceSymbol childNamespace)
{
var found = FindTypeByName(childNamespace, typeName);
if (found != null)
{
return found;
}
}
}

return null;
// Prefixes: T: (type), M: (method), P: (property), F: (field), E: (event)#
return DocumentationCommentId.GetSymbolsForDeclarationId(documentationId, compilation).FirstOrDefault();
}

public static bool IsNullableType(this ITypeSymbol typeSymbol)
Expand Down
Loading