diff --git a/src/CookieCrumble/src/CookieCrumble/Snapshot.cs b/src/CookieCrumble/src/CookieCrumble/Snapshot.cs index 52413ca9d5d..736abbcda5e 100644 --- a/src/CookieCrumble/src/CookieCrumble/Snapshot.cs +++ b/src/CookieCrumble/src/CookieCrumble/Snapshot.cs @@ -370,6 +370,14 @@ public void MatchInline(string expected) } } + public string Render() + { + var writer = new ArrayBufferWriter(); + WriteSegments(writer); + EnsureEndOfBufferNewline(writer); + return Encoding.UTF8.GetString(writer.WrittenSpan); + } + private void WriteSegments(IBufferWriter writer) { if (_segments.Count == 1) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/GeneratorUtils.cs b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/GeneratorUtils.cs index 31cc518252b..25638e28c71 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/GeneratorUtils.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/GeneratorUtils.cs @@ -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]); } } @@ -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; diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs index bc975c8a46e..f7eaa1841f7 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs @@ -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; @@ -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) { @@ -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 @@ -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 @@ -202,18 +203,18 @@ public static MethodDescription GetDescription(this IMethodSymbol method, Compil } /// - /// 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). /// - private static string? GetDocumentationWithInheritance(ISymbol symbol, Compilation compilation) + private static string? GetSummaryDocumentationWithInheritance(ISymbol symbol, Compilation compilation) { var visited = new HashSet(SymbolEqualityComparer.Default); - return GetDocumentationWithInheritanceCore(symbol, compilation, visited); + return GetSummaryDocumentationWithInheritanceCore(symbol, compilation, visited); } /// /// Core implementation with cycle detection. /// - private static string? GetDocumentationWithInheritanceCore( + private static string? GetSummaryDocumentationWithInheritanceCore( ISymbol symbol, Compilation compilation, HashSet visited) @@ -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; } } @@ -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; } @@ -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; @@ -389,7 +494,7 @@ 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. /// - private static ISymbol? ResolveDocumentationId(string documentationId, Compilation compilation, ISymbol contextSymbol) + private static ISymbol? ResolveDocumentationId(string documentationId, Compilation compilation) { if (string.IsNullOrEmpty(documentationId)) { @@ -397,98 +502,8 @@ public static MethodDescription GetDescription(this IMethodSymbol method, Compil } // 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 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; - } - - /// - /// Recursively searches for a type by name in a namespace. - /// - 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) diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/ObjectTypeTests.XmlDocInference.Ported.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/ObjectTypeTests.XmlDocInference.Ported.cs new file mode 100644 index 00000000000..242266d126f --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/ObjectTypeTests.XmlDocInference.Ported.cs @@ -0,0 +1,367 @@ +namespace HotChocolate.Types; + +//// Ported tests from XmlDocumentationProviderTests to ensure identical behavior between both documentation providers. + +public partial class ObjectTypeXmlDocInferenceTests +{ + [Fact] + public void When_xml_doc_is_missing_then_description_is_empty() + { + var snap = TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + + namespace TestNamespace; + + [QueryType] + internal static partial class Query + { + public static string GetUser() + => "User"; + } + """); + + Assert.Empty(DescriptionExtractorRegex().Matches(snap.Render())); + } + + [Fact] + public void When_xml_doc_with_multiple_breaks_is_read_then_they_are_not_stripped_away() + { + var snap = TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + + namespace TestNamespace; + + [QueryType] + internal static partial class Query + { + /// + /// Query and manages users. + /// + /// Please note: + /// * Users ... + /// * Users ... + /// * Users ... + /// * Users ... + /// + /// You need one of the following role: Owner, + /// Editor, use XYZ to manage permissions. + /// + public static string? Foo { get; set; } + } + """); + + const string expected = + "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."; + + var emitted = DescriptionExtractorRegex().Matches(snap.Render()).Single().Groups; + Assert.Equal(expected, emitted[1].Value); + } + + [Fact] + public void When_description_has_see_tag_then_it_is_converted() + { + var snap = TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + + namespace TestNamespace; + + public class Record; + + [QueryType] + internal static partial class Query + { + /// + /// for the default . + /// See this and + /// this at + /// . + /// + public static string? Foo { get; set; } + } + """); + + const string expected = "null for the default Record.\\nSee this and\\nthis at\\nhttps://foo.com/bar/baz."; + + var emitted = DescriptionExtractorRegex().Matches(snap.Render()).Single().Groups; + Assert.Equal(expected, emitted[1].Value); + } + + [Fact] + public void When_description_has_paramref_tag_then_it_is_converted() + { + var snap = TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + + namespace TestNamespace; + + [QueryType] + internal static partial class Query + { + /// + /// This is a parameter reference to . + /// + public int Foo(int id) => id; + } + """); + + const string expected = "This is a parameter reference to id."; + + var emitted = DescriptionExtractorRegex().Matches(snap.Render()).Single().Groups; + Assert.Equal(expected, emitted[1].Value); + } + + [Fact] + public void When_description_has_generic_tags_then_it_is_converted() + { + var snap = TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + + namespace TestNamespace; + + [QueryType] + internal static partial class Query + { + /// These are some tags. + public int Foo() => 0; + } + """); + + const string expected = "These are some tags."; + + var emitted = DescriptionExtractorRegex().Matches(snap.Render()).Single().Groups; + Assert.Equal(expected, emitted[1].Value); + } + + [Fact] + public void When_method_has_inheritdoc_then_it_is_resolved() + { + var snap = TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + + namespace TestNamespace; + + /// + /// I am the most base class. + /// + public class BaseBaseClass + { + /// Method doc. + /// Parameter details. + public virtual void Bar(string baz) { } + } + + [QueryType] + internal static partial class Query + { + /// + public static int Bar(string baz) => 0; + } + """); + + const string expected = "Method doc."; + + var emitted = DescriptionExtractorRegex().Matches(snap.Render()).Single().Groups; + Assert.Equal(expected, emitted[1].Value); + } + + [Fact] + public void When_class_has_description_then_it_is_converted() + { + var snap = TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + + namespace TestNamespace; + + [QueryType] + internal static partial class Query + { + /// + /// I am a test class. This should not be escaped: > + /// + public static int Bar() => 0; + } + """); + + const string expected = "I am a test class. This should not be escaped: >"; + + var emitted = DescriptionExtractorRegex().Matches(snap.Render()).Single().Groups; + Assert.Equal(expected, emitted[1].Value); + } + + [Fact] + public void When_method_has_returns_then_it_is_converted() + { + var snap = TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + + namespace TestNamespace; + + [QueryType] + internal static partial class Query + { + /// + /// Query and manages users. + /// + /// Bar + public static int Bar() => 0; + } + """); + + const string expected = "Query and manages users.\\n\\n\\n**Returns:**\\nBar"; + + var emitted = DescriptionExtractorRegex().Matches(snap.Render()).Single().Groups; + Assert.Equal(expected, emitted[1].Value); + } + + [Fact] + public void When_method_has_exceptions_then_it_is_converted() + { + var snap = TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + + namespace TestNamespace; + + [QueryType] + internal static partial class Query + { + /// + /// Query and manages users. + /// + /// Bar + /// Foo Error + /// Bar Error + public static int Bar() => 0; + } + """); + + const string expected = "Query and manages users.\\n\\n\\n**Returns:**\\nBar\\n\\n**Errors:**\\n1. FOO_ERROR: Foo Error\\n2. BAR_ERROR: Bar Error"; + + var emitted = DescriptionExtractorRegex().Matches(snap.Render()).Single().Groups; + Assert.Equal(expected, emitted[1].Value); + } + + [Fact] + public void When_method_has_exceptions_then_exceptions_with_no_code_will_be_ignored() + { + var snap = TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + + namespace TestNamespace; + + [QueryType] + internal static partial class Query + { + /// + /// Query and manages users. + /// + /// Bar + /// Foo Error + /// Foo Error + /// Bar Error + public static int Bar() => 0; + } + """); + + const string expected = "Query and manages users.\\n\\n\\n**Returns:**\\nBar\\n\\n**Errors:**\\n1. FOO_ERROR: Foo Error\\n2. BAR_ERROR: Bar Error"; + + var emitted = DescriptionExtractorRegex().Matches(snap.Render()).Single().Groups; + Assert.Equal(expected, emitted[1].Value); + } + + [Fact] + public void When_method_has_only_exceptions_with_no_code_then_error_section_will_not_be_written() + { + var snap = TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + + namespace TestNamespace; + + [QueryType] + internal static partial class Query + { + /// + /// Query and manages users. + /// + /// Bar + /// Foo Error + public static int Bar() => 0; + } + """); + + const string expected = "Query and manages users.\\n\\n\\n**Returns:**\\nBar"; + + var emitted = DescriptionExtractorRegex().Matches(snap.Render()).Single().Groups; + Assert.Equal(expected, emitted[1].Value); + } + + [System.Text.RegularExpressions.GeneratedRegex("configuration.Description = \"(.*)\";")] + private static partial System.Text.RegularExpressions.Regex DescriptionExtractorRegex(); +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/ObjectTypeTests.XmlDocInference.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/ObjectTypeTests.XmlDocInference.cs new file mode 100644 index 00000000000..298b6d39812 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/ObjectTypeTests.XmlDocInference.cs @@ -0,0 +1,237 @@ +namespace HotChocolate.Types; + +public partial class ObjectTypeXmlDocInferenceTests +{ + [Fact] + public void Method_WithInheritdoc_And_MultipleLayersOfInheritance() + { + var snap = TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + + namespace TestNamespace; + + public class BaseBaseClass + { + /// Method doc. + public virtual void Bar() { } + } + + public class BaseClass : BaseBaseClass + { + /// + public override void Bar() { } + } + + [QueryType] + internal static partial class Query + { + /// + public static int Bar(string baz) => 0; + } + """); + + const string expected = "Method doc."; + + var emitted = DescriptionExtractorRegex().Matches(snap.Render()).Single().Groups; + Assert.Equal(expected, emitted[1].Value); + } + + [Fact] + public void Method_WithInheritdoc_ThatContainsInheritdoc() + { + var snap = TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + + namespace TestNamespace; + + public class BaseBaseClass + { + /// Method doc. + public virtual void Bar() { } + } + + public class BaseClass : BaseBaseClass + { + /// + /// Concrete Method doc. + /// + /// + public override void Bar() { } + } + + public class ConcreteClass : BaseClass + { + /// + public override void Bar() { } + } + + [QueryType] + internal static partial class Query + { + /// + public static int Bar(string baz) => 0; + } + """); + + const string expected = "Concrete Method doc.\\nMethod doc."; + + var emitted = DescriptionExtractorRegex().Matches(snap.Render()).Single().Groups; + Assert.Equal(expected, emitted[1].Value); + } + + [Fact] + public void XmlDocumentation_Is_Overriden_By_DescriptionAttribute() + { + var snap = TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + + namespace TestNamespace; + + [QueryType] + internal static partial class Query + { + /// + /// This is ... + /// + [GraphQLDescription("Nothing")] + public static string GetUser() => "User"; + } + """); + + const string expected = "Nothing"; + + var emitted = DescriptionExtractorRegex().Matches(snap.Render()).Single().Groups; + Assert.Equal(expected, emitted[1].Value); + } + + [Fact] + public async Task XmlDocumentation_With_InheritdocCref_AllPossibleTargets() + { + await TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using HotChocolate.Types; + + namespace TestNamespace; + + /// + /// Type description. + /// + public class Foo + { + /// + /// Field description. + /// + public static int F = 0; + + /// + /// Property description. + /// + public static int P => 0; + + /// + /// Method description. + /// + public static int M() => 0; + + /// + /// Event description. + /// + public event EventHandler E; + + /// + /// Int-overloaded method description. + /// + public static Bar GetBar(int id) => new Bar(); + + /// + /// String-overloaded method description. + /// + public static Bar GetBar(string id) => new Bar(); + + public class Bar + { + /// + /// Nested type instance field description. + /// + public int B = 0; + } + } + + [QueryType] + public static partial class Query + { + /// + public static string? T() => null; + /// + public static string? F() => null; + /// + public static string? P() => null; + /// + public static string? M() => null; + /// + public static string? E() => null; + /// + public static string? Nested() => null; + /// + public static string? Overloaded(int id) => null; + } + """).MatchMarkdownAsync(); + } + + [Fact] + public void XmlDocumentation_With_Nested_InheritdocCref() + { + var snap = TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using HotChocolate.Types; + + namespace TestNamespace; + + /// + /// This type is similar useless to ''. + /// + public class FooType + { + } + + /// + /// The Bar type. + /// + public class BarType + { + } + + [QueryType] + public static partial class Query + { + /// + public static string? Foo() => null; + } + """); + + const string expected = "This type is similar useless to 'The Bar type.'."; + + var emitted = DescriptionExtractorRegex().Matches(snap.Render()).Single().Groups; + Assert.Equal(expected, emitted[1].Value); + } +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ObjectTypeXmlDocInferenceTests.XmlDocumentation_With_InheritdocCref_AllPossibleTargets.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ObjectTypeXmlDocInferenceTests.XmlDocumentation_With_InheritdocCref_AllPossibleTargets.md new file mode 100644 index 00000000000..773e7c221b8 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ObjectTypeXmlDocInferenceTests.XmlDocumentation_With_InheritdocCref_AllPossibleTargets.md @@ -0,0 +1,379 @@ +# XmlDocumentation_With_InheritdocCref_AllPossibleTargets + +## HotChocolateTypeModule.735550c.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class TestsTypesRequestExecutorBuilderExtensions + { + public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) + { + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.Query", + global::HotChocolate.Types.OperationTypeNames.Query, + () => global::TestNamespace.Query.Initialize)); + builder.ConfigureSchema( + b => b.TryAddRootType( + () => new global::HotChocolate.Types.ObjectType( + d => d.Name(global::HotChocolate.Types.OperationTypeNames.Query)), + HotChocolate.Language.OperationType.Query)); + return builder; + } + } +} + +``` + +## Query.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public static partial class Query + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var extension = descriptor.Extend(); + var configuration = extension.Configuration; + var thisType = typeof(global::TestNamespace.Query); + var bindingResolver = extension.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(bindingResolver); + + HotChocolate.Internal.ConfigurationHelper.ApplyConfiguration( + extension.Context, + descriptor, + null, + new global::HotChocolate.Types.QueryTypeAttribute()); + configuration.ConfigurationsAreApplied = true; + + var naming = descriptor.Extend().Context.Naming; + + descriptor + .Field(naming.GetMemberName("T", global::HotChocolate.Types.MemberKind.ObjectField)) + .ExtendWith(static (field, context) => + { + var configuration = field.Configuration; + var typeInspector = field.Context.TypeInspector; + var bindingResolver = field.Context.ParameterBindingResolver; + var naming = field.Context.Naming; + + configuration.Description = "Type description."; + configuration.Type = typeInspector.GetTypeRef(typeof(string), HotChocolate.Types.TypeContext.Output); + configuration.ResultType = typeof(string); + + configuration.SetSourceGeneratorFlags(); + + configuration.Resolvers = context.Resolvers.T(); + }, + (Resolvers: resolvers, ThisType: thisType)); + + descriptor + .Field(naming.GetMemberName("F", global::HotChocolate.Types.MemberKind.ObjectField)) + .ExtendWith(static (field, context) => + { + var configuration = field.Configuration; + var typeInspector = field.Context.TypeInspector; + var bindingResolver = field.Context.ParameterBindingResolver; + var naming = field.Context.Naming; + + configuration.Description = "Field description."; + configuration.Type = typeInspector.GetTypeRef(typeof(string), HotChocolate.Types.TypeContext.Output); + configuration.ResultType = typeof(string); + + configuration.SetSourceGeneratorFlags(); + + configuration.Resolvers = context.Resolvers.F(); + }, + (Resolvers: resolvers, ThisType: thisType)); + + descriptor + .Field(naming.GetMemberName("P", global::HotChocolate.Types.MemberKind.ObjectField)) + .ExtendWith(static (field, context) => + { + var configuration = field.Configuration; + var typeInspector = field.Context.TypeInspector; + var bindingResolver = field.Context.ParameterBindingResolver; + var naming = field.Context.Naming; + + configuration.Description = "Property description."; + configuration.Type = typeInspector.GetTypeRef(typeof(string), HotChocolate.Types.TypeContext.Output); + configuration.ResultType = typeof(string); + + configuration.SetSourceGeneratorFlags(); + + configuration.Resolvers = context.Resolvers.P(); + }, + (Resolvers: resolvers, ThisType: thisType)); + + descriptor + .Field(naming.GetMemberName("M", global::HotChocolate.Types.MemberKind.ObjectField)) + .ExtendWith(static (field, context) => + { + var configuration = field.Configuration; + var typeInspector = field.Context.TypeInspector; + var bindingResolver = field.Context.ParameterBindingResolver; + var naming = field.Context.Naming; + + configuration.Description = "Method description."; + configuration.Type = typeInspector.GetTypeRef(typeof(string), HotChocolate.Types.TypeContext.Output); + configuration.ResultType = typeof(string); + + configuration.SetSourceGeneratorFlags(); + + configuration.Resolvers = context.Resolvers.M(); + }, + (Resolvers: resolvers, ThisType: thisType)); + + descriptor + .Field(naming.GetMemberName("E", global::HotChocolate.Types.MemberKind.ObjectField)) + .ExtendWith(static (field, context) => + { + var configuration = field.Configuration; + var typeInspector = field.Context.TypeInspector; + var bindingResolver = field.Context.ParameterBindingResolver; + var naming = field.Context.Naming; + + configuration.Description = "Event description."; + configuration.Type = typeInspector.GetTypeRef(typeof(string), HotChocolate.Types.TypeContext.Output); + configuration.ResultType = typeof(string); + + configuration.SetSourceGeneratorFlags(); + + configuration.Resolvers = context.Resolvers.E(); + }, + (Resolvers: resolvers, ThisType: thisType)); + + descriptor + .Field(naming.GetMemberName("Nested", global::HotChocolate.Types.MemberKind.ObjectField)) + .ExtendWith(static (field, context) => + { + var configuration = field.Configuration; + var typeInspector = field.Context.TypeInspector; + var bindingResolver = field.Context.ParameterBindingResolver; + var naming = field.Context.Naming; + + configuration.Description = "Nested type instance field description."; + configuration.Type = typeInspector.GetTypeRef(typeof(string), HotChocolate.Types.TypeContext.Output); + configuration.ResultType = typeof(string); + + configuration.SetSourceGeneratorFlags(); + + configuration.Resolvers = context.Resolvers.Nested(); + }, + (Resolvers: resolvers, ThisType: thisType)); + + descriptor + .Field(naming.GetMemberName("Overloaded", global::HotChocolate.Types.MemberKind.ObjectField)) + .ExtendWith(static (field, context) => + { + var configuration = field.Configuration; + var typeInspector = field.Context.TypeInspector; + var bindingResolver = field.Context.ParameterBindingResolver; + var naming = field.Context.Naming; + + configuration.Description = "Int-overloaded method description."; + configuration.Type = typeInspector.GetTypeRef(typeof(string), HotChocolate.Types.TypeContext.Output); + configuration.ResultType = typeof(string); + + configuration.SetSourceGeneratorFlags(); + + var bindingInfo = field.Context.ParameterBindingResolver; + var parameter = context.Resolvers.CreateParameterDescriptor_Overloaded_id(); + var parameterInfo = bindingInfo.GetBindingInfo(parameter); + + if(parameterInfo.Kind is global::HotChocolate.Internal.ArgumentKind.Argument) + { + var argumentConfiguration = new global::HotChocolate.Types.Descriptors.Configurations.ArgumentConfiguration + { + Name = naming.GetMemberName("id", global::HotChocolate.Types.MemberKind.Argument), + Type = global::HotChocolate.Types.Descriptors.TypeReference.Create( + typeInspector.GetTypeRef(typeof(int), HotChocolate.Types.TypeContext.Input), + new global::HotChocolate.Language.NonNullTypeNode(new global::HotChocolate.Language.NamedTypeNode("int"))), + RuntimeType = typeof(int) + }; + + configuration.Arguments.Add(argumentConfiguration); + } + + configuration.Resolvers = context.Resolvers.Overloaded(); + }, + (Resolvers: resolvers, ThisType: thisType)); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + private readonly global::HotChocolate.Internal.IParameterBinding _binding_Overloaded_id; + + public __Resolvers(global::HotChocolate.Resolvers.ParameterBindingResolver bindingResolver) + { + _binding_Overloaded_id = bindingResolver.GetBinding(CreateParameterDescriptor_Overloaded_id()); + } + + public HotChocolate.Resolvers.FieldResolverDelegates T() + { + return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: T); + } + + private global::System.Object? T(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = global::TestNamespace.Query.T(); + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates F() + { + return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: F); + } + + private global::System.Object? F(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = global::TestNamespace.Query.F(); + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates P() + { + return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: P); + } + + private global::System.Object? P(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = global::TestNamespace.Query.P(); + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates M() + { + return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: M); + } + + private global::System.Object? M(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = global::TestNamespace.Query.M(); + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates E() + { + return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: E); + } + + private global::System.Object? E(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = global::TestNamespace.Query.E(); + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Nested() + { + return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Nested); + } + + private global::System.Object? Nested(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = global::TestNamespace.Query.Nested(); + return result; + } + + public global::HotChocolate.Internal.ParameterDescriptor CreateParameterDescriptor_Overloaded_id() + => new HotChocolate.Internal.ParameterDescriptor( + "id", + typeof(int), + isNullable: false, + []); + + public HotChocolate.Resolvers.FieldResolverDelegates Overloaded() + { + var isPureResolver = _binding_Overloaded_id.IsPure; + + return isPureResolver + ? new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Overloaded) + : new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: c => new(Overloaded(c))); + } + + private global::System.Object? Overloaded(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0 = _binding_Overloaded_id.Execute(context); + var result = global::TestNamespace.Query.Overloaded(args0); + return result; + } + } + } +} + + +``` + +## Compilation Diagnostics + +```json +[ + { + "Id": "CS0067", + "Title": "Event is never used", + "Severity": "Warning", + "WarningLevel": 3, + "Location": ": (28,30)-(28,31)", + "HelpLinkUri": "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS0067)", + "MessageFormat": "The event '{0}' is never used", + "Message": "The event 'Foo.E' is never used", + "Category": "Compiler", + "CustomTags": [ + "Compiler", + "Telemetry" + ] + } +] +``` + +## Assembly Emit Diagnostics + +```json +[ + { + "Id": "CS0067", + "Title": "Event is never used", + "Severity": "Warning", + "WarningLevel": 3, + "Location": ": (28,30)-(28,31)", + "HelpLinkUri": "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS0067)", + "MessageFormat": "The event '{0}' is never used", + "Message": "The event 'Foo.E' is never used", + "Category": "Compiler", + "CustomTags": [ + "Compiler", + "Telemetry" + ] + } +] +``` +