Skip to content
Draft
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
73 changes: 49 additions & 24 deletions mdoc/Mono.Documentation/MDocUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3002,38 +3002,63 @@ static void ReorderNodes (XmlNode node, XmlNodeList children, string[] ordering)
"param",
"summary",
"typeparam",
"value", // for properties
};

private void UpdateExtensionMethods (XmlElement e, DocsNodeInfo info)
{
if (!writeIndex)
return;

MethodDefinition me = info.Member as MethodDefinition;
if (me == null)
return;
if (info.Parameters.Count < 1)
return;
if (!DocUtils.IsExtensionMethod (me))
return;
// Handle extension methods
if (info.Member is MethodDefinition method)
{
if (info.Parameters.Count < 1)
return;
if (!DocUtils.IsExtensionMethod(method))
return;

CreateExtensionMemberXml(e, info, "ExtensionMethod");
}
// Handle extension properties (including indexers)
else if (info.Member is PropertyDefinition property)
{
if (!DocUtils.IsExtensionProperty(property))
return;

XmlNode em = e.OwnerDocument.CreateElement ("ExtensionMethod");
XmlNode member = e.CloneNode (true);
em.AppendChild (member);
RemoveExcept (member, ValidExtensionMembers);
RemoveExcept (member.SelectSingleNode ("Docs"), ValidExtensionDocMembers);
WriteElementText (member, "MemberType", "ExtensionMethod");
XmlElement link = member.OwnerDocument.CreateElement ("Link");
var linktype = FormatterManager.SlashdocFormatter.GetName (me.DeclaringType);
var linkmember = FormatterManager.SlashdocFormatter.GetDeclaration (me);
link.SetAttribute ("Type", linktype);
link.SetAttribute ("Member", linkmember);
member.AppendChild (link);
AddTargets (em, info);

if (!IsMultiAssembly || (IsMultiAssembly && !extensionMethods.Any (ex => ex.SelectSingleNode ("Member/Link/@Type").Value == linktype && ex.SelectSingleNode ("Member/Link/@Member").Value == linkmember)))
{
extensionMethods.Add (em);
// Check if it's an indexer (has parameters) or regular property
string extensionType = DocUtils.IsExtensionIndexer(property) ? "ExtensionIndexer" : "ExtensionProperty";
CreateExtensionMemberXml(e, info, extensionType);
}
// Handle extension operators
else if (info.Member is MethodDefinition operatorMethod && DocUtils.IsExtensionOperator(operatorMethod))
{
if (info.Parameters.Count < 1)
return;

CreateExtensionMemberXml(e, info, "ExtensionOperator");
}
}

private void CreateExtensionMemberXml(XmlElement e, DocsNodeInfo info, string extensionMemberType)
{
XmlNode em = e.OwnerDocument.CreateElement(extensionMemberType);
XmlNode member = e.CloneNode(true);
em.AppendChild(member);
RemoveExcept(member, ValidExtensionMembers);
RemoveExcept(member.SelectSingleNode("Docs"), ValidExtensionDocMembers);
WriteElementText(member, "MemberType", extensionMemberType);
XmlElement link = member.OwnerDocument.CreateElement("Link");
var linktype = FormatterManager.SlashdocFormatter.GetName(info.Member.DeclaringType);
var linkmember = FormatterManager.SlashdocFormatter.GetDeclaration(info.Member);
link.SetAttribute("Type", linktype);
link.SetAttribute("Member", linkmember);
member.AppendChild(link);
AddTargets(em, info);

if (!IsMultiAssembly || (IsMultiAssembly && !extensionMethods.Any(ex => ex.SelectSingleNode("Member/Link/@Type").Value == linktype && ex.SelectSingleNode("Member/Link/@Member").Value == linkmember)))
{
extensionMethods.Add(em);
}
}

Expand Down
32 changes: 32 additions & 0 deletions mdoc/Mono.Documentation/Updater/DocUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,38 @@ public static bool IsExtensionMethod (MethodDefinition method)
.Any (m => m.AttributeType.FullName == "System.Runtime.CompilerServices.ExtensionAttribute");
}

public static bool IsExtensionProperty (PropertyDefinition property)
{
if (property == null) return false;

return
property.CustomAttributes
.Any (m => m.AttributeType.FullName == "System.Runtime.CompilerServices.ExtensionAttribute")
&& property.DeclaringType.CustomAttributes
.Any (m => m.AttributeType.FullName == "System.Runtime.CompilerServices.ExtensionAttribute");
}

public static bool IsExtensionIndexer (PropertyDefinition property)
{
if (property == null) return false;

// Indexers have parameters and are extension properties
return IsExtensionProperty(property) && property.Parameters.Count > 0;
}

