From 03063963d5f984818792ba545d1e8de23db863e6 Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Mon, 25 May 2026 10:07:58 +0800 Subject: [PATCH] feat(tool): emit inheritdoc for documented mapper interface members When generating mapper implementations from interfaces with XML documentation, include /// on the corresponding generated members so documentation flows to the implementation class. Fixes #565 Co-authored-by: Cursor --- .../ExpressionTranslator.cs | 16 ++++++++++ src/Mapster.Tool.Tests/Mappers/IUserMapper.cs | 5 +++ src/Mapster.Tool.Tests/Mappers/UserMapper.cs | 3 ++ .../Mapster.Tool.Tests.csproj | 1 + .../WhenGeneratingMapperWithDocumentation.cs | 18 +++++++++++ src/Mapster.Tool/Program.cs | 4 +++ src/Mapster.Tool/XmlDocumentationReader.cs | 32 +++++++++++++++++++ 7 files changed, 79 insertions(+) create mode 100644 src/Mapster.Tool.Tests/WhenGeneratingMapperWithDocumentation.cs create mode 100644 src/Mapster.Tool/XmlDocumentationReader.cs diff --git a/src/ExpressionTranslator/ExpressionTranslator.cs b/src/ExpressionTranslator/ExpressionTranslator.cs index 6c219f86..13874458 100644 --- a/src/ExpressionTranslator/ExpressionTranslator.cs +++ b/src/ExpressionTranslator/ExpressionTranslator.cs @@ -36,6 +36,8 @@ public class ExpressionTranslator : ExpressionVisitor private List? _properties; public List Properties => _properties ??= new List(); + private HashSet? _inheritDocMembers; + public bool HasDynamic { get; private set; } public TypeDefinitions? Definitions { get; } @@ -46,6 +48,18 @@ public ExpressionTranslator(TypeDefinitions? definitions = null) ResetIndentLevel(); } + public void AddInheritDocMember(string memberName) + { + _inheritDocMembers ??= new HashSet(); + _inheritDocMembers.Add(memberName); + } + + private void WriteInheritDocIfNeeded(string memberName) + { + if (_inheritDocMembers?.Contains(memberName) == true) + WriteNextLine("/// "); + } + private void ResetIndentLevel() { _indentLevel = 0; @@ -1204,6 +1218,7 @@ public Expression VisitLambda(LambdaExpression node, LambdaType type, string? me if (!isInternal) isInternal = node.ReturnType.GetTypeInfo().IsNotPublic || node.Parameters.Any(it => it.Type.GetTypeInfo().IsNotPublic); + WriteInheritDocIfNeeded(name); WriteModifierNextLine(isInternal ? "internal" : "public"); var funcType = MakeDelegateType(node.ReturnType, node.Parameters.Select(it => it.Type).ToArray()); var exprType = typeof(Expression<>).MakeGenericType(funcType); @@ -1238,6 +1253,7 @@ public Expression VisitLambda(LambdaExpression node, LambdaType type, string? me if (!isInternal) isInternal = node.ReturnType.GetTypeInfo().IsNotPublic || node.Parameters.Any(it => it.Type.GetTypeInfo().IsNotPublic); + WriteInheritDocIfNeeded(name); WriteModifierNextLine(isInternal ? "internal" : "public"); Methods[name] = node.Type; } diff --git a/src/Mapster.Tool.Tests/Mappers/IUserMapper.cs b/src/Mapster.Tool.Tests/Mappers/IUserMapper.cs index 5a62a4d4..5428f978 100644 --- a/src/Mapster.Tool.Tests/Mappers/IUserMapper.cs +++ b/src/Mapster.Tool.Tests/Mappers/IUserMapper.cs @@ -5,7 +5,12 @@ namespace Mapster.Tool.Tests.Mappers; [Mapper] public interface IUserMapper { + /// Gets the user projection expression. Expression> UserProjection { get; } + + /// Maps a user to a DTO. _UserDto MapTo(_User user); + + /// Maps a user onto an existing DTO. _UserDto MapTo(_User user, _UserDto userDto); } \ No newline at end of file diff --git a/src/Mapster.Tool.Tests/Mappers/UserMapper.cs b/src/Mapster.Tool.Tests/Mappers/UserMapper.cs index a6b18409..ff9d4ade 100644 --- a/src/Mapster.Tool.Tests/Mappers/UserMapper.cs +++ b/src/Mapster.Tool.Tests/Mappers/UserMapper.cs @@ -7,11 +7,13 @@ namespace Mapster.Tool.Tests.Mappers { public partial class UserMapper : IUserMapper { + /// public Expression> UserProjection => p1 => new _UserDto() { Id = p1.Id, Name = p1.Name }; + /// public _UserDto MapTo(_User p2) { return p2 == null ? null : new _UserDto() @@ -20,6 +22,7 @@ public _UserDto MapTo(_User p2) Name = p2.Name }; } + /// public _UserDto MapTo(_User p3, _UserDto p4) { if (p3 == null) diff --git a/src/Mapster.Tool.Tests/Mapster.Tool.Tests.csproj b/src/Mapster.Tool.Tests/Mapster.Tool.Tests.csproj index 32897bb6..b020e6bf 100644 --- a/src/Mapster.Tool.Tests/Mapster.Tool.Tests.csproj +++ b/src/Mapster.Tool.Tests/Mapster.Tool.Tests.csproj @@ -6,6 +6,7 @@ enable true false + true diff --git a/src/Mapster.Tool.Tests/WhenGeneratingMapperWithDocumentation.cs b/src/Mapster.Tool.Tests/WhenGeneratingMapperWithDocumentation.cs new file mode 100644 index 00000000..bb61bca2 --- /dev/null +++ b/src/Mapster.Tool.Tests/WhenGeneratingMapperWithDocumentation.cs @@ -0,0 +1,18 @@ +using FluentAssertions; + +namespace Mapster.Tool.Tests; + +public class WhenGeneratingMapperWithDocumentation +{ + [Fact] + public void GeneratedMapperShouldIncludeInheritDocForDocumentedMembers() + { + var generatedMapperPath = Path.GetFullPath( + Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "Mappers", "UserMapper.cs") + ); + + File.Exists(generatedMapperPath).Should().BeTrue(); + var content = File.ReadAllText(generatedMapperPath); + content.Should().Contain("/// "); + } +} diff --git a/src/Mapster.Tool/Program.cs b/src/Mapster.Tool/Program.cs index 1347cb13..7dc5cd30 100644 --- a/src/Mapster.Tool/Program.cs +++ b/src/Mapster.Tool/Program.cs @@ -138,6 +138,8 @@ private static void GenerateMappers(MapperOptions opt) var funcArgs = propArgs.GetGenericArguments(); var tuple = new TypeTuple(funcArgs[0], funcArgs[1]); var expr = config.CreateMapExpression(tuple, MapType.Projection); + if (XmlDocumentationReader.HasDocumentation(assembly, prop)) + translator.AddInheritDocMember(prop.Name); translator.VisitLambda( expr, ExpressionTranslator.LambdaType.PublicLambda, @@ -162,6 +164,8 @@ private static void GenerateMappers(MapperOptions opt) tuple, methodArgs.Length == 1 ? MapType.Map : MapType.MapToTarget ); + if (XmlDocumentationReader.HasDocumentation(assembly, method)) + translator.AddInheritDocMember(method.Name); translator.VisitLambda( expr, ExpressionTranslator.LambdaType.PublicMethod, diff --git a/src/Mapster.Tool/XmlDocumentationReader.cs b/src/Mapster.Tool/XmlDocumentationReader.cs new file mode 100644 index 00000000..0259eedc --- /dev/null +++ b/src/Mapster.Tool/XmlDocumentationReader.cs @@ -0,0 +1,32 @@ +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Metadata; +using System.Xml.Linq; + +namespace Mapster.Tool +{ + internal static class XmlDocumentationReader + { + public static bool HasDocumentation(Assembly assembly, MemberInfo member) + { + var assemblyPath = assembly.Location; + if (string.IsNullOrEmpty(assemblyPath)) + return false; + + var xmlPath = Path.ChangeExtension(assemblyPath, ".xml"); + if (!File.Exists(xmlPath)) + return false; + + var memberId = DocumentationCommentId.CreateMemberId(member); + var element = XDocument.Load(xmlPath) + .Root? + .Element("members")? + .Elements("member") + .FirstOrDefault(it => (string?)it.Attribute("name") == memberId); + + return element != null && + element.Elements().Any(it => !string.IsNullOrWhiteSpace(it.Value)); + } + } +}