diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 81c5761..fb99bfc 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,8 @@ +#### 8.9.0 - April 23, 2026 + +- Tests: Add `GenerativeNestedTypesTests` — 5 tests for generative nested types: constructors, instance/static properties, instance methods inside nested classes +- Refactor: `GetNestedTypes`/`GetMembers` on `ProvidedTypeDefinition` now use `canBindNestedType` for consistent BindingFlags-aware filtering of nested types + #### 8.8.0 - April 21, 2026 - Tests: Add `GenerativeInheritanceTests` — 5 tests for generative type inheritance: abstract base class, concrete derived classes, virtual method override dispatch verified at runtime diff --git a/src/ProvidedTypes.fs b/src/ProvidedTypes.fs index 1547219..6e1c33f 100644 --- a/src/ProvidedTypes.fs +++ b/src/ProvidedTypes.fs @@ -1613,7 +1613,7 @@ and ProvidedTypeDefinition(isTgt: bool, container:TypeContainer, className: stri override __.GetNestedTypes bindingFlags = (//save ("nested", bindingFlags, None) (fun () -> getMembers() - |> Array.choose (function :? Type as m when memberBinds true bindingFlags false m.IsPublic || m.IsNestedPublic -> Some m | _ -> None) + |> Array.choose (function :? Type as m when canBindNestedType bindingFlags m -> Some m | _ -> None) |> (if hasFlag bindingFlags BindingFlags.DeclaredOnly || isNull this.BaseType then id else (fun mems -> Array.append mems (this.ErasedBaseType.GetNestedTypes(bindingFlags))))) override this.GetConstructorImpl(bindingFlags, _binder, _callConventions, _types, _modifiers) = @@ -1687,7 +1687,7 @@ and ProvidedTypeDefinition(isTgt: bool, container:TypeContainer, className: stri | :? FieldInfo as m when memberBinds false bindingFlags m.IsStatic m.IsPublic -> yield (m :> _) | :? PropertyInfo as m when memberBinds false bindingFlags m.IsStatic m.IsPublic -> yield (m :> _) | :? EventInfo as m when memberBinds false bindingFlags m.IsStatic m.IsPublic -> yield (m :> _) - | :? Type as m when memberBinds true bindingFlags false m.IsPublic || m.IsNestedPublic -> yield (m :> _) + | :? Type as m when canBindNestedType bindingFlags m -> yield (m :> _) | _ -> () |] override this.GetMember(name, mt, _bindingFlags) = diff --git a/tests/FSharp.TypeProviders.SDK.Tests.fsproj b/tests/FSharp.TypeProviders.SDK.Tests.fsproj index 9f39319..adbd809 100644 --- a/tests/FSharp.TypeProviders.SDK.Tests.fsproj +++ b/tests/FSharp.TypeProviders.SDK.Tests.fsproj @@ -26,6 +26,7 @@ + diff --git a/tests/GenerativeNestedTypesTests.fs b/tests/GenerativeNestedTypesTests.fs new file mode 100644 index 0000000..7477a8e --- /dev/null +++ b/tests/GenerativeNestedTypesTests.fs @@ -0,0 +1,172 @@ +module TPSDK.GenerativeNestedTypesTests + +// Tests for generative nested types: one ProvidedTypeDefinition hosting other +// ProvidedTypeDefinitions as nested (inner) types. +// +// Scenario: +// class Library +// nested class Book { Title: string; Author: string; ctor(t,a); GetSummary(): string } +// nested class Shelf { Label: string; ctor(label); MaxItems: int (static) } +// +// Tests verify that nested types are emitted correctly, that their constructors, +// fields, instance methods, and static members are all accessible at runtime. + +#nowarn "760" // IDisposable needs new + +open System +open System.Reflection +open Microsoft.FSharp.Core.CompilerServices +open Microsoft.FSharp.Quotations +open Xunit +open ProviderImplementation.ProvidedTypes +open ProviderImplementation.ProvidedTypesTesting +open UncheckedQuotations + +[] +type GenerativeNestedProvider (config: TypeProviderConfig) as this = + inherit TypeProviderForNamespaces (config) + + let ns = "Nested.Provided" + let tempAssembly = ProvidedAssembly() + let container = ProvidedTypeDefinition(tempAssembly, ns, "Library", Some typeof, isErased = false) + + do + // ── nested class: Book ─────────────────────────────────────────────── + let bookType = ProvidedTypeDefinition("Book", Some typeof, isErased = false) + + let titleField = ProvidedField("_title", typeof) + let authorField = ProvidedField("_author", typeof) + bookType.AddMember titleField + bookType.AddMember authorField + + // constructor: Book(title: string, author: string) + bookType.AddMember + (ProvidedConstructor( + [ ProvidedParameter("title", typeof) + ProvidedParameter("author", typeof) ], + invokeCode = fun args -> + Expr.Sequential( + Expr.FieldSetUnchecked(args.[0], titleField, args.[1]), + Expr.FieldSetUnchecked(args.[0], authorField, args.[2])))) + + // property: Title : string + let titleProp = + ProvidedProperty("Title", typeof, + getterCode = fun args -> Expr.FieldGetUnchecked(args.[0], titleField)) + bookType.AddMember titleProp + + // instance method: GetSummary() : string → "«title» by «author»" + let getSummaryMethod = + ProvidedMethod("GetSummary", [], typeof, isStatic = false, + invokeCode = fun args -> + let t = Expr.FieldGetUnchecked(args.[0], titleField) + let a = Expr.FieldGetUnchecked(args.[0], authorField) + <@@ (%%t : string) + " by " + (%%a : string) @@>) + bookType.AddMember getSummaryMethod + + // ── nested class: Shelf ────────────────────────────────────────────── + let shelfType = ProvidedTypeDefinition("Shelf", Some typeof, isErased = false) + + let labelField = ProvidedField("_label", typeof) + shelfType.AddMember labelField + + // constructor: Shelf(label: string) + shelfType.AddMember + (ProvidedConstructor( + [ ProvidedParameter("label", typeof) ], + invokeCode = fun args -> + Expr.FieldSetUnchecked(args.[0], labelField, args.[1]))) + + // property: Label : string + let labelProp = + ProvidedProperty("Label", typeof, + getterCode = fun args -> Expr.FieldGetUnchecked(args.[0], labelField)) + shelfType.AddMember labelProp + + // static property: MaxItems : int + let maxItemsProp = + ProvidedProperty("MaxItems", typeof, isStatic = true, + getterCode = fun _args -> <@@ 100 @@>) + shelfType.AddMember maxItemsProp + + // ── register both nested types ─────────────────────────────────────── + container.AddMember bookType + container.AddMember shelfType + + tempAssembly.AddTypes [container] + this.AddNamespace(ns, [container]) + +let loadTestAssembly () = + let runtimeAssemblyRefs = Targets.DotNetStandard20FSharpRefs() + let runtimeAssembly = runtimeAssemblyRefs.[0] + let cfg = Testing.MakeSimulatedTypeProviderConfig (__SOURCE_DIRECTORY__, runtimeAssembly, runtimeAssemblyRefs) + let tp = GenerativeNestedProvider(cfg) :> TypeProviderForNamespaces + let providedType = tp.Namespaces.[0].GetTypes().[0] :?> ProvidedTypeDefinition + Assert.Equal("Library", providedType.Name) + let bytes = (tp :> ITypeProvider).GetGeneratedAssemblyContents(providedType.Assembly) + let assem = Assembly.Load bytes + assem.ExportedTypes |> Seq.find (fun t -> t.Name = "Library") + +[] +let ``Nested types Book and Shelf are present in generated assembly``() = + let library = loadTestAssembly () + let book = library.GetNestedType("Book", BindingFlags.Public) + let shelf = library.GetNestedType("Shelf", BindingFlags.Public) + Assert.NotNull(book) + Assert.NotNull(shelf) + Assert.Equal("Book", book.Name) + Assert.Equal("Shelf", shelf.Name) + +[] +let ``Book nested type has Title property and GetSummary method``() = + let library = loadTestAssembly () + let book = library.GetNestedType("Book", BindingFlags.Public) + Assert.NotNull(book) + + let titleProp = book.GetProperty("Title", BindingFlags.Instance ||| BindingFlags.Public) + let summaryMeth = book.GetMethod ("GetSummary", BindingFlags.Instance ||| BindingFlags.Public) + Assert.NotNull(titleProp) + Assert.NotNull(summaryMeth) + Assert.Equal(typeof, titleProp.PropertyType) + Assert.Equal(typeof, summaryMeth.ReturnType) + +[] +let ``Book nested type constructor sets fields; GetSummary returns expected string``() = + let library = loadTestAssembly () + let book = library.GetNestedType("Book", BindingFlags.Public) + Assert.NotNull(book) + + let ctor = book.GetConstructor([| typeof; typeof |]) + Assert.NotNull(ctor) + let instance = ctor.Invoke([| box "FSharp for Fun"; box "Don Syme" |]) + Assert.NotNull(instance) + + let summaryMeth = book.GetMethod("GetSummary", BindingFlags.Instance ||| BindingFlags.Public) + let result = summaryMeth.Invoke(instance, [||]) :?> string + Assert.Equal("FSharp for Fun by Don Syme", result) + +[] +let ``Shelf nested type Label property reflects constructor argument``() = + let library = loadTestAssembly () + let shelf = library.GetNestedType("Shelf", BindingFlags.Public) + Assert.NotNull(shelf) + + let ctor = shelf.GetConstructor([| typeof |]) + Assert.NotNull(ctor) + let instance = ctor.Invoke([| box "Science Fiction" |]) + + let labelProp = shelf.GetProperty("Label", BindingFlags.Instance ||| BindingFlags.Public) + Assert.NotNull(labelProp) + let value = labelProp.GetValue(instance) :?> string + Assert.Equal("Science Fiction", value) + +[] +let ``Shelf nested type static property MaxItems returns 100``() = + let library = loadTestAssembly () + let shelf = library.GetNestedType("Shelf", BindingFlags.Public) + Assert.NotNull(shelf) + + let maxItemsProp = shelf.GetProperty("MaxItems", BindingFlags.Static ||| BindingFlags.Public) + Assert.NotNull(maxItemsProp) + let value = maxItemsProp.GetValue(null) :?> int + Assert.Equal(100, value)