public static bool IsExtensionOperator (MethodDefinition method)
{
if (method == null) return false;

// Extension operators are special methods that start with "op_" and have the ExtensionAttribute
return method.IsSpecialName
&& method.Name.StartsWith("op_", StringComparison.Ordinal)
&& method.CustomAttributes
.Any (m => m.AttributeType.FullName == "System.Runtime.CompilerServices.ExtensionAttribute")
&& method.DeclaringType.CustomAttributes
.Any (m => m.AttributeType.FullName == "System.Runtime.CompilerServices.ExtensionAttribute");
}

public static bool IsDelegate (TypeDefinition type)
{
TypeReference baseRef = type.BaseType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,23 @@ protected override string GetPropertyDeclaration (PropertyDefinition property)

if (property.Parameters.Count != 0)
{
AppendParameters (buf, method, property.Parameters, '[', ']');
// Handle extension indexers by adding "this" to the first parameter if it's an extension
if (DocUtils.IsExtensionIndexer(property))
{
buf.Append ('[');
buf.Append ("this ");
AppendParameter(buf, property.Parameters[0]);
for (int i = 1; i < property.Parameters.Count; ++i)
{
buf.Append(", ");
AppendParameter(buf, property.Parameters[i]);
}
buf.Append (']');
}
else
{
AppendParameters (buf, method, property.Parameters, '[', ']');
}
}

buf.Append (" {");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,23 @@ protected override string GetPropertyDeclaration(PropertyDefinition property)

if (property.Parameters.Count != 0)
{
AppendParameters(buf, method, property.Parameters, '(', ')');
// Handle extension indexers by adding extension marker to first parameter
if (DocUtils.IsExtensionIndexer(property))
{
buf.Append('(');
buf.Append("<Extension> ");
AppendParameter(buf, property.Parameters[0]);
for (int i = 1; i < property.Parameters.Count; ++i)
{
buf.Append(", ");
AppendParameter(buf, property.Parameters[i]);
}
buf.Append(')');
}
else
{
AppendParameters(buf, method, property.Parameters, '(', ')');
}
}
buf.Append(" As ");
buf.Append(GetTypeName(property.PropertyType, AttributeParserContext.Create(property)));
Expand Down
5 changes: 5 additions & 0 deletions mdoc/mdoc.Test/BasicTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ protected PropertyDefinition GetProperty(Type type, Func<PropertyDefinition, boo
return member;
}

protected PropertyDefinition GetProperty(Type type, string name)
{
return GetProperty(type, p => p.Name == name);
}

protected Dictionary<string, List<MemberReference>> GetClassInterface(TypeDefinition type)
{
return DocUtils.GetImplementedMembersFingerprintLookup(type);
Expand Down
80 changes: 80 additions & 0 deletions mdoc/mdoc.Test/DocUtilsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,5 +158,85 @@ public void HasCustomAttributeTest()
Assert.IsTrue(DocUtils.HasCustomAttribute(type, Consts.IsReadOnlyAttribute));
Assert.IsTrue(DocUtils.HasCustomAttribute(type, Consts.IsByRefLikeAttribute));
}

[Test]
public void IsExtensionMethod_WithExtensionAttribute_ReturnsTrue()
{
// Use existing extension method from test assembly
var type = GetType("DocTest.dll", "Mono.DocTest.Generic.Extensions");
var method = type.Methods.FirstOrDefault(m => m.Name == "ToEnumerable");

Assert.NotNull(method, "ToEnumerable extension method should exist");
Assert.IsTrue(DocUtils.IsExtensionMethod(method));
}

[Test]
public void IsExtensionMethod_WithoutExtensionAttribute_ReturnsFalse()
{
var method = GetMethod(typeof(SomeClass), nameof(SomeClass.get_Method));

Assert.IsFalse(DocUtils.IsExtensionMethod(method));
}

[Test]
public void IsExtensionMethod_WithNullMethod_ReturnsFalse()
{
Assert.IsFalse(DocUtils.IsExtensionMethod(null));
}

[Test]
public void IsExtensionProperty_WithNullProperty_ReturnsFalse()
{
Assert.IsFalse(DocUtils.IsExtensionProperty(null));
}

[Test]
public void IsExtensionProperty_WithRegularProperty_ReturnsFalse()
{
var property = GetProperty(typeof(SomeClass), nameof(SomeClass.Property));

Assert.IsFalse(DocUtils.IsExtensionProperty(property));
}

[Test]
public void IsExtensionIndexer_WithNullProperty_ReturnsFalse()
{
Assert.IsFalse(DocUtils.IsExtensionIndexer(null));
}

[Test]
public void IsExtensionIndexer_WithRegularProperty_ReturnsFalse()
{
var property = GetProperty(typeof(SomeClass), nameof(SomeClass.Property));

Assert.IsFalse(DocUtils.IsExtensionIndexer(property));
}

[Test]
public void IsExtensionOperator_WithNullMethod_ReturnsFalse()
{
Assert.IsFalse(DocUtils.IsExtensionOperator(null));
}

[Test]
public void IsExtensionOperator_WithRegularMethod_ReturnsFalse()
{
var method = GetMethod(typeof(SomeClass), nameof(SomeClass.get_Method));

Assert.IsFalse(DocUtils.IsExtensionOperator(method));
}

[Test]
public void IsExtensionOperator_WithRegularOperator_ReturnsFalse()
{
// Get a regular operator that doesn't have ExtensionAttribute
var type = GetType("DocTest.dll", "Mono.DocTest.Widget");
var operatorMethod = type.Methods.FirstOrDefault(m => m.Name == "op_Addition");

if (operatorMethod != null)
{
Assert.IsFalse(DocUtils.IsExtensionOperator(operatorMethod));
}
}
}
}
86 changes: 86 additions & 0 deletions mdoc/mdoc.Test/ExtensionMemberTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System;
using System.IO;
using System.Linq;
using System.Xml;
using NUnit.Framework;
using Mono.Documentation;
using Mono.Documentation.Updater;
using Mono.Cecil;

