Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make GetXmlDocsPath public and inheritdoc cref= functionality #83

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/Namotion.Reflection/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

[*.cs]

# Microsoft .NET properties
csharp_indent_braces = false
csharp_new_line_before_open_brace = control_blocks,local_functions,methods,types
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_parameter_list_parentheses = false

# ReSharper properties
resharper_blank_lines_after_block_statements = 0
resharper_blank_lines_after_control_transfer_statements = 1
resharper_case_block_braces = end_of_line
resharper_csharp_max_line_length = 135
resharper_csharp_space_within_parentheses = false
resharper_int_align_binary_expressions = false
resharper_int_align_nested_ternary = false
resharper_nested_ternary_style = compact
resharper_place_simple_embedded_statement_on_same_line = false
resharper_space_within_array_access_brackets = false
resharper_space_within_array_rank_brackets = false
resharper_space_within_if_parentheses = false
resharper_space_within_while_parentheses = false
2 changes: 1 addition & 1 deletion src/Namotion.Reflection/Performance/CachingXDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
namespace Namotion.Reflection
{
/// <summary>
/// Caching layer hiding the details of of accessing DLL documentation.
/// Caching layer hiding the details of accessing DLL documentation.
/// </summary>
internal class CachingXDocument
{
Expand Down
241 changes: 203 additions & 38 deletions src/Namotion.Reflection/XmlDocsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ public static string GetXmlDocs(this ParameterInfo parameter)
/// <param name="parameter">The reflected parameter or return info.</param>
/// <param name="pathToXmlFile">The path to the XML documentation file.</param>
/// <returns>The contents of the "returns" or "param" tag.</returns>
public static XElement? GetXmlDocsElement(this ParameterInfo parameter, string pathToXmlFile)
public static XElement? GetXmlDocsElement(this ParameterInfo parameter, string? pathToXmlFile)
{
try
{
Expand Down Expand Up @@ -398,47 +398,53 @@ public static string ToXmlDocsContent(this XElement? element)
}
}

private static XElement? GetXmlDocsWithoutLock(this MemberInfo member)
private static XElement? GetXmlDocsWithoutLock(this MemberInfo? member)
{
if (DynamicApis.SupportsXPathApis == false || DynamicApis.SupportsFileApis == false || DynamicApis.SupportsPathApis == false)
if (member is null)
{
return null;
}

var assemblyName = member.Module.Assembly.GetName();
if (DynamicApis.SupportsXPathApis == false || DynamicApis.SupportsFileApis == false ||
DynamicApis.SupportsPathApis == false)
{
return null;
}

var assemblyName = member.Module.Assembly?.GetName();
if (assemblyName is null)
{
return null;
}
if (IsAssemblyIgnored(assemblyName))
{
return null;
}

var documentationPath = GetXmlDocsPath(member.Module.Assembly);

return GetXmlDocsWithoutLock(member, documentationPath);
}

private static XElement? GetXmlDocsWithoutLock(this MemberInfo member, string? pathToXmlFile)
{
try
if (DynamicApis.SupportsXPathApis == false || DynamicApis.SupportsFileApis == false || DynamicApis.SupportsPathApis == false)
{
if (DynamicApis.SupportsXPathApis == false || DynamicApis.SupportsFileApis == false || DynamicApis.SupportsPathApis == false)
{
return null;
}

var assemblyName = member.Module.Assembly.GetName();
var document = TryGetXmlDocsDocument(assemblyName, pathToXmlFile);
if (document == null)
{
return null;
}

var element = GetXmlDocsElement(member, document);
ReplaceInheritdocElements(member, element);
return element;
return null;
}
catch

var assemblyName = member.Module.Assembly.GetName();
var document = TryGetXmlDocsDocument(assemblyName, pathToXmlFile);
if (document == null)
{
return null;
}

var element = GetXmlDocsElement(member, document);
ReplaceInheritdocElements(member, element);


return element;
}

private static CachingXDocument? TryGetXmlDocsDocument(AssemblyName assemblyName, string? pathToXmlFile)
Expand Down Expand Up @@ -490,7 +496,7 @@ private static bool IsAssemblyIgnored(AssemblyName assemblyName)
ReplaceInheritdocElements(parameter.Member, element);

IEnumerable result;
if (parameter.IsRetval || string.IsNullOrEmpty(parameter.Name))
if (parameter.IsRetval || String.IsNullOrEmpty(parameter.Name))
{
result = element.Elements("returns");
}
Expand Down Expand Up @@ -518,7 +524,17 @@ private static void ReplaceInheritdocElements(this MemberInfo member, XElement?
{
if (child.Name.LocalName.ToLowerInvariant() == "inheritdoc")
{
var baseType = member.DeclaringType.GetTypeInfo().BaseType;

#if !NETSTANDARD1_0
// if this is not a member of a class/type // is a Type itself
if ( member.MemberType == MemberTypes.TypeInfo ) {
ProcessInheritdocTypeElements( member, element, child );
continue;
}


#endif
var baseType = member.DeclaringType?.GetTypeInfo().BaseType;
var baseMember = baseType?.GetTypeInfo().DeclaredMembers.SingleOrDefault(m => m.Name == member.Name);
if (baseMember != null)
{
Expand All @@ -541,9 +557,143 @@ private static void ReplaceInheritdocElements(this MemberInfo member, XElement?
}
}

#if !NETSTANDARD1_0

/// <summary>
/// Get Type from a referencing string such as <c>!:MyType</c> or <c>!:MyType.MyProperty</c>
/// </summary>
/// <param name="referencingType">
/// The type whose documentation contains the reference to <paramref name="referencedTypeXmlId"/>
/// </param>
/// <param name="referencedTypeXmlId">
/// String within <c>inheritdoc cref=</c> referencing an unknown type (prefaced with <c>!:</c>
/// </param>
/// <returns></returns>
private static MemberInfo? resolveBrokenTypeReference(MemberInfo referencingType, string referencedTypeXmlId)
{
var matches = Regex.Match(
referencedTypeXmlId,
@"[A-Z!]:(?<FullName>(?<TypeName>[a-zA-Z]*)\.?(?<MemberName>[a-zA-Z]*)?)");
var referencedTypeName = matches.Groups["TypeName"].Value;
var referencedMemberName = matches.Groups["MemberName"].Value;
var lookupNamespace = referencingType.ReflectedType?.Namespace
?? referencingType.DeclaringType?.Namespace
?? (referencingType as Type)?.Namespace
?? throw new Exception($"failed to lookup namespace on type {referencingType}");
Type referencedType = referencingType.Module.Assembly.GetType(lookupNamespace + "." + referencedTypeName);
if (referencedType is not null)
{
return !String.IsNullOrEmpty(referencedMemberName)
? referencedType.GetMember(referencedMemberName).Single()
: referencedType;
}

return null;
}

private static void ProcessInheritdocTypeElements(this MemberInfo member, XElement element, XElement child)
{
var referencedTypeXmlId = child.Attribute("cref")?.Value;

if (referencedTypeXmlId is not null)
{
Match? matches;
string? referencedTypeName;
MemberInfo? referencedType = null;
Assembly? docAssembly = null;
switch ( referencedTypeXmlId[0] )
{
case 'P':
matches = Regex.Match(
referencedTypeXmlId,
@"(?<FullName>(?<FullTypeName>(?<AssemblyName>[a-zA-Z.]*)\.(?<TypeName>[a-zA-Z]*))\.(?<MemberName>[a-zA-Z]*))");
referencedTypeName = matches.Groups["FullTypeName"].Value;
break;
case '!':
referencedType = resolveBrokenTypeReference(member, referencedTypeXmlId);
referencedTypeName = referencedType?.Name;
docAssembly = referencedType?.Module.Assembly;
if (referencedType is not null)
{
referencedTypeXmlId = GetMemberElementName(referencedType);
}
break;
default:
matches = Regex.Match(
referencedTypeXmlId,
@"[A-Z]:(?<FullName>(?<Namespace>[a-zA-Z.]*)\.(?<TypeName>[a-zA-Z]*))");
referencedTypeName = matches.Groups["FullName"].Value;
break;
}


if (docAssembly is null && referencedTypeName is not null)
{
docAssembly = member.Module.Assembly;
referencedType = docAssembly.GetType(referencedTypeName);
// check member's assembly first
if (referencedType is null )
{
foreach ( var assembly in AppDomain.CurrentDomain.GetAssemblies() )
{
referencedType = assembly.GetType(referencedTypeName);
// limit the Assemblies that are searched by doing a basic name check.
if (referencedTypeXmlId.Contains(assembly.GetName().Name))
{
if (referencedType != null)
{
docAssembly = assembly;
break;
}
}
}
}
}

if (referencedType is null ||
docAssembly is null)
{
return;
}

XElement? referencedDocs = TryGetXmlDocsDocument(
docAssembly.GetName(),
GetXmlDocsPath(docAssembly)
)?.GetXmlDocsElement(referencedTypeXmlId);

/* for Record types ( as opposed to Class types ) the above lookup will fail for parameters defined in
* shorthand form on the Constructor. Constructor-defined Properties will show up on the constructor
* as <param name="PropertyName">...</param> rather than have the xml doc member element as a typical
* property would.
*/
if (referencedDocs is null && referencedType.MemberType == MemberTypes.Property)
{
var documentationPath = GetXmlDocsPath(member.Module.Assembly);
var parentElement = GetXmlDocsWithoutLock(
referencedType.DeclaringType.GetTypeInfo(),
documentationPath
);
referencedDocs = parentElement?
.Elements("param")?
.FirstOrDefault(x => x.Attribute("name")?
.Value == referencedType.Name);
// for records, replace node with the entirety of the found docs. So the whole <param> tag.
child.ReplaceWith(referencedDocs);
return;
}
if (referencedDocs != null)
{
var nodes = referencedDocs.Nodes().OfType<object>().ToArray();
child.ReplaceWith(nodes);
}
}
}


#endif
private static void ProcessInheritdocInterfaceElements(this MemberInfo member, XElement child)
{
foreach (var baseInterface in member.DeclaringType.GetTypeInfo().ImplementedInterfaces)
foreach (var baseInterface in member.DeclaringType?.GetTypeInfo().ImplementedInterfaces ?? new Type[]{})
{
var baseMember = baseInterface?.GetTypeInfo().DeclaredMembers.SingleOrDefault(m => m.Name == member.Name);
if (baseMember != null)
Expand Down Expand Up @@ -577,6 +727,11 @@ private static string RemoveLineBreakWhiteSpaces(string? documentation)
/// <exception cref="ArgumentException">Unknown member type.</exception>
internal static string GetMemberElementName(dynamic member)
{
if (member is null)
{
throw new ArgumentNullException( nameof(member) );
}

char prefixCode;
string memberName;
string memberTypeName;
Expand Down Expand Up @@ -613,8 +768,9 @@ internal static string GetMemberElementName(dynamic member)
}
else
{
memberName = member is Type type && !string.IsNullOrEmpty(memberType.FullName) ?
type.FullName.Split('[')[0] : ((string)member.DeclaringType.FullName).Split('[')[0] + "." + member.Name;
memberName = member is Type type && !String.IsNullOrEmpty(memberType.FullName) ?
type.FullName?.Split('[')[0] ?? ""
: ((string)member.DeclaringType.FullName).Split('[')[0] + "." + member.Name;

memberTypeName = (string)member.MemberType.ToString();
}
Expand Down Expand Up @@ -687,8 +843,18 @@ internal static string GetMemberElementName(dynamic member)
return string.Format("{0}:{1}", prefixCode, memberName.Replace("+", "."));
}

private static string? GetXmlDocsPath(dynamic? assembly)
/// <summary>
/// Retrieve path to XML documentation for <paramref name="assembly"/>
/// </summary>

#if NETSTANDARD1_0
public static string? GetXmlDocsPath(dynamic? assembly)
{
#else

public static string? GetXmlDocsPath(Assembly? assembly)
{
#endif
try
{
if (assembly == null)
Expand All @@ -708,12 +874,11 @@ internal static string GetMemberElementName(dynamic member)
return null;
}

try
{
string? path;
if (!string.IsNullOrEmpty(assembly.Location))
try {
string? path = assembly?.Location;
if (path is not null && !String.IsNullOrEmpty(path))
{
var assemblyDirectory = DynamicApis.PathGetDirectoryName((string)assembly.Location);
var assemblyDirectory = DynamicApis.PathGetDirectoryName(path);
path = DynamicApis.PathCombine(assemblyDirectory, assemblyName.Name + ".xml");
if (DynamicApis.FileExists(path))
{
Expand All @@ -723,7 +888,7 @@ internal static string GetMemberElementName(dynamic member)

if (ObjectExtensions.HasProperty(assembly, "CodeBase"))
{
var codeBase = (string)assembly.CodeBase;
var codeBase = (string)assembly?.CodeBase!;
if (!string.IsNullOrEmpty(codeBase))
{
path = DynamicApis.PathCombine(DynamicApis.PathGetDirectoryName(codeBase
Expand All @@ -741,15 +906,15 @@ internal static string GetMemberElementName(dynamic member)
if (currentDomain?.HasProperty("BaseDirectory") == true)
{
var baseDirectory = currentDomain.TryGetPropertyValue("BaseDirectory", "");
if (!string.IsNullOrEmpty(baseDirectory))
if (!String.IsNullOrEmpty(baseDirectory))
{
path = DynamicApis.PathCombine(baseDirectory, assemblyName.Name + ".xml");
path = DynamicApis.PathCombine(baseDirectory!, assemblyName.Name + ".xml");
if (DynamicApis.FileExists(path))
{
return path;
}

path = DynamicApis.PathCombine(baseDirectory, "bin/" + assemblyName.Name + ".xml");
path = DynamicApis.PathCombine(baseDirectory!, "bin/" + assemblyName.Name + ".xml");
if (DynamicApis.FileExists(path))
{
return path;
Expand All @@ -758,13 +923,13 @@ internal static string GetMemberElementName(dynamic member)
}

var currentDirectory = DynamicApis.DirectoryGetCurrentDirectory();
path = DynamicApis.PathCombine(currentDirectory, assembly.GetName().Name + ".xml");
path = DynamicApis.PathCombine(currentDirectory, assembly?.GetName().Name + ".xml");
if (DynamicApis.FileExists(path))
{
return path;
}

path = DynamicApis.PathCombine(currentDirectory, "bin/" + assembly.GetName().Name + ".xml");
path = DynamicApis.PathCombine(currentDirectory, "bin/" + assembly?.GetName().Name + ".xml");
if (DynamicApis.FileExists(path))
{
return path;
Expand Down