diff --git a/src/Strings.props b/src/Strings.props index 58b6aadc34..37828ead35 100644 --- a/src/Strings.props +++ b/src/Strings.props @@ -196,7 +196,7 @@ - %(AdditionalIncludeDirectories);$(IntDir) + %(ClCompile.AdditionalIncludeDirectories);$(IntDir) diff --git a/src/cswinrt/code_writers.h b/src/cswinrt/code_writers.h index 9971ae62c8..77d28d07d5 100644 --- a/src/cswinrt/code_writers.h +++ b/src/cswinrt/code_writers.h @@ -14,6 +14,9 @@ namespace cswinrt { using namespace winmd::reader; + template + void write_obsolete_attribute(writer& w, T const& row); + static const struct { char const* csharp; @@ -1206,6 +1209,11 @@ namespace cswinrt return; } + if (is_removed(method)) + { + return; + } + auto access_spec = is_protected || is_overridable ? "protected " : "public "; std::string method_spec = ""; @@ -1261,6 +1269,7 @@ namespace cswinrt auto static_method_params = call_static_method.has_value() ? std::optional(std::pair(call_static_method.value(), method)) : std::nullopt; if (!is_private) { + write_obsolete_attribute(w, method); write_method(w, signature, method.Name(), return_type, static_method_params.has_value() ? w.write_temp("%", bind(static_method_params.value().first)) : interface_member, access_spec, method_spec, platform_attribute, static_method_params); @@ -1536,6 +1545,13 @@ remove => %; void write_class_event(writer& w, Event const& event, TypeDef const& class_type, bool is_overridable, bool is_protected, std::string_view interface_member, std::string_view platform_attribute = ""sv, std::optional call_static_method = {}) { + auto [add, remove] = get_event_methods(event); + // MIDL places DeprecatedAttribute on the add method, not the Event row + if (is_removed(add)) + { + return; + } + auto visibility = "public "; if (is_protected) @@ -1548,10 +1564,10 @@ remove => %; visibility = "protected virtual "; } - auto [add, _] = get_event_methods(event); bool is_private = is_implemented_as_private_method(w, class_type, add); if (!is_private) { + write_obsolete_attribute(w, add); write_event(w, event.Name(), event, call_static_method.has_value() ? w.write_temp("%", bind(call_static_method.value())) : interface_member, visibility, ""sv, platform_attribute, call_static_method.has_value() ? std::optional(std::tuple(call_static_method.value(), event, false)) : std::nullopt); @@ -1816,6 +1832,29 @@ remove => %; return w.write_temp("%", bind(type.CustomAttribute())); } + template + void write_obsolete_attribute(writer& w, T const& row) + { + if (is_deprecated_not_removed(row)) + { + auto msg = get_deprecated_message(row); + if (!msg.empty()) + { + w.write("[global::System.Obsolete(\"%\")]\n", msg); + } + else + { + w.write("[global::System.Obsolete]\n"); + } + } + } + + template + std::string write_obsolete_attribute_temp(writer& w, T const& row) + { + return w.write_temp("%", bind(row)); + } + void write_custom_attributes(writer& w, std::pair const& custom_attributes, bool enable_platform_attrib) { std::map> attributes; @@ -1876,6 +1915,7 @@ remove => %; void write_type_custom_attributes(writer& w, TypeDef const& type, bool enable_platform_attrib) { write_custom_attributes(w, type.CustomAttribute(), enable_platform_attrib); + write_obsolete_attribute(w, type); } struct attributed_type @@ -2104,6 +2144,10 @@ private static class _% auto platform_attribute = write_platform_attribute_temp(w, factory_type); for (auto&& method : factory_type.MethodList()) { + if (is_removed(method)) + { + continue; + } method_signature signature{ method }; if (settings.netstandard_compat) { @@ -2451,6 +2495,10 @@ Marshal.Release(inner); { return; } + if (is_removed(method)) + { + return; + } method_signature signature{ method }; auto return_type = w.write_temp("%", [&](writer& w) { write_projection_return_type(w, signature); @@ -2498,6 +2546,11 @@ Marshal.Release(inner); for (auto&& prop : factory.type.PropertyList()) { auto [getter, setter] = get_property_methods(prop); + // MIDL places DeprecatedAttribute on getter method, not Property row + if (getter && is_removed(getter)) + { + continue; + } auto prop_type = write_prop_type(w, prop); auto [prop_targets, inserted] = properties.try_emplace(std::string(prop.Name()), @@ -3278,6 +3331,7 @@ visibility, self, objref_name); { std::set writtenInterfaces; std::map>, std::optional>>> properties; + std::map property_deprecation; auto fast_abi_class_val = get_fast_abi_class_for_class(type); auto write_class_interface = [&](TypeDef const& interface_type, bool is_default_interface, bool is_overridable_interface, bool is_protected_interface, type_semantics semantics) @@ -3334,6 +3388,11 @@ private % AsInternal(InterfaceTag<%> _) => % ?? Make_%(); { MethodDef getter, setter; std::tie(getter, setter) = get_property_methods(prop); + // MIDL places DeprecatedAttribute on the getter method, not the Property row + if (getter && is_removed(getter)) + { + continue; + } auto prop_type = write_prop_type(w, prop); auto is_private = getter && is_implemented_as_private_method(w, type, getter); // for explicitly implemented interfaces, assume there is always a get. auto property_name = is_private ? w.write_temp("%.%", interface_name, prop.Name()) : std::string(prop.Name()); @@ -3349,6 +3408,15 @@ private % AsInternal(InterfaceTag<%> _) => % ?? Make_%(); call_static_method && getter ? std::optional(std::pair(semantics_for_abi_call, prop)) : std::nullopt, call_static_method && setter ? std::optional(std::pair(semantics_for_abi_call, prop)) : std::nullopt ); + if (inserted) + { + // Store the getter MethodDef for deprecation checking + // (MIDL places DeprecatedAttribute on getter, not Property row) + if (getter) + { + property_deprecation.try_emplace(property_name, getter); + } + } if (!inserted) { auto& [property_type, getter_target, getter_platform, setter_target, setter_platform, is_overridable, is_public, _, getter_prop, setter_prop] = prop_targets->second; @@ -3464,6 +3532,10 @@ private % AsInternal(InterfaceTag<%> _) => % ?? Make_%(); { auto& [prop_type, getter_target, getter_platform, setter_target, setter_platform, is_overridable, is_public, is_private, getter_prop, setter_prop] = prop_data; if (is_private) continue; + if (auto it = property_deprecation.find(prop_name); it != property_deprecation.end()) + { + write_obsolete_attribute(w, it->second); + } std::string_view access_spec = is_public ? "public "sv : "protected "sv; std::string_view method_spec = is_overridable ? "virtual "sv : ""sv; write_property(w, prop_name, prop_name, prop_type, @@ -3648,6 +3720,11 @@ private % AsInternal(InterfaceTag<%> _) => % ?? Make_%(); void write_winrt_exposed_type_class(writer& w, TypeDef const& type, bool isFactory) { + if (is_removed(type)) + { + return; + } + if (should_write_winrt_exposed_type_attribute(type, isFactory)) { if (get_category(type) == category::class_type && isFactory) @@ -3909,7 +3986,16 @@ private static global::System.Runtime.CompilerServices.ConditionalWeakTable(get_type_semantics(evt.EventType()), typedef_name_type::Projected, false), @@ -5022,6 +5129,11 @@ remove => %.Unsubscribe(value); { continue; } + if (is_removed(method)) + { + continue; + } + write_obsolete_attribute(w, method); method_signature signature{ method }; auto [invoke_target, is_generic] = get_invoke_info(w, method); w.write(R"( @@ -5050,6 +5162,11 @@ remove => %.Unsubscribe(value); for (auto&& prop : type.PropertyList()) { + if (is_removed(prop)) + { + continue; + } + write_obsolete_attribute(w, prop); auto [getter, setter] = get_property_methods(prop); w.write(R"( %unsafe % %% @@ -5110,7 +5227,12 @@ return %; int index = 0; for (auto&& evt : type.EventList()) { - auto semantics = get_type_semantics(evt.EventType()); + if (is_removed(evt)) + { + index++; + continue; + } + write_obsolete_attribute(w, evt); auto event_source = w.write_temp(settings.netstandard_compat ? "_%" : "Get_%2()", evt.Name()); w.write(R"( %event % %% @@ -6540,6 +6662,23 @@ return 0;)", bool have_generic_params = std::find_if(generic_abi_types.begin(), generic_abi_types.end(), [](auto&& pair) { return !pair.second.empty(); }) != generic_abi_types.end(); + // For removed methods, generate a stub that returns E_NOTIMPL. + // The vtable slot must exist for ABI compatibility, but the method + // is no longer on the projected interface so we can't call it. + if (is_removed(method)) + { + w.write(R"( +% +private static unsafe int Do_Abi_%% +{ +return unchecked((int)0x80004001); // E_NOTIMPL +})", + !settings.netstandard_compat && !generic_type ? "[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]" : "", + vmethod_name, + bind(method)); + return; + } + w.write(R"( % private static unsafe int Do_Abi_%% @@ -6586,6 +6725,40 @@ bind_list(", ", signature.params())); void write_property_abi_invoke(writer& w, Property const& prop) { auto [getter, setter] = get_property_methods(prop); + // MIDL places DeprecatedAttribute on getter, not Property row + if (getter && is_removed(getter)) + { + // Property is removed from the projected interface, but the vtable slot + // must still exist for ABI compatibility. Generate E_NOTIMPL stubs. + auto generic_type = distance(prop.Parent().GenericParam()) > 0; + if (setter) + { + auto vmethod_name = get_vmethod_name(w, setter.Parent(), setter); + w.write(R"( +% +private static unsafe int Do_Abi_%% +{ +return unchecked((int)0x80004001); // E_NOTIMPL +})", + !settings.netstandard_compat && !generic_type ? "[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]" : "", + vmethod_name, + bind(setter)); + } + if (getter) + { + auto vmethod_name = get_vmethod_name(w, getter.Parent(), getter); + w.write(R"( +% +private static unsafe int Do_Abi_%% +{ +return unchecked((int)0x80004001); // E_NOTIMPL +})", + !settings.netstandard_compat && !generic_type ? "[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]" : "", + vmethod_name, + bind(getter)); + } + return; + } auto type_name = write_type_name_temp(w, prop.Parent()); auto generic_type = distance(prop.Parent().GenericParam()) > 0; if (setter) @@ -6697,10 +6870,37 @@ prop.Name()); void write_event_abi_invoke(writer& w, Event const& evt) { + auto [add_method, remove_method] = get_event_methods(evt); + // MIDL places DeprecatedAttribute on add method, not Event row + if (is_removed(add_method)) + { + // Event is removed from the projected interface, but the vtable slots + // must still exist for ABI compatibility. Generate E_NOTIMPL stubs. + auto generic_type = distance(evt.Parent().GenericParam()) > 0; + auto add_vmethod_name = get_vmethod_name(w, add_method.Parent(), add_method); + auto remove_vmethod_name = get_vmethod_name(w, remove_method.Parent(), remove_method); + w.write(R"( +% +private static unsafe int Do_Abi_%% +{ +return unchecked((int)0x80004001); // E_NOTIMPL +} +% +private static unsafe int Do_Abi_%% +{ +return unchecked((int)0x80004001); // E_NOTIMPL +})", + !settings.netstandard_compat && !generic_type ? "[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]" : "", + add_vmethod_name, + bind(add_method), + !settings.netstandard_compat && !generic_type ? "[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]" : "", + remove_vmethod_name, + bind(remove_method)); + return; + } auto type_name = write_type_name_temp(w, evt.Parent()); auto generic_type = distance(evt.Parent().GenericParam()) > 0; auto semantics = get_type_semantics(evt.EventType()); - auto [add_method, remove_method] = get_event_methods(evt); auto add_signature = method_signature{ add_method }; auto handler_parameter_name = add_signature.params().back().first.Name(); @@ -7372,6 +7572,11 @@ IInspectableVftbl = global::WinRT.IInspectable.Vftbl.AbiToProjectionVftable, void write_interface(writer& w, TypeDef const& type) { + if (is_removed(type)) + { + return; + } + XLANG_ASSERT(get_category(type) == category::interface_type); auto type_name = write_type_name_temp(w, type, "%", typedef_name_type::CCW); @@ -7394,6 +7599,11 @@ IInspectableVftbl = global::WinRT.IInspectable.Vftbl.AbiToProjectionVftable, bool write_abi_interface_netstandard(writer& w, TypeDef const& type) { + if (is_removed(type)) + { + return false; + } + XLANG_ASSERT(get_category(type) == category::interface_type); auto type_name = write_type_name_temp(w, type, "%", typedef_name_type::ABI); auto nongenerics_class = w.write_temp("%_Delegates", bind(type, typedef_name_type::ABI, false)); @@ -7610,6 +7820,11 @@ private IObjectReference % => __% ?? Make__%(); void write_static_abi_classes(writer& w, TypeDef const& iface) { + if (is_removed(iface)) + { + return; + } + auto fast_abi_class_val = get_fast_abi_class_for_interface(iface); if (fast_abi_class_val.has_value()) { @@ -8084,6 +8299,11 @@ NativeMemory.Free((void*)abiToProjectionVftablePtr); bool write_abi_interface(writer& w, TypeDef const& type) { + if (is_removed(type)) + { + return false; + } + bool is_generic = distance(type.GenericParam()) > 0; XLANG_ASSERT(get_category(type) == category::interface_type); auto type_name = write_type_name_temp(w, type, "%", typedef_name_type::ABI); @@ -8642,6 +8862,11 @@ _defaultLazy = new Lazy<%>(() => GetDefaultReference<%.Vftbl>()); void write_class(writer& w, TypeDef const& type) { + if (is_removed(type)) + { + return; + } + writer::write_platform_guard guard{ w }; if (settings.component) @@ -8898,6 +9123,11 @@ global::System.Collections.Concurrent.ConcurrentDictionary MarshalInterface<% void write_delegate(writer& w, TypeDef const& type) { + if (is_removed(type)) + { + return; + } + if (settings.component) { write_authoring_metadata_type(w, type); @@ -9025,6 +9265,11 @@ public static ObjectReferenceValue CreateMarshaler2(% obj) => MarshalInterface<% void write_abi_delegate(writer& w, TypeDef const& type) { + if (is_removed(type)) + { + return; + } + auto method = get_delegate_invoke(type); method_signature signature{ method }; auto type_name = write_type_name_temp(w, type); @@ -9687,6 +9932,11 @@ return true; void write_enum(writer& w, TypeDef const& type) { + if (is_removed(type)) + { + return; + } + if (settings.component) { write_authoring_metadata_type(w, type); @@ -9713,6 +9963,11 @@ return true; { if (auto constant = field.Constant()) { + if (is_removed(field)) + { + continue; + } + write_obsolete_attribute(w, field); w.write("%% = unchecked((%)%),\n", bind(field.CustomAttribute()), field.Name(), enum_underlying_type, bind(constant)); @@ -9724,6 +9979,11 @@ return true; void write_struct(writer& w, TypeDef const& type) { + if (is_removed(type)) + { + return; + } + if (settings.component) { write_authoring_metadata_type(w, type); @@ -9821,6 +10081,11 @@ public override int GetHashCode() => %; return; } + if (is_removed(type)) + { + return; + } + w.write("[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n% struct %\n{\n", internal_accessibility(), bind(type, typedef_name_type::ABI, false)); @@ -10139,6 +10404,10 @@ bind_list(", ", signature.params()) void write_factory_class(writer& w, TypeDef const& type) { + if (is_removed(type)) + { + return; + } auto factory_type_name = write_type_name_temp(w, type, "%ServerActivationFactory", typedef_name_type::CCW); auto is_activatable = !is_static(type) && has_default_constructor(type); auto type_name = write_type_name_temp(w, type, "%", typedef_name_type::Projected); @@ -11153,4 +11422,4 @@ return true; //------------------------------------------------------------------------------ )", VERSION_STRING); } -} \ No newline at end of file +} diff --git a/src/cswinrt/helpers.h b/src/cswinrt/helpers.h index 9968b22ef1..d6bbe2b43c 100644 --- a/src/cswinrt/helpers.h +++ b/src/cswinrt/helpers.h @@ -34,6 +34,58 @@ namespace cswinrt return has_attribute(prop, "Windows.Foundation.Metadata", "NoExceptionAttribute"); } + template + bool is_deprecated(T const& row) + { + return has_attribute(row, "Windows.Foundation.Metadata", "DeprecatedAttribute"); + } + + template + auto get_deprecated_message(T const& row) + { + auto attr = get_attribute(row, "Windows.Foundation.Metadata", "DeprecatedAttribute"); + if (attr) + { + auto sig = attr.Value(); + auto const& fixedArgs = sig.FixedArgs(); + if (fixedArgs.size() >= 1) + { + auto const& elemSig = std::get(fixedArgs[0].value); + if (std::holds_alternative(elemSig.value)) + { + return std::string(std::get(elemSig.value)); + } + } + } + return std::string{}; + } + + template + bool is_removed(T const& row) + { + auto attr = get_attribute(row, "Windows.Foundation.Metadata", "DeprecatedAttribute"); + if (!attr) + { + return false; + } + auto sig = attr.Value(); + auto const& fixedArgs = sig.FixedArgs(); + if (fixedArgs.size() >= 2) + { + // DeprecationType enum: Deprecate=0, Remove=1 + auto const& elemSig = std::get(fixedArgs[1].value); + auto const& enumVal = std::get(elemSig.value); + return std::visit([](auto v) -> bool { return static_cast(v) == 1; }, enumVal.value); + } + return false; + } + + template + bool is_deprecated_not_removed(T const& row) + { + return is_deprecated(row) && !is_removed(row); + } + bool is_exclusive_to(TypeDef const& type) { return get_category(type) == category::interface_type && has_attribute(type, "Windows.Foundation.Metadata"sv, "ExclusiveToAttribute"sv); diff --git a/src/cswinrt/main.cpp b/src/cswinrt/main.cpp index 0a2241eb10..2bb8032a41 100644 --- a/src/cswinrt/main.cpp +++ b/src/cswinrt/main.cpp @@ -164,6 +164,7 @@ Where is one or more of: for (auto&& type : members.classes) { if (!settings.filter.includes(type)) { continue; } + if (is_removed(type)) { continue; } for (auto&& attribute : type.CustomAttribute()) { auto attribute_name = attribute.TypeNamespaceAndName(); diff --git a/tests/DeprecatedRemovedTest/.gitignore b/tests/DeprecatedRemovedTest/.gitignore new file mode 100644 index 0000000000..a7ec8e02c7 --- /dev/null +++ b/tests/DeprecatedRemovedTest/.gitignore @@ -0,0 +1 @@ +tests/DeprecatedRemovedTest/output/ diff --git a/tests/DeprecatedRemovedTest/DeprecatedRemovedTest.idl b/tests/DeprecatedRemovedTest/DeprecatedRemovedTest.idl new file mode 100644 index 0000000000..64fcd52048 --- /dev/null +++ b/tests/DeprecatedRemovedTest/DeprecatedRemovedTest.idl @@ -0,0 +1,189 @@ +// Test IDL for verifying deprecated and removed API support +// Contains types with DeprecationType.Deprecate (0) and DeprecationType.Remove (1) +// +// This IDL covers ALL deprecation gaps found during functional testing: +// - Type-level: enums, enum fields, structs, delegates, interfaces, classes +// - Member-level on [default_interface] runtimeclass: methods, properties, events +// - ABI code generation for removed types (must not reference missing projected types) +// - MIDL attribute placement (DeprecatedAttribute on getter/add methods, not Property/Event rows) +import "Windows.Foundation.idl"; + +namespace DeprecatedRemovedTest +{ + // ======================================================================== + // ENUMS + // ======================================================================== + + // Fully removed enum - should NOT appear in projection + [deprecated("RemovedEnum has been removed.", remove, 2)] + enum RemovedEnum + { + Alpha = 0, + Beta = 1 + }; + + // Partially removed enum - Hidden value should be absent + enum PartiallyRemovedEnum + { + Visible = 0, + [deprecated("Hidden value has been removed.", remove, 2)] + Hidden = 1, + AlsoVisible = 2 + }; + + // Deprecated enum - should have [Obsolete] but still be present + [deprecated("DeprecatedEnum is deprecated for testing.", deprecate, 1)] + enum DeprecatedEnum + { + First = 0, + Second = 1 + }; + + // Normal enum for comparison + enum NormalEnum + { + One = 0, + Two = 1 + }; + + // ======================================================================== + // DELEGATES + // ======================================================================== + + // Deprecated delegate + [deprecated("DeprecatedDelegate is deprecated.", deprecate, 1)] + delegate void DeprecatedDelegate(); + + // Removed delegate - should NOT appear in projection + [deprecated("RemovedDelegate is gone.", remove, 2)] + delegate void RemovedDelegate(); + + // Normal delegate for comparison + delegate void NormalDelegate(); + + // Event handler delegate for event tests + delegate void TestEventHandler(String message); + + // ======================================================================== + // STRUCTS + // ======================================================================== + + // Deprecated struct + [deprecated("DeprecatedStruct is deprecated.", deprecate, 1)] + struct DeprecatedStruct + { + Int32 Value; + }; + + // Removed struct - should NOT appear in projection + [deprecated("RemovedStruct has been removed.", remove, 2)] + struct RemovedStruct + { + Int32 Value; + }; + + // ======================================================================== + // INTERFACES + // ======================================================================== + + // Interface with mix of normal, deprecated, and removed methods + interface IVtableTest + { + void NormalMethod(); + [deprecated("DeprecatedMethod is deprecated.", deprecate, 1)] + void DeprecatedMethod(); + [deprecated("RemovedMethod is gone.", remove, 2)] + void RemovedMethod(); + void AnotherNormalMethod(); + }; + + // Deprecated interface - should have [Obsolete] but still be present + [deprecated("IDeprecatedInterface is deprecated.", deprecate, 1)] + interface IDeprecatedInterface + { + void DeprecatedWork(); + }; + + // Removed interface - should NOT appear in projection at all + // (Previously caused CS0234 errors when ABI code referenced missing projected type) + [deprecated("IRemovedInterface has been removed.", remove, 2)] + interface IRemovedInterface + { + void RemovedWork(); + }; + + // ======================================================================== + // RUNTIMECLASSES + // ======================================================================== + + // Main test class with [default_interface] — tests member-level deprecation + // on the projected class (write_class_method, write_class_event, write_class_members) + [default_interface] + runtimeclass TestClass + { + TestClass(); + + [deprecated("Constructor with name is deprecated.", deprecate, 1)] + TestClass(String name); + + [deprecated("Constructor with name and config has been removed.", remove, 2)] + TestClass(String name, Int32 config); + + // --- Methods --- + void ActiveMethod(); + [deprecated("RemovedMethod has been removed.", remove, 2)] + void RemovedMethod(); + [deprecated("DeprecatedMethod is deprecated.", deprecate, 1)] + void DeprecatedMethod(); + static void ActiveStaticMethod(); + [deprecated("RemovedStaticMethod has been removed.", remove, 2)] + static void RemovedStaticMethod(); + + // --- Properties (Gap: MIDL puts DeprecatedAttr on getter, not Property row) --- + String NormalProp { get; }; + [deprecated("DeprecatedProp is deprecated.", deprecate, 1)] + String DeprecatedProp { get; }; + [deprecated("RemovedProp has been removed.", remove, 2)] + String RemovedProp { get; }; + + // --- Read-write properties --- + String WritableProp; + [deprecated("WritableDeprecatedProp is deprecated.", deprecate, 1)] + String WritableDeprecatedProp; + [deprecated("WritableRemovedProp has been removed.", remove, 2)] + String WritableRemovedProp; + + // --- Static properties --- + static String StaticProp { get; }; + [deprecated("StaticDeprecatedProp is deprecated.", deprecate, 1)] + static String StaticDeprecatedProp { get; }; + [deprecated("StaticRemovedProp has been removed.", remove, 2)] + static String StaticRemovedProp { get; }; + + // --- Events (Gap: MIDL puts DeprecatedAttr on add method, not Event row) --- + event TestEventHandler NormalEvent; + [deprecated("DeprecatedEvent is deprecated.", deprecate, 1)] + event TestEventHandler DeprecatedEvent; + [deprecated("RemovedEvent has been removed.", remove, 2)] + event TestEventHandler RemovedEvent; + } + + // Fully deprecated class + [deprecated("DeprecatedClass is deprecated.", deprecate, 1)] + [default_interface] + runtimeclass DeprecatedClass + { + DeprecatedClass(); + void Method(); + } + + // Fully removed class - should NOT appear in projection + // (Previously caused CS0234 when ABI code referenced missing projected type) + [deprecated("RemovedClass has been removed.", remove, 2)] + [default_interface] + runtimeclass RemovedClass + { + RemovedClass(); + void Method(); + } +} diff --git a/tests/DeprecatedRemovedTest/DeprecatedRemovedTest.winmd b/tests/DeprecatedRemovedTest/DeprecatedRemovedTest.winmd new file mode 100644 index 0000000000..6f56ca4ac4 Binary files /dev/null and b/tests/DeprecatedRemovedTest/DeprecatedRemovedTest.winmd differ diff --git a/tests/DeprecatedRemovedTest/run_test.ps1 b/tests/DeprecatedRemovedTest/run_test.ps1 new file mode 100644 index 0000000000..7ed0c27f9c --- /dev/null +++ b/tests/DeprecatedRemovedTest/run_test.ps1 @@ -0,0 +1,436 @@ +# End-to-end test for CSWinRT deprecated and removed API support +# Compiles a test IDL with deprecated/removed types to WinMD, +# generates C# projection with cswinrt.exe, and verifies the output. +# +# Regression coverage for gaps found during functional testing: +# - Commit 4c180d3f: Member-level deprecation on [default_interface] runtimeclass +# - Commit ed2ebda2: Type-level ABI code referencing removed projected types (CS0234) +# - Commit b4ba8896: MIDL attribute on getter/add methods, not Property/Event rows +# +# Usage: pwsh -File run_test.ps1 [-CsWinRTExe path\to\cswinrt.exe] + +param( + [string]$CsWinRTExe = "_build\x64\Release\cswinrt\bin\cswinrt.exe" +) + +$ErrorActionPreference = "Stop" +$script:passed = 0 +$script:failed = 0 + +function Test-Result { + param([bool]$Condition, [string]$Message) + if ($Condition) { + Write-Host " PASS: $Message" -ForegroundColor Green + $script:passed++ + } else { + Write-Host " FAIL: $Message" -ForegroundColor Red + $script:failed++ + } +} + +$testDir = "tests\DeprecatedRemovedTest" +$outputDir = "$testDir\output" +$winmd = "$testDir\DeprecatedRemovedTest.winmd" +$outputFile = "$outputDir\DeprecatedRemovedTest.cs" + +# ============================================================================ +# Step 1: Compile IDL to WinMD (if not already done) +# ============================================================================ +Write-Host "`n=== Step 1: Compile IDL to WinMD ===" -ForegroundColor Cyan + +if (!(Test-Path $winmd)) { + $vcvarsall = "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" + $sdkUnion = "C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0" + $sdkIncWinrt = "C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\winrt" + + $midlResult = cmd /c "`"$vcvarsall`" x64 >nul 2>&1 && midl /winrt /metadata_dir `"$sdkUnion`" /nomidl /W1 /nologo /I `"$sdkIncWinrt`" /h nul /dlldata nul /iid nul /proxy nul /notlb /winmd `"$winmd`" `"$testDir\DeprecatedRemovedTest.idl`"" 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host " ERROR: MIDL compilation failed" -ForegroundColor Red + $midlResult | ForEach-Object { Write-Host " $_" } + exit 1 + } +} +Test-Result (Test-Path $winmd) "WinMD file exists" + +# ============================================================================ +# Step 2: Generate C# projection +# ============================================================================ +Write-Host "`n=== Step 2: Generate C# projection ===" -ForegroundColor Cyan + +if (!(Test-Path $CsWinRTExe)) { + Write-Host " ERROR: cswinrt.exe not found at $CsWinRTExe" -ForegroundColor Red + Write-Host " Build CSWinRT first." -ForegroundColor Yellow + exit 1 +} + +New-Item -ItemType Directory -Force $outputDir | Out-Null +& $CsWinRTExe -input $winmd -input local -include DeprecatedRemovedTest -output $outputDir 2>&1 | Out-Null +Test-Result (Test-Path $outputFile) "C# projection file generated" + +$content = Get-Content $outputFile -Raw + +# ============================================================================ +# Step 3: Verify REMOVED types are excluded from projection +# ============================================================================ +Write-Host "`n=== Step 3: Removed types excluded ===" -ForegroundColor Cyan + +# RemovedEnum - entire enum should not exist +Test-Result ($content -notmatch 'enum RemovedEnum') ` + "RemovedEnum type definition excluded" + +# RemovedStruct - entire struct should not exist in user namespace +Test-Result ($content -notmatch '(?m)^\s*public struct RemovedStruct\b') ` + "RemovedStruct not in user namespace" + +# RemovedClass - no user-facing class definition +$hasRemovedClassDef = $content -match '(?m)(public|internal) (sealed )?class RemovedClass[^R]' +Test-Result (-not $hasRemovedClassDef) ` + "RemovedClass class definition excluded" + +# RemovedDelegate - no user-facing delegate declaration +Test-Result ($content -notmatch 'delegate void RemovedDelegate\(\)') ` + "RemovedDelegate declaration excluded" + +# IRemovedInterface - projected interface should NOT exist +# (Regression: commit ed2ebda2 — previously generated empty interface with broken ABI reference) +Test-Result ($content -notmatch 'interface IRemovedInterface') ` + "IRemovedInterface projected interface excluded" + +# ============================================================================ +# Step 4: Verify DEPRECATED types have [Obsolete] annotations +# ============================================================================ +Write-Host "`n=== Step 4: Deprecated types have [Obsolete] ===" -ForegroundColor Cyan + +Test-Result ($content -match 'Obsolete.*DeprecatedEnum is deprecated') ` + "DeprecatedEnum has [Obsolete] with message" + +Test-Result ($content -match 'Obsolete.*DeprecatedClass is deprecated') ` + "DeprecatedClass has [Obsolete] with message" + +Test-Result ($content -match 'Obsolete.*DeprecatedDelegate is deprecated') ` + "DeprecatedDelegate has [Obsolete] with message" + +Test-Result ($content -match 'Obsolete.*DeprecatedStruct is deprecated') ` + "DeprecatedStruct has [Obsolete] with message" + +Test-Result ($content -match 'Obsolete.*IDeprecatedInterface is deprecated') ` + "IDeprecatedInterface has [Obsolete] with message" + +# ============================================================================ +# Step 5: Verify NORMAL types are present without [Obsolete] +# ============================================================================ +Write-Host "`n=== Step 5: Normal types present ===" -ForegroundColor Cyan + +Test-Result ($content -match 'enum NormalEnum') ` + "NormalEnum type is present" + +Test-Result ($content -match 'delegate void NormalDelegate\(\)') ` + "NormalDelegate declaration present" + +Test-Result ($content -match 'class TestClass\b') ` + "TestClass class is present" + +# ============================================================================ +# Step 6: PartiallyRemovedEnum - removed field excluded, others present +# ============================================================================ +Write-Host "`n=== Step 6: PartiallyRemovedEnum field removal ===" -ForegroundColor Cyan + +Test-Result ($content -match 'Visible =') ` + "PartiallyRemovedEnum.Visible is present" + +Test-Result ($content -match 'AlsoVisible =') ` + "PartiallyRemovedEnum.AlsoVisible is present" + +Test-Result ($content -notmatch 'Hidden =') ` + "PartiallyRemovedEnum.Hidden is excluded (removed)" + +# ============================================================================ +# Step 7: IVtableTest - vtable slots preserved, removed method hidden +# ============================================================================ +Write-Host "`n=== Step 7: IVtableTest vtable preservation ===" -ForegroundColor Cyan + +Test-Result ($content -match 'NormalMethod') ` + "NormalMethod accessible in projection" + +Test-Result ($content -match 'AnotherNormalMethod') ` + "AnotherNormalMethod accessible in projection" + +Test-Result ($content -match 'Obsolete.*DeprecatedMethod is deprecated') ` + "DeprecatedMethod has [Obsolete] annotation" + +# ============================================================================ +# Step 8: TestClass method-level removal and deprecation +# ============================================================================ +Write-Host "`n=== Step 8: TestClass method-level removal ===" -ForegroundColor Cyan + +# Removed methods should NOT have [Obsolete] - they're completely hidden from user API +Test-Result ($content -notmatch 'Obsolete.*RemovedMethod has been removed') ` + "Removed methods do NOT get [Obsolete] (fully hidden)" + +Test-Result ($content -match 'Obsolete.*DeprecatedMethod is deprecated') ` + "TestClass.DeprecatedMethod has [Obsolete]" + +# ============================================================================ +# Step 9: TestClass PROPERTY deprecation/removal +# (Regression: commit b4ba8896 — MIDL puts DeprecatedAttr on getter, not Property row) +# ============================================================================ +Write-Host "`n=== Step 9: TestClass property deprecation/removal ===" -ForegroundColor Cyan + +# NormalProp should be present on the projected class +Test-Result ($content -match '(?m)public string NormalProp\b') ` + "NormalProp is present on projected TestClass" + +# DeprecatedProp should have [Obsolete] on the projected class +Test-Result ($content -match 'Obsolete.*DeprecatedProp is deprecated') ` + "DeprecatedProp has [Obsolete] on projected TestClass" + +# RemovedProp should NOT be on the projected class +# (Regression: previously not removed because is_removed() checked Property row, not getter) +$hasRemovedPropOnClass = $content -match '(?m)public string RemovedProp\b' +Test-Result (-not $hasRemovedPropOnClass) ` + "RemovedProp excluded from projected TestClass" + +# ============================================================================ +# Step 10: TestClass EVENT deprecation/removal +# (Regression: commit b4ba8896 — MIDL puts DeprecatedAttr on add method, not Event row) +# ============================================================================ +Write-Host "`n=== Step 10: TestClass event deprecation/removal ===" -ForegroundColor Cyan + +# NormalEvent should be present on the projected class +Test-Result ($content -match '(?m)public event.*NormalEvent\b') ` + "NormalEvent is present on projected TestClass" + +# DeprecatedEvent should have [Obsolete] on the projected class +Test-Result ($content -match 'Obsolete.*DeprecatedEvent is deprecated') ` + "DeprecatedEvent has [Obsolete] on projected TestClass" + +# RemovedEvent should NOT be on the projected class +$hasRemovedEventOnClass = $content -match '(?m)public event.*RemovedEvent\b' +Test-Result (-not $hasRemovedEventOnClass) ` + "RemovedEvent excluded from projected TestClass" + +# ============================================================================ +# Step 11: Interface member signatures (ITestClass) +# (Regression: commit 4c180d3f — write_interface_member_signatures not filtering) +# ============================================================================ +Write-Host "`n=== Step 11: Interface member signatures ===" -ForegroundColor Cyan + +# Extract internal interface ITestClass body using brace-counting (avoid early match on { get; }) +$lines = $content -split "`n" +$inIface = $false; $depth = 0; $ifaceLines = @() +foreach ($line in $lines) { + if ($line -match 'internal interface ITestClass\b') { $inIface = $true; $depth = 0 } + if ($inIface) { + $depth += ([regex]::Matches($line, '\{')).Count + $depth -= ([regex]::Matches($line, '\}')).Count + $ifaceLines += $line + if ($depth -le 0 -and $ifaceLines.Count -gt 1) { break } + } +} +$ifaceBody = $ifaceLines -join "`n" +if ($ifaceBody) { + + Test-Result ($ifaceBody -match 'void ActiveMethod\(\)') ` + "ITestClass interface has ActiveMethod" + + Test-Result ($ifaceBody -notmatch 'void RemovedMethod\(\)') ` + "ITestClass interface excludes RemovedMethod" + + Test-Result ($ifaceBody -notmatch 'RemovedProp') ` + "ITestClass interface excludes RemovedProp" + + Test-Result ($ifaceBody -notmatch 'RemovedEvent') ` + "ITestClass interface excludes RemovedEvent" + + Test-Result ($ifaceBody -match 'Obsolete.*DeprecatedMethod') ` + "ITestClass interface has [Obsolete] on DeprecatedMethod" + + Test-Result ($ifaceBody -match 'Obsolete.*DeprecatedProp') ` + "ITestClass interface has [Obsolete] on DeprecatedProp" + + Test-Result ($ifaceBody -match 'Obsolete.*DeprecatedEvent') ` + "ITestClass interface has [Obsolete] on DeprecatedEvent" +} else { + Write-Host " FAIL: Could not find internal interface ITestClass" -ForegroundColor Red + $script:failed += 7 +} + +# ============================================================================ +# Step 12: ABI code integrity for removed types +# (Regression: commit ed2ebda2 — ABI code referenced removed projected types) +# ============================================================================ +Write-Host "`n=== Step 12: ABI code integrity for removed types ===" -ForegroundColor Cyan + +# ABI namespace should NOT reference IRemovedClass or IRemovedInterface as helper types +# These would cause CS0234 if generated because the projected types don't exist +Test-Result ($content -notmatch 'WindowsRuntimeHelperType.*IRemovedClass') ` + "No WindowsRuntimeHelperType reference to IRemovedClass" + +Test-Result ($content -notmatch 'WindowsRuntimeHelperType.*IRemovedInterface') ` + "No WindowsRuntimeHelperType reference to IRemovedInterface" + +# No RcwFactoryAttribute for removed class +Test-Result ($content -notmatch 'RemovedClassRcwFactoryAttribute') ` + "No RcwFactoryAttribute for RemovedClass" + +# ============================================================================ +# Step 13: Read-write property deprecation/removal +# ============================================================================ +Write-Host "`n=== Step 13: Read-write property deprecation/removal ===" -ForegroundColor Cyan + +Test-Result ($content -match '(?m)public string WritableProp\b') ` + "WritableProp is present on projected TestClass" + +Test-Result ($content -match 'Obsolete.*WritableDeprecatedProp is deprecated') ` + "WritableDeprecatedProp has [Obsolete] on projected TestClass" + +$hasWritableRemovedProp = $content -match '(?m)public string WritableRemovedProp\b' +Test-Result (-not $hasWritableRemovedProp) ` + "WritableRemovedProp excluded from projected TestClass" + +# Check setter exists for writable prop +Test-Result ($content -match 'set_WritableProp') ` + "WritableProp setter path exists in ABI" + +# ============================================================================ +# Step 14: Static property deprecation/removal +# ============================================================================ +Write-Host "`n=== Step 14: Static property deprecation/removal ===" -ForegroundColor Cyan + +Test-Result ($content -match '(?m)(public )?static string StaticProp\b') ` + "StaticProp is present on projected TestClass" + +Test-Result ($content -match 'Obsolete.*StaticDeprecatedProp is deprecated') ` + "StaticDeprecatedProp has [Obsolete] on projected TestClass" + +$hasStaticRemovedProp = $content -match '(?m)(public )?static string StaticRemovedProp\b' +Test-Result (-not $hasStaticRemovedProp) ` + "StaticRemovedProp excluded from projected TestClass" + +# ============================================================================ +# Step 15: Constructor deprecation/removal +# ============================================================================ +Write-Host "`n=== Step 15: Constructor deprecation/removal ===" -ForegroundColor Cyan + +# Default constructor should be present +Test-Result ($content -match 'TestClass\(\)') ` + "Default constructor TestClass() is present" + +# Deprecated constructor should have [Obsolete] +Test-Result ($content -match 'Obsolete.*Constructor with name is deprecated') ` + "Deprecated constructor has [Obsolete] annotation" + +# Removed constructor should NOT be present in user-facing code +$hasRemovedCtor = $content -match 'Obsolete.*Constructor with name and config has been removed' +$hasRemovedCtorDef = $content -match 'TestClass\(string\s+\w+,\s*int\s+\w+\)' +Test-Result (-not $hasRemovedCtor -and -not $hasRemovedCtorDef) ` + "Removed constructor excluded from projected TestClass" + +# ============================================================================ +# Step 16: Interface checks for new constructs +# ============================================================================ +Write-Host "`n=== Step 16: Interface checks for new constructs ===" -ForegroundColor Cyan + +if ($ifaceBody) { + Test-Result ($ifaceBody -notmatch 'WritableRemovedProp') ` + "ITestClass interface excludes WritableRemovedProp" + + Test-Result ($ifaceBody -match 'Obsolete.*WritableDeprecatedProp') ` + "ITestClass interface has [Obsolete] on WritableDeprecatedProp" + + Test-Result ($ifaceBody -match 'WritableProp') ` + "ITestClass interface has WritableProp" +} else { + Write-Host " SKIP: ITestClass not found for new construct checks" -ForegroundColor Yellow +} + +# ============================================================================ +# Step 17: Compile generated C# code +# (Ultimate regression test — catches any ABI reference to missing types) +# ============================================================================ +Write-Host "`n=== Step 17: Compile generated C# code ===" -ForegroundColor Cyan + +$compileDir = "$testDir\compile_test" +if (Test-Path $compileDir) { Remove-Item $compileDir -Recurse -Force } +New-Item -ItemType Directory -Force $compileDir | Out-Null + +# Create a minimal .csproj that compiles the generated code +$csproj = @" + + + net8.0 + Library + true + false + false + CS0618;CS0612 + + + + + + + ..\..\..\src\WinRT.Runtime\bin\Release\net8.0\WinRT.Runtime.dll + + + +"@ +$csproj | Set-Content "$compileDir\CompileTest.csproj" + +$buildResult = dotnet build "$compileDir\CompileTest.csproj" --nologo 2>&1 +$buildOutput = $buildResult | Out-String +$buildSuccess = $buildOutput -match 'Build succeeded' +$hasErrors = $buildOutput -match 'error CS' + +Test-Result $buildSuccess ` + "Generated C# code compiles without errors" + +if (-not $buildSuccess -and $hasErrors) { + # Show the specific errors + $buildResult | Select-String "error CS" | ForEach-Object { Write-Host " $_" -ForegroundColor Yellow } +} + +# Cleanup compile test +Remove-Item $compileDir -Recurse -Force -ErrorAction SilentlyContinue + +# ============================================================================ +# Step 18: Component generation excludes removed classes +# ============================================================================ +Write-Host "`n=== Step 18: Component activation factory excludes removed classes ===" -ForegroundColor Cyan + +$componentDir = "$testDir\component_output" +New-Item -ItemType Directory -Force $componentDir | Out-Null +& $CsWinRTExe -input $winmd -input local -include DeprecatedRemovedTest -output $componentDir -component 2>&1 | Out-Null + +$moduleFile = "$componentDir\WinRT_Module.cs" +Test-Result (Test-Path $moduleFile) ` + "WinRT_Module.cs generated in component mode" + +if (Test-Path $moduleFile) { + $moduleContent = Get-Content $moduleFile -Raw + + # RemovedClass must NOT appear in the activation factory + Test-Result ($moduleContent -notmatch 'RemovedClass') ` + "RemovedClass excluded from component activation factory" + + # Normal and deprecated classes MUST still appear + Test-Result ($moduleContent -match 'TestClass') ` + "TestClass present in component activation factory" + Test-Result ($moduleContent -match 'DeprecatedClass') ` + "DeprecatedClass present in component activation factory" +} + +# Cleanup component output +Remove-Item $componentDir -Recurse -Force -ErrorAction SilentlyContinue + +# ============================================================================ +# Summary +# ============================================================================ +Write-Host "`n=== Summary ===" -ForegroundColor Cyan +Write-Host "Passed: $($script:passed)" -ForegroundColor Green +if ($script:failed -gt 0) { + Write-Host "Failed: $($script:failed)" -ForegroundColor Red + exit 1 +} else { + Write-Host "All checks passed!" -ForegroundColor Green +} diff --git a/tests/verify_removed.ps1 b/tests/verify_removed.ps1 new file mode 100644 index 0000000000..9a5139b93a --- /dev/null +++ b/tests/verify_removed.ps1 @@ -0,0 +1,91 @@ +# Verify deprecated and removed API support in CSWinRT +# Validates helpers, code generation, and generated output + +$ErrorActionPreference = "Stop" +$script:passed = 0 +$script:failed = 0 + +function Assert-Contains { + param([string]$File, [string]$Pattern, [string]$Message) + $content = Get-Content $File -Raw + if ($content -match $Pattern) { + Write-Host " PASS: $Message" -ForegroundColor Green + $script:passed++ + } else { + Write-Host " FAIL: $Message" -ForegroundColor Red + Write-Host " Expected pattern: $Pattern" -ForegroundColor Yellow + $script:failed++ + } +} + +Write-Host "`n=== helpers.h: deprecated/removed helpers ===" -ForegroundColor Cyan + +$helpersFile = "src\cswinrt\helpers.h" +Assert-Contains $helpersFile "is_removed" "is_removed() function exists" +Assert-Contains $helpersFile "is_deprecated" "is_deprecated() function exists" +Assert-Contains $helpersFile "get_deprecated_message" "get_deprecated_message() function exists" +Assert-Contains $helpersFile "is_deprecated_not_removed" "is_deprecated_not_removed() helper exists" +Assert-Contains $helpersFile "DeprecatedAttribute" "References DeprecatedAttribute" + +Write-Host "`n=== code_writers.h: deprecated/removed handling ===" -ForegroundColor Cyan + +$writersFile = "src\cswinrt\code_writers.h" +Assert-Contains $writersFile "is_removed" "code_writers.h has is_removed() checks" +Assert-Contains $writersFile "write_obsolete_attribute" "code_writers.h has write_obsolete_attribute" +Assert-Contains $writersFile "System.Obsolete" "Generates [System.Obsolete] annotations" + +Write-Host "`n=== Vtable preservation: write_vtable has NO is_removed checks ===" -ForegroundColor Cyan + +# The write_vtable function must iterate ALL methods including removed ones. +# Verify it does NOT contain is_removed checks (vtable slots must be preserved). +$writersContent = Get-Content $writersFile -Raw +$vtableSection = [regex]::Match($writersContent, 'void write_vtable\(writer.*?\n\s{4}\}', [System.Text.RegularExpressions.RegexOptions]::Singleline).Value +if ($vtableSection -and $vtableSection -notmatch 'is_removed') { + Write-Host " PASS: write_vtable does NOT skip removed methods (vtable preserved)" -ForegroundColor Green + $script:passed++ +} elseif (!$vtableSection) { + Write-Host " SKIP: Could not isolate write_vtable function" -ForegroundColor Yellow +} else { + Write-Host " FAIL: write_vtable contains is_removed check (vtable slots may be broken)" -ForegroundColor Red + $script:failed++ +} + +# Verify write_interface_members DOES have is_removed checks (user projection hidden) +$membersSection = [regex]::Match($writersContent, 'void write_interface_members\(writer.*?(?=\n\s{4}void\s)', [System.Text.RegularExpressions.RegexOptions]::Singleline).Value +if ($membersSection -and $membersSection -match 'is_removed') { + Write-Host " PASS: write_interface_members skips removed methods (user API hidden)" -ForegroundColor Green + $script:passed++ +} else { + Write-Host " FAIL: write_interface_members missing is_removed check" -ForegroundColor Red + $script:failed++ +} + +Write-Host "`n=== Generated output verification ===" -ForegroundColor Cyan + +$cswinrtExe = "_build\x64\Release\cswinrt\bin\cswinrt.exe" +if (Test-Path $cswinrtExe) { + $testDir = "_test_verify" + New-Item -ItemType Directory -Force $testDir | Out-Null + & $cswinrtExe -input local -include Windows.Media.PlayTo -output $testDir 2>&1 | Out-Null + $outputFile = Join-Path $testDir "Windows.Media.PlayTo.cs" + if (Test-Path $outputFile) { + Assert-Contains $outputFile "System.Obsolete" "[Obsolete] annotations present in generated C#" + Assert-Contains $outputFile "PlayToConnection may be altered" "Deprecation message preserved in [Obsolete]" + Assert-Contains $outputFile "enum PlayToConnectionState" "Deprecated enum type generated" + Assert-Contains $outputFile "Disconnected" "Deprecated enum values generated" + } else { + Write-Host " SKIP: Generated file not found" -ForegroundColor Yellow + } + Remove-Item $testDir -Recurse -Force -ErrorAction SilentlyContinue +} else { + Write-Host " SKIP: cswinrt.exe not built" -ForegroundColor Yellow +} + +Write-Host "`n=== Summary ===" -ForegroundColor Cyan +Write-Host "Passed: $($script:passed)" -ForegroundColor Green +if ($script:failed -gt 0) { + Write-Host "Failed: $($script:failed)" -ForegroundColor Red + exit 1 +} else { + Write-Host "All checks passed!" -ForegroundColor Green +}