From 9132c37d132c7c283b2df55ba4a0f789e009a27e Mon Sep 17 00:00:00 2001 From: Jose Date: Thu, 30 Apr 2026 12:59:18 +0200 Subject: [PATCH] Map TypeAlias doc links to the underlying C# type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #4494 For inline @link to a TypeAlias, emit when the mapped type is non-generic, or MappedType (XML-escaped) when it is generic — closed generics like IList have no valid C# cref form, so a clickable link isn't possible. For @see, emit only for non-generic mapped types and skip generic ones. Add typealias coverage to DocumentationTests.slice for non-generic, sequence, dictionary, and result mappings. --- .../DocCommentFormatter.cs | 31 +++++++++++++++++-- .../DocumentationTests.slice | 28 +++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/ZeroC.Slice.Generator/DocCommentFormatter.cs b/src/ZeroC.Slice.Generator/DocCommentFormatter.cs index 9a19bf2949..68c9faadb6 100644 --- a/src/ZeroC.Slice.Generator/DocCommentFormatter.cs +++ b/src/ZeroC.Slice.Generator/DocCommentFormatter.cs @@ -36,7 +36,22 @@ internal static IEnumerable FormatSeeAlsoTags(Comment? comment, stri foreach (CommentLink link in seeTags) { - if (link is ResolvedCommentLink r) + if (link is not ResolvedCommentLink r) + { + continue; + } + + if (r.Entity is TypeAlias alias) + { + // Type aliases don't generate a C# type; map to the underlying C# type. We can't reliably + // produce a seealso for a generic mapped type without C# parsing, so skip those. + string mapped = alias.UnderlyingType.Type.ToTypeString(currentNamespace); + if (!mapped.Contains('<', StringComparison.Ordinal)) + { + yield return new CommentTag("seealso", "cref", mapped, ""); + } + } + else { yield return new CommentTag("seealso", "cref", FormatEntityCref(r.Entity, currentNamespace), ""); } @@ -45,13 +60,23 @@ internal static IEnumerable FormatSeeAlsoTags(Comment? comment, stri private static string FormatInlineLink(CommentLink link, string currentNamespace) => link switch { - // Type aliases don't generate C# types, so output the identifier as plain text. - ResolvedCommentLink { Entity: TypeAlias } r => $"{CommentTag.XmlEscape(r.Entity.Identifier)}", + ResolvedCommentLink { Entity: TypeAlias alias } => FormatTypeAliasInline(alias, currentNamespace), ResolvedCommentLink r => $"""""", UnresolvedCommentLink u => $"{CommentTag.XmlEscape(u.Identifier)}", _ => "" }; + private static string FormatTypeAliasInline(TypeAlias alias, string currentNamespace) + { + // Type aliases don't generate a C# type; link to the underlying C# type instead. For generic mapped + // types we emit the type name as inline code (XML-escaped) to avoid constructing cref-friendly forms + // like IList{T} or IDictionary{T, U}. + string mapped = alias.UnderlyingType.Type.ToTypeString(currentNamespace); + return mapped.Contains('<', StringComparison.Ordinal) + ? $"{CommentTag.XmlEscape(mapped)}" + : $""""""; + } + private static string FormatEntityCref(Entity entity, string currentNamespace) { string name = entity switch diff --git a/tests/IceRpc.Slice.Generator.Tests/DocumentationTests.slice b/tests/IceRpc.Slice.Generator.Tests/DocumentationTests.slice index 721b120a1a..a3a5ee0c7c 100644 --- a/tests/IceRpc.Slice.Generator.Tests/DocumentationTests.slice +++ b/tests/IceRpc.Slice.Generator.Tests/DocumentationTests.slice @@ -4,7 +4,35 @@ module IceRpc::Slice::Generator::Tests // This file contains Slice definitions for testing the mapping of doc comments +// Type aliases used to test how doc comment links to a typealias are mapped to its underlying C# type: +// - UserName maps to a non-generic C# type (string), so links resolve to . +// - UserList, UserMap, UserResult map to generic C# types, so inline @link is emitted as ... +// (XML-escaped) and @see is skipped. +typealias UserName = string typealias UserList = Sequence +typealias UserMap = Dictionary +typealias UserResult = Result + +/// Tests how doc comment links to type aliases are mapped to their underlying C# types. +/// The {@link UserName} alias maps to a non-generic type while {@link UserList}, +/// {@link UserMap}, and {@link UserResult} map to generic types. +/// @see UserName +/// @see UserList +/// @see UserMap +/// @see UserResult +interface MyTypealiasDocs { + /// Returns the {@link UserName} for the active user. + getName() -> UserName + + /// Returns a {@link UserList} of active user names. + getNames() -> UserList + + /// Returns a {@link UserMap} keyed by user name. + getSessions() -> UserMap + + /// Returns a {@link UserResult} containing the session or an error message. + getResult() -> UserResult +} /// This is the session manager interface summary. The caller must use the {@link MyAuthorizationManager::createAuthToken} /// operation to create an authentication token before creating a session.