diff --git a/src/Authoring/WinRT.SourceGenerator/WinRTTypeWriter.cs b/src/Authoring/WinRT.SourceGenerator/WinRTTypeWriter.cs index c163ff2dd..b17ea3a44 100644 --- a/src/Authoring/WinRT.SourceGenerator/WinRTTypeWriter.cs +++ b/src/Authoring/WinRT.SourceGenerator/WinRTTypeWriter.cs @@ -1552,7 +1552,9 @@ private void AddOverloadAttribute(EntityHandle methodHandle, string methodName) private void AddOverloadAttributeForInterfaceMethods(TypeDeclaration interfaceTypeDeclaration) { - // Generate unique names for any overloaded methods + // Generate unique names for any overloaded methods. + // If the user has applied [OverloadAttribute("name")] on a method, honor that name + // instead of auto-generating a sequential suffix. foreach (var methodName in interfaceTypeDeclaration.MethodsByName.Where(symbol => symbol.Value.Count > 1)) { var methodSymbols = methodName.Value.Where(symbol => symbol is IMethodSymbol); @@ -1569,10 +1571,34 @@ private void AddOverloadAttributeForInterfaceMethods(TypeDeclaration interfaceTy continue; } - string overloadedMethodName = methodName.Key + (++lastSuffix); - while (interfaceTypeDeclaration.MethodsByName.ContainsKey(overloadedMethodName)) + // Check if the user explicitly applied [Windows.Foundation.Metadata.OverloadAttribute("...")] + // and honor the user-specified name instead of auto-generating one. + string overloadedMethodName = null; + if (method is IMethodSymbol methodSymbol) + { + var userOverloadAttr = methodSymbol.GetAttributes() + .FirstOrDefault(a => + a.AttributeClass?.ToDisplayString() == "Windows.Foundation.Metadata.OverloadAttribute"); + if (userOverloadAttr != null + && userOverloadAttr.ConstructorArguments is { IsDefaultOrEmpty: false } + && userOverloadAttr.ConstructorArguments[0].Value is string userSpecifiedName) + { + overloadedMethodName = userSpecifiedName; + Logger.Log("Using user-specified overload name for " + methodSymbol.Name + ": " + overloadedMethodName); + } + else if (userOverloadAttr != null) + { + Logger.Log("warning: [OverloadAttribute] found on " + methodSymbol.Name + " but could not extract name; auto-generating"); + } + } + + if (overloadedMethodName == null) { overloadedMethodName = methodName.Key + (++lastSuffix); + while (interfaceTypeDeclaration.MethodsByName.ContainsKey(overloadedMethodName)) + { + overloadedMethodName = methodName.Key + (++lastSuffix); + } } Logger.Log("Overloading " + methodName.Key + " with " + overloadedMethodName); @@ -1717,6 +1743,13 @@ private void AddCustomAttributes(IEnumerable attributes, EntityHa continue; } + // OverloadAttribute is emitted by AddOverloadAttributeForInterfaceMethods; + // skip any user-applied instance to avoid duplicates in the winmd. + if (attributeType.ToString() == "Windows.Foundation.Metadata.OverloadAttribute") + { + continue; + } + // Skip the [GeneratedBindableCustomProperty] attribute. It is valid to add this on types in WinRT // components (if they need to be exposed and implement ICustomPropertyProvider), but the attribute // should never show up in the .winmd file (it would also cause build errors in the projections). @@ -2529,11 +2562,11 @@ void AddType(INamedTypeSymbol type, bool treatAsProjectedType = false) { // Prioritize any mapped types before treating an attribute as a projected type. AddProjectedType(type); - } - // Check if CsWinRT component projected type from another project. - else if (Model.Compilation.GetTypeByMetadataName(GeneratorHelper.GetAuthoringMetadataTypeName(qualifiedName)) != null) - { - AddProjectedType(type); + } + // Check if CsWinRT component projected type from another project. + else if (Model.Compilation.GetTypeByMetadataName(GeneratorHelper.GetAuthoringMetadataTypeName(qualifiedName)) != null) + { + AddProjectedType(type); } else { diff --git a/src/Tests/AuthoringConsumptionTest/test.cpp b/src/Tests/AuthoringConsumptionTest/test.cpp index 60539ed14..70b8aca70 100644 --- a/src/Tests/AuthoringConsumptionTest/test.cpp +++ b/src/Tests/AuthoringConsumptionTest/test.cpp @@ -734,6 +734,51 @@ TEST(AuthoringTest, CustomInterfaceGuid) EXPECT_EQ(customInterface.HelloWorld(), L"Hello World!"); } +TEST(AuthoringTest, CustomOverloadNames) +{ + // Test interface-level overloads with user-specified OverloadAttribute names. + CustomOverloadNamesClass obj; + EXPECT_EQ(obj.Lookup(hstring(L"test")), hstring(L"found:test")); + EXPECT_EQ(obj.Lookup(42), 420); + + // Through the interface + ICustomOverloadNames iface = obj; + EXPECT_EQ(iface.Lookup(hstring(L"hello")), hstring(L"found:hello")); + EXPECT_EQ(iface.Lookup(7), 70); + + // Test class-level overloads with mixed user-specified and auto-generated names. + EXPECT_EQ(obj.Transform(hstring(L"abc")), hstring(L"ABC")); + EXPECT_EQ(obj.Transform(5), 10); + EXPECT_EQ(obj.Transform(1.0), 1.5); + + // Verify the user-specified ABI names actually appear in the generated projection. + // The abi::type vtable has virtual methods named exactly + // after the [Overload("...")] values. If the winmd had auto-generated names + // (e.g. "Lookup2") instead of "LookupByIndex", these calls would fail to compile. + using abi_type = winrt::impl::abi_t; + auto raw = static_cast(winrt::get_abi(iface)); + { + int32_t result = 0; + EXPECT_EQ(raw->LookupByIndex(7, &result), S_OK); + EXPECT_EQ(result, 70); + } + { + bool result = false; + EXPECT_EQ(raw->LookupByFlag(false, &result), S_OK); + EXPECT_TRUE(result); + } + + // Same check for the class-level synthesized interface. + using class_abi_type = winrt::impl::abi_t; + ICustomOverloadNamesClassClass classIface = obj.as(); + auto rawClass = static_cast(winrt::get_abi(classIface)); + { + int32_t result = 0; + EXPECT_EQ(rawClass->TransformNumber(5, &result), S_OK); + EXPECT_EQ(result, 10); + } +} + TEST(AuthoringTest, NonActivatableFactory) { EXPECT_EQ(NonActivatableFactory::Create().GetText(), L"Test123"); diff --git a/src/Tests/AuthoringTest/Program.cs b/src/Tests/AuthoringTest/Program.cs index f4abf226f..2536c789e 100644 --- a/src/Tests/AuthoringTest/Program.cs +++ b/src/Tests/AuthoringTest/Program.cs @@ -1969,6 +1969,37 @@ public sealed class CustomInterfaceGuidClass : ICustomInterfaceGuid public string HelloWorld() => "Hello World!"; } + // Test that user-specified [OverloadAttribute] names are emitted in the winmd. + // The interface has 3 overloads of Lookup where the non-default ones carry custom ABI names. + public interface ICustomOverloadNames + { + [Windows.Foundation.Metadata.DefaultOverload()] + string Lookup(string key); + + [Windows.Foundation.Metadata.Overload("LookupByIndex")] + int Lookup(int index); + + [Windows.Foundation.Metadata.Overload("LookupByFlag")] + bool Lookup(bool flag); + } + + // Test a mix of user-specified and auto-generated OverloadAttribute names. + public sealed class CustomOverloadNamesClass : ICustomOverloadNames + { + public string Lookup(string key) => "found:" + key; + public int Lookup(int index) => index * 10; + public bool Lookup(bool flag) => !flag; + + // Class-level overloads: first gets custom name, second auto-generated. + [Windows.Foundation.Metadata.DefaultOverload()] + public string Transform(string input) => input.ToUpper(); + + [Windows.Foundation.Metadata.Overload("TransformNumber")] + public int Transform(int value) => value * 2; + + public double Transform(double value) => value + 0.5; + } + public sealed class NonActivatableType { private readonly string _text;