Skip to content

Commit 7af7aae

Browse files
tests: add GenerativeCustomAttributeTests (5 regression tests for custom attr encoding); prepare release 8.7.0
Adds GenerativeCustomAttributeTests.fs with 5 focused tests covering custom attribute encoding in the generative IL writer for attribute targets and argument types not previously exercised: - ObsoleteAttribute(string, bool) on the generated TYPE ITSELF - DescriptionAttribute(string) on an instance METHOD - Multiple attributes on the same method (all-attrs preserved path) - DebuggerBrowsableAttribute(DebuggerBrowsableState) on a property (enum argument round-trip via underlying int) - Scalar bool argument round-trip Also updates RELEASE_NOTES.md with 8.7.0 entry covering this and the previously-merged GenerativeMethodsTests (#505). All 147 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent bbea8e0 commit 7af7aae

3 files changed

Lines changed: 160 additions & 0 deletions

File tree

RELEASE_NOTES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
#### 8.7.0 - April 19, 2026
2+
3+
- Tests: Add `GenerativeMethodsTests` covering instance/static methods and method-count in generative types #505
4+
- Tests: Add `GenerativeCustomAttributeTests` — 5 regression tests for custom attribute encoding on types, methods, and properties (string, bool, enum arguments; multiple attributes per member)
5+
16
#### 8.6.0 - April 15, 2026
27

38
- Bug fix: Fix `ProvidedTypeDefinition.Logger` creating a new delegate reference on each call #501

tests/FSharp.TypeProviders.SDK.Tests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<Compile Include="GenerativePropertiesTests.fs" />
2525
<Compile Include="GenerativeDelegateTests.fs" />
2626
<Compile Include="GenerativeMethodsTests.fs" />
27+
<Compile Include="GenerativeCustomAttributeTests.fs" />
2728
<Compile Include="ReferencedAssemblies.fs" />
2829
</ItemGroup>
2930
<ItemGroup>
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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

Comments
 (0)