Skip to content

Commit 03b04ba

Browse files
author
Dmitriy
authored
inheritdoc cref support (#109)
* - added cfer paramter in xml comments for referencing types * - updated XmlDocsExtensions logic for nested and generic types - added unit tests for these cases: inheritdoc cref for property inheritdoc cref for type inheritdoc cref for generic property inheritdoc cref fot nested class property
1 parent 776fb29 commit 03b04ba

File tree

2 files changed

+270
-5
lines changed

2 files changed

+270
-5
lines changed

src/Namotion.Reflection.Tests/XmlDocsExtensionsTests.cs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,126 @@ namespace Namotion.Reflection.Tests
1414
{
1515
public class XmlDocsExtensionsTests
1616
{
17+
/// <summary>WithSummary</summary>
18+
public class SummaryXmlDoc
19+
{
20+
/// <summary>Foo</summary>
21+
public string Foo { get; set; }
22+
}
23+
24+
public class CrefInheritXmlDoc
25+
{
26+
/// <inheritdoc cref="SummaryXmlDoc.Foo"/>
27+
public string Bar { get; set; }
28+
}
29+
30+
[Fact]
31+
public void When_xml_doc_with_cref_inheritdoc_on_property_is_read_then_inherited_type_is_valid()
32+
{
33+
// Arrange
34+
XmlDocs.ClearCache();
35+
36+
// Act
37+
var fooElement = typeof(SummaryXmlDoc).GetProperty("Foo").GetXmlDocsElement();
38+
var fooResponse = fooElement.Elements("summary");
39+
var fooValue = fooResponse.Single().Value;
40+
41+
var barElement = typeof(CrefInheritXmlDoc).GetProperty("Bar").GetXmlDocsElement();
42+
var barResponse = barElement.Elements("summary");
43+
var varValue = barResponse.Single().Value;
44+
45+
Assert.Equal(fooValue, varValue);
46+
}
47+
48+
public class CrefInheritXmlDocForType
49+
{
50+
/// <inheritdoc cref="SummaryXmlDoc"/>
51+
public string Bar { get; set; }
52+
}
53+
54+
[Fact]
55+
public void When_xml_doc_with_cref_inheritdoc_on_type_is_read_then_inherited_type_is_valid()
56+
{
57+
// Arrange
58+
XmlDocs.ClearCache();
59+
60+
// Act
61+
var summaryElement = typeof(SummaryXmlDoc).GetXmlDocsElement();
62+
var summaryResponse = summaryElement.Elements("summary");
63+
var summaryValue = summaryResponse.Single().Value;
64+
65+
var barElement = typeof(CrefInheritXmlDocForType).GetProperty("Bar").GetXmlDocsElement();
66+
var barResponse = barElement.Elements("summary");
67+
var barValue = barResponse.Single().Value;
68+
69+
Assert.Equal(summaryValue, barValue);
70+
}
71+
72+
/// <summary>WithSummary</summary>
73+
public class SummaryXmlDocGeneric<T>
74+
{
75+
/// <summary>FooGeneric</summary>
76+
public List<T> Foo { get; set; }
77+
}
78+
79+
public class CrefInheritXmlDocGeneric
80+
{
81+
/// <inheritdoc cref="SummaryXmlDocGeneric{T}.Foo"/>
82+
public string Bar { get; set; }
83+
}
84+
85+
[Fact]
86+
public void When_xml_doc_with_cref_inheritdoc_on_generic_property_is_read_then_inherited_type_is_valid()
87+
{
88+
// Arrange
89+
XmlDocs.ClearCache();
90+
91+
// Act
92+
var fooElement = typeof(SummaryXmlDocGeneric<object>).GetProperty("Foo").GetXmlDocsElement();
93+
var fooResponse = fooElement.Elements("summary");
94+
var fooValue = fooResponse.Single().Value;
95+
96+
var barElement = typeof(CrefInheritXmlDocGeneric).GetProperty("Bar").GetXmlDocsElement();
97+
var barResponse = barElement.Elements("summary");
98+
var barValue = barResponse.Single().Value;
99+
100+
Assert.Equal(fooValue, barValue);
101+
}
102+
103+
/// <summary>WithSummary</summary>
104+
public class SummaryXmlDocParent
105+
{
106+
public class SummaryXmlDocChild
107+
{
108+
/// <summary>Foo</summary>
109+
public string Foo { get; set; }
110+
}
111+
}
112+
113+
public class CrefInheritNestedXmlDoc
114+
{
115+
/// <inheritdoc cref="SummaryXmlDocParent.SummaryXmlDocChild.Foo"/>
116+
public string Bar { get; set; }
117+
}
118+
119+
[Fact]
120+
public void When_xml_doc_with_cref_inheritdoc_on_nested_class_property_is_read_then_inherited_type_is_valid()
121+
{
122+
// Arrange
123+
XmlDocs.ClearCache();
124+
125+
// Act
126+
var fooElement = typeof(SummaryXmlDocParent.SummaryXmlDocChild).GetProperty("Foo").GetXmlDocsElement();
127+
var fooResponse = fooElement.Elements("summary");
128+
var fooValue = fooResponse.Single().Value;
129+
130+
var barElement = typeof(CrefInheritNestedXmlDoc).GetProperty("Bar").GetXmlDocsElement();
131+
var barResponse = barElement.Elements("summary");
132+
var barValue = barResponse.Single().Value;
133+
134+
Assert.Equal(fooValue, barValue);
135+
}
136+
17137
public class WithComplexXmlDoc
18138
{
19139
/// <summary>

src/Namotion.Reflection/XmlDocsExtensions.cs

Lines changed: 150 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public static void ClearCache()
4040
public static class XmlDocsExtensions
4141
{
4242
private static readonly ConcurrentDictionary<string, CachingXDocument?> Cache =
43-
new ConcurrentDictionary<string, CachingXDocument?>(StringComparer.OrdinalIgnoreCase);
43+
new(StringComparer.OrdinalIgnoreCase);
4444

4545
internal static void ClearCache()
4646
{
@@ -218,7 +218,9 @@ public static string GetXmlDocsRemarks(this MemberInfo member, bool resolveExter
218218
{
219219
try
220220
{
221-
if (DynamicApis.SupportsXPathApis == false || DynamicApis.SupportsFileApis == false || DynamicApis.SupportsPathApis == false)
221+
if (DynamicApis.SupportsXPathApis == false
222+
|| DynamicApis.SupportsFileApis == false
223+
|| DynamicApis.SupportsPathApis == false)
222224
{
223225
return null;
224226
}
@@ -503,7 +505,10 @@ private static bool IsAssemblyIgnored(AssemblyName assemblyName, bool resolveExt
503505
return null;
504506
}
505507

506-
private static void ReplaceInheritdocElements(this MemberInfo member, XElement? element, bool resolveExternalXmlDocs = true)
508+
private static void ReplaceInheritdocElements(
509+
this MemberInfo member,
510+
XElement? element,
511+
bool resolveExternalXmlDocs = true)
507512
{
508513
#if !NET40
509514
if (element == null)
@@ -516,6 +521,14 @@ private static void ReplaceInheritdocElements(this MemberInfo member, XElement?
516521
{
517522
if (child.Name.LocalName.ToLowerInvariant() == "inheritdoc")
518523
{
524+
#if !NETSTANDARD1_0
525+
// if this a class/type
526+
if (child.HasAttributes && (member.MemberType is MemberTypes.TypeInfo or MemberTypes.Property))
527+
{
528+
ProcessInheritDocTypeElements(member, element, child);
529+
continue;
530+
}
531+
#endif
519532
var baseType = member.DeclaringType.GetTypeInfo().BaseType;
520533
var baseMember = baseType?.GetTypeInfo().DeclaredMembers.SingleOrDefault(m => m.Name == member.Name);
521534
if (baseMember != null)
@@ -541,7 +554,7 @@ private static void ReplaceInheritdocElements(this MemberInfo member, XElement?
541554

542555
private static void ProcessInheritdocInterfaceElements(this MemberInfo member, XElement child, bool resolveExternalXmlDocs = true)
543556
{
544-
foreach (var baseInterface in member.DeclaringType.GetTypeInfo().ImplementedInterfaces)
557+
foreach (var baseInterface in member.DeclaringType.GetTypeInfo().ImplementedInterfaces ?? new Type[]{})
545558
{
546559
var baseMember = baseInterface?.GetTypeInfo().DeclaredMembers.SingleOrDefault(m => m.Name == member.Name);
547560
if (baseMember != null)
@@ -582,6 +595,11 @@ internal static string GetMemberElementName(dynamic member)
582595
string memberName;
583596
string memberTypeName;
584597

598+
if (member is null)
599+
{
600+
throw new ArgumentNullException(nameof(member));
601+
}
602+
585603
if (member is MemberInfo memberInfo &&
586604
memberInfo.DeclaringType != null &&
587605
memberInfo.DeclaringType.GetTypeInfo().IsGenericType)
@@ -692,8 +710,14 @@ internal static string GetMemberElementName(dynamic member)
692710
return string.Format("{0}:{1}", prefixCode, memberName.Replace("+", "."));
693711
}
694712

695-
private static string? GetXmlDocsPath(dynamic? assembly, bool resolveExternalXmlDocs = true)
713+
#if NETSTANDARD1_0
714+
// ReSharper disable once MemberCanBePrivate.Global
715+
public static string? GetXmlDocsPath(dynamic? assembly, bool resolveExternalXmlDocs = true)
696716
{
717+
#else
718+
public static string? GetXmlDocsPath(Assembly? assembly, bool resolveExternalXmlDocs = true)
719+
{
720+
#endif
697721
try
698722
{
699723
if (assembly == null)
@@ -807,6 +831,127 @@ internal static string GetMemberElementName(dynamic member)
807831
return null;
808832
}
809833
}
834+
835+
#if !NETSTANDARD1_0
836+
837+
/// <summary>
838+
/// Get Type from a referencing string such as <c>!:MyType</c> or <c>!:MyType.MyProperty</c>
839+
/// </summary>
840+
/// <param name="referencingType">
841+
/// The type whose documentation contains the reference to <paramref name="referencedTypeXmlId"/>
842+
/// </param>
843+
/// <param name="referencedTypeXmlId">
844+
/// String within <c>inheritdoc cref=</c> referencing an unknown type (prefaced with <c>!:</c>
845+
/// </param>
846+
/// <returns></returns>
847+
private static void ProcessInheritDocTypeElements(this MemberInfo member, XElement element, XElement child)
848+
{
849+
var referencedTypeXmlId = child.Attribute("cref")?.Value;
850+
851+
if (referencedTypeXmlId is not null)
852+
{
853+
Match? matches;
854+
string? referencedTypeName;
855+
MemberInfo? referencedType = null;
856+
Assembly? docAssembly = null;
857+
switch (referencedTypeXmlId[0])
858+
{
859+
case 'P':
860+
matches = Regex.Match(
861+
referencedTypeXmlId,
862+
@"(?<FullName>(?<FullTypeName>(?<AssemblyName>[a-zA-Z.]*)\.(?<TypeName>[a-zA-Z]*))\.(?<MemberName>[a-zA-Z]*))");
863+
referencedTypeName = matches.Groups["FullTypeName"].Value;
864+
break;
865+
default:
866+
matches = Regex.Match(
867+
referencedTypeXmlId,
868+
@"[A-Z]:(?<FullName>(?<Namespace>[a-zA-Z.]*)\.(?<TypeName>[a-zA-Z]*))");
869+
referencedTypeName = matches.Groups["FullName"].Value;
870+
break;
871+
}
872+
873+
if (docAssembly is null && referencedTypeName is not null)
874+
{
875+
docAssembly = member.Module.Assembly;
876+
referencedType = docAssembly.GetType(referencedTypeName);
877+
// check member's assembly first
878+
if (referencedType is null)
879+
{
880+
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
881+
{
882+
// limit the Assemblies that are searched by doing a basic name check.
883+
if (referencedTypeXmlId.Contains(assembly.GetName().Name))
884+
{
885+
referencedType = GetTypeByXmlDocTypeName(referencedTypeName, assembly);
886+
if (referencedType != null)
887+
{
888+
docAssembly = assembly;
889+
break;
890+
}
891+
}
892+
}
893+
}
894+
}
895+
896+
if (referencedType is null ||
897+
docAssembly is null)
898+
{
899+
return;
900+
}
901+
902+
var referencedDocs = TryGetXmlDocsDocument(
903+
docAssembly.GetName(),
904+
GetXmlDocsPath(docAssembly),
905+
true
906+
)?.GetXmlDocsElement(referencedTypeXmlId);
907+
908+
/* for Record types ( as opposed to Class types ) the above lookup will fail for parameters defined in
909+
* shorthand form on the Constructor. Constructor-defined Properties will show up on the constructor
910+
* as <param name="PropertyName">...</param> rather than have the xml doc member element as a typical
911+
* property would.
912+
*/
913+
if (referencedDocs is null && referencedType.MemberType == MemberTypes.Property)
914+
{
915+
var documentationPath = GetXmlDocsPath(member.Module.Assembly);
916+
if (documentationPath is null)
917+
return;
918+
var parentElement = GetXmlDocsElement(
919+
referencedType.DeclaringType.GetTypeInfo(),
920+
documentationPath
921+
);
922+
referencedDocs = parentElement?
923+
.Elements("param")?
924+
.FirstOrDefault(x => x.Attribute("name")?
925+
.Value == referencedType.Name);
926+
// for records, replace node with the entirety of the found docs. So the whole <param> tag.
927+
child.ReplaceWith(referencedDocs);
928+
return;
929+
}
930+
931+
if (referencedDocs != null)
932+
{
933+
var nodes = referencedDocs.Nodes().OfType<object>().ToArray();
934+
child.ReplaceWith(nodes);
935+
}
936+
}
937+
}
938+
939+
private static Type? GetTypeByXmlDocTypeName(string xmlDocTypeName, Assembly assembly)
940+
{
941+
var assemblyTypeNames = assembly.GetTypes()
942+
.Select(type => new KeyValuePair<string, Type>(NormalizeTypeName(type.FullName!), type))
943+
.ToDictionary(x => x.Key, x => x.Value);
944+
assemblyTypeNames.TryGetValue(NormalizeTypeName(xmlDocTypeName), out var resultType);
945+
return resultType;
946+
}
947+
948+
private static string NormalizeTypeName(string typeName)
949+
{
950+
return typeName
951+
.Replace(".", string.Empty)
952+
.Replace("+", string.Empty);
953+
}
954+
#endif
810955

811956
private static string? GetPathByOs(dynamic? assembly, AssemblyName assemblyName)
812957
{

0 commit comments

Comments
 (0)