diff --git a/src/Tests/AuthoringConsumptionTest/test.cpp b/src/Tests/AuthoringConsumptionTest/test.cpp index b516e7111..f9245fe3d 100644 --- a/src/Tests/AuthoringConsumptionTest/test.cpp +++ b/src/Tests/AuthoringConsumptionTest/test.cpp @@ -977,10 +977,22 @@ TEST(AuthoringTest, AsyncMethodClass) TEST(AuthoringTest, DeprecatedMembersClass) { DeprecatedMembersClass obj; + + // Members marked [Deprecated(DeprecationType.Remove)] are omitted from the projection, so they are + // intentionally not referenced here. Their ABI vtable slot is still preserved (stubbed to E_NOTIMPL): + // the new members below sit after the removed slots in vtable order, so their correct dispatch confirms + // the removed slots remain in place and the layout did not shift. The deprecated members and the new + // members both remain projected and fully usable. obj.OldMethod(); obj.NewMethod(); - EXPECT_EQ(obj.OldProp(), L""); - EXPECT_EQ(obj.NewProp(), L""); + EXPECT_EQ(obj.OldProp(), L"OldProp"); + EXPECT_EQ(obj.NewProp(), L"NewProp"); + + DeprecatedMembersClass::OldStatic(); + DeprecatedMembersClass::NewStatic(); + + auto oldToken = obj.OldEvent(auto_revoke, [](IInspectable const&, int32_t const&) {}); + auto newToken = obj.NewEvent(auto_revoke, [](IInspectable const&, int32_t const&) {}); } TEST(AuthoringTest, FullFeaturedClass) diff --git a/src/Tests/AuthoringTest/Program.cs b/src/Tests/AuthoringTest/Program.cs index c7adaa37f..0cf3ab67d 100644 --- a/src/Tests/AuthoringTest/Program.cs +++ b/src/Tests/AuthoringTest/Program.cs @@ -2271,12 +2271,34 @@ public sealed class DeprecatedMembersClass [Windows.Foundation.Metadata.Deprecated("Use NewMethod instead", Windows.Foundation.Metadata.DeprecationType.Deprecate, 1u)] public void OldMethod() { } + [Windows.Foundation.Metadata.Deprecated("RemovedMethod is gone", Windows.Foundation.Metadata.DeprecationType.Remove, 2u)] + public void RemovedMethod() { } + public void NewMethod() { } + [Windows.Foundation.Metadata.Deprecated("Use NewStatic instead", Windows.Foundation.Metadata.DeprecationType.Deprecate, 1u)] + public static void OldStatic() { } + + [Windows.Foundation.Metadata.Deprecated("RemovedStatic is gone", Windows.Foundation.Metadata.DeprecationType.Remove, 2u)] + public static void RemovedStatic() { } + + public static void NewStatic() { } + [Windows.Foundation.Metadata.Deprecated("Use NewProp instead", Windows.Foundation.Metadata.DeprecationType.Deprecate, 1u)] - public string OldProp => ""; + public string OldProp => "OldProp"; + + [Windows.Foundation.Metadata.Deprecated("RemovedProp is gone", Windows.Foundation.Metadata.DeprecationType.Remove, 2u)] + public string RemovedProp => "RemovedProp"; + + public string NewProp => "NewProp"; + + [Windows.Foundation.Metadata.Deprecated("Use NewEvent instead", Windows.Foundation.Metadata.DeprecationType.Deprecate, 1u)] + public event System.EventHandler OldEvent; + + [Windows.Foundation.Metadata.Deprecated("RemovedEvent is gone", Windows.Foundation.Metadata.DeprecationType.Remove, 2u)] + public event System.EventHandler RemovedEvent; - public string NewProp => ""; + public event System.EventHandler NewEvent; } // Class implementing INotifyPropertyChanged + custom interface diff --git a/src/Tests/TestComponentCSharp/DeprecatedClasses.cpp b/src/Tests/TestComponentCSharp/DeprecatedClasses.cpp new file mode 100644 index 000000000..d55d5b0c5 --- /dev/null +++ b/src/Tests/TestComponentCSharp/DeprecatedClasses.cpp @@ -0,0 +1,60 @@ +#include "pch.h" +#include "DeprecatedClasses.h" +#include "DeprecatedConstructorClass.g.cpp" +#include "RemovedActivationClass.g.cpp" +#include "RemovedComposableClass.g.cpp" + +namespace winrt::TestComponentCSharp::implementation +{ + // The value encodes which constructor ran, so a test can prove the surviving constructor still + // dispatches through its original factory slot even though the one before it was removed. + DeprecatedConstructorClass::DeprecatedConstructorClass(int32_t first) + { + m_value = first; + } + + DeprecatedConstructorClass::DeprecatedConstructorClass(int32_t first, int32_t second) + { + m_value = first + second; + } + + DeprecatedConstructorClass::DeprecatedConstructorClass(int32_t first, int32_t second, int32_t third) + { + m_value = first + second + third; + } + + int32_t DeprecatedConstructorClass::Value() + { + return m_value; + } + + RemovedActivationClass::RemovedActivationClass(int32_t initialValue) + { + m_value = initialValue; + } + + TestComponentCSharp::RemovedActivationClass RemovedActivationClass::Create(int32_t initialValue) + { + return winrt::make(initialValue); + } + + int32_t RemovedActivationClass::Value() + { + return m_value; + } + + TestComponentCSharp::RemovedComposableClass RemovedComposableClass::Create(int32_t initialValue) + { + return winrt::make(initialValue); + } + + RemovedComposableClass::RemovedComposableClass(int32_t initialValue) + { + m_value = initialValue; + } + + int32_t RemovedComposableClass::Value() + { + return m_value; + } +} diff --git a/src/Tests/TestComponentCSharp/DeprecatedClasses.h b/src/Tests/TestComponentCSharp/DeprecatedClasses.h new file mode 100644 index 000000000..1f26ec32f --- /dev/null +++ b/src/Tests/TestComponentCSharp/DeprecatedClasses.h @@ -0,0 +1,62 @@ +#pragma once +#include "DeprecatedConstructorClass.g.h" +#include "RemovedActivationClass.g.h" +#include "RemovedComposableClass.g.h" + +namespace winrt::TestComponentCSharp::implementation +{ + // Each constructor stores a distinct value so a test can tell which factory slot was + // actually dispatched to (the removed overloads keep their slot, but are not projected). + struct DeprecatedConstructorClass : DeprecatedConstructorClassT + { + DeprecatedConstructorClass(int32_t first); + DeprecatedConstructorClass(int32_t first, int32_t second); + DeprecatedConstructorClass(int32_t first, int32_t second, int32_t third); + + int32_t Value(); + + private: + int32_t m_value{ 0 }; + }; + + struct RemovedActivationClass : RemovedActivationClassT + { + RemovedActivationClass(int32_t initialValue); + + static TestComponentCSharp::RemovedActivationClass Create(int32_t initialValue); + + int32_t Value(); + + private: + int32_t m_value{ 0 }; + }; + + struct RemovedComposableClass : RemovedComposableClassT + { + // The parameterless constructor is the one the (removed) composable factory method uses; + // the other is implementation-only, for the static factory method below + RemovedComposableClass() = default; + explicit RemovedComposableClass(int32_t initialValue); + + static TestComponentCSharp::RemovedComposableClass Create(int32_t initialValue); + + int32_t Value(); + + private: + int32_t m_value{ 0 }; + }; +} +namespace winrt::TestComponentCSharp::factory_implementation +{ + struct DeprecatedConstructorClass : DeprecatedConstructorClassT + { + }; + + struct RemovedActivationClass : RemovedActivationClassT + { + }; + + struct RemovedComposableClass : RemovedComposableClassT + { + }; +} diff --git a/src/Tests/TestComponentCSharp/TestComponentCSharp.idl b/src/Tests/TestComponentCSharp/TestComponentCSharp.idl index d56bdf92c..ac08a3aa9 100644 --- a/src/Tests/TestComponentCSharp/TestComponentCSharp.idl +++ b/src/Tests/TestComponentCSharp/TestComponentCSharp.idl @@ -798,6 +798,56 @@ And this is another one" Int32 Value{ get; set; }; } + // A constructor has no metadata row of its own: it is projected as a method on the activation + // (or composable) factory interface, and that method is where '[deprecated]' lives. These three + // classes cover the constructor cases the projection has to handle. + + // A deprecated constructor is projected with '[Obsolete]'; a removed one is dropped from the + // projection while its factory vtable slot is preserved, so the constructor declared after it + // must still dispatch through the correct slot. + [default_interface] + runtimeclass DeprecatedConstructorClass + { + [deprecated("Use the three-argument constructor instead", deprecate, 1)] + DeprecatedConstructorClass(Int32 first); + + [deprecated("The two-argument constructor is gone", remove, 1)] + DeprecatedConstructorClass(Int32 first, Int32 second); + + DeprecatedConstructorClass(Int32 first, Int32 second, Int32 third); + + Int32 Value{ get; }; + } + + // Every constructor is removed, so the (sealed, activatable) class cannot be constructed from the + // projection at all and instances come from the static factory method instead. + // + // Note: the constructor parameter cannot be named 'value'. MIDL projects a constructor as + // 'CreateInstance' on the activation factory interface and names its '[out, retval]' parameter + // 'value', so a user parameter of that name collides with it (MIDL5161). + [default_interface] + runtimeclass RemovedActivationClass + { + [deprecated("Use RemovedActivationClass.Create instead", remove, 1)] + RemovedActivationClass(Int32 initialValue); + + static RemovedActivationClass Create(Int32 initialValue); + + Int32 Value{ get; }; + } + + // As above, but unsealed, so its factory is composable rather than activatable. + [default_interface] + unsealed runtimeclass RemovedComposableClass + { + [deprecated("Use RemovedComposableClass.Create instead", remove, 1)] + RemovedComposableClass(); + + static RemovedComposableClass Create(Int32 initialValue); + + Int32 Value{ get; }; + } + // Compile time test for sub windows namespace namespace Windows { diff --git a/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj b/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj index 180eb5b9d..02e88f05d 100644 --- a/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj +++ b/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj @@ -88,6 +88,7 @@ + @@ -111,6 +112,7 @@ + diff --git a/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj.filters b/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj.filters index 489731f49..5626e9dd3 100644 --- a/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj.filters +++ b/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj.filters @@ -21,6 +21,7 @@ + @@ -40,6 +41,7 @@ + diff --git a/src/Tests/UnitTest/ComInteropTests.cs b/src/Tests/UnitTest/ComInteropTests.cs index 18907ece2..1c034f65a 100644 --- a/src/Tests/UnitTest/ComInteropTests.cs +++ b/src/Tests/UnitTest/ComInteropTests.cs @@ -76,8 +76,13 @@ public void TestInputPane() [TestMethod] public void TestPlayToManager() { + // 'PlayToManager' is deprecated in the Windows SDK, so it is projected with '[Obsolete]'. This + // test covers its interop extensions regardless, so suppress 'CS0618' to keep the intentional + // usage from breaking the build (warnings are treated as errors). +#pragma warning disable CS0618 Assert.ThrowsExactly(() => PlayToManager.GetForWindow(new IntPtr(0))); PlayToManager.ShowPlayToUIForWindow(new IntPtr(0)); +#pragma warning restore CS0618 } [TestMethod] diff --git a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs index a811b7de3..e7edfb6a5 100644 --- a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs +++ b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs @@ -2000,6 +2000,74 @@ public void TestFactoriesWithExplicitlyImplementedIUnknown() Assert.AreEqual(22, cls2.Value); } + [TestMethod] + public void TestDeprecatedConstructors() + { + // A '[deprecated]' constructor is still projected, just with '[Obsolete]' on it +#pragma warning disable CS0618 + var deprecated = new DeprecatedConstructorClass(1); +#pragma warning restore CS0618 + + Assert.AreEqual(1, deprecated.Value); + + // The two-argument constructor is '[deprecated(remove)]', so it is not projected at all. + // Its factory vtable slot is preserved though, which is exactly what lets the three-argument + // constructor declared after it still dispatch through the right slot (it sums its arguments, + // so a wrong slot would either fail or produce a different value). + Assert.IsNull(typeof(DeprecatedConstructorClass).GetConstructor([typeof(int), typeof(int)])); + + var live = new DeprecatedConstructorClass(1, 2, 3); + + Assert.AreEqual(6, live.Value); + + static bool IsObsolete(params Type[] parameterTypes) + { + ConstructorInfo constructor = typeof(DeprecatedConstructorClass).GetConstructor(parameterTypes); + + Assert.IsNotNull(constructor); + + return constructor.GetCustomAttribute() is not null; + } + + Assert.IsTrue(IsObsolete(typeof(int))); + Assert.IsFalse(IsObsolete(typeof(int), typeof(int), typeof(int))); + } + + [TestMethod] + public void TestRemovedConstructors() + { + // Every constructor of these two classes is '[deprecated(remove)]', so neither is + // constructible from the projection: the activatable (sealed) one and the composable + // (unsealed) one both have to drop their only constructor. + // + // 'HasPublicParameterlessConstructor' resolves the 'new()' constrained overload only when the + // type *as compiled against* really exposes a public parameterless constructor, so it asserts + // the reference projection's surface. That matters because dropping every constructor without + // emitting a non-public one in its place would let the C# compiler synthesize an implicit + // public parameterless constructor, which the implementation projection does not have. + Assert.IsTrue(HasPublicParameterlessConstructor()); + Assert.IsFalse(HasPublicParameterlessConstructor()); + Assert.IsFalse(HasPublicParameterlessConstructor()); + + // The implementation projection agrees with the reference projection above + Assert.AreEqual(0, typeof(RemovedActivationClass).GetConstructors().Length); + Assert.AreEqual(0, typeof(RemovedComposableClass).GetConstructors().Length); + + // Both types are still fully usable through their static factory methods + Assert.AreEqual(42, RemovedActivationClass.Create(42).Value); + Assert.AreEqual(42, RemovedComposableClass.Create(42).Value); + } + + /// + /// Compile-time probe for a public parameterless constructor: the new() constrained + /// overload is only a candidate when has one, so a call resolves to + /// the fallback overload otherwise. + /// + private static bool HasPublicParameterlessConstructor() where T : new() => true; + + /// + private static bool HasPublicParameterlessConstructor(int _ = 0) => false; + [TestMethod] public void TestStaticMembers() { diff --git a/src/WinRT.Projection.Writer/Builders/ProjectionFileBuilder.cs b/src/WinRT.Projection.Writer/Builders/ProjectionFileBuilder.cs index 5bc218b1a..b2f3550b5 100644 --- a/src/WinRT.Projection.Writer/Builders/ProjectionFileBuilder.cs +++ b/src/WinRT.Projection.Writer/Builders/ProjectionFileBuilder.cs @@ -120,11 +120,18 @@ public enum {{typeName}} : {{enumUnderlyingType}} continue; } + // Skip enum fields removed via '[Deprecated(..., DeprecationType.Remove, ...)]' + if (field.IsRemoved) + { + continue; + } + string fieldName = field.GetRawName(); string constantValue = field.Constant.FormatLiteral(); // Emits per-enum-field '[SupportedOSPlatform]' when the field has a '[ContractVersion]' CustomAttributeFactory.WritePlatformAttribute(writer, context, field); + CustomAttributeFactory.WriteObsoleteAttribute(writer, field); writer.WriteLine($"{fieldName} = unchecked(({enumUnderlyingType}){constantValue}),"); } diff --git a/src/WinRT.Projection.Writer/Extensions/IHasCustomAttributeExtensions.cs b/src/WinRT.Projection.Writer/Extensions/IHasCustomAttributeExtensions.cs index 1e7f310f0..847839a95 100644 --- a/src/WinRT.Projection.Writer/Extensions/IHasCustomAttributeExtensions.cs +++ b/src/WinRT.Projection.Writer/Extensions/IHasCustomAttributeExtensions.cs @@ -94,5 +94,50 @@ public bool HasWindowsFoundationMetadataAttribute(string name) { return member.GetAttribute(WellKnownNamespaces.WindowsFoundationMetadata, name); } + + /// + /// Gets whether the member carries a [Windows.Foundation.Metadata.Deprecated] attribute. + /// + public bool IsDeprecated => member.HasWindowsFoundationMetadataAttribute("DeprecatedAttribute"); + + /// + /// Gets whether the member is marked as removed: it carries a + /// [Windows.Foundation.Metadata.Deprecated] attribute whose DeprecationType + /// is Remove. A removed member is omitted from the projection, while its ABI vtable + /// slot is preserved (stubbed to return E_NOTIMPL) for binary compatibility. + /// + /// + /// DeprecatedAttribute(string message, DeprecationType type, ...): the second fixed + /// argument is the DeprecationType enum, where Deprecate is 0 and Remove is 1. + /// + public bool IsRemoved => member.GetWindowsFoundationMetadataAttribute("DeprecatedAttribute") is { Signature.FixedArguments: [_, { Element: 1 }, ..] }; + + /// + /// Gets whether the member is deprecated but not removed (i.e. it is projected with an + /// [Obsolete] attribute rather than being omitted). + /// + public bool IsDeprecatedNotRemoved => member.IsDeprecated && !member.IsRemoved; + + /// + /// Gets the message from the member's [Windows.Foundation.Metadata.Deprecated] + /// attribute (the first fixed argument), or if the member is not + /// deprecated or the attribute carries no message. + /// + /// + /// DeprecatedAttribute(string message, ...): the first fixed argument is the message. + /// AsmResolver returns Utf8String for string custom-attribute args, so it is converted. + /// + public string? DeprecatedMessage + { + get + { + if (member.GetWindowsFoundationMetadataAttribute("DeprecatedAttribute") is not { Signature.FixedArguments: [{ Element: { } message }, ..] }) + { + return null; + } + + return message.ToString(); + } + } } } \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Extensions/ProjectionWriterExtensions.cs b/src/WinRT.Projection.Writer/Extensions/ProjectionWriterExtensions.cs index 860263526..fd9cf8c27 100644 --- a/src/WinRT.Projection.Writer/Extensions/ProjectionWriterExtensions.cs +++ b/src/WinRT.Projection.Writer/Extensions/ProjectionWriterExtensions.cs @@ -53,6 +53,7 @@ public void WriteFileHeader(ProjectionEmitContext context) #pragma warning disable CSWINRT3001 // "Type or member '...' is a private implementation detail" #pragma warning disable CSWINRT3002 // "Type '...' is a private implementation detail" #pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type + #pragma warning disable CS0612, CS0618 // "'...' is obsolete" (deprecated Windows Runtime APIs are projected as '[Obsolete]', but generated code still has to reference them) """); } diff --git a/src/WinRT.Projection.Writer/Extensions/TypeDefinitionExtensions.cs b/src/WinRT.Projection.Writer/Extensions/TypeDefinitionExtensions.cs index 79cf0e43a..b41636af5 100644 --- a/src/WinRT.Projection.Writer/Extensions/TypeDefinitionExtensions.cs +++ b/src/WinRT.Projection.Writer/Extensions/TypeDefinitionExtensions.cs @@ -195,14 +195,44 @@ public void MarkRequiredInterfacesVisited(MetadataCache cache, HashSet - /// Returns whether the type declares a parameterless instance constructor. + /// Returns whether the type declares a parameterless instance constructor that is usable for + /// default activation, i.e. one that is not marked as removed ([Deprecated(DeprecationType.Remove)]). /// - /// if the type has a default constructor; otherwise . - public bool HasDefaultConstructor() + /// + /// A removed default constructor is omitted from the projection, so the type is no longer + /// default-activatable: the activation factory cannot emit new T() for it (the C# compiler + /// treats a call to a removed member as an error), and default activation returns E_NOTIMPL instead. + /// + /// if the type has a non-removed default constructor; otherwise . + public bool HasActivatableDefaultConstructor() { foreach (MethodDefinition m in type.Methods) { if (m.IsDefaultConstructor) + { + return !m.IsRemoved; + } + } + + return false; + } + + /// + /// Returns whether the type (an activation or composable factory interface) declares at least one + /// method that still projects to a constructor, i.e. one that is neither special nor marked as + /// removed ([Deprecated(DeprecationType.Remove)]). + /// + /// + /// A factory interface with no such methods contributes no constructors to the projected class, so + /// (in reference-projection mode) the class needs a synthetic non-public parameterless constructor + /// to suppress the C# compiler's implicit public default constructor. + /// + /// if the factory interface has a non-removed factory method; otherwise . + public bool HasActivatableFactoryMethod() + { + foreach (MethodDefinition m in type.GetNonSpecialMethods()) + { + if (!m.IsRemoved) { return true; } diff --git a/src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs b/src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs index 034cb5571..1dd82c447 100644 --- a/src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs @@ -371,6 +371,23 @@ public static nint Vtable // this order: methods first, then properties (setter before getter), then events. HashSet propertyAccessors = [.. type.GetPropertyAccessors()]; + // Map each property accessor (getter/setter) to its property, so a removed property (whose + // removal marker is on the getter per the MIDL convention) can stub both accessor bodies. + Dictionary propertyMap = []; + + foreach (PropertyDefinition prop in type.Properties) + { + if (prop.GetMethod is { } propertyGetter) + { + propertyMap[propertyGetter] = prop; + } + + if (prop.SetMethod is { } propertySetter) + { + propertyMap[propertySetter] = prop; + } + } + // Local helper to emit a single Do_Abi method body for a given MethodDefinition. void EmitOneDoAbi(MethodDefinition method) { @@ -378,9 +395,17 @@ void EmitOneDoAbi(MethodDefinition method) MethodSignatureInfo sig = new(method); string mname = method.GetRawName(); - // If this method is an event add accessor, emit the per-event ConditionalWeakTable - // before the Do_Abi method. - if (eventMap is not null && eventMap.TryGetValue(method, out EventDefinition? evt) && evt.AddMethod == method) + // A removed member (DeprecationType.Remove) keeps its vtable slot for ABI compatibility, but + // is no longer part of the projection, so its CCW entry returns E_NOTIMPL. This applies in + // both consuming and component (authoring) mode: the projected interface omits the member, and + // generated code cannot dispatch to it even in component mode, because the C# compiler treats a + // call to a '[Deprecated(Remove)]' member as an obsolete-as-error (CS0619). The vtable slot is + // preserved so the layout stays stable for existing native callers. + bool removed = IsAbiMemberRemoved(method, eventMap, propertyMap); + + // If this method is an event add accessor, emit the per-event ConditionalWeakTable before the + // Do_Abi method. Removed events are stubbed to E_NOTIMPL and never use the table, so it is skipped. + if (!removed && eventMap is not null && eventMap.TryGetValue(method, out EventDefinition? evt) && evt.AddMethod == method) { EventTableFactory.EmitEventTableField(writer, context, evt, ifaceFullName); } @@ -391,6 +416,18 @@ void EmitOneDoAbi(MethodDefinition method) private static unsafe int Do_Abi_{{vm}}({{doAbiParams}}) """); + if (removed) + { + writer.WriteLine(); + writer.WriteLine(""" + { + return unchecked((int)0x80004001); + } + """); + + return; + } + if (eventMap is not null && eventMap.TryGetValue(method, out EventDefinition? evt2)) { if (evt2.AddMethod == method) @@ -453,6 +490,27 @@ void EmitOneDoAbi(MethodDefinition method) } } + /// + /// Determines whether the CCW entry for must be stubbed because the + /// member is removed (DeprecationType.Remove). The removal marker lives on the property + /// getter / event add accessor (MIDL convention), so both accessors of a removed property or + /// event are reported as removed. + /// + private static bool IsAbiMemberRemoved(MethodDefinition method, Dictionary? eventMap, Dictionary propertyMap) + { + if (eventMap is not null && eventMap.TryGetValue(method, out EventDefinition? evt)) + { + return evt.AddMethod is { IsRemoved: true }; + } + + if (propertyMap.TryGetValue(method, out PropertyDefinition? prop)) + { + return (prop.GetMethod ?? prop.SetMethod) is { IsRemoved: true }; + } + + return method.IsRemoved; + } + /// /// Emits the per-interface marshaller class ({Name}Marshaller) with the boxing/unboxing helpers used by user code to marshal references across the ABI. /// diff --git a/src/WinRT.Projection.Writer/Factories/AbiInterfaceIDicFactory.cs b/src/WinRT.Projection.Writer/Factories/AbiInterfaceIDicFactory.cs index a8a5ff988..7fdca41a5 100644 --- a/src/WinRT.Projection.Writer/Factories/AbiInterfaceIDicFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/AbiInterfaceIDicFactory.cs @@ -246,6 +246,12 @@ internal static void WriteInterfaceIdicImplMembersForInheritedInterface(Indented foreach (MethodDefinition method in type.GetNonSpecialMethods()) { + // Removed members are omitted from the projected interface, so emit no DIM forwarder + if (method.IsRemoved) + { + continue; + } + MethodSignatureInfo sig = new(method); string mname = method.GetRawName(); @@ -259,6 +265,13 @@ internal static void WriteInterfaceIdicImplMembersForInheritedInterface(Indented foreach (PropertyDefinition prop in type.Properties) { (MethodDefinition? getter, MethodDefinition? setter) = prop.GetMethods(); + + // MIDL places '[Deprecated]' on the accessor (the getter for read/write properties) + if ((getter ?? setter) is { IsRemoved: true }) + { + continue; + } + string pname = prop.GetRawName(); string propType = InterfaceFactory.WritePropType(context, prop); @@ -290,6 +303,11 @@ internal static void WriteInterfaceIdicImplMembersForInheritedInterface(Indented foreach (EventDefinition evt in type.Events) { + if (evt.AddMethod is { IsRemoved: true }) + { + continue; + } + string evtName = evt.GetRawName(); writer.WriteLine(); IndentedTextWriterCallback eventType = TypedefNameWriter.WriteEventType(context, evt); @@ -367,6 +385,12 @@ internal static void WriteInterfaceIdicImplMembersForInterface(IndentedTextWrite foreach (MethodDefinition method in type.GetNonSpecialMethods()) { + // Removed members are omitted from the projected interface, so emit no DIM forwarder + if (method.IsRemoved) + { + continue; + } + MethodSignatureInfo sig = new(method); string mname = method.GetRawName(); IndentedTextWriterCallback ret = MethodFactory.WriteProjectionReturnType(context, sig); @@ -388,6 +412,13 @@ internal static void WriteInterfaceIdicImplMembersForInterface(IndentedTextWrite foreach (PropertyDefinition prop in type.Properties) { (MethodDefinition? getter, MethodDefinition? setter) = prop.GetMethods(); + + // MIDL places '[Deprecated]' on the accessor (the getter for read/write properties) + if ((getter ?? setter) is { IsRemoved: true }) + { + continue; + } + string pname = prop.GetRawName(); string propType = InterfaceFactory.WritePropType(context, prop); @@ -446,6 +477,11 @@ void WriteSetter(IndentedTextWriter writer) // dispatch through the static ABI Methods class's event accessor (returns an EventSource). foreach (EventDefinition evt in type.Events) { + if (evt.AddMethod is { IsRemoved: true }) + { + continue; + } + string evtName = evt.GetRawName(); writer.WriteLine(); IndentedTextWriterCallback eventType = TypedefNameWriter.WriteEventType(context, evt); diff --git a/src/WinRT.Projection.Writer/Factories/ClassFactory.cs b/src/WinRT.Projection.Writer/Factories/ClassFactory.cs index 1aff3fdc8..709fe3cd4 100644 --- a/src/WinRT.Projection.Writer/Factories/ClassFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/ClassFactory.cs @@ -265,6 +265,13 @@ public static void WriteStaticClassMembers(IndentedTextWriter writer, Projection TypeDefinition staticIface = factory.Type; + // Skip static members from a removed static factory interface: the interface is omitted + // from the projection and ABI, so its IID / ABI Methods class would not exist to dispatch to. + if (staticIface.IsRemoved) + { + continue; + } + // Compute the objref name for this static factory interface. string objRef = ObjRefNameGenerator.GetObjRefName(context, staticIface); @@ -284,10 +291,18 @@ public static void WriteStaticClassMembers(IndentedTextWriter writer, Projection // Methods foreach (MethodDefinition method in staticIface.GetNonSpecialMethods()) { + // Skip removed static methods (omitted from the projection) + if (method.IsRemoved) + { + continue; + } + MethodSignatureInfo sig = new(method); string mname = method.GetRawName(); writer.WriteLine(); + CustomAttributeFactory.WriteObsoleteAttribute(writer, method); + writer.WriteIf(!string.IsNullOrEmpty(platformAttribute), platformAttribute); IndentedTextWriterCallback ret = MethodFactory.WriteProjectionReturnType(context, sig); @@ -309,9 +324,20 @@ public static void WriteStaticClassMembers(IndentedTextWriter writer, Projection // Events: dispatch via static ABI class which returns an event source. foreach (EventDefinition evt in staticIface.Events) { + // MIDL places '[Deprecated]' on the event 'add' accessor, not on the Event row. + if (evt.AddMethod is { IsRemoved: true }) + { + continue; + } + string evtName = evt.GetRawName(); writer.WriteLine(); + if (evt.AddMethod is { } addMethod) + { + CustomAttributeFactory.WriteObsoleteAttribute(writer, addMethod); + } + writer.WriteIf(!string.IsNullOrEmpty(platformAttribute), platformAttribute); IndentedTextWriterCallback eventType = TypedefNameWriter.WriteEventType(context, evt); @@ -343,6 +369,16 @@ public static event {{eventType}} {{evtName}} { string propName = prop.GetRawName(); (MethodDefinition? getter, MethodDefinition? setter) = prop.GetMethods(); + + // MIDL places '[Deprecated]' on the accessor (the getter for read/write properties), + // not on the Property row, so removal/deprecation is checked on the accessor. + MethodDefinition? accessor = getter ?? setter; + + if (accessor is { IsRemoved: true }) + { + continue; + } + string propType = InterfaceFactory.WritePropType(context, prop); if (!properties.TryGetValue(propName, out StaticPropertyAccessorState? state)) @@ -350,9 +386,14 @@ public static event {{eventType}} {{evtName}} state = new StaticPropertyAccessorState { PropTypeText = propType, + DeprecationAccessor = accessor, }; properties[propName] = state; } + else + { + state.DeprecationAccessor ??= accessor; + } if (getter is not null && !state.HasGetter) { @@ -378,6 +419,11 @@ public static event {{eventType}} {{evtName}} StaticPropertyAccessorState s = kv.Value; writer.WriteLine(); + if (s.DeprecationAccessor is { } deprecationAccessor) + { + CustomAttributeFactory.WriteObsoleteAttribute(writer, deprecationAccessor); + } + // when getter and setter platforms match; otherwise emit per-accessor. string getterPlat = s.GetterPlatformAttribute; string setterPlat = s.SetterPlatformAttribute; @@ -590,23 +636,11 @@ void WriteCtorBody(IndentedTextWriter writer) // // Sealed classes can never be a base, so they only need case 1; unsealed classes need a // parameterless ctor whenever they don't already emit one (which also covers case 1). + // Both checks live in 'ConstructorFactory' so they stay in sync with what it actually emits + // (in particular, they must agree on which factories and overloads are skipped as removed). if (type.IsSealed) { - bool hasRefModeCtors = false; - foreach (KeyValuePair kv in AttributedTypes.Get(type, context.Cache)) - { - AttributedType factory = kv.Value; - - // Activatable always emits at least one ctor; a composable factory only emits ctors - // when it has methods (a composable factory with no methods emits none). - if (factory.Activatable || (factory.Composable && factory.Type is not null && factory.Type.Methods.Count > 0)) - { - hasRefModeCtors = true; - break; - } - } - - if (!hasRefModeCtors) + if (!ConstructorFactory.EmitsAnyConstructor(type, context.Cache)) { RefModeStubFactory.EmitSyntheticPrivateCtor(writer, typeName, isSealed: true); } diff --git a/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteClassMembers.cs b/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteClassMembers.cs index 6442a5120..0b382b4a2 100644 --- a/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteClassMembers.cs +++ b/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteClassMembers.cs @@ -86,6 +86,11 @@ public static void WriteClassMembers(IndentedTextWriter writer, ProjectionEmitCo setterPlat = string.Empty; } + if (s.DeprecationAccessor is { } deprecationAccessor) + { + CustomAttributeFactory.WriteObsoleteAttribute(writer, deprecationAccessor); + } + writer.WriteIf(!string.IsNullOrEmpty(propertyPlat), propertyPlat); writer.Write($"{s.Access}{s.MethodSpec}{s.PropTypeText} {kvp.Key}"); diff --git a/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteInterfaceMembers.cs b/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteInterfaceMembers.cs index b4c7a0f84..51c4fdfab 100644 --- a/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteInterfaceMembers.cs +++ b/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteInterfaceMembers.cs @@ -267,6 +267,13 @@ private static void WriteInterfaceMembers(IndentedTextWriter writer, ProjectionE continue; } + // Skip members removed via '[Deprecated(..., DeprecationType.Remove, ...)]': the ABI + // vtable slot is preserved separately, but they are omitted from the projected class. + if (method.IsRemoved) + { + continue; + } + // Detect a 'string ToString()' that overrides Object.ToString() and force the // 'override' modifier on the emitted member. string methodSpecForThis = methodSpec; @@ -331,6 +338,8 @@ private static void WriteInterfaceMembers(IndentedTextWriter writer, ProjectionE parameterList: $"WindowsRuntimeObjectReference thisReference{accessorParams}"); } + CustomAttributeFactory.WriteObsoleteAttribute(writer, method); + writer.WriteLine(isMultiline: true, $$""" {{platformTrimmed}} {{access}}{{methodSpecForThis}}{{ret}} {{name}}({{parms}}) => {{body}} @@ -340,6 +349,8 @@ private static void WriteInterfaceMembers(IndentedTextWriter writer, ProjectionE { writer.WriteLine(); + CustomAttributeFactory.WriteObsoleteAttribute(writer, method); + writer.WriteIf(!string.IsNullOrEmpty(platformAttribute), platformAttribute); IndentedTextWriterCallback ret = MethodFactory.WriteProjectionReturnType(context, sig); @@ -381,6 +392,15 @@ private static void WriteInterfaceMembers(IndentedTextWriter writer, ProjectionE string name = prop.GetRawName(); (MethodDefinition? getter, MethodDefinition? setter) = prop.GetMethods(); + // MIDL places '[Deprecated]' on the property accessor (the getter for read/write + // properties), not on the Property row, so removal/deprecation is checked on the accessor. + MethodDefinition? accessor = getter ?? setter; + + if (accessor is { IsRemoved: true }) + { + continue; + } + if (!propertyState.TryGetValue(name, out PropertyAccessorState? state)) { state = new PropertyAccessorState @@ -390,9 +410,14 @@ private static void WriteInterfaceMembers(IndentedTextWriter writer, ProjectionE MethodSpec = methodSpec, IsOverridable = isOverridable, OverridableInterface = isOverridable ? originalInterface : null, + DeprecationAccessor = accessor, }; propertyState[name] = state; } + else + { + state.DeprecationAccessor ??= accessor; + } if (getter is not null && !state.HasGetter) { @@ -431,6 +456,13 @@ private static void WriteInterfaceMembers(IndentedTextWriter writer, ProjectionE continue; } + // MIDL places '[Deprecated]' on the event 'add' accessor, not on the Event row. + // Skip events removed via 'DeprecationType.Remove' (the ABI slot is preserved separately). + if (evt.AddMethod is { IsRemoved: true }) + { + continue; + } + // Compute event handler type and event source type strings. TypeSignature evtSig = evt.EventType!.ToTypeSignature(false); @@ -543,6 +575,12 @@ private static void WriteInterfaceMembers(IndentedTextWriter writer, ProjectionE // Emit the public/protected event with Subscribe/Unsubscribe. writer.WriteLine(); + // MIDL places '[Deprecated]' on the event 'add' accessor, not on the Event row. + if (evt.AddMethod is { } addMethod) + { + CustomAttributeFactory.WriteObsoleteAttribute(writer, addMethod); + } + // string to each event emission. In ref mode this produces e.g. // [global::System.Runtime.Versioning.SupportedOSPlatform("Windows10.0.16299.0")]. writer.WriteIf(!string.IsNullOrEmpty(platformAttribute), platformAttribute); diff --git a/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs b/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs index c2e29a11b..6a4c8b493 100644 --- a/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs @@ -86,7 +86,10 @@ void WriteBaseInterfaceList(IndentedTextWriter writer) // Writes the body of the 'ActivateInstance' method (it throws for non-activatable types) void WriteActivateInstanceBody(IndentedTextWriter writer) { - bool isActivatable = !type.IsStatic && type.HasDefaultConstructor(); + // A type whose default constructor is removed ([Deprecated(DeprecationType.Remove)]) is no longer + // default-activatable: 'new T()' cannot be emitted (it would call the removed authored member), + // so default activation falls through to the 'throw' below, which marshals to E_NOTIMPL. + bool isActivatable = !type.IsStatic && type.HasActivatableDefaultConstructor(); if (isActivatable) { @@ -172,7 +175,11 @@ private static void WriteAdditionalActivationFactoryMethods( { foreach (MethodDefinition method in info.Type.Methods) { - if (method.IsConstructor) + // Removed members (DeprecationType.Remove) are omitted from the factory class: the + // projected factory/static interface drops them, their vtable slot is stubbed to + // E_NOTIMPL, and generated code cannot call the authored member anyway (the C# + // compiler treats a call to a '[Deprecated(Remove)]' member as an error). + if (method.IsConstructor || method.IsRemoved) { continue; } @@ -184,7 +191,7 @@ private static void WriteAdditionalActivationFactoryMethods( { foreach (MethodDefinition method in info.Type.Methods) { - if (method.IsConstructor) + if (method.IsConstructor || method.IsRemoved) { continue; } @@ -193,10 +200,20 @@ private static void WriteAdditionalActivationFactoryMethods( } foreach (PropertyDefinition prop in info.Type.Properties) { + if ((prop.GetMethod ?? prop.SetMethod) is { IsRemoved: true }) + { + continue; + } + WriteStaticFactoryProperty(writer, context, prop, projectedTypeName); } foreach (EventDefinition evt in info.Type.Events) { + if (evt.AddMethod is { IsRemoved: true }) + { + continue; + } + WriteStaticFactoryEvent(writer, context, evt, projectedTypeName); } } diff --git a/src/WinRT.Projection.Writer/Factories/ConstructorFactory.AttributedTypes.cs b/src/WinRT.Projection.Writer/Factories/ConstructorFactory.AttributedTypes.cs index 72d2093c8..2cbc4418c 100644 --- a/src/WinRT.Projection.Writer/Factories/ConstructorFactory.AttributedTypes.cs +++ b/src/WinRT.Projection.Writer/Factories/ConstructorFactory.AttributedTypes.cs @@ -64,6 +64,13 @@ public static void WriteAttributedTypes(IndentedTextWriter writer, ProjectionEmi { AttributedType factory = kv.Value; + // Skip constructors generated from a removed factory interface: the interface is omitted + // from the projection and ABI, so its IID / ABI Methods class would not exist to dispatch to. + if (factory.Type is { IsRemoved: true }) + { + continue; + } + if (factory.Activatable) { WriteFactoryConstructors(writer, context, factory.Type, classType); @@ -106,6 +113,15 @@ public static void WriteFactoryConstructors(IndentedTextWriter writer, Projectio continue; } + // Skip removed constructor overloads; the factory vtable slot is preserved (methodIndex + // still advances) so the remaining constructors dispatch through the correct slot. + if (method.IsRemoved) + { + methodIndex++; + + continue; + } + MethodSignatureInfo sig = new(method); string callbackName = (method.Name?.Value ?? "Create") + "_" + sig.Parameters.Count.ToString(CultureInfo.InvariantCulture); string argsName = callbackName + "Args"; @@ -113,6 +129,8 @@ public static void WriteFactoryConstructors(IndentedTextWriter writer, Projectio // Emit the public constructor. writer.WriteLine(); + CustomAttributeFactory.WriteObsoleteAttribute(writer, method); + writer.WriteIf(!string.IsNullOrEmpty(platformAttribute), platformAttribute); writer.Write($"public unsafe {typeName}("); @@ -207,6 +225,47 @@ public static void WriteFactoryConstructors(IndentedTextWriter writer, Projectio } } + /// + /// Determines whether emits at least one public constructor for + /// the given runtime class. + /// + /// + /// Used in reference-projection mode to decide whether a sealed class needs a synthetic non-public + /// parameterless constructor to suppress the C# compiler's implicit public default constructor + /// (see ). Emitting it matters for more than + /// tidiness: the implementation projection never emits an implicit public default constructor, so + /// leaving one on the reference surface would let consumers compile a new T() call that fails + /// at runtime against the implementation projection. + /// + public static bool EmitsAnyConstructor(TypeDefinition classType, MetadataCache cache) + { + foreach (KeyValuePair kv in AttributedTypes.Get(classType, cache)) + { + AttributedType factory = kv.Value; + + // A removed factory interface is skipped entirely by 'WriteAttributedTypes', so it emits nothing + if (factory.Type is { IsRemoved: true }) + { + continue; + } + + // A default '[Activatable(uint version)]' (no factory interface) always emits 'public TypeName()' + if (factory.Activatable && factory.Type is null) + { + return true; + } + + // Both activation and composable factories emit one constructor per factory method, so a factory + // whose methods are all special or removed (or which has none at all) emits no constructors. + if ((factory.Activatable || factory.Composable) && factory.Type is { } factoryType && factoryType.HasActivatableFactoryMethod()) + { + return true; + } + } + + return false; + } + /// /// Determines whether emits at least one parameterless public /// constructor for the given runtime class (a default [Activatable] ctor, an activation-factory @@ -223,6 +282,12 @@ public static bool EmitsParameterlessConstructor(TypeDefinition classType, Metad { AttributedType factory = kv.Value; + // A removed factory interface is skipped entirely by 'WriteAttributedTypes', so it emits nothing + if (factory.Type is { IsRemoved: true }) + { + continue; + } + // A default '[Activatable(uint version)]' (no factory interface) emits 'public TypeName()'. if (factory.Activatable && factory.Type is null) { @@ -241,7 +306,8 @@ public static bool EmitsParameterlessConstructor(TypeDefinition classType, Metad { foreach (MethodDefinition method in factory.Type.Methods) { - if (method.IsSpecial) + // Special methods and removed overloads emit no constructor + if (method.IsSpecial || method.IsRemoved) { continue; } diff --git a/src/WinRT.Projection.Writer/Factories/ConstructorFactory.Composable.cs b/src/WinRT.Projection.Writer/Factories/ConstructorFactory.Composable.cs index 20eb77b55..df186b163 100644 --- a/src/WinRT.Projection.Writer/Factories/ConstructorFactory.Composable.cs +++ b/src/WinRT.Projection.Writer/Factories/ConstructorFactory.Composable.cs @@ -56,6 +56,15 @@ public static void WriteComposableConstructors(IndentedTextWriter writer, Projec continue; } + // Skip removed composable constructor overloads; the factory vtable slot is preserved + // (methodIndex still advances) so the remaining constructors dispatch through the correct slot. + if (method.IsRemoved) + { + methodIndex++; + + continue; + } + // Composable factory methods have signature like: // T CreateInstance(args, object baseInterface, out object innerInterface) // For the constructor on the projected class, we exclude the trailing two params. @@ -72,6 +81,8 @@ public static void WriteComposableConstructors(IndentedTextWriter writer, Projec writer.WriteLine(); + CustomAttributeFactory.WriteObsoleteAttribute(writer, method); + writer.WriteIf(!string.IsNullOrEmpty(platformAttribute), platformAttribute); writer.Write(visibility); diff --git a/src/WinRT.Projection.Writer/Factories/CustomAttributeFactory.cs b/src/WinRT.Projection.Writer/Factories/CustomAttributeFactory.cs index 22e6c1660..064264059 100644 --- a/src/WinRT.Projection.Writer/Factories/CustomAttributeFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/CustomAttributeFactory.cs @@ -484,6 +484,31 @@ public static void WriteCustomAttributes(IndentedTextWriter writer, ProjectionEm } } + /// + /// Writes a [System.Obsolete] attribute when is deprecated but + /// not removed. Removed members are omitted from the projection entirely, so they get no attribute. + /// + /// The writer to emit to. + /// The member to inspect for [Windows.Foundation.Metadata.Deprecated]. + public static void WriteObsoleteAttribute(IndentedTextWriter writer, IHasCustomAttribute member) + { + if (!member.IsDeprecatedNotRemoved) + { + return; + } + + string? message = member.DeprecatedMessage; + + if (string.IsNullOrEmpty(message)) + { + writer.WriteLine("[global::System.Obsolete]"); + } + else + { + writer.WriteLine($"[global::System.Obsolete(@\"{EscapeVerbatimString(message)}\")]"); + } + } + /// /// Returns whether a Windows Runtime metadata attribute application should be carried over to the projection. /// @@ -542,6 +567,7 @@ private static bool ShouldCarryOverAttribute(ProjectionEmitContext context, stri public static void WriteTypeCustomAttributes(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type, bool enablePlatformAttrib) { WriteCustomAttributes(writer, context, type, enablePlatformAttrib); + WriteObsoleteAttribute(writer, type); } /// @@ -562,6 +588,7 @@ internal static void WriteTypeCustomAttributesBody(IndentedTextWriter writer, Pr int before = writer.Length; WriteCustomAttributes(writer, context, type, enablePlatformAttrib); + WriteObsoleteAttribute(writer, type); // If anything was written, the buffer ends with a trailing newline that came from the // last attribute's WriteLine. Trim it so the callback can be inlined into a multiline diff --git a/src/WinRT.Projection.Writer/Factories/InterfaceFactory.cs b/src/WinRT.Projection.Writer/Factories/InterfaceFactory.cs index f75c59aaa..9bd623821 100644 --- a/src/WinRT.Projection.Writer/Factories/InterfaceFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/InterfaceFactory.cs @@ -218,11 +218,19 @@ public static void WriteInterfaceMemberSignatures(IndentedTextWriter writer, Pro { foreach (MethodDefinition method in type.GetNonSpecialMethods()) { + // Skip members removed via '[Deprecated(..., DeprecationType.Remove, ...)]': their ABI + // vtable slot is preserved separately, but they are omitted from the projected interface. + if (method.IsRemoved) + { + continue; + } + MethodSignatureInfo sig = new(method); // Carried-over metadata attributes ([Overload], [DefaultOverload], [Experimental]) are // reference-projection-only. WriteMethodCustomAttributes(writer, context, method); + CustomAttributeFactory.WriteObsoleteAttribute(writer, method); IndentedTextWriterCallback ret = MethodFactory.WriteProjectionReturnType(context, sig); IndentedTextWriterCallback parms = MethodFactory.WriteParameterList(context, sig); writer.WriteLine($"{ret} {method.GetRawName()}({parms});"); @@ -232,6 +240,15 @@ public static void WriteInterfaceMemberSignatures(IndentedTextWriter writer, Pro { (MethodDefinition? getter, MethodDefinition? setter) = prop.GetMethods(); + // MIDL places '[Deprecated]' on the property accessor (the getter for read/write + // properties), not on the Property row, so deprecation is checked on the accessor. + MethodDefinition? accessor = getter ?? setter; + + if (accessor is { IsRemoved: true }) + { + continue; + } + // Add 'new' when this interface has a setter-only property AND a property of the same // name exists on a base interface (typically the getter-only counterpart). This hides // the inherited member. @@ -239,6 +256,12 @@ public static void WriteInterfaceMemberSignatures(IndentedTextWriter writer, Pro && TryFindPropertyInBaseInterfaces(context.Cache, type, prop.GetRawName(), out _)) ? "new " : string.Empty; string propType = WritePropType(context, prop); + + if (accessor is not null) + { + CustomAttributeFactory.WriteObsoleteAttribute(writer, accessor); + } + writer.Write($"{newKeyword}{propType} {prop.GetRawName()} {{"); writer.WriteIf(getter is not null || setter is not null, " get;"); @@ -250,6 +273,19 @@ public static void WriteInterfaceMemberSignatures(IndentedTextWriter writer, Pro foreach (EventDefinition evt in type.Events) { + // MIDL places '[Deprecated]' on the event 'add' accessor, not on the Event row. + MethodDefinition? addMethod = evt.AddMethod; + + if (addMethod is { IsRemoved: true }) + { + continue; + } + + if (addMethod is not null) + { + CustomAttributeFactory.WriteObsoleteAttribute(writer, addMethod); + } + IndentedTextWriterCallback eventType = TypedefNameWriter.WriteEventType(context, evt); writer.WriteLine($"event {eventType} {evt.Name?.Value};"); } diff --git a/src/WinRT.Projection.Writer/Factories/MetadataAttributeFactory.cs b/src/WinRT.Projection.Writer/Factories/MetadataAttributeFactory.cs index aff7bfa81..b98847425 100644 --- a/src/WinRT.Projection.Writer/Factories/MetadataAttributeFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/MetadataAttributeFactory.cs @@ -605,6 +605,7 @@ void WriteMetadataEntries(IndentedTextWriter writer) using WindowsRuntime; #pragma warning disable CSWINRT3001 + #pragma warning disable CS0612, CS0618 namespace ABI; @@ -653,6 +654,7 @@ void WriteInterfaceMappings(IndentedTextWriter writer) using WindowsRuntime; #pragma warning disable CSWINRT3001 + #pragma warning disable CS0612, CS0618 namespace ABI; diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.GeneratedIids.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.GeneratedIids.cs index 1af69abb2..f51691c67 100644 --- a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.GeneratedIids.cs +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.GeneratedIids.cs @@ -84,6 +84,12 @@ internal void WriteGeneratedInterfaceIidsFile() continue; } + // Skip fully removed types (omitted from both the projection and the ABI) + if (type.IsRemoved) + { + continue; + } + if (type.IsGeneric) { continue; diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs index 53b963907..d70a06b6a 100644 --- a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs @@ -48,6 +48,12 @@ internal bool ProcessNamespace(string ns, NamespaceMembers members, ProjectionGe continue; } + // Skip fully removed types (omitted from both the projection and the ABI) + if (type.IsRemoved) + { + continue; + } + if (type.IsGeneric) { continue; @@ -116,6 +122,12 @@ internal bool ProcessNamespace(string ns, NamespaceMembers members, ProjectionGe continue; } + // Skip fully removed types (omitted from both the projection and the ABI) + if (type.IsRemoved) + { + continue; + } + (string ns2, string nm2) = type.Names(); // Skip generic types and mapped types @@ -176,6 +188,12 @@ internal bool ProcessNamespace(string ns, NamespaceMembers members, ProjectionGe continue; } + // Skip fully removed types (omitted from both the projection and the ABI) + if (type.IsRemoved) + { + continue; + } + if (TypeKindResolver.Resolve(type) != TypeKind.Class) { continue; @@ -204,6 +222,12 @@ internal bool ProcessNamespace(string ns, NamespaceMembers members, ProjectionGe continue; } + // Skip fully removed types (omitted from both the projection and the ABI) + if (type.IsRemoved) + { + continue; + } + if (type.IsGeneric) { continue; diff --git a/src/WinRT.Projection.Writer/Models/PropertyAccessorState.cs b/src/WinRT.Projection.Writer/Models/PropertyAccessorState.cs index 999013a47..9d699f5ed 100644 --- a/src/WinRT.Projection.Writer/Models/PropertyAccessorState.cs +++ b/src/WinRT.Projection.Writer/Models/PropertyAccessorState.cs @@ -28,6 +28,13 @@ internal sealed class PropertyAccessorState /// public bool HasSetter { get; set; } + /// + /// Gets or sets the accessor method used to determine whether the property is deprecated (the + /// getter when present, otherwise the setter). MIDL places [Deprecated] on the accessor, + /// not on the Property row, so the projected property's [Obsolete] is derived from it. + /// + public MethodDefinition? DeprecationAccessor { get; set; } + /// /// Gets or sets the projected C# type text of the property (for the unified getter+setter declaration). /// diff --git a/src/WinRT.Projection.Writer/Models/StaticPropertyAccessorState.cs b/src/WinRT.Projection.Writer/Models/StaticPropertyAccessorState.cs index 160337221..8302b8df9 100644 --- a/src/WinRT.Projection.Writer/Models/StaticPropertyAccessorState.cs +++ b/src/WinRT.Projection.Writer/Models/StaticPropertyAccessorState.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using AsmResolver.DotNet; + namespace WindowsRuntime.ProjectionWriter.Models; /// @@ -19,6 +21,13 @@ internal sealed class StaticPropertyAccessorState /// public bool HasSetter { get; set; } + /// + /// Gets or sets the accessor method used to determine whether the static property is deprecated + /// (the getter when present, otherwise the setter). MIDL places [Deprecated] on the + /// accessor, not on the Property row. + /// + public MethodDefinition? DeprecationAccessor { get; set; } + /// /// Gets or sets the projected C# type text of the property (for the unified getter+setter declaration). /// diff --git a/src/WinRT.Projection.Writer/Resources/Base/ComInteropExtensions.cs b/src/WinRT.Projection.Writer/Resources/Base/ComInteropExtensions.cs index b62492647..ac07dabe9 100644 --- a/src/WinRT.Projection.Writer/Resources/Base/ComInteropExtensions.cs +++ b/src/WinRT.Projection.Writer/Resources/Base/ComInteropExtensions.cs @@ -25,6 +25,11 @@ // minimum Windows SDK that is currently supported. See this mapping // in the Windows SDK projection project. The two should be kept in sync. +// Some of the types wrapped below are deprecated in the Windows SDK, so they are projected with +// '[Obsolete]'. That is guidance for consumers of these extensions, not for the extensions +// themselves, which have to name those types to wrap them. +#pragma warning disable CS0612, CS0618 + using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; diff --git a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Attributes.cs b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Attributes.cs index 25147f287..29dfa5a79 100644 --- a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Attributes.cs +++ b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Attributes.cs @@ -331,24 +331,80 @@ private int GetVersion(TypeDefinition type) /// /// The source element to copy attributes from. /// The target element to copy attributes to. - private void CopyCustomAttributes(IHasCustomAttribute source, IHasCustomAttribute target) + /// + /// Whether to skip the [Windows.Foundation.Metadata.Deprecated] attribute. This is set when + /// copying to a property or event row, because the deprecation attribute is emitted on the accessor + /// method instead (see ). + /// + private void CopyCustomAttributes(IHasCustomAttribute source, IHasCustomAttribute target, bool skipDeprecated = false) { foreach (CustomAttribute attribute in source.CustomAttributes) { - if (!ShouldCopyAttribute(attribute, _runtimeContext)) + // The '[Deprecated]' attribute on properties and events is emitted on the accessor method + // (matching MIDL), so it is skipped here when copying attributes to the property or event row + if (skipDeprecated && IsDeprecatedAttribute(attribute)) { continue; } - if (ImportAttributeConstructor(attribute.Constructor) is not MemberReference importedCtor) - { - continue; - } + CopyCustomAttribute(attribute, target); + } + } - CustomAttributeSignature clonedSignature = CloneAttributeSignature(attribute.Signature); + /// + /// Copies a single custom attribute from a source element to a target element, applying the same + /// filtering and import logic as . + /// + /// The custom attribute to copy. + /// The target element to copy the attribute to. + private void CopyCustomAttribute(CustomAttribute attribute, IHasCustomAttribute target) + { + if (!ShouldCopyAttribute(attribute, _runtimeContext)) + { + return; + } - target.CustomAttributes.Add(new CustomAttribute(importedCtor, clonedSignature)); + if (ImportAttributeConstructor(attribute.Constructor) is not MemberReference importedCtor) + { + return; } + + CustomAttributeSignature clonedSignature = CloneAttributeSignature(attribute.Signature); + + target.CustomAttributes.Add(new CustomAttribute(importedCtor, clonedSignature)); + } + + /// + /// Copies the [Windows.Foundation.Metadata.Deprecated] attribute (if any) from a property or + /// event onto its accessor method (the getter for properties, the add accessor for events). + /// + /// + /// Windows Runtime metadata places the deprecation attribute on the accessor method rather than the + /// property or event row (this is the placement MIDL produces). Emitting it on the accessor keeps + /// authored components consistent with the Windows SDK, so that both CsWinRT and other consumers + /// (e.g. windows-rs) resolve member deprecation the same way. + /// + /// The source property or event to read the attribute from. + /// The accessor method (or fallback element) to copy the attribute to. + private void CopyDeprecatedAttributeToAccessor(IHasCustomAttribute source, IHasCustomAttribute accessor) + { + foreach (CustomAttribute attribute in source.CustomAttributes) + { + if (IsDeprecatedAttribute(attribute)) + { + CopyCustomAttribute(attribute, accessor); + } + } + } + + /// + /// Returns whether the given custom attribute is a [Windows.Foundation.Metadata.Deprecated] attribute. + /// + /// The custom attribute to evaluate. + /// if the attribute is the deprecation attribute; otherwise, . + private static bool IsDeprecatedAttribute(CustomAttribute attribute) + { + return attribute.Constructor?.DeclaringType?.FullName == "Windows.Foundation.Metadata.DeprecatedAttribute"; } /// diff --git a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs index dd89151f1..8cfaeb9bf 100644 --- a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs +++ b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs @@ -274,6 +274,9 @@ private void AddPropertyToType(TypeDefinition outputType, PropertyDefinition inp attributes: PropertyAttributes.None, signature: isStatic ? PropertySignature.CreateStatic(propertyType) : PropertySignature.CreateInstance(propertyType)); + MethodDefinition? getter = null; + MethodDefinition? setter = null; + // Add getter if (inputProperty.GetMethod is not null) { @@ -295,7 +298,7 @@ private void AddPropertyToType(TypeDefinition outputType, PropertyDefinition inp ? MethodSignature.CreateStatic(propertyType) : MethodSignature.CreateInstance(propertyType); - MethodDefinition getter = new("get_" + inputProperty.Name.Value, attributes, getSignature); + getter = new("get_" + inputProperty.Name.Value, attributes, getSignature); if (!isInterfaceParent) { getter.ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed; @@ -325,7 +328,7 @@ private void AddPropertyToType(TypeDefinition outputType, PropertyDefinition inp ? MethodSignature.CreateStatic(_outputModule.CorLibTypeFactory.Void, [propertyType]) : MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, [propertyType]); - MethodDefinition setter = new("put_" + inputProperty.Name.Value, attributes, setSignature); + setter = new("put_" + inputProperty.Name.Value, attributes, setSignature); if (!isInterfaceParent) { setter.ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed; @@ -340,8 +343,11 @@ private void AddPropertyToType(TypeDefinition outputType, PropertyDefinition inp outputType.Properties.Add(outputProperty); - // Copy custom attributes from the input property - CopyCustomAttributes(inputProperty, outputProperty); + // Copy custom attributes from the input property. The '[Deprecated]' attribute is emitted on the + // accessor (the getter, or the setter for write-only properties) rather than the property row, + // matching the placement used by MIDL so that property deprecation resolves consistently + CopyCustomAttributes(inputProperty, outputProperty, skipDeprecated: true); + CopyDeprecatedAttributeToAccessor(inputProperty, getter ?? setter ?? (IHasCustomAttribute)outputProperty); } /// @@ -369,7 +375,10 @@ private void AddSetterOnlyPropertyToType(TypeDefinition outputType, PropertyDefi outputType.Properties.Add(outputProperty); - CopyCustomAttributes(inputProperty, outputProperty); + // Copy custom attributes from the input property. The '[Deprecated]' attribute is emitted on the + // setter accessor rather than the property row, matching the placement used by MIDL + CopyCustomAttributes(inputProperty, outputProperty, skipDeprecated: true); + CopyDeprecatedAttributeToAccessor(inputProperty, setter); } /// @@ -403,6 +412,8 @@ private void AddEventToType(TypeDefinition outputType, EventDefinition inputEven // For interface parents (synthesized interfaces), always use instance signatures bool isStatic = !isInterfaceParent && inputEvent.AddMethod?.IsStatic == true; + MethodDefinition adder; + // Add method { MethodAttributes attributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName; @@ -426,7 +437,7 @@ private void AddEventToType(TypeDefinition outputType, EventDefinition inputEven ? MethodSignature.CreateStatic(tokenSignature, [handlerSignature]) : MethodSignature.CreateInstance(tokenSignature, [handlerSignature]); - MethodDefinition adder = new("add_" + inputEvent.Name.Value, attributes, addSignature); + adder = new("add_" + inputEvent.Name.Value, attributes, addSignature); if (!isInterfaceParent) { adder.ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed; @@ -472,7 +483,9 @@ private void AddEventToType(TypeDefinition outputType, EventDefinition inputEven outputType.Events.Add(outputEvent); - // Copy custom attributes from the input event - CopyCustomAttributes(inputEvent, outputEvent); + // Copy custom attributes from the input event. The '[Deprecated]' attribute is emitted on the + // 'add' accessor rather than the event row, matching the placement used by MIDL + CopyCustomAttributes(inputEvent, outputEvent, skipDeprecated: true); + CopyDeprecatedAttributeToAccessor(inputEvent, adder); } } \ No newline at end of file