diff --git a/src/Microsoft.DocAsCode.DataContracts.ManagedReference/ApiParameter.cs b/src/Microsoft.DocAsCode.DataContracts.ManagedReference/ApiParameter.cs index 3b09a1da5b9..a391ac772fc 100644 --- a/src/Microsoft.DocAsCode.DataContracts.ManagedReference/ApiParameter.cs +++ b/src/Microsoft.DocAsCode.DataContracts.ManagedReference/ApiParameter.cs @@ -34,5 +34,14 @@ public class ApiParameter [JsonProperty("attributes")] [MergeOption(MergeOption.Ignore)] public List Attributes { get; set; } + + public void CopyInheritedData(ApiParameter src) + { + if (src == null) + throw new ArgumentNullException(nameof(src)); + + if (Description == null) + Description = src.Description; + } } } diff --git a/src/Microsoft.DocAsCode.DataContracts.ManagedReference/ExceptionInfo.cs b/src/Microsoft.DocAsCode.DataContracts.ManagedReference/ExceptionInfo.cs index 2c5fb4f0baa..e6733a4fea5 100644 --- a/src/Microsoft.DocAsCode.DataContracts.ManagedReference/ExceptionInfo.cs +++ b/src/Microsoft.DocAsCode.DataContracts.ManagedReference/ExceptionInfo.cs @@ -28,5 +28,10 @@ public class ExceptionInfo [JsonProperty("description")] [MarkdownContent] public string Description { get; set; } + + public ExceptionInfo Clone() + { + return (ExceptionInfo)MemberwiseClone(); + } } } diff --git a/src/Microsoft.DocAsCode.DataContracts.ManagedReference/LinkInfo.cs b/src/Microsoft.DocAsCode.DataContracts.ManagedReference/LinkInfo.cs index 4245e8b8b5f..d974c102208 100644 --- a/src/Microsoft.DocAsCode.DataContracts.ManagedReference/LinkInfo.cs +++ b/src/Microsoft.DocAsCode.DataContracts.ManagedReference/LinkInfo.cs @@ -29,6 +29,11 @@ public class LinkInfo [YamlMember(Alias = "altText")] [JsonProperty("altText")] public string AltText { get; set; } + + public LinkInfo Clone() + { + return (LinkInfo)MemberwiseClone(); + } } [Serializable] diff --git a/src/Microsoft.DocAsCode.Metadata.ManagedReference/Models/MetadataItem.cs b/src/Microsoft.DocAsCode.Metadata.ManagedReference/Models/MetadataItem.cs index f218dad9197..67e68375e0f 100644 --- a/src/Microsoft.DocAsCode.Metadata.ManagedReference/Models/MetadataItem.cs +++ b/src/Microsoft.DocAsCode.Metadata.ManagedReference/Models/MetadataItem.cs @@ -5,6 +5,7 @@ namespace Microsoft.DocAsCode.Metadata.ManagedReference { using System; using System.Collections.Generic; + using System.Linq; using Newtonsoft.Json; using YamlDotNet.Serialization; @@ -154,6 +155,10 @@ public class MetadataItem : ICloneable [JsonProperty("references")] public Dictionary References { get; set; } + [YamlIgnore] + [JsonIgnore] + public bool IsInheritDoc { get; set; } + [YamlIgnore] [JsonIgnore] public TripleSlashCommentModel CommentModel { get; set; } @@ -167,5 +172,30 @@ public object Clone() { return MemberwiseClone(); } + + public void CopyInheritedData(MetadataItem src) + { + if (src == null) + throw new ArgumentNullException(nameof(src)); + + if (Summary == null) + Summary = src.Summary; + if (Remarks == null) + Remarks = src.Remarks; + + if (Exceptions == null && src.Exceptions != null) + Exceptions = src.Exceptions.Select(e => e.Clone()).ToList(); + if (Sees == null && src.Sees != null) + Sees = src.Sees.Select(s => s.Clone()).ToList(); + if (SeeAlsos == null && src.SeeAlsos != null) + SeeAlsos = src.SeeAlsos.Select(s => s.Clone()).ToList(); + if (Examples == null && src.Examples != null) + Examples = new List(src.Examples); + + if (CommentModel != null && src.CommentModel != null) + CommentModel.CopyInheritedData(src.CommentModel); + if (Syntax != null && src.Syntax != null) + Syntax.CopyInheritedData(src.Syntax); + } } } diff --git a/src/Microsoft.DocAsCode.Metadata.ManagedReference/Models/SyntaxDetail.cs b/src/Microsoft.DocAsCode.Metadata.ManagedReference/Models/SyntaxDetail.cs index f8f9d91bc2c..e2b210cfb0e 100644 --- a/src/Microsoft.DocAsCode.Metadata.ManagedReference/Models/SyntaxDetail.cs +++ b/src/Microsoft.DocAsCode.Metadata.ManagedReference/Models/SyntaxDetail.cs @@ -3,6 +3,7 @@ namespace Microsoft.DocAsCode.Metadata.ManagedReference { + using System; using System.Collections.Generic; using Newtonsoft.Json; @@ -27,5 +28,31 @@ public class SyntaxDetail [YamlMember(Alias = "return")] [JsonProperty("return")] public ApiParameter Return { get; set; } + + public void CopyInheritedData(SyntaxDetail src) + { + if (src == null) + throw new ArgumentNullException(nameof(src)); + + CopyInheritedParameterList(Parameters, src.Parameters); + CopyInheritedParameterList(TypeParameters, src.TypeParameters); + if (Return != null && src.Return != null) + Return.CopyInheritedData(src.Return); + } + + static void CopyInheritedParameterList(List dest, List src) + { + if (dest == null || src == null || dest.Count != src.Count) + return; + for (int ndx = 0; ndx < dest.Count; ndx++) + { + var myParam = dest[ndx]; + var srcParam = src[ndx]; + if (myParam.Name == srcParam.Name && myParam.Type == srcParam.Type) + { + myParam.CopyInheritedData(srcParam); + } + } + } } } diff --git a/src/Microsoft.DocAsCode.Metadata.ManagedReference/Parsers/TripleSlashCommentModel.cs b/src/Microsoft.DocAsCode.Metadata.ManagedReference/Parsers/TripleSlashCommentModel.cs index 88ee44431cd..bdc29b1d092 100644 --- a/src/Microsoft.DocAsCode.Metadata.ManagedReference/Parsers/TripleSlashCommentModel.cs +++ b/src/Microsoft.DocAsCode.Metadata.ManagedReference/Parsers/TripleSlashCommentModel.cs @@ -36,6 +36,7 @@ public class TripleSlashCommentModel public List Examples { get; private set; } public Dictionary Parameters { get; private set; } public Dictionary TypeParameters { get; private set; } + public bool IsInheritDoc { get; private set; } private TripleSlashCommentModel(string xml, SyntaxLanguage language, ITripleSlashCommentParserContext context) { @@ -59,6 +60,7 @@ private TripleSlashCommentModel(string xml, SyntaxLanguage language, ITripleSlas Examples = GetExamples(nav, context); Parameters = GetParameters(nav, context); TypeParameters = GetTypeParameters(nav, context); + IsInheritDoc = GetIsInheritDoc(nav, context); } public static TripleSlashCommentModel CreateModel(string xml, SyntaxLanguage language, ITripleSlashCommentParserContext context) @@ -82,6 +84,32 @@ public static TripleSlashCommentModel CreateModel(string xml, SyntaxLanguage lan } } + public void CopyInheritedData(TripleSlashCommentModel src) + { + if (src == null) + throw new ArgumentNullException(nameof(src)); + + if (Summary == null) + Summary = src.Summary; + if (Remarks == null) + Remarks = src.Remarks; + if (Returns == null) + Returns = src.Returns; + + if (Exceptions == null && src.Exceptions != null) + Exceptions = src.Exceptions.Select(e => e.Clone()).ToList(); + if (Sees == null && src.Sees != null) + Sees = src.Sees.Select(s => s.Clone()).ToList(); + if (SeeAlsos == null && src.SeeAlsos != null) + SeeAlsos = src.SeeAlsos.Select(s => s.Clone()).ToList(); + if (Examples == null && src.Examples != null) + Examples = new List(src.Examples); + if (Parameters == null && src.Parameters != null) + Parameters = new Dictionary(src.Parameters); + if (TypeParameters == null && src.TypeParameters != null) + TypeParameters = new Dictionary(src.TypeParameters); + } + public string GetParameter(string name) { if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); @@ -215,6 +243,22 @@ private List GetExamples(XPathNavigator nav, ITripleSlashCommentParserCo return GetMultipleExampleNodes(nav, "/member/example").ToList(); } + private bool GetIsInheritDoc(XPathNavigator nav, ITripleSlashCommentParserContext context) + { + var node = nav.SelectSingleNode("/member/inheritdoc"); + if (node == null) + return false; + if (node.HasAttributes) + { + //The Sandcastle implementation of supports two attributes: 'cref' and 'select'. + //These attributes allow changing the source of the inherited doc and controlling what is inherited. + //Until these attributes are supported, ignoring inheritdoc elements with attributes, so as not to misinterpret them. + Logger.LogWarning("Attributes on elements are not supported; inheritdoc element will be ignored."); + return false; + } + return true; + } + private Dictionary GetListContent(XPathNavigator navigator, string xpath, string contentType, ITripleSlashCommentParserContext context) { var iterator = navigator.Select(xpath); diff --git a/src/Microsoft.DocAsCode.Metadata.ManagedReference/Resolvers/CopyInheritedDocumentation.cs b/src/Microsoft.DocAsCode.Metadata.ManagedReference/Resolvers/CopyInheritedDocumentation.cs new file mode 100644 index 00000000000..34c0e5ea16d --- /dev/null +++ b/src/Microsoft.DocAsCode.Metadata.ManagedReference/Resolvers/CopyInheritedDocumentation.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DocAsCode.Metadata.ManagedReference +{ + using Microsoft.DocAsCode.Common; + using Microsoft.DocAsCode.DataContracts.ManagedReference; + using System.Collections.Generic; + using System.Diagnostics; + + /// + /// Copies doc comments to items marked with 'inheritdoc' from interfaces and base classes. + /// + public class CopyInherited : IResolverPipeline + { + public void Run(MetadataModel yaml, ResolverContext context) + { + TreeIterator.Preorder(yaml.TocYamlViewModel, null, + s => s.IsInvalid ? null : s.Items, + (current, parent) => + { + if (current.IsInheritDoc) + InheritDoc(current, context); + return true; + }); + } + + static void InheritDoc(MetadataItem dest, ResolverContext context) + { + dest.IsInheritDoc = false; + + switch (dest.Type) + { + case MemberType.Constructor: + if (dest.Parent == null || dest.Syntax == null || dest.Syntax.Parameters == null) + return; + Debug.Assert(dest.Parent.Type == MemberType.Class); + + //try to find the base class + if (dest.Parent.Inheritance?.Count == 0) + return; + MetadataItem baseClass; + if (!context.Members.TryGetValue(dest.Parent.Inheritance[dest.Parent.Inheritance.Count - 1], out baseClass)) + return; + if (baseClass.Items == null) + return; + + //look a constructor in the base class which has a matching signature + foreach (var ctor in baseClass.Items) + { + if (ctor.Type != MemberType.Constructor) + continue; + if (ctor.Syntax == null || ctor.Syntax.Parameters == null) + continue; + if (ctor.Syntax.Parameters.Count != dest.Syntax.Parameters.Count) + continue; + + bool parametersMatch = true; + for (int ndx = 0; ndx < dest.Syntax.Parameters.Count; ndx++) + { + var myParam = dest.Syntax.Parameters[ndx]; + var baseParam = ctor.Syntax.Parameters[ndx]; + if (myParam.Name != baseParam.Name) + parametersMatch = false; + if (myParam.Type != baseParam.Type) + parametersMatch = false; + } + + if (parametersMatch) + { + Copy(dest, ctor, context); + return; + } + } + break; + + case MemberType.Method: + case MemberType.Property: + case MemberType.Event: + Copy(dest, dest.Overridden, context); + if (dest.Implements != null) + { + foreach (var item in dest.Implements) + { + Copy(dest, item, context); + } + } + break; + + case MemberType.Class: + if (dest.Inheritance.Count != 0) + { + Copy(dest, dest.Inheritance[dest.Inheritance.Count - 1], context); + } + if (dest.Implements != null) + { + foreach (var item in dest.Implements) + { + Copy(dest, item, context); + } + } + break; + } + } + + static void Copy(MetadataItem dest, string srcName, ResolverContext context) + { + MetadataItem src; + if (string.IsNullOrEmpty(srcName) || !context.Members.TryGetValue(srcName, out src)) + return; + + Copy(dest, src, context); + } + + static void Copy(MetadataItem dest, MetadataItem src, ResolverContext context) + { + if (src.IsInheritDoc) + { + InheritDoc(src, context); + } + + dest.CopyInheritedData(src); + } + } +} diff --git a/src/Microsoft.DocAsCode.Metadata.ManagedReference/Resolvers/ResolverContext.cs b/src/Microsoft.DocAsCode.Metadata.ManagedReference/Resolvers/ResolverContext.cs index 0f187772b23..6dee2cbc5fd 100644 --- a/src/Microsoft.DocAsCode.Metadata.ManagedReference/Resolvers/ResolverContext.cs +++ b/src/Microsoft.DocAsCode.Metadata.ManagedReference/Resolvers/ResolverContext.cs @@ -12,5 +12,7 @@ public class ResolverContext public bool PreserveRawInlineComments { get; set; } public Dictionary References { get; set; } + + public Dictionary Members { get; set; } } } diff --git a/src/Microsoft.DocAsCode.Metadata.ManagedReference/Resolvers/YamlMetadataResolver.cs b/src/Microsoft.DocAsCode.Metadata.ManagedReference/Resolvers/YamlMetadataResolver.cs index a5d4a828eaa..6774b50e621 100644 --- a/src/Microsoft.DocAsCode.Metadata.ManagedReference/Resolvers/YamlMetadataResolver.cs +++ b/src/Microsoft.DocAsCode.Metadata.ManagedReference/Resolvers/YamlMetadataResolver.cs @@ -15,6 +15,7 @@ public static class YamlMetadataResolver { new LayoutCheckAndCleanup(), new SetParent(), + new CopyInherited(), new ResolveReference(), new NormalizeSyntax(), new BuildMembers(), @@ -46,6 +47,7 @@ public static MetadataModel ResolveMetadata( { ApiFolder = apiFolder, References = allReferences, + Members = allMembers, PreserveRawInlineComments = preserveRawInlineComments, }; diff --git a/src/Microsoft.DocAsCode.Metadata.ManagedReference/Visitors/VisitorHelper.cs b/src/Microsoft.DocAsCode.Metadata.ManagedReference/Visitors/VisitorHelper.cs index 772af28f662..b7847ef4241 100644 --- a/src/Microsoft.DocAsCode.Metadata.ManagedReference/Visitors/VisitorHelper.cs +++ b/src/Microsoft.DocAsCode.Metadata.ManagedReference/Visitors/VisitorHelper.cs @@ -32,6 +32,7 @@ public static void FeedComments(MetadataItem item, ITripleSlashCommentParserCont item.Sees = commentModel.Sees; item.SeeAlsos = commentModel.SeeAlsos; item.Examples = commentModel.Examples; + item.IsInheritDoc = commentModel.IsInheritDoc; item.CommentModel = commentModel; } } diff --git a/test/Microsoft.DocAsCode.Metadata.ManagedReference.Tests/TripleSlashParserUnitTest.cs b/test/Microsoft.DocAsCode.Metadata.ManagedReference.Tests/TripleSlashParserUnitTest.cs index d4087f315ee..24a17f07e25 100644 --- a/test/Microsoft.DocAsCode.Metadata.ManagedReference.Tests/TripleSlashParserUnitTest.cs +++ b/test/Microsoft.DocAsCode.Metadata.ManagedReference.Tests/TripleSlashParserUnitTest.cs @@ -103,6 +103,7 @@ Check empty code. var commentModel = TripleSlashCommentModel.CreateModel(input, SyntaxLanguage.CSharp, context); + Assert.False(commentModel.IsInheritDoc, nameof(commentModel.IsInheritDoc)); var summary = commentModel.Summary; Assert.Equal(@" @@ -183,5 +184,25 @@ Check empty code. Assert.Equal("http://www.bing.com", seeAlsos[2].AltText); Assert.Equal("http://www.bing.com", seeAlsos[2].LinkId); } + + [Trait("Related", "TripleSlashComments")] + [Fact] + public void InheritDoc() + { + const string input = @" + + +"; + var context = new TripleSlashCommentParserContext + { + AddReferenceDelegate = null, + Normalize = true, + PreserveRawInlineComments = false, + }; + + var commentModel = TripleSlashCommentModel.CreateModel(input, SyntaxLanguage.CSharp, context); + Assert.True(commentModel.IsInheritDoc); + + } } }