namespace mdoc.Test
{
[TestFixture]
public class ExtensionMemberTests : BasicTests
{
[Test]
public void ExtensionMembersTestLibrary_HasExtensionMethods()
{
// Use the existing test assembly that contains our SampleClasses
var extensionType = GetType(typeof(mdoc.Test.SampleClasses.ExtensionMembersExample));

Assert.IsNotNull(extensionType, "ExtensionMembersExample class should exist");

// Verify extension methods are detected correctly
var extensionMethods = extensionType.Methods.Where(DocUtils.IsExtensionMethod).ToList();
Assert.IsTrue(extensionMethods.Count >= 2, $"Should have at least 2 extension methods, found {extensionMethods.Count}");

// Verify specific extension methods exist
var getDisplayNameMethod = extensionMethods.FirstOrDefault(m => m.Name == "GetDisplayName");
Assert.IsNotNull(getDisplayNameMethod, "GetDisplayName extension method should exist");

var setNameAndValueMethod = extensionMethods.FirstOrDefault(m => m.Name == "SetNameAndValue");
Assert.IsNotNull(setNameAndValueMethod, "SetNameAndValue extension method should exist");
}

[Test]
public void ExtensionMemberDetection_WithNullValues_ReturnsfalseGracefully()
{
// Test null safety of our new extension member detection methods
Assert.IsFalse(DocUtils.IsExtensionProperty(null));
Assert.IsFalse(DocUtils.IsExtensionIndexer(null));
Assert.IsFalse(DocUtils.IsExtensionOperator(null));
}

[Test]
public void ExtensionMemberDetection_WithRegularMembers_ReturnsFalse()
{
// Test that regular members are not detected as extension members
var someClass = GetType(typeof(mdoc.Test.SampleClasses.SomeClass));

// Test regular property
var regularProperty = someClass.Properties.FirstOrDefault(p => p.Name == "Property");
if (regularProperty != null)
{
Assert.IsFalse(DocUtils.IsExtensionProperty(regularProperty));
Assert.IsFalse(DocUtils.IsExtensionIndexer(regularProperty));
}

// Test regular method
var regularMethod = someClass.Methods.FirstOrDefault(m => m.Name == "get_Method");
if (regularMethod != null)
{
Assert.IsFalse(DocUtils.IsExtensionMethod(regularMethod));
Assert.IsFalse(DocUtils.IsExtensionOperator(regularMethod));
}
}

[Test]
public void ExtensionMemberDetection_WithExtensionMethods_WorksCorrectly()
{
// Integration test that verifies extension member detection works
var extensionType = GetType(typeof(mdoc.Test.SampleClasses.ExtensionMembersExample));

// Test extension method detection
var extensionMethods = extensionType.Methods.Where(DocUtils.IsExtensionMethod).ToList();
Assert.IsTrue(extensionMethods.Count >= 2, $"Should detect extension methods, found {extensionMethods.Count}");

// Verify that extension attribute is properly detected
foreach (var method in extensionMethods)
{
Assert.IsTrue(method.CustomAttributes.Any(attr =>
attr.AttributeType.FullName == "System.Runtime.CompilerServices.ExtensionAttribute"),
$"Extension method {method.Name} should have ExtensionAttribute");
}
}
}
}
Loading