|
| 1 | +module TPSDK.GenerativeCustomAttributeTests |
| 2 | + |
| 3 | +#nowarn "760" // IDisposable needs new |
| 4 | + |
| 5 | +open System |
| 6 | +open System.ComponentModel |
| 7 | +open System.Diagnostics |
| 8 | +open System.Reflection |
| 9 | +open Microsoft.FSharp.Core.CompilerServices |
| 10 | +open Microsoft.FSharp.Quotations |
| 11 | +open Xunit |
| 12 | +open ProviderImplementation.ProvidedTypes |
| 13 | +open ProviderImplementation.ProvidedTypesTesting |
| 14 | + |
| 15 | +// --------------------------------------------------------------------------- |
| 16 | +// Provider: custom attributes on the type, method, and property. |
| 17 | +// |
| 18 | +// Covers attribute targets not addressed by the property-focused tests in |
| 19 | +// BasicGenerativeProvisionTests.fs: |
| 20 | +// - ObsoleteAttribute(string, bool) on the TYPE ITSELF |
| 21 | +// - DescriptionAttribute(string) on an instance METHOD |
| 22 | +// - Two attrs on the SAME method (multiple-attribute path) |
| 23 | +// - DebuggerBrowsableAttribute(DebuggerBrowsableState) on a PROPERTY (enum arg) |
| 24 | +// --------------------------------------------------------------------------- |
| 25 | + |
| 26 | +[<TypeProvider>] |
| 27 | +type GenerativeCustomAttrProvider (config: TypeProviderConfig) as this = |
| 28 | + inherit TypeProviderForNamespaces (config) |
| 29 | + |
| 30 | + let ns = "CustomAttrTargets.Provided" |
| 31 | + let tempAssembly = ProvidedAssembly() |
| 32 | + let container = ProvidedTypeDefinition(tempAssembly, ns, "Container", Some typeof<obj>, isErased = false) |
| 33 | + |
| 34 | + do |
| 35 | + let obsoleteCtor2 = typeof<ObsoleteAttribute>.GetConstructor([| typeof<string>; typeof<bool> |]) |
| 36 | + let descriptionCtor = typeof<DescriptionAttribute>.GetConstructor([| typeof<string> |]) |
| 37 | + let browsableCtor = typeof<DebuggerBrowsableAttribute>.GetConstructor([| typeof<DebuggerBrowsableState> |]) |
| 38 | + |
| 39 | + let widgetType = ProvidedTypeDefinition("Widget", Some typeof<obj>, isErased = false) |
| 40 | + |
| 41 | + // Attribute on the TYPE ITSELF: ObsoleteAttribute("old api", false) |
| 42 | + widgetType.AddCustomAttribute { |
| 43 | + new CustomAttributeData() with |
| 44 | + member _.Constructor = obsoleteCtor2 |
| 45 | + member _.ConstructorArguments = upcast [| CustomAttributeTypedArgument(typeof<string>, box "old api"); CustomAttributeTypedArgument(typeof<bool>, box false) |] |
| 46 | + member _.NamedArguments = upcast [||] } |
| 47 | + |
| 48 | + // Method with TWO attributes: DescriptionAttribute and ObsoleteAttribute |
| 49 | + let runCode (_args: Expr list) = <@@ "ok" @@> |
| 50 | + let meth = ProvidedMethod("Run", [], typeof<string>, invokeCode = runCode, isStatic = false) |
| 51 | + meth.AddCustomAttribute { |
| 52 | + new CustomAttributeData() with |
| 53 | + member _.Constructor = descriptionCtor |
| 54 | + member _.ConstructorArguments = upcast [| CustomAttributeTypedArgument(typeof<string>, box "my method") |] |
| 55 | + member _.NamedArguments = upcast [||] } |
| 56 | + meth.AddCustomAttribute { |
| 57 | + new CustomAttributeData() with |
| 58 | + member _.Constructor = obsoleteCtor2 |
| 59 | + member _.ConstructorArguments = upcast [| CustomAttributeTypedArgument(typeof<string>, box "old method"); CustomAttributeTypedArgument(typeof<bool>, box false) |] |
| 60 | + member _.NamedArguments = upcast [||] } |
| 61 | + widgetType.AddMember meth |
| 62 | + |
| 63 | + // Property with DebuggerBrowsableAttribute(Never) — tests enum-valued argument |
| 64 | + let hiddenCode (_args: Expr list) = <@@ 0 @@> |
| 65 | + let prop = ProvidedProperty("Hidden", typeof<int>, isStatic = false, getterCode = hiddenCode) |
| 66 | + prop.AddCustomAttribute { |
| 67 | + new CustomAttributeData() with |
| 68 | + member _.Constructor = browsableCtor |
| 69 | + member _.ConstructorArguments = |
| 70 | + upcast [| CustomAttributeTypedArgument(typeof<DebuggerBrowsableState>, box DebuggerBrowsableState.Never) |] |
| 71 | + member _.NamedArguments = upcast [||] } |
| 72 | + widgetType.AddMember prop |
| 73 | + |
| 74 | + widgetType.AddMember (ProvidedConstructor([], invokeCode = fun _ -> <@@ () @@>)) |
| 75 | + container.AddMember widgetType |
| 76 | + tempAssembly.AddTypes [container] |
| 77 | + this.AddNamespace(ns, [container]) |
| 78 | + |
| 79 | +let loadAttrTestAssembly () = |
| 80 | + let runtimeAssemblyRefs = Targets.DotNetStandard20FSharpRefs() |
| 81 | + let runtimeAssembly = runtimeAssemblyRefs.[0] |
| 82 | + let cfg = Testing.MakeSimulatedTypeProviderConfig (__SOURCE_DIRECTORY__, runtimeAssembly, runtimeAssemblyRefs) |
| 83 | + let tp = GenerativeCustomAttrProvider(cfg) :> TypeProviderForNamespaces |
| 84 | + let providedType = tp.Namespaces.[0].GetTypes().[0] |
| 85 | + let bytes = (tp :> ITypeProvider).GetGeneratedAssemblyContents(providedType.Assembly) |
| 86 | + let assem = Assembly.Load bytes |
| 87 | + assem.GetType("CustomAttrTargets.Provided.Container").GetNestedType("Widget") |
| 88 | + |
| 89 | +[<Fact>] |
| 90 | +let ``Custom attribute on a generative type round-trips correctly``() = |
| 91 | + // Verifies that ObsoleteAttribute placed on the type definition itself survives the |
| 92 | + // IL write/read round-trip inside the generative assembly writer. |
| 93 | + let widgetType = loadAttrTestAssembly () |
| 94 | + Assert.NotNull(widgetType) |
| 95 | + let attrs = widgetType.GetCustomAttributesData() |
| 96 | + let obsolete = attrs |> Seq.tryFind (fun a -> a.Constructor.DeclaringType = typeof<ObsoleteAttribute>) |
| 97 | + Assert.True(obsolete.IsSome, "ObsoleteAttribute should be present on Widget type") |
| 98 | + let attr = obsolete.Value |
| 99 | + Assert.Equal(2, attr.ConstructorArguments.Count) |
| 100 | + Assert.Equal("old api", attr.ConstructorArguments.[0].Value :?> string) |
| 101 | + Assert.Equal(false, attr.ConstructorArguments.[1].Value :?> bool) |
| 102 | + |
| 103 | +[<Fact>] |
| 104 | +let ``Custom attribute with bool constructor argument round-trips correctly``() = |
| 105 | + // Verifies that bool values survive the encode/decode path (ECMA-335 §II.23.3). |
| 106 | + let widgetType = loadAttrTestAssembly () |
| 107 | + Assert.NotNull(widgetType) |
| 108 | + // isError = false survives |
| 109 | + let typeAttrs = widgetType.GetCustomAttributesData() |
| 110 | + let obs = typeAttrs |> Seq.find (fun a -> a.Constructor.DeclaringType = typeof<ObsoleteAttribute>) |
| 111 | + Assert.Equal(false, obs.ConstructorArguments.[1].Value :?> bool) |
| 112 | + |
| 113 | +[<Fact>] |
| 114 | +let ``Custom attribute with enum constructor argument round-trips correctly``() = |
| 115 | + // DebuggerBrowsableAttribute takes a DebuggerBrowsableState enum value. |
| 116 | + // Enum values are encoded as their underlying int in custom attribute blobs (ECMA-335 §II.23.3). |
| 117 | + let widgetType = loadAttrTestAssembly () |
| 118 | + Assert.NotNull(widgetType) |
| 119 | + let prop = widgetType.GetProperty("Hidden") |
| 120 | + Assert.NotNull(prop) |
| 121 | + let attrs = prop.GetCustomAttributesData() |
| 122 | + let browsable = attrs |> Seq.tryFind (fun a -> a.Constructor.DeclaringType = typeof<DebuggerBrowsableAttribute>) |
| 123 | + Assert.True(browsable.IsSome, "DebuggerBrowsableAttribute should be present on Hidden property") |
| 124 | + let attr = browsable.Value |
| 125 | + Assert.Equal(1, attr.ConstructorArguments.Count) |
| 126 | + // The binary reader decodes enum args as their underlying integral value. |
| 127 | + // DebuggerBrowsableState.Never == 0; verify the round-tripped value equals 0. |
| 128 | + let rawValue = Convert.ToInt32(attr.ConstructorArguments.[0].Value) |
| 129 | + Assert.Equal(int DebuggerBrowsableState.Never, rawValue) |
| 130 | + |
| 131 | +[<Fact>] |
| 132 | +let ``Multiple custom attributes on a single generative method are all preserved``() = |
| 133 | + // Tests that defineCustomAttrs writes all attributes for a method, not just the first. |
| 134 | + let widgetType = loadAttrTestAssembly () |
| 135 | + Assert.NotNull(widgetType) |
| 136 | + let meth = widgetType.GetMethod("Run", BindingFlags.Instance ||| BindingFlags.Public) |
| 137 | + Assert.NotNull(meth) |
| 138 | + let attrs = meth.GetCustomAttributesData() |
| 139 | + let desc = attrs |> Seq.tryFind (fun a -> a.Constructor.DeclaringType = typeof<DescriptionAttribute>) |
| 140 | + let obs = attrs |> Seq.tryFind (fun a -> a.Constructor.DeclaringType = typeof<ObsoleteAttribute>) |
| 141 | + Assert.True(desc.IsSome, "DescriptionAttribute should be present on Run method") |
| 142 | + Assert.True(obs.IsSome, "ObsoleteAttribute should be present on Run method") |
| 143 | + Assert.Equal("my method", desc.Value.ConstructorArguments.[0].Value :?> string) |
| 144 | + Assert.Equal("old method", obs.Value.ConstructorArguments.[0].Value :?> string) |
| 145 | + |
| 146 | +[<Fact>] |
| 147 | +let ``Custom attribute on a generative method has correct string argument``() = |
| 148 | + let widgetType = loadAttrTestAssembly () |
| 149 | + Assert.NotNull(widgetType) |
| 150 | + let meth = widgetType.GetMethod("Run", BindingFlags.Instance ||| BindingFlags.Public) |
| 151 | + let attrs = meth.GetCustomAttributesData() |
| 152 | + let desc = attrs |> Seq.find (fun a -> a.Constructor.DeclaringType = typeof<DescriptionAttribute>) |
| 153 | + Assert.Equal(1, desc.ConstructorArguments.Count) |
| 154 | + Assert.Equal("my method", desc.ConstructorArguments.[0].Value :?> string) |
0 commit